Skip to content

Feat: add dictionaries as a supported group column type - #23187

Open
Rich-T-kid wants to merge 8 commits into
apache:mainfrom
Rich-T-kid:rich-T-kid/dictionary-groupValuesColumn-impl
Open

Feat: add dictionaries as a supported group column type#23187
Rich-T-kid wants to merge 8 commits into
apache:mainfrom
Rich-T-kid:rich-T-kid/dictionary-groupValuesColumn-impl

Conversation

@Rich-T-kid

@Rich-T-kid Rich-T-kid commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

This PR introduces a specialized GroupColumn implementation for dictionary-typed columns inside GroupValuesColumn, allowing dictionary columns to participate in the columnar, vectorized aggregation path instead of the row-based fallback.

The Implementation is only about 175+ lines of code. the remaining LOC is adding extensive test at the GroupColumn trait level as well as testing the GroupValuesColumn GroupValues trait and how it inter-opts with multi-dictionary group by's.

What changes are included in this PR?

  • Adds a DictionaryGroupValueBuilder struct implementing the GroupColumn trait for Dictionary-typed group-by columns, supporting a configurable subset of value types
  • Extends the type-check gate in GroupValuesColumn::try_new (the matches! block) to accept Dictionary(_, value_type) where value_type is already supported.
  • Adds schema-level support so emitted dictionary group key columns round-trip through the output schema correctly
  • removes casting thats done for each dictionary array in emit

Are these changes tested?

yes. a majority of this PR is test

Are there any user-facing changes?

no. this is a pure perf boost for users.

@github-actions github-actions Bot added the physical-plan Changes to the physical-plan crate label Jun 25, 2026
@Rich-T-kid
Rich-T-kid force-pushed the rich-T-kid/dictionary-groupValuesColumn-impl branch 2 times, most recently from 3f7ff57 to e6b6dce Compare July 1, 2026 04:25
@Rich-T-kid

Copy link
Copy Markdown
Contributor Author

