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
Improve performance of ClickBench Q18, Q35, #13449
SELECT"UserID", extract(minute FROM to_timestamp_seconds("EventTime")) AS m, "SearchPhrase", COUNT(*) FROM hits GROUP BY"UserID", m, "SearchPhrase"ORDER BYCOUNT(*) DESCLIMIT10;
SELECT"ClientIP", "ClientIP"-1, "ClientIP"-2, "ClientIP"-3, COUNT(*) AS c FROM hits GROUP BY"ClientIP", "ClientIP"-1, "ClientIP"-2, "ClientIP"-3ORDER BY c DESCLIMIT10;
Describe the solution you'd like
I would like the queries to go faster
Describe alternatives you've considered
Both queries look like
SELECTCOUNT(...) cnt ... ORDER BY cnt DESCLIMIT10
In other words they are "top 10 count" style queries
By default, DataFusion will compute the counts for all groups, and then pick only the top 10.
I suspect there is some fancier way to do this, perhaps by finding the top 10 values of count when emitting from the group operator or something. It would be interesting to see if we can see what other engines like DuckDB do with this query
For q18, I found string view lead to some regression in my local?
[db@localhost.localdomain] 16:10:45 ~/arrow-datafusion
$ /home/db/arrow-datafusion/target/release/dfbench clickbench --iterations 5 --path /home/db/arrow-datafusion/benchmarks/data/hits.parquet --queries-path /home/db/arrow-datafusion/benchmarks/queries/clickbench/queries.sql --query 18 --force-view-types
Running benchmarks with the following options: RunOpt { query: Some(18), common: CommonOpt { iterations: 5, partitions: None, batch_size: 8192, debug: false, force_view_types: true }, path: "/home/db/arrow-datafusion/benchmarks/data/hits.parquet", queries_path: "/home/db/arrow-datafusion/benchmarks/queries/clickbench/queries.sql", output_path: None }
Q18: SELECT "UserID", extract(minute FROM to_timestamp_seconds("EventTime")) AS m, "SearchPhrase", COUNT(*) FROM hits GROUP BY "UserID", m, "SearchPhrase" ORDER BY COUNT(*) DESC LIMIT 10;
Query 18 iteration 0 took 8186.2 ms and returned 10 rows
Query 18 iteration 1 took 8292.9 ms and returned 10 rows
Query 18 iteration 2 took 8276.1 ms and returned 10 rows
Query 18 iteration 3 took 8327.8 ms and returned 10 rows
Query 18 iteration 4 took 8165.0 ms and returned 10 rows
[ perf record: Woken up 762 times to write data ]
[ perf record: Captured and wrote 190.757 MB perf.data (24029 samples) ]
[db@localhost.localdomain] 16:11:38 ~/arrow-datafusion
$ /home/db/arrow-datafusion/target/release/dfbench clickbench --iterations 5 --path /home/db/arrow-datafusion/benchmarks/data/hits.parquet --queries-path /home/db/arrow-datafusion/benchmarks/queries/clickbench/queries.sql --query 18
Running benchmarks with the following options: RunOpt { query: Some(18), common: CommonOpt { iterations: 5, partitions: None, batch_size: 8192, debug: false, force_view_types: false }, path: "/home/db/arrow-datafusion/benchmarks/data/hits.parquet", queries_path: "/home/db/arrow-datafusion/benchmarks/queries/clickbench/queries.sql", output_path: None }
Q18: SELECT "UserID", extract(minute FROM to_timestamp_seconds("EventTime")) AS m, "SearchPhrase", COUNT(*) FROM hits GROUP BY "UserID", m, "SearchPhrase" ORDER BY COUNT(*) DESC LIMIT 10;
Query 18 iteration 0 took 7820.3 ms and returned 10 rows
Query 18 iteration 1 took 7648.6 ms and returned 10 rows
Query 18 iteration 2 took 7847.3 ms and returned 10 rows
Query 18 iteration 3 took 7629.2 ms and returned 10 rows
Query 18 iteration 4 took 7709.3 ms and returned 10 rows
For the queries it seems also possible (but tricky) if the cardinality is high enough (i.e. copying into aggregation columns doesn't reduce memory usage very much), to first execute the aggregation, keep original data in RecordBatches and delay the materialization of other columns until at the end.
// Before
SELECT "ClientIP", "ClientIP" - 1, "ClientIP" - 2, "ClientIP" - 3, COUNT(*) AS c FROM hits GROUP BY "ClientIP", "ClientIP" - 1, "ClientIP" - 2, "ClientIP" - 3 ORDER BY c DESC LIMIT 10;
// After
SELECT "ClientIP", "ClientIP" - 1, "ClientIP" - 2, "ClientIP" - 3, COUNT(*) AS c FROM hits GROUP BY "ClientIP" ORDER BY c DESC LIMIT 10;
Amazing! q35 get 1.35x faster in my local, when add this optimize rule!
// Before
Q35: SELECT "ClientIP", "ClientIP" - 1, "ClientIP" - 2, "ClientIP" - 3, COUNT(*) AS c FROM hits GROUP BY "ClientIP", "ClientIP" - 1, "ClientIP" - 2, "ClientIP" - 3 ORDER BY c DESC LIMIT 10;
Query 35 iteration 0 took 2371.1 ms and returned 10 rows
// After
Q35: SELECT "ClientIP", "ClientIP" - 1, "ClientIP" - 2, "ClientIP" - 3, COUNT(*) AS c FROM hits GROUP BY "ClientIP", "ClientIP" - 1, "ClientIP" - 2, "ClientIP" - 3 ORDER BY c DESC LIMIT 10;
Query 35 iteration 0 took 1491.4 ms and returned 10 rows
But it still confuse me, why duckdb run q35 so fast. As I know, duckdb will not optimize the plan like clickhouse.
Activity
alamb commentedon Nov 16, 2024
Perhaps we can use some variant of the topk grouping: https://github.com/apache/datafusion/tree/main/datafusion/physical-plan/src/aggregates/topk
alamb commentedon Nov 16, 2024
First step for this project would be to profile the queries and see what we can improve
jayzhan211 commentedon Nov 17, 2024
I run them and store here. Can download it for analysis
jayzhan211 commentedon Nov 17, 2024
It seems
GroupedTopKAggregateStreamis not used for handling topK but SortExec with fetch 🤔Dandandan commentedon Nov 17, 2024
Note that q18 and q35 got some speedup in #12996
Rachelint commentedon Nov 17, 2024
For q18, I found string view lead to some regression in my local?
Dandandan commentedon Nov 17, 2024
Yes, the regression was tracked here #13188
Also #13275 tracks some further improvements in
vectorized_appendwhich seems to be pretty hot in the flamegraphs.Rachelint commentedon Nov 17, 2024
Yes, I profiled a verbose version flamegraph for
q18and foundVec::pushinvectorized_appendis so hot...I am working on improving
vectorized_equal_toandvectorized_appendfor primitives, seems can help forq18.Dandandan commentedon Nov 17, 2024
Also
date_partfromextract(minute FROM to_timestamp_seconds("EventTime")seems to be taking some timeI added the following items to query 18 in the issue description:
GroupColumn#13275Dandandan commentedon Nov 17, 2024
For the queries it seems also possible (but tricky) if the cardinality is high enough (i.e. copying into aggregation columns doesn't reduce memory usage very much), to first execute the aggregation, keep original data in
RecordBatches and delay the materialization of other columns until at the end.jayzhan211 commentedon Nov 18, 2024
Possible improvement for date_part (required to upstream chrono crate) apache/arrow-rs#6746
Rachelint commentedon Dec 1, 2024
I think we can optimize the plan to improve q35.
I found clickhouse has following optimization:
I am trying it in #13617
Rachelint commentedon Dec 1, 2024
Amazing! q35 get 1.35x faster in my local, when add this optimize rule!
But it still confuse me, why duckdb run q35 so fast. As I know, duckdb will not optimize the plan like clickhouse.
jayzhan211 commentedon Dec 3, 2024
It seems like a hack on specific query 😆 but still great 👍🏻 !
waruto210 commentedon Dec 11, 2024
Similar optimization proposals were found in ClickHouse:
ClickHouse/ClickHouse#72610
waruto210 commentedon Dec 11, 2024
count(*)needs to record count for all keys, top k aggregation is not applicable here.