Uh oh!
There was an error while loading. Please reload this page.
Optimize Dictionary groupings - #21765
Conversation
Rich-T-kid
commented
Apr 21, 2026
If your interested in some of the discussions made leading up to this PR please view #21589 |
Rich-T-kid
commented
Apr 21, 2026
@alamb this PR should resolve the initial regression that was caused (described here) as well as provide performance boost. I would love to hear your thoughts this approach
seeing as how this PR is already large, I think this would be a nice follow up PR since as far as i know this work was never finished since #9017 was closed due to inactivity |
alamb
commented
Apr 21, 2026
Do we have any performance benchmarks for this PR? |
alamb
commented
Apr 21, 2026
(basically it is hard to justify an optimization without benchmark results, even if they need to be run manually at first) |
Rich-T-kid
commented
Apr 21, 2026
There were also micro benchmarks linked in the previous PR |
837b382 to
6675b68CompareRich-T-kid
commented
Apr 22, 2026
Removing any extra allocations like .to_vec() and repeated hash look ups into map |
speed up : 24 approachnormalize_dict_hash() is the path for arrays with values that arent expected to fit in cpu caches, that gives this implementation a guide but a loose one.
with that being said I think these results show a generally a large improvement over the current approach to dealing with dictionary encoded columns in data-fusion. the update to normalize_dict_hash() is minor compared to the PR as a whole. @alamb |
Rich-T-kid
commented
Apr 22, 2026
Pre-allocating the values buffer causes a regression |
Dandandan
commented
Apr 22, 2026
@Rich-T-kid are you sure tpch-1 looks at the data at all in your benchmarks? It looks suspicially fast, I think it might only plan/setup the queries but not look at the data. |
Rich-T-kid
commented
Apr 22, 2026
reran the benchmarks after generating data. |
Rich-T-kid
commented
Apr 22, 2026
Rich-T-kid
commented
Apr 22, 2026
Rich-T-kid
commented
Apr 22, 2026
Rich-T-kid
commented
Apr 22, 2026
Rich-T-kid
commented
Apr 22, 2026
@adriangb these benchmarks reflect what my design doc mentions the more rows/greater scale the better it should perform. these were all being run on a mac-book maybe running it with the |
e30d6d1 to
5d6a9b0CompareDandandan
commented
Apr 23, 2026
Are these queries even using dictionary data types? |
Dandandan
commented
Apr 23, 2026
run benchmarks |
adriangbot
commented
Apr 23, 2026
Benchmark for this request failed. Last 20 lines of output: Click to expandFile an issue against this benchmark runner |
Dandandan
commented
Apr 23, 2026
adriangbot
commented
Apr 23, 2026
Benchmark for this request failed. Last 20 lines of output: Click to expandFile an issue against this benchmark runner |
1 similar comment
adriangbot
commented
Apr 23, 2026
Benchmark for this request failed. Last 20 lines of output: Click to expandFile an issue against this benchmark runner |
Rich-T-kid
commented
Apr 26, 2026
Yea it seems that no benchmarks currently perform a group by using dictionary encodeded columns. I created this PR to address that. @Dandandan could you please take a look? #21860 |
alamb
commented
Jun 10, 2026
Is there any way to reduce the overhead so the difference is not as much? |
I've experimented with switching between |
2010YOUY01
commented
Jun 11, 2026
This looks very cool! I have a background question: how do these dictionary arrays enter the aggregation operator? Do we have a way to read dictionary arrays directly from Parquet, or to preserve them through |
When arrow-rs reads RLE-encoded strings from a Parquet file, it decodes them into dictionary arrays. These flow through operators like any other array. For operators like |
080daa9 to
bf62c82Comparebf62c82 to
81a5c1bComparelocal results of benchmarks for the 1:1 cardinality case (ran against main) @kumarUjjawal could you run the benchmarks again? |
alamb
commented
Jun 16, 2026
I think you need to configure the parquet reader with a schema that specifies Dictionary as the data type when reading such a column from parquet files (we use this at InfluxData). However, I don't think there is way to do this via SQL today in DataFusion, though you can do it programmatically (by providing a schema to the table provider) |
Rich-T-kid
commented
Jun 16, 2026
@alamb could you re-run the micro-benchmarks? |
alamb
commented
Jun 16, 2026
run benchmark dictionary_group_values |
adriangbot
commented
Jun 16, 2026
Benchmark for this request failed. Last 20 lines of output: Click to expandFile an issue against this benchmark runner |
Rich-T-kid
commented
Jun 16, 2026
@alamb@adriangbot failed |
kumarUjjawal
commented
Jun 17, 2026
run benchmark dictionary_group_values |
adriangbot
commented
Jun 17, 2026
🤖 Benchmark running (GKE) | trigger CPU Details (lscpu)Comparing rich-t-kid/optimize-dictionary-grouping (d440824) to 96a6096 (merge-base) diff using: dictionary_group_values File an issue against this benchmark runner |
adriangbot
commented
Jun 17, 2026
🤖 Benchmark completed (GKE) | trigger Instance: CPU Details (lscpu)DetailsResource Usagedictionary_group_values — base (merge-base)
dictionary_group_values — branch
File an issue against this benchmark runner |
Rich-T-kid
commented
Jun 17, 2026
@kumarUjjawal@alamb with #22078's performance gains, there are now no regressions even in the worst case 🚀 |
Rich-T-kid
commented
Jun 17, 2026
kumarUjjawal
commented
Jun 18, 2026
@alamb This looks like a good improvement to me, what do you think? |
alamb
commented
Jun 22, 2026
FYI @zhuqi-lucas -- perhaps is this part of your work to complete type support of Dictionaries? |
alamb
left a comment
There was a problem hiding this comment.
Thanks for working on this @Rich-T-kid and @kumarUjjawal -- I think this is a valuable direction to be pushing, but I think we should try and get something more holistic in place (e.g. have a plan for nested types in general not just Dictionary)
| .unwrap_or(self.row_buffer.len()); | ||
| Some(&self.row_buffer[start..end]) | ||
| }); | ||
| match &self.value_dt { |
There was a problem hiding this comment.
In general I am worried about needing special code for different element types in the dictionary
In general, this seems like:
- It limits the types of DictionaryArray that can be supported (e.g. this doesn't support Dicts with struct elements)
- It will have substantial amounts of code (b/c each type of dictionary now gets its own branch in this match statement)
What I think @zhuqi-lucas is trying to do as part of
And some related PRs is to make a generic way to handle nested types that doesn't require generating code for all possible type combinations.
There was a problem hiding this comment.
thank you for the review @alamb
I agree on avoiding ballooning LOC, adding support for every data type becomes more trouble than the compute savings justify. That said, I think restricting support to Dictionary<_, Utf8/Utf8View> is the right call. It significantly simplifies the code paths while still delivering a large performance boost for the most common production data shapes (low-cardinality group bys), and similar to other single-column group by specializations, it leaves a viable fallback that loses nothing when the fast path isn't hit.
With that in mind, @zhuqi-lucas's work looks very interesting -- I had a similar idea with #22891 and #21878. The issue was that multi-column group-bys on dictionaries fell through to GroupValuesRows, which was slower than we'd like. I'm actually working on a multi-column dictionary group by in #22983, but I'm running into similar issues around handling multiple distinct types. For the multi-column case it makes more sense to build on @zhuqi-lucas's work, since the number of types that need to be supported explodes and there are diminishing returns to exploiting dictionary properties across multiple columns and intern calls (key space cache, arc ptr cache, compute keys once and reuse).
As for this PR specifically, I do think it adds significant value. Would restricting the supported value types be a viable path forward? It would keep the type combinations from exploding while still letting the community benefit from the speedup.
There was a problem hiding this comment.
I touched on this in the above comment but there are properties of single-column dictionary group bys that multi-column group bys can't practically take advantage of:
null handling: in a single-column group by, all nulls map to the same group id. This avoids the combinatorial null space that emerges with multiple columns -- (null, _, ), (, null, ), (, _, null), etc. In the singular case we can store the null group id directly and look it up with no hashing, a meaningful win for data with a high null percentage.
arc pointer identity cache: consecutive batches of the same dictionary column often share the same underlying Arc buffer; a pointer equality check lets us skip re-hashing the values buffer entirely across batches.
group id precomputation: group ids can be computed once per unique dictionary key and stored in a flat lookup table, avoiding per-row hashing. In the multi-column case the number of possible key combinations grows multiplicatively -- two columns with cardinalities 100 and 200 produce up to 20,000 combinations, making this impractical.
Uh oh!
There was an error while loading. Please reload this page.







Which issue does this PR close?
This PR make an effort towards #7000 & closing #7647 + #21466
This PR aim to close half of #7647
A separate follow up PR aims to close the multi-column + dictionary column case
Rationale for this change
Issue #7647 (Materialize Dictionaries in Group Keys) identified that DataFusion was not taking advantage of dictionary encoding during hash aggregation, instead of operating on the compact dictionary representation, it was deserializing dictionary arrays into a generic row-based format, throwing away all the encoding benefits. An initial attempt was made to fix this but it caused regressions and was ultimately rolled back.
This PR
This PR takes a different approach. Rather than materializing the dictionary into a generic representation, it introduces GroupValuesDictionary, a specialized implementation that operates directly on the dictionary's structure. The key insight is that dictionary encoded columns have inherently low cardinality by design, meaning the same values repeat frequently across rows. Instead of hashing and comparing every row independently, we maintain a mapping of unique value hashes to group IDs so that repeated values are resolved in O(1) after the first encounter. Null values are similarly cached so that subsequent null rows never pay the lookup cost again. For a more detailed explanation of the implementation see this design doc (needs to be updated but the high level idea is still present).
What changes are included in this PR?
Update the match statement in new_group_values to include a custom dictionary encoding branch that works for single fields that are of type Dictionary array
Are these changes tested?
Yes, about half of the code in this PR is about testing edge cases.
Are there any user-facing changes?
No