@kumarUjjawal could you run the dictionary benchmarks on this PR? Thx

}
}
DataType::Dictionary(key_dt, value_dt) => {
let new_field = Field::new("", *value_dt.clone(), true);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this field is never read again it may be fine to ignore the name field.

should be weary of similar issues to #21765 (comment)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kind of annoying that make_group_column takes a field instead of a DataType. Maybe we can change that in a follow up PR?

Comment thread datafusion/physical-plan/src/aggregates/group_values/multi_group_by/dictionary.rs Outdated
@Rich-T-kid

Rich-T-kid commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

@kumarUjjawal wanted to bump this 😄

@geoffreyclaude

Copy link
Copy Markdown
Contributor

run benchmark dictionary_group_values

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c4856707692-778-smmmp 6.12.85+ #1 SMP Mon May 11 08:17:35 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing rich-T-kid/dictionary-groupValuesColumn-impl (eb41915) to 01bf68c (merge-base) diff using: dictionary_group_values
Results will be posted here when complete


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Details

group                                                                             HEAD                                   rich-T-kid_dictionary-groupValuesColumn-impl
-----                                                                             ----                                   --------------------------------------------
dict_intern_emit/intern_emit/size_65536_card_1000_null_0.00                       1.15    869.8±5.08µs 71.9 MElem/sec    1.00  756.8±139.44µs 82.6 MElem/sec
dict_intern_emit/intern_emit/size_65536_card_20_null_0.00                         1.22    779.9±5.80µs 80.1 MElem/sec    1.00   636.7±43.49µs 98.2 MElem/sec
dict_intern_emit/intern_emit/size_65536_card_300_null_0.00                        1.17    809.2±5.84µs 77.2 MElem/sec    1.00  692.8±108.32µs 90.2 MElem/sec
dict_intern_emit/intern_emit/size_65536_card_65536_null_0.00                      2.45      6.3±0.01ms 10.0 MElem/sec    1.00      2.6±0.04ms 24.5 MElem/sec
dict_intern_emit/intern_emit/size_65536_card_75_null_0.00                         1.24    800.2±6.35µs 78.1 MElem/sec    1.00    645.5±6.09µs 96.8 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_1000_null_0.00                        1.42    160.1±0.87µs 48.8 MElem/sec    1.00   112.7±15.66µs 69.3 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_20_null_0.00                          1.28    104.0±1.39µs 75.1 MElem/sec    1.00     81.5±0.45µs 95.8 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_300_null_0.00                         1.30    121.2±1.05µs 64.5 MElem/sec    1.00     92.9±9.63µs 84.1 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_75_null_0.00                          1.17    109.7±1.26µs 71.2 MElem/sec    1.00    94.1±13.82µs 83.0 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_8192_null_0.00                        2.66    678.0±2.09µs 11.5 MElem/sec    1.00   255.3±12.37µs 30.6 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_1000_null_0.10     1.04      4.4±0.02ms 57.4 MElem/sec    1.00      4.2±0.01ms 59.7 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_20_null_0.10       1.02      4.1±0.02ms 60.5 MElem/sec    1.00      4.1±0.01ms 61.5 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_300_null_0.10      1.01      4.2±0.02ms 59.2 MElem/sec    1.00      4.2±0.01ms 60.1 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_65536_null_0.10    1.54     16.5±0.05ms 15.2 MElem/sec    1.00     10.7±0.06ms 23.4 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_75_null_0.10       1.01      4.2±0.02ms 59.3 MElem/sec    1.00      4.2±0.01ms 60.2 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_1000_null_0.10      1.13    618.7±3.27µs 50.5 MElem/sec    1.00    549.9±6.40µs 56.8 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_20_null_0.10        1.00    512.1±3.06µs 61.0 MElem/sec    1.00    511.2±3.10µs 61.1 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_300_null_0.10       1.05    550.0±3.63µs 56.8 MElem/sec    1.00    522.5±2.60µs 59.8 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_75_null_0.10        1.02    526.6±2.85µs 59.3 MElem/sec    1.00    515.0±2.45µs 60.7 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_8192_null_0.10      1.74   1479.0±6.20µs 21.1 MElem/sec    1.00    847.9±5.26µs 36.9 MElem/sec

Resource Usage

dictionary_group_values — base (merge-base)

Metric Value
Wall time 380.1s
Peak memory 592.6 MiB
Avg memory 94.8 MiB
CPU user 227.9s
CPU sys 14.8s
Peak spill 0 B

dictionary_group_values — branch

Metric Value
Wall time 330.1s
Peak memory 464.6 MiB
Avg memory 67.9 MiB
CPU user 225.8s
CPU sys 19.2s
Peak spill 0 B

File an issue against this benchmark runner

@Rich-T-kid
Rich-T-kid force-pushed the rich-T-kid/dictionary-groupValuesColumn-impl branch 2 times, most recently from 770abfe to 243a557 Compare July 1, 2026 15:32
@Rich-T-kid Rich-T-kid changed the title [Draft][22715] introduce dictionarys as a supported group column type Feat:[Draft][22715] add dictionaries as a supported group column type Jul 1, 2026
@Rich-T-kid

Copy link
Copy Markdown
Contributor Author

@codex review

@kumarUjjawal

Copy link
Copy Markdown
Contributor

@kumarUjjawal wanted to bump this 😄

@Rich-T-kid Thank you! I have been sick so I won't be available for review. I will probably get back next week.

@Rich-T-kid

Copy link
Copy Markdown
Contributor Author

@Rich-T-kid Thank you! I have been sick so I won't be available for review. I will probably get back next week.

@kumarUjjawal Sorry to hear that. I hope you feel better! no rush on the review!

@Rich-T-kid Rich-T-kid left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revision #2

Comment thread datafusion/physical-plan/src/aggregates/group_values/multi_group_by/dictionary.rs Outdated
Comment thread datafusion/physical-plan/src/aggregates/group_values/multi_group_by/mod.rs Outdated
@Rich-T-kid
Rich-T-kid force-pushed the rich-T-kid/dictionary-groupValuesColumn-impl branch from 4ee52da to 7af7080 Compare July 2, 2026 19:18

@Rich-T-kid Rich-T-kid left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revision #3

Comment thread datafusion/physical-plan/src/aggregates/group_values/multi_group_by/mod.rs Outdated
@Rich-T-kid
Rich-T-kid force-pushed the rich-T-kid/dictionary-groupValuesColumn-impl branch from 152c1f0 to f3387c5 Compare July 5, 2026 03:59
@Rich-T-kid

Copy link
Copy Markdown
Contributor Author
Image 7-6-26 at 4 05 PM Latest PR speeds up the low cardinality case

@Rich-T-kid

Copy link
Copy Markdown
Contributor Author

@geoffreyclaude could you run the benchmarks command again when you get a chance. Thanks 🚀

@geoffreyclaude

Copy link
Copy Markdown
Contributor

run benchmark dictionary_group_values

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c4897199089-877-zms2m 6.12.85+ #1 SMP Mon May 11 08:17:35 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing rich-T-kid/dictionary-groupValuesColumn-impl (3d1e1c9) to 01bf68c (merge-base) diff using: dictionary_group_values
Results will be posted here when complete


File an issue against this benchmark runner

@Rich-T-kid Rich-T-kid left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revision #4 Last revision before making this open for review

Comment thread datafusion/physical-plan/src/aggregates/group_values/multi_group_by/dictionary.rs Outdated
Comment thread datafusion/physical-plan/src/aggregates/group_values/multi_group_by/dictionary.rs Outdated
Comment thread datafusion/physical-plan/src/aggregates/group_values/multi_group_by/dictionary.rs Outdated
Comment thread datafusion/physical-plan/src/aggregates/group_values/multi_group_by/dictionary.rs Outdated
@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Details

group                                                                             HEAD                                   rich-T-kid_dictionary-groupValuesColumn-impl
-----                                                                             ----                                   --------------------------------------------
dict_intern_emit/intern_emit/size_65536_card_1000_null_0.00                       1.23    871.4±8.87µs 71.7 MElem/sec    1.00    707.5±5.34µs 88.3 MElem/sec
dict_intern_emit/intern_emit/size_65536_card_20_null_0.00                         1.30    790.0±9.68µs 79.1 MElem/sec    1.00    608.0±7.50µs 102.8 MElem/sec
dict_intern_emit/intern_emit/size_65536_card_300_null_0.00                        1.23   815.1±10.24µs 76.7 MElem/sec    1.00    665.2±5.90µs 94.0 MElem/sec
dict_intern_emit/intern_emit/size_65536_card_65536_null_0.00                      2.51      6.2±0.02ms 10.0 MElem/sec    1.00      2.5±0.01ms 25.1 MElem/sec
dict_intern_emit/intern_emit/size_65536_card_75_null_0.00                         1.23    804.8±8.79µs 77.7 MElem/sec    1.00    655.6±9.93µs 95.3 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_1000_null_0.00                        1.35    161.5±1.39µs 48.4 MElem/sec    1.00   119.8±17.59µs 65.2 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_20_null_0.00                          1.31    104.8±1.68µs 74.5 MElem/sec    1.00     80.0±0.85µs 97.7 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_300_null_0.00                         1.34    123.0±1.59µs 63.5 MElem/sec    1.00    92.0±10.05µs 84.9 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_75_null_0.00                          1.29    112.0±6.33µs 69.7 MElem/sec    1.00    86.6±30.70µs 90.2 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_8192_null_0.00                        2.70    681.0±3.01µs 11.5 MElem/sec    1.00    252.0±1.80µs 31.0 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_1000_null_0.10     1.45      4.4±0.03ms 57.1 MElem/sec    1.00      3.0±0.01ms 82.8 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_20_null_0.10       1.58      4.2±0.02ms 60.1 MElem/sec    1.00      2.6±0.02ms 94.8 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_300_null_0.10      1.48      4.2±0.02ms 58.9 MElem/sec    1.00      2.9±0.01ms 87.4 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_65536_null_0.10    1.40     17.0±0.13ms 14.7 MElem/sec    1.00     12.2±0.13ms 20.5 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_75_null_0.10       1.51      4.2±0.03ms 59.1 MElem/sec    1.00      2.8±0.02ms 89.1 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_1000_null_0.10      1.29    620.6±3.17µs 50.4 MElem/sec    1.00    480.4±1.96µs 65.1 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_20_null_0.10        1.54    513.2±3.32µs 60.9 MElem/sec    1.00    332.5±5.61µs 94.0 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_300_null_0.10       1.48    549.7±3.56µs 56.8 MElem/sec    1.00    370.5±1.52µs 84.3 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_75_null_0.10        1.53    528.9±3.45µs 59.1 MElem/sec    1.00    344.6±4.98µs 90.7 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_8192_null_0.10      1.52   1486.9±5.41µs 21.0 MElem/sec    1.00    975.5±4.94µs 32.0 MElem/sec

Resource Usage

dictionary_group_values — base (merge-base)

Metric Value
Wall time 290.1s
Peak memory 587.7 MiB
Avg memory 124.8 MiB
CPU user 227.7s
CPU sys 15.5s
Peak spill 0 B

dictionary_group_values — branch

Metric Value
Wall time 320.1s
Peak memory 486.4 MiB
Avg memory 71.3 MiB
CPU user 210.2s
CPU sys 15.2s
Peak spill 0 B

File an issue against this benchmark runner

@Rich-T-kid

Copy link
Copy Markdown
Contributor Author

🤔 benchmarks show good improvement in every case but it should be much larger. Currently every intern() call causes the cache to be rebuilt from the bottom up. Instead of a flat vector it may make sense to use a hashtable that last in-between calls.

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5139289178-1321-q5dkt 6.12.85+ #1 SMP Mon May 11 08:17:35 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing rich-T-kid/dictionary-groupValuesColumn-impl (c98c47f) to 30ae8bf (merge-base) diff

Run configuration
run benchmark dictionary_group_values

Results will be posted here when complete


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

Comparing rich-T-kid/dictionary-groupValuesColumn-impl (c98c47f) to 30ae8bf (merge-base) diff

Run configuration
run benchmark dictionary_group_values
CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Details

group                                                                             HEAD                                   rich-T-kid_dictionary-groupValuesColumn-impl
-----                                                                             ----                                   --------------------------------------------
dict_intern_emit/intern_emit/size_65536_card_1000_null_0.00                       1.76    869.7±6.63µs 71.9 MElem/sec    1.00    494.8±3.92µs 126.3 MElem/sec
dict_intern_emit/intern_emit/size_65536_card_20_null_0.00                         1.86    779.0±5.49µs 80.2 MElem/sec    1.00   418.7±10.22µs 149.3 MElem/sec
dict_intern_emit/intern_emit/size_65536_card_300_null_0.00                        1.80    808.5±4.98µs 77.3 MElem/sec    1.00    448.1±4.75µs 139.5 MElem/sec
dict_intern_emit/intern_emit/size_65536_card_65536_null_0.00                      1.02      6.1±0.01ms 10.2 MElem/sec    1.00      6.0±0.01ms 10.4 MElem/sec
dict_intern_emit/intern_emit/size_65536_card_75_null_0.00                         1.83    804.3±8.17µs 77.7 MElem/sec    1.00    438.9±6.71µs 142.4 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_1000_null_0.00                        1.25    159.7±1.12µs 48.9 MElem/sec    1.00    127.2±1.90µs 61.4 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_20_null_0.00                          1.82    103.3±0.93µs 75.6 MElem/sec    1.00     56.6±0.70µs 138.0 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_300_null_0.00                         1.58    121.5±1.08µs 64.3 MElem/sec    1.00     77.1±0.50µs 101.3 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_75_null_0.00                          1.73    109.8±1.14µs 71.2 MElem/sec    1.00     63.3±9.43µs 123.3 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_8192_null_0.00                        1.22    659.2±2.62µs 11.9 MElem/sec    1.00    538.3±2.83µs 14.5 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_1000_null_0.10     1.81      4.3±0.01ms 58.4 MElem/sec    1.00      2.4±0.00ms 106.0 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_20_null_0.10       1.83      4.1±0.01ms 61.6 MElem/sec    1.00      2.2±0.00ms 113.0 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_300_null_0.10      1.78      4.1±0.01ms 60.4 MElem/sec    1.00      2.3±0.00ms 107.7 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_65536_null_0.10    1.09     17.1±0.05ms 14.6 MElem/sec    1.00     15.8±0.05ms 15.9 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_75_null_0.10       1.80      4.1±0.01ms 60.5 MElem/sec    1.00      2.3±0.00ms 109.0 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_1000_null_0.10      1.49    598.9±2.08µs 52.2 MElem/sec    1.00    400.8±1.71µs 78.0 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_20_null_0.10        1.82    493.7±2.33µs 63.3 MElem/sec    1.00    271.4±1.20µs 115.2 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_300_null_0.10       1.68    526.0±2.11µs 59.4 MElem/sec    1.00    313.8±2.21µs 99.6 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_75_null_0.10        1.77    505.8±2.21µs 61.8 MElem/sec    1.00    285.4±2.00µs 109.5 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_8192_null_0.10      1.21   1471.3±6.77µs 21.2 MElem/sec    1.00   1219.6±4.86µs 25.6 MElem/sec

Resource Usage

dictionary_group_values — base (merge-base)

Metric Value
Wall time 530.1s
Peak memory 505.7 MiB
Avg memory 95.8 MiB
CPU user 230.5s
CPU sys 10.4s
Peak spill 0 B

dictionary_group_values — branch

Metric Value
Wall time 530.1s
Peak memory 886.9 MiB
Avg memory 64.6 MiB
CPU user 228.9s
CPU sys 14.0s
Peak spill 0 B

File an issue against this benchmark runner

}

