Repository navigation
ClickBench Q10 slows down when filter pushdown is enabled #20325
Copy link
Copy link
Open
Labels
enhancementNew feature or requestNew feature or request
Description
Is your feature request related to a problem or challenge?
When you run Q10 with predicate pushdown enabled (see #20324 for details of what that means) it goes more slowly:
SELECT "MobilePhoneModel", COUNT(DISTINCT "UserID") AS u FROM hits WHERE "MobilePhoneModel" <> '' GROUP BY "MobilePhoneModel" ORDER BY u DESC LIMIT 10;Specifically, on my test I see it going 30% slower
Benchmark clickbench_partitioned.json
┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query ┃ HEAD ┃ alamb_pushdown_and_arrow_58 ┃ Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 10 │ 333.30 ms │ 447.25 ms │ 1.34x slower │
You can repro it like this:
Repro Script
set datafusion.execution.parquet.binary_as_string = true; -- needed for ClickBench data
SET datafusion.execution.target_partitions = 1; -- set to 1 to reduce variability
create external table hits stored as parquet location '/home/ec2-user/datafusion/benchmarks/data/hits_partitioned';
-- Q10 (default configuration, no pushdown)
SELECT "MobilePhoneModel", COUNT(DISTINCT "UserID") AS u FROM hits WHERE "MobilePhoneModel" <> '' GROUP BY "MobilePhoneModel" ORDER BY u DESC LIMIT 10;
SELECT "MobilePhoneModel", COUNT(DISTINCT "UserID") AS u FROM hits WHERE "MobilePhoneModel" <> '' GROUP BY "MobilePhoneModel" ORDER BY u DESC LIMIT 10;
SELECT "MobilePhoneModel", COUNT(DISTINCT "UserID") AS u FROM hits WHERE "MobilePhoneModel" <> '' GROUP BY "MobilePhoneModel" ORDER BY u DESC LIMIT 10;
SELECT "MobilePhoneModel", COUNT(DISTINCT "UserID") AS u FROM hits WHERE "MobilePhoneModel" <> '' GROUP BY "MobilePhoneModel" ORDER BY u DESC LIMIT 10;
SELECT "MobilePhoneModel", COUNT(DISTINCT "UserID") AS u FROM hits WHERE "MobilePhoneModel" <> '' GROUP BY "MobilePhoneModel" ORDER BY u DESC LIMIT 10;
-- Q10 enable with pushdown enabled
SET datafusion.execution.parquet.pushdown_filters = true;
SELECT "MobilePhoneModel", COUNT(DISTINCT "UserID") AS u FROM hits WHERE "MobilePhoneModel" <> '' GROUP BY "MobilePhoneModel" ORDER BY u DESC LIMIT 10;
SELECT "MobilePhoneModel", COUNT(DISTINCT "UserID") AS u FROM hits WHERE "MobilePhoneModel" <> '' GROUP BY "MobilePhoneModel" ORDER BY u DESC LIMIT 10;
SELECT "MobilePhoneModel", COUNT(DISTINCT "UserID") AS u FROM hits WHERE "MobilePhoneModel" <> '' GROUP BY "MobilePhoneModel" ORDER BY u DESC LIMIT 10;
SELECT "MobilePhoneModel", COUNT(DISTINCT "UserID") AS u FROM hits WHERE "MobilePhoneModel" <> '' GROUP BY "MobilePhoneModel" ORDER BY u DESC LIMIT 10;
SELECT "MobilePhoneModel", COUNT(DISTINCT "UserID") AS u FROM hits WHERE "MobilePhoneModel" <> '' GROUP BY "MobilePhoneModel" ORDER BY u DESC LIMIT 10;When I run this with datafusion-cli -f /tmp/q.sql | grep Elapsed you can clearly see
Elapsed 0.001 seconds.
Elapsed 0.000 seconds.
Elapsed 0.063 seconds.
Elapsed 1.419 seconds. <-- First query run
Elapsed 1.432 seconds.
Elapsed 1.387 seconds.
Elapsed 1.399 seconds.
Elapsed 1.388 seconds.
Elapsed 0.000 seconds. <---- turn on filter pushdown
Elapsed 1.690 seconds. <-- now the query runs 30% slower
Elapsed 1.671 seconds.
Elapsed 1.695 seconds.
Elapsed 1.695 seconds.
Elapsed 1.718 seconds.Describe the solution you'd like
I want there to be no slowdown when I run Q10
Describe alternatives you've considered
No response
Additional context
No response
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request
Activity
alamb commentedon Feb 12, 2026
There are two major differences with the pushdown path:
alamb commentedon Feb 12, 2026
Some theories I tested with codex
Tuning Thresholds for the adaptive row selection representation
alamb commentedon Feb 12, 2026
Another thing I did was to look at the actual pattern of selected rows
You can see the overall predicate is quite selective and each selection is often a few rows (2, 3) but sometimes has many (e.g. 44, 34)
Here is a Q10 Filter Pattern Visualization
hits_0.parquetfirst 1,000,000 rows: selected1.964%, skipped98.036%.hits_1..hits_99, pattern is not uniform:5.599708%, sd2.290834, min0.2681%(hits_48), max13.0079%(hits_17).alamb commentedon Feb 12, 2026
Some random analysis I found while reviewing profiling:
Handling of Cached batches takes non trivial time
Without pushdown

With pushdown
adriangb commentedon Feb 12, 2026
I think a third one is that parallelism is different.
FilterExecoften sits on top of aRepartitionExec. Unless there are more files than cores (even if there are, some may be smaller/larger, etc.) or infra-file re-partitioning is turned on the parallelism is going to be lower with predicate pushdown than in aFilterExec.Dandandan commentedon Feb 12, 2026
Filter execution is also a bit different.
FilterExecand-combine multiple predicates and then filters all columns, whereas filter pushdown always filters a single array (so it can not cache the row indices / ranges).Here apache/arrow-rs#9284 (comment) i have some good performance results by implementing/using
foldfor the iterators to speed things up for selective filters on single arrays:Not sure if this is really related to the slowdown, but it will help regardless and close the gap between the two ways of filtering.
adriangb commentedon Feb 12, 2026
Would it be a reasonable experiment to take FilterExec and make it evaluate each predicate independently to see if we can reproduce the slowdown?
I feel the intuition is that if there are some very selective predicates evaluating those first, slicing and then evaluating the rest is beneficial because you immediately discard work. But maybe the overhead of slicing the data is high enough that this is not worth it unless the filter is very selective (and with contiguous ranges) and the other filters are very expensive?
alamb commentedon Feb 13, 2026
This could be -- however, my tests I have set the parallelism to 1 via
target_partitionsso I think there are other things going on as well.Dandandan commentedon Feb 15, 2026
In addition to some of the overhead you already mentioned (CachedArrayReader / skips / filter + concat) that could be reduced, I think a lot is might be the IO pattern.
Some observations/thoughts when running target_partitions=cpu cores on my machine:
LocalFileSystemobject store is also adding overhead (open / close / allocation / metadata) as it reads more smaller ranges, probably more context switching etc. going on:I think it might help in this case to cache / remember the file handles (open / close is called many times for each range): Remove file-handle from object store GET operations arrow-rs-object-store#18 as ~25% of IO is spent on just that.
Tokio (with this query) spreads the IO out to ~32 blocking threads (and keeps them available / spreads the work on them) as it uses
spawn_blockingin the object store (perhaps we can bring our own thread pool to reduce context switching) or use blocking IO as we're not doing anything else when waiting on the data. The 32 threads is because during metadata reading we use a concurrency of 32 (meta_fetch_concurrency).Perhaps we can consider do more prefetching in parquet reader or also consider coalescing ranges for
LocalFileSystem?Dandandan commentedon Feb 16, 2026
So I think, in summary some things that could be tried out to optimize the IO side of things
LocalFilesystem(Gemini advises 4kB - 64kB max gap) to do fewer system calls(?)Dandandan commentedon Feb 16, 2026
Another "problem" of this query is that the predicate is a string column (
MobilePhoneModel) while the second column benefiting from the filter pushdown is a primitive column (userid) - so there is not much to gain IO and decoding-wise (and only overhead).Dandandan commentedon Feb 17, 2026
Just thinking out loud: I think what also would help most ClickBench queries with
<> ''filters if instead of materializing the Parquet data before evaluating the filter, the filter could be evaluated during decoding.We could implement Parquet decoders that push down e.g. scalar (in)equality that produces only the arrow array for the rows matching this predicate (e.g.
<> '') and a boolean mask (when required) for the matching rows.This would
The downside mainly being is that it only works for the predicates implemented (e.g. equality / inequality), but it seems it would be general enough to be worth it.
adriangb commentedon Feb 17, 2026
That’s basically what Vortex do. It’s a lot of work, it basically means implementing operators / a mini query engine for each file format.
Dandandan commentedon Feb 17, 2026
Yeah I was reading on Vortex.
I am not sure it is that much work though as we have the basic setup there already, I had some vibe coded PoC that does implement
<>for byteview arrays and on dictionary encoded columns and it seems it improves the performance roughly ~10% in the arrow clickbench benchmark for various queries.[Minor] Update object_store to 0.12.5 (#20378)
[Minor] Update object_store to 0.12.5 (apache#20378)
alamb commentedon Feb 26, 2026
I think the trick is that you need (potentially) specialized implementations for each operator for each encoding, which will become a large matrix ad increase both complexity and code size 🤔
l1t1 commentedon Mar 16, 2026
could you tell me how to query vertex file, I didn't find it in the datafusion document but found it at ClickBench.
alamb commentedon Mar 16, 2026
I was referring to https://vortex.dev/
I am not really an expert in vortex
Here is the scripts used to gather their ClickBench results. Maybe it will contain some wisdom for you: https://github.com/ClickHouse/ClickBench/blob/main/datafusion-vortex-partitioned/run.sh
[Minor] Update object_store to 0.12.5 (apache#20378)