You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
TPCH q1 with no predicates is 2x slower than duckdb #18411
andrewlamb@Andrews-MacBook-Pro-3:~/Downloads$ du -s -h lineitem.parquet
246G lineitem.parquet
datafusion-cli
time datafusion-cli -c "select l_returnflag,l_linestatus, count(*) as count_order from 'lineitem.parquet' group by l_returnflag, l_linestatus;"
DataFusion CLI v50.3.0
+--------------+--------------+-------------+
| l_returnflag | l_linestatus | count_order |
+--------------+--------------+-------------+
| N | F | 38657144 |
| N | O | 2999975927 |
| A | F | 1480681438 |
| R | F | 1480675200 |
+--------------+--------------+-------------+
4 row(s) fetched.
Elapsed 12.473 seconds.
real 0m12.711s
user 2m53.323s
sys 0m5.760s
Duckdb completes it almost 2x faster:
andrewlamb@Andrews-MacBook-Pro-3:~/Downloads$ time duckdb -c "select l_returnflag,l_linestatus, count(*) as count_order from 'lineitem.parquet' group by l_returnflag, l_linestatus;"
100% ▕██████████████████████████████████████▏ (00:00:06.37 elapsed)
┌──────────────┬──────────────┬─────────────┐
│ l_returnflag │ l_linestatus │ count_order │
│ varchar │ varchar │ int64 │
├──────────────┼──────────────┼─────────────┤
│ R │ F │ 1480675200 │
│ N │ F │ 38657144 │
│ A │ F │ 1480681438 │
│ N │ O │ 2999975927 │
└──────────────┴──────────────┴─────────────┘
real 0m7.220s
user 1m34.961s
sys 0m4.062s
samply record -- datafusion-cli -c "select l_returnflag,l_linestatus, count(*) as count_order from 'lineitem.parquet' group by l_returnflag, l_linestatus;"
Looks to me like there are some obvious room for improvement in the code:
andrewlamb@Andrews-MacBook-Pro-3:~/Downloads$ time duckdb -c "select l_returnflag,l_linestatus, count(*) as count_order from 'lineitem.parquet' group by l_returnflag, l_linestatus;"
┌──────────────┬──────────────┬─────────────┐
│ l_returnflag │ l_linestatus │ count_order │
│ varchar │ varchar │ int64 │
├──────────────┼──────────────┼─────────────┤
│ R │ F │ 148067261 │
│ N │ O │ 300058170 │
│ N │ F │ 3864590 │
│ A │ F │ 148047881 │
└──────────────┴──────────────┴─────────────┘
real 0m0.910s
user 0m9.005s
sys 0m0.666s
andrewlamb@Andrews-MacBook-Pro-3:~/Downloads$ time datafusion-cli -c "select l_returnflag,l_linestatus, count(*) as count_order from 'lineitem.parquet' group by l_returnflag, l_linestatus;"
DataFusion CLI v50.3.0
+--------------+--------------+-------------+
| l_returnflag | l_linestatus | count_order |
+--------------+--------------+-------------+
| N | O | 300058170 |
| R | F | 148067261 |
| A | F | 148047881 |
| N | F | 3864590 |
+--------------+--------------+-------------+
4 row(s) fetched.
Elapsed 1.291 seconds.
real 0m1.359s
user 0m16.846s
sys 0m0.708s
somehow on my amd64 desktop duckdb is similar but DataFusion is much worse. See below:
cam@cam-System-Product-Name:~/Embucket/repos/datafusion$ time datafusion-cli -c "select l_returnflag,l_linestatus, count(*) as count_order from 'sf1000/lineitem/lineitem*.parquet' group by l_returnflag, l_linestatus;"
DataFusion CLI v50.0.0
+--------------+--------------+-------------+
| l_returnflag | l_linestatus | count_order |
+--------------+--------------+-------------+
| R | F | 1480675200 |
| N | O | 2999975927 |
| A | F | 1480681438 |
| N | F | 38657144 |
+--------------+--------------+-------------+
4 row(s) fetched.
Elapsed 20.319 seconds.
real 0m20.379s
user 3m46.062s
sys 0m1.353s
given it is a higher percentage of a longer time, just that memcpy takes more then twice longer on my machine than on Andrew's when DuckDB somehow is roughly the same. So perhaps I have another amd64 related issue on my machine on top of the mentioned above inefficiency
time duckdb -c "select l_returnflag,l_linestatus, count(*) as count_order from 'lineitem.parquet' group by l_returnflag, l_linestatus;"
100% ▕██████████████████████████████████████▏ (00:00:09.37 elapsed)
┌──────────────┬──────────────┬─────────────┐
│ l_returnflag │ l_linestatus │ count_order │
│ varchar │ varchar │ int64 │
├──────────────┼──────────────┼─────────────┤
│ R │ F │ 1480675200 │
│ N │ F │ 38657144 │
│ A │ F │ 1480681438 │
│ N │ O │ 2999975927 │
└──────────────┴──────────────┴─────────────┘
real 0m10.657s
user 1m38.246s
sys 0m16.266s
time datafusion-cli -c "select l_returnflag,l_linestatus, count(*) as count_order from 'lineitem.parquet' group by l_returnflag, l_linestatus;"
DataFusion CLI v50.3.0
+--------------+--------------+-------------+
| l_returnflag | l_linestatus | count_order |
+--------------+--------------+-------------+
| N | O | 2999975927 |
| A | F | 1480681438 |
| R | F | 1480675200 |
| N | F | 38657144 |
+--------------+--------------+-------------+
4 row(s) fetched.
Elapsed 15.924 seconds.
real 0m16.128s
user 5m19.545s
sys 0m17.042s
Thank you. I see that DataFusion is faster on your end and DuckDB is slower
so the difference is completely off.
Somehow on my end I have almost the same factor on amd and on macbook which
is about x3. Perhaps it has something to do with the way parquets are
generated. I've tried to regenerate SF1000 with the gentpch-rs defaults and
it produces single parquet files for each table and while DuckDB
performance hasn't changed much, DataFusion performance got worse by factor
of x1.36 so the difference got even bigger
Tried to build with native flag as Andrew Lamb suggested above performance
got improved only by less than 10%
I think somehow DuckDB is slow on your endb and not DataFusion being slow
on my end.
If we just focus on DataFusion performance, it is easy to see, that for
low cardinality grouping with two dict encoded columns it is spending too
much time on hashing and on memcmp, it is two single character columns with
4 unique character combinations, so many opportunities for optimization. I
am sure neither DuckDB nor DataFusion exploit all of it, but DuckDB somehow
exploits more.
If we just focus on DataFusion performance, it is easy to see, that for
low cardinality grouping with two dict encoded columns it is spending too
much time on hashing and on memcmp,
🤔 figuring out how to avoid re-hashing the same short values over and over again (StringView can be thought of as a type of dictionary encoding) is a very interesting possibility
For small number of groups we can also use a faster hash map that is optimized for small number of keys
Perhaps column statistics could be used to show the range of values is small, and switch to direct indexing instead (i.e. even faster than a hash table).
For small number of groups we can also use a faster hash map that is optimized for small number of keys
Perhaps column statistics could be used to show the range of values is small, and switch to direct indexing instead (i.e. even faster than a hash table).
If we know the range is small we could do that. I just don't think we have any statistics that can tell for sure that the range will be small.
If we know the range is small we could do that. I just don't think we have any statistics that can tell for sure that the range will be small.
I agree with that. Unfortunately min/max statistics don't tell you anything about string length.
That's why I was asking about char(1) initially which does provide the information we're looking for. In an ideal world this type hint would be carried through all the way to the grouping code where it could be used to make this choice.
If we know the range is small we could do that. I just don't think we have any statistics that can tell for sure that the range will be small.
I agree with that. Unfortunately min/max statistics don't tell you anything about string length.
That's why I was asking about char(1) initially which does provide the information we're looking for. In an ideal world this type hint would be carried through all the way to the grouping code where it could be used to make this choice.
Special casing (just) Char(1) smells to me like an overly TPCH specific benchmaxxing strategy though. Optimizing for small strings (like < 12 bytes) seems much more widely useful
However, I don't actually have any data to back up my feeling that char(1) (or knowing char(1)) isn't common in practice
Yep, that’s fair. There’s not much else available as signal at the moment, is there? Would an is_short_strings test that loops over offsets or view lengths be sufficiently fast to still get a performance gain?
Yep, that’s fair. There’s not much else available as signal at the moment, is there? Would an is_short_strings test that loops over offsets or view lengths be sufficiently fast to still get a performance gain?
yeah, I don't know -- we would probably have to hack it up and see if we can make it fast enough
Once we have data about the cardinality of the columns we can reorder them for highest cardinality first and with my pr to skip already all unique (but this won't help this case as there are only few output rows)
Looking at the the query and profiling the full q1 query, I think there is a interesting optimization we can do for AVG.
Quite a bit of time is spent in the (sum/avg/count) accumulators:
select
l_returnflag,
l_linestatus,
sum(l_quantity) as sum_qty,
sum(l_extendedprice) as sum_base_price,
sum(l_extendedprice * (1 - l_discount)) as sum_disc_price,
sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)) as sum_charge,
avg(l_quantity) as avg_qty,
avg(l_extendedprice) as avg_price,
avg(l_discount) as avg_disc,
count(*) as count_order
from
lineitem
where
l_shipdate <= date '1998-09-02'
group by
l_returnflag,
l_linestatus
order by
l_returnflag,
l_linestatus;
Zooming in on the avg:
avg(l_quantity) as avg_qty,
avg(l_extendedprice) as avg_price,
avg(l_discount) as avg_disc,
count(*) as count_order
Those accumulators will all (redundantly) compute the count.
We should be able to rewrite it to use a shared count (and thus faster accumulators / less memory):
sum(l_quantity) / count_order as avg_qty,
sum(l_extendedprice) / count_order as avg_price,
sum(l_discount) / count_order as avg_disc,
count_order
Next, sum(l_quantity) and sum(l_extendedprice) are already computed, so the CSE can simplify it to only create it once!
Activity
alamb commentedon Oct 31, 2025
I can reproduce some speed difference locally with the tpchgen dataset:
Make dataset with https://github.com/clflushopt/tpchgen-rs
Dataset is 246GB:
andrewlamb@Andrews-MacBook-Pro-3:~/Downloads$ du -s -h lineitem.parquet 246G lineitem.parquetdatafusion-cli
Duckdb completes it almost 2x faster:
alamb commentedon Oct 31, 2025
Here are some sampling results:
Profile with samply:
samply record -- datafusion-cli -c "select l_returnflag,l_linestatus, count(*) as count_order from 'lineitem.parquet' group by l_returnflag, l_linestatus;"Looks to me like there are some obvious room for improvement in the code:
datafusion/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/bytes_view.rs
Lines 225 to 237 in 057583d
There are probably other ways to make this faster too
alamb commentedon Oct 31, 2025
BTW DuckDB is also 2x faster with the tPCH SF=100 dataset (which is much faster to generate)
Create dataset:
[-]Report that TPCH q1 is 3x slower than duckdb[/-][+]TPCH q1 is 2x slower than duckdb[/+][-]TPCH q1 is 2x slower than duckdb[/-][+]TPCH q1 with no predicates is 2x slower than duckdb[/+]Dandandan commentedon Oct 31, 2025
For optimizing
create_hashes, we could maybe look into creating specialized code for creating hashes for 2 or 3 columns.camuel commentedon Oct 31, 2025
somehow on my amd64 desktop duckdb is similar but DataFusion is much worse. See below:
given it is a higher percentage of a longer time, just that memcpy takes more then twice longer on my machine than on Andrew's when DuckDB somehow is roughly the same. So perhaps I have another amd64 related issue on my machine on top of the mentioned above inefficiency
alamb commentedon Nov 1, 2025
One thing to potentially try is compiling datafusion-cli with this (which I think allows it to use avx-512 and other more recent instructions)
RUSTFLAGS='-C target-cpu=native'Omega359 commentedon Nov 1, 2025
I'm running on a AMD Ryzen AI 9 HX 370 so I'll try and reproduce this as well.
Omega359 commentedon Nov 3, 2025
Omega359 commentedon Nov 3, 2025
This is the samply output I see on my machine:
camuel commentedon Nov 4, 2025
alamb commentedon Nov 5, 2025
🤔 figuring out how to avoid re-hashing the same short values over and over again (StringView can be thought of as a type of dictionary encoding) is a very interesting possibility
27 remaining items
Optimize muti-column grouping with StringView/ByteView (option 2) - 2…
Optimize hashing for StringView and ByteView (15-70% faster) (#19374)
Dandandan commentedon Dec 20, 2025
Perhaps column statistics could be used to show the range of values is small, and switch to direct indexing instead (i.e. even faster than a hash table).
alamb commentedon Dec 20, 2025
If we know the range is small we could do that. I just don't think we have any statistics that can tell for sure that the range will be small.
pepijnve commentedon Dec 20, 2025
I agree with that. Unfortunately min/max statistics don't tell you anything about string length.
That's why I was asking about
char(1)initially which does provide the information we're looking for. In an ideal world this type hint would be carried through all the way to the grouping code where it could be used to make this choice.Dandandan commentedon Dec 20, 2025
Yes agreed.
alamb commentedon Dec 20, 2025
Yeah, we could potentially do that
Special casing (just) Char(1) smells to me like an overly TPCH specific benchmaxxing strategy though. Optimizing for small strings (like < 12 bytes) seems much more widely useful
However, I don't actually have any data to back up my feeling that char(1) (or knowing char(1)) isn't common in practice
pepijnve commentedon Dec 20, 2025
Yep, that’s fair. There’s not much else available as signal at the moment, is there? Would an
is_short_stringstest that loops over offsets or view lengths be sufficiently fast to still get a performance gain?alamb commentedon Dec 20, 2025
yeah, I don't know -- we would probably have to hack it up and see if we can make it fast enough
rluvaton commentedon Dec 20, 2025
I'm thinking for a while to use different hasher that might be faster
Like https://github.com/ogxd/gxhash
rluvaton commentedon Dec 20, 2025
Once we have data about the cardinality of the columns we can reorder them for highest cardinality first and with my pr to skip already all unique (but this won't help this case as there are only few output rows)
alamb commentedon Dec 21, 2025
I think we have the benchmarks in place now (
cargo bench --bench with_hashes) to explore a new hasherDandandan commentedon Jan 3, 2026
Looking at the the query and profiling the full q1 query, I think there is a interesting optimization we can do for
AVG.Quite a bit of time is spent in the (sum/avg/count) accumulators:
Zooming in on the avg:
Those accumulators will all (redundantly) compute the count.
We should be able to rewrite it to use a shared count (and thus faster accumulators / less memory):
Next,
sum(l_quantity)andsum(l_extendedprice)are already computed, so the CSE can simplify it to only create it once!Dandandan commentedon Jan 3, 2026
I found also some opportunity to optimize
Nullstateand accumuluators:#19625
Dandandan commentedon Jan 3, 2026
#19624
This is a poc of the avg to sum conversion.