fn take_n(&mut self, n: usize) -> ArrayRef {
let old_inner_len = self.inner.len();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every partial emit still drains, hashes, and rebuilds all surviving values, retaining the O(G² / batch_size) cost and high peak memory. Also, Arrow take retains full backing storage for Utf8View, BinaryView, and nested dictionaries.

@Rich-T-kid Rich-T-kid Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, this isn't the most performant way to implement take_n() addressing them properly would require either:

  1. Lazy compaction with a base-offset scheme (defer the rebuild until the surviving set actually shrinks meaningfully), or
  2. Tracking which inner slots are still live without a full drain-and-rehash cycle

Both touch internals in a very meaningful way, and we currently have no benchmark that targets partial-emit patterns, so there's no good baseline to validate against. I'd rather land this with correct behaviour and open a follow-up issue to tackle the performance side with proper benchmarks in place. If that sounds good I can create the issue or add onto what @kosiew wrote in #23993

Comment thread datafusion/physical-plan/src/aggregates/group_values/multi_group_by/dictionary.rs Outdated
Rich-T-kid and others added 6 commits August 3, 2026 21:04
 add schema support for dictionarys

introduce high level GroupValuesColumn test

 introduce groupColumn trait test

git issues

introduce edge case/ regression section

inital impl

working implementation of dictionary for groupValuesCOlumns

benchmarks show perf boost over groupvaluerows, TODO:dedupe items before inner append

fix clippy errors & inline final builder

add cache for arc ptr

trim down test

trim test LOC again

trim PR

revision 3

speed up low cardinlaity case

working version

introduce inter-batch caching

break complex types into seperate parts

fixed breaking test, re-allocate hashtable on each intern() call

 re-introduce cache

remove mutex

add cache to concat pointers to avoid un-needed allocations

remove ptr caches and concat call

reduce LOC

revised PR comments

 add regression test to align with GroupValueRows & re-order overflow check

add test to assert de-duplicated output dictionary

tmp low card speed up

optimize low-card case

wip

re-use allocations across calls

add test

final clean up
@Rich-T-kid
Rich-T-kid force-pushed the rich-T-kid/dictionary-groupValuesColumn-impl branch from c98c47f to 41192e5 Compare August 4, 2026 03:45
@Rich-T-kid
Rich-T-kid requested a review from kumarUjjawal August 4, 2026 03:46
@Rich-T-kid
Rich-T-kid force-pushed the rich-T-kid/dictionary-groupValuesColumn-impl branch from 41192e5 to 44ad7d1 Compare August 4, 2026 03:49
@Rich-T-kid

Copy link
Copy Markdown
Contributor Author

commit 44ad7d1 should address all of your comments except for the take_n() optimization. I think that would make sense as a follow up @kumarUjjawal, happy to hear your thoughts

@kumarUjjawal

Copy link
Copy Markdown
Contributor

run benchmark dictionary_group_values

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5175755158-1373-7r6tf 6.12.85+ #1 SMP Wed Jun 17 20:31:55 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing rich-T-kid/dictionary-groupValuesColumn-impl (63d4021) to 47ca490 (merge-base) diff

Run configuration
run benchmark dictionary_group_values

Results will be posted here when complete


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

Comparing rich-T-kid/dictionary-groupValuesColumn-impl (63d4021) to 47ca490 (merge-base) diff

Run configuration
run benchmark dictionary_group_values
CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Details

group                                                                             HEAD                                   rich-T-kid_dictionary-groupValuesColumn-impl
-----                                                                             ----                                   --------------------------------------------
dict_intern_emit/intern_emit/size_65536_card_1000_null_0.00                       1.73    877.4±8.70µs 71.2 MElem/sec    1.00   506.7±14.15µs 123.3 MElem/sec
dict_intern_emit/intern_emit/size_65536_card_20_null_0.00                         1.83    780.9±5.99µs 80.0 MElem/sec    1.00    426.5±6.24µs 146.5 MElem/sec
dict_intern_emit/intern_emit/size_65536_card_300_null_0.00                        1.79    817.3±7.97µs 76.5 MElem/sec    1.00    457.4±5.18µs 136.6 MElem/sec
dict_intern_emit/intern_emit/size_65536_card_65536_null_0.00                      1.01      6.2±0.06ms 10.1 MElem/sec    1.00      6.1±0.01ms 10.2 MElem/sec
dict_intern_emit/intern_emit/size_65536_card_75_null_0.00                         1.81    802.4±5.79µs 77.9 MElem/sec    1.00    444.4±4.42µs 140.7 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_1000_null_0.00                        1.26    159.8±1.36µs 48.9 MElem/sec    1.00   127.2±11.00µs 61.4 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_20_null_0.00                          1.81    104.4±1.25µs 74.8 MElem/sec    1.00     57.7±0.76µs 135.5 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_300_null_0.00                         1.55    121.6±1.27µs 64.3 MElem/sec    1.00     78.3±7.45µs 99.8 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_75_null_0.00                          1.75    111.2±6.16µs 70.2 MElem/sec    1.00     63.4±0.62µs 123.2 MElem/sec
dict_intern_emit/intern_emit/size_8192_card_8192_null_0.00                        1.27    660.9±2.95µs 11.8 MElem/sec    1.00    519.4±2.52µs 15.0 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_1000_null_0.10     1.80      4.3±0.01ms 58.2 MElem/sec    1.00      2.4±0.00ms 104.9 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_20_null_0.10       1.84      4.1±0.01ms 61.2 MElem/sec    1.00      2.2±0.00ms 112.6 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_300_null_0.10      1.80      4.2±0.01ms 59.9 MElem/sec    1.00      2.3±0.00ms 107.6 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_65536_null_0.10    1.06     17.8±0.13ms 14.0 MElem/sec    1.00     16.8±0.15ms 14.8 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_65536_card_75_null_0.10       1.81      4.1±0.01ms 60.3 MElem/sec    1.00      2.3±0.00ms 109.2 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_1000_null_0.10      1.46    599.2±2.48µs 52.2 MElem/sec    1.00    410.6±1.88µs 76.1 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_20_null_0.10        1.81    498.5±2.66µs 62.7 MElem/sec    1.00    275.1±2.33µs 113.6 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_300_null_0.10       1.67    531.3±3.11µs 58.8 MElem/sec    1.00    317.6±2.21µs 98.4 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_75_null_0.10        1.76    507.7±2.26µs 61.5 MElem/sec    1.00    289.2±1.86µs 108.1 MElem/sec
dict_repeated_intern_emit/repeated_intern_emit/size_8192_card_8192_null_0.10      1.13  1467.4±10.66µs 21.3 MElem/sec    1.00   1294.1±6.57µs 24.1 MElem/sec

Resource Usage

dictionary_group_values — base (merge-base)

Metric Value
Wall time 380.1s
Peak memory 593.5 MiB
Avg memory 87.7 MiB
CPU user 225.7s
CPU sys 15.2s
Peak spill 0 B

dictionary_group_values — branch

Metric Value
Wall time 385.1s
Peak memory 886.3 MiB
Avg memory 82.9 MiB
CPU user 227.2s
CPU sys 13.4s
Peak spill 0 B

File an issue against this benchmark runner

@kumarUjjawal kumarUjjawal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @Rich-T-kid

This looks good. Please create a separate issue for the optimization as you mentioned.

@Rich-T-kid

Copy link
Copy Markdown
Contributor Author

created a follow up issue here #24089

thank you for the reviews @kumarUjjawal @zhuqi-lucas @jayshrivastava 🚀

@kumarUjjawal

Copy link
Copy Markdown
Contributor

I will merge this in 1-2 days in no one else has any objections.

@alamb alamb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this @Rich-T-kid and @kumarUjjawal

I think it would be really great to show that this works end to end (and improves performance) -- I suspect it does but it would be good to verify

One way we could do this is run TPCH Q1 https://github.com/apache/datafusion/blob/main/benchmarks/queries/q1.sql

You could test this locally like

Make data

tpcgen-cli tpch parquet --scale-factor=10 --tables=lineitem
lineitem               [==================] (100%)

Then convert the relevant columns to dictioanry

> COPY (select arrow_cast(l_returnflag, 'Dictionary(Int32, Utf8)') as l_returnflag, arrow_cast(l_linestatus, 'Dictionary(Int32, Utf8)') as l_linestatus, l_quantity, l_extendedprice, l_discount, l_shipdate, l_tax from 'lineitem.parquet') to 'lineitem_dict.parquet';
+----------+
| count    |
+----------+
| 59986052 |
+----------+
1 row(s) fetched.
Elapsed 1.767 seconds.

Then run query

> 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_dict.parquet'
where
        l_shipdate <= date '1998-09-02'
group by
    l_returnflag,
    l_linestatus
order by
    l_returnflag,
    l_linestatus;
+--------------+--------------+--------------+------------------+--------------------+----------------------+-----------+--------------+----------+-------------+
| l_returnflag | l_linestatus | sum_qty      | sum_base_price   | sum_disc_price     | sum_charge           | avg_qty   | avg_price    | avg_disc | count_order |
+--------------+--------------+--------------+------------------+--------------------+----------------------+-----------+--------------+----------+-------------+
| A            | F            | 377518399.00 | 566065727797.25  | 537759104278.0656  | 559276670892.116819  | 25.500975 | 38237.151008 | 0.050006 | 14804077    |
| N            | F            | 9851614.00   | 14767438399.17   | 14028805792.2114   | 14590490998.366737   | 25.522448 | 38257.810660 | 0.049973 | 385998      |
| N            | O            | 743124873.00 | 1114302286901.88 | 1058580922144.9638 | 1100937000170.591854 | 25.498075 | 38233.902923 | 0.050000 | 29144351    |
| R            | F            | 377732830.00 | 566431054976.00  | 538110922664.7677  | 559634780885.086257  | 25.508384 | 38251.219273 | 0.049996 | 14808183    |
+--------------+--------------+--------------+------------------+--------------------+----------------------+-----------+--------------+----------+-------------+
4 row(s) fetched.
Elapsed 0.370 seconds.

}
};

// TODO: Materialize dictionaries in group keys (#7647)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is pretty nice

@alamb

alamb commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Another thing that might be valuable is to make a variant of the tpch benchmarks, where you tell the parquet reader that the low cardinality columns (that would be good for dictionary encoding) should be read as Dictionary... Arrays -- and then run the queries and see how much improvement this PR gives

I actually think that would be helpful in general if your goal is to improve the overall performance of the dictionaries in DataFusion. I will file a ticket with more detail

@Rich-T-kid

Rich-T-kid commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@alamb I think the second approach is easier to work with and less error-prone. But from my understanding, this isn't currently possible, or at least isn't exposed in DataFusion. The only config that currently exists is for writing: https://docs.rs/datafusion/latest/datafusion/config/struct.ParquetOptions.html#structfield.dictionary_enabled

here you tell the parquet reader that the low cardinality columns (that would be good for dictionary encoding) should be read as Dictionary... Arrays

I think it'd be nice to expose this as a session prop so users can configure it with SET notation in SQL:

SET datafusion.execution.batch_size = 4096;
SET datafusion.parquet.dictionary_read_enable;
....

The only caveat that comes to mind is this may cause the Parquet reader to read all string columns into dictionary arrays, which isn't what we want for precision.

the concept your referring to is actually quite similar to what I raised in this issue: #22891. Ideally we would be able to determine which columns are low-cardinality and change their type dictionary at physical planning time. with #23187 this should provide a nice perf boost. the only issue would be determining what threshold counts as "low cardinality".

cc @asolimando

@Rich-T-kid

Copy link
Copy Markdown
Contributor Author

So I think some nice follow ups would be

  1. expose a parquet dictionary encoding read flag to users
  2. allow a way to read only specific string columns as dictionary encoded (this may already be possible I need to check)

by hand we can experiment with transforming low-card string group keys into Dict<k,v> and then #22891 will be in a position to be implemented. it may be possible to use column statistics as a guide for "low cardinality" columns but due to the state of statistics in datafusion this may or may not be accurate. ideally this will be tunable as either a hard set value or a percentage. There are some other issues that come to mind like schema management but I think its best to leave that in #22891's thread

@Rich-T-kid

Copy link
Copy Markdown
Contributor Author

I ran TPCH Q4 against main and these were my results

tpchgen-cli -s 10 --tables orders --format=parquet

main

datafusion-cli -c "
SELECT o_orderpriority, count(*) AS order_count
FROM 'orders.parquet'
WHERE o_orderdate >= date '1993-07-01'
  AND o_orderdate < date '1993-10-01'
  AND EXISTS (
    SELECT 1 FROM 'lineitem.parquet'
    WHERE l_orderkey = o_orderkey AND l_commitdate < l_receiptdate
  )
GROUP BY o_orderpriority
ORDER BY o_orderpriority;
"

DataFusion CLI v54.0.0
+-----------------+-------------+
| o_orderpriority | order_count |
+-----------------+-------------+
| 1-URGENT        | 105214      |
| 2-HIGH          | 104821      |
| 3-MEDIUM        | 105227      |
| 4-NOT SPECIFIED | 105422      |
| 5-LOW           | 105356      |
+-----------------+-------------+
5 row(s) fetched. 
Elapsed 0.161 seconds.

cast the columns to dictionary using

datafusion-cli -c "
COPY (
  SELECT o_orderkey, o_orderdate,
    arrow_cast(o_orderpriority, 'Dictionary(Int32, Utf8)') AS o_orderpriority
  FROM 'orders.parquet'
) TO 'orders_dict.parquet';
"

this branch

datafusion-cli -c "
SELECT o_orderpriority, count(*) AS order_count
FROM 'orders_dict.parquet'
WHERE o_orderdate >= date '1993-07-01'
  AND o_orderdate < date '1993-10-01'
  AND EXISTS (
    SELECT 1 FROM 'lineitem.parquet'
    WHERE l_orderkey = o_orderkey AND l_commitdate < l_receiptdate
  )
GROUP BY o_orderpriority
ORDER BY o_orderpriority;
"

DataFusion CLI v54.0.0
+-----------------+-------------+
| o_orderpriority | order_count |
+-----------------+-------------+
| 1-URGENT        | 105214      |
| 2-HIGH          | 104821      |
| 3-MEDIUM        | 105227      |
| 4-NOT SPECIFIED | 105422      |
| 5-LOW           | 105356      |
+-----------------+-------------+
5 row(s) fetched. 
Elapsed 0.095 seconds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

physical-plan Changes to the physical-plan crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants