Skip to content

perf: coalesce small input batches before sorting - #26931

Open
geoffreyclaude wants to merge 1 commit into
apache:mainfrom
geoffreyclaude:codex/sort-small-batches
Open

geoffreyclaude wants to merge 1 commit into
apache:mainfrom
geoffreyclaude:codex/sort-small-batches

Conversation

@geoffreyclaude

@geoffreyclaude geoffreyclaude commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Follow-up to the minimum-run-size suggestion in #26242.

Rationale for this change

A sort processes chunks of input separately, then merges the sorted chunks, called runs. Small input batches create many tiny runs. Processing and merging so many runs adds overhead.

What changes are included in this PR?

Combine adjacent small batches before sorting until each run reaches the configured batch_size, except for the last run. This applies to sorts with one or several keys, including runs written to spill files. Single-key sorts can still form larger runs based on memory size.

Keep the original batches while trying to combine them. If concatenation fails or the combined data cannot fit its memory reservation, sort those original batches separately. For example, two valid dictionaries can contain too many distinct values to fit in one combined dictionary.

Example with multiple sort keys and batch_size = 4 (numbers are rows):

Before: [2] [2] [4] [1] -> sort -> merge 4 runs
After:  [  4  ] [4] [1] -> sort -> merge 3 runs
         2 + 2

What is the testing strategy for this PR?

Tests check batch grouping and row preservation, successful spilling with a full memory pool, and complete sorted output from dictionary batches that cannot be concatenated.

Bot results compare b5358e61298 with baseline d4c2a329a8f. The existing small_batches/two_keys benchmark, with two million rows stored in 32-row batches, improved from 1.212 s to 185 ms, about 6.6× faster.

The standard SF1 and SF10 SQL sort suites classified all 22 cases as no change.

Are there any user-facing changes?

Rows with equal sort keys may change relative order when batches become one run. Trying concatenation temporarily retains both original and combined arrays.

Prepared with AI assistance.

@comphead

Copy link
Copy Markdown
Contributor

This is a great PR , @geoffreyclaude are you planning to move on this?

@geoffreyclaude
geoffreyclaude force-pushed the codex/sort-small-batches branch 2 times, most recently from 61a0ddf to b5358e6 Compare November 2, 2026 10:34
@geoffreyclaude

Copy link
Copy Markdown
Contributor Author

run benchmark sort_tpch sort_tpch10

@geoffreyclaude

Copy link
Copy Markdown
Contributor Author

run benchmark sort

env:
BENCH_FILTER: small_batches

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c6575610617-4231-lptmz 6.12.105+ #1 SMP Sat Sep 19 07:55:25 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 codex/sort-small-batches (b5358e6) to d4c2a32 (merge-base) diff

Run configuration
run benchmark sort_tpch

Results will be posted here when complete


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c6575610617-4232-qz845 6.12.105+ #1 SMP Sat Sep 19 07:55:25 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 codex/sort-small-batches (b5358e6) to d4c2a32 (merge-base) diff

Run configuration
run benchmark sort_tpch10

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 codex/sort-small-batches (b5358e6) to d4c2a32 (merge-base) diff

Run configuration
run benchmark sort_tpch
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

Comparing HEAD and codex_sort-small-batches
--------------------
Benchmark sort_tpch1.json
--------------------
┏━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Query ┃      HEAD ┃ codex_sort-small-batches ┃    Change ┃
┡━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ Q1    │ 110.74 ms │                111.67 ms │ no change │
│ Q2    │  98.51 ms │                 98.95 ms │ no change │
│ Q3    │ 615.42 ms │                630.01 ms │ no change │
│ Q4    │ 157.75 ms │                162.52 ms │ no change │
│ Q5    │ 233.92 ms │                235.71 ms │ no change │
│ Q6    │ 246.67 ms │                249.41 ms │ no change │
│ Q7    │ 391.43 ms │                396.06 ms │ no change │
│ Q8    │ 292.45 ms │                298.00 ms │ no change │
│ Q9    │ 309.09 ms │                315.73 ms │ no change │
│ Q10   │ 452.52 ms │                452.43 ms │ no change │
│ Q11   │ 240.75 ms │                238.84 ms │ no change │
└───────┴───────────┴──────────────────────────┴───────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Benchmark Summary                       ┃           ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ Total Time (HEAD)                       │ 3149.24ms │
│ Total Time (codex_sort-small-batches)   │ 3189.34ms │
│ Average Time (HEAD)                     │  286.29ms │
│ Average Time (codex_sort-small-batches) │  289.94ms │
│ Queries Faster                          │         0 │
│ Queries Slower                          │         0 │
│ Queries with No Change                  │        11 │
│ Queries with Failure                    │         0 │
└─────────────────────────────────────────┴───────────┘

Distribution per query (min / mean ±stddev / max):

Comparing HEAD and codex_sort-small-batches
--------------------
Benchmark sort_tpch1.json
--------------------
┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Query ┃                               HEAD ┃           codex_sort-small-batches ┃    Change ┃
┡━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ Q1    │  110.74 / 112.37 ±1.14 / 114.28 ms │  111.67 / 112.54 ±1.06 / 114.59 ms │ no change │
│ Q2    │    98.51 / 99.49 ±0.94 / 101.26 ms │    98.95 / 99.75 ±0.85 / 101.31 ms │ no change │
│ Q3    │  615.42 / 619.71 ±4.49 / 625.22 ms │  630.01 / 631.63 ±1.21 / 633.48 ms │ no change │
│ Q4    │  157.75 / 158.66 ±0.68 / 159.37 ms │  162.52 / 165.72 ±3.94 / 173.38 ms │ no change │
│ Q5    │  233.92 / 234.55 ±0.42 / 235.11 ms │  235.71 / 237.16 ±1.68 / 239.93 ms │ no change │
│ Q6    │  246.67 / 247.27 ±0.60 / 248.21 ms │  249.41 / 251.01 ±2.23 / 255.44 ms │ no change │
│ Q7    │  391.43 / 392.81 ±1.59 / 395.89 ms │  396.06 / 397.29 ±1.02 / 399.10 ms │ no change │
│ Q8    │  292.45 / 298.72 ±4.37 / 303.30 ms │  298.00 / 303.83 ±4.96 / 312.62 ms │ no change │
│ Q9    │ 309.09 / 328.35 ±14.00 / 346.31 ms │  315.73 / 324.55 ±5.97 / 332.43 ms │ no change │
│ Q10   │  452.52 / 458.58 ±4.84 / 465.36 ms │ 452.43 / 461.83 ±12.04 / 485.56 ms │ no change │
│ Q11   │ 240.75 / 249.13 ±10.17 / 268.21 ms │ 238.84 / 252.83 ±14.95 / 276.06 ms │ no change │
└───────┴────────────────────────────────────┴────────────────────────────────────┴───────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Benchmark Summary                       ┃           ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ Total Time (HEAD)                       │ 3199.65ms │
│ Total Time (codex_sort-small-batches)   │ 3238.14ms │
│ Average Time (HEAD)                     │  290.88ms │
│ Average Time (codex_sort-small-batches) │  294.38ms │
│ Queries Faster                          │         0 │
│ Queries Slower                          │         0 │
│ Queries with No Change                  │        11 │
│ Queries with Failure                    │         0 │
└─────────────────────────────────────────┴───────────┘

Resource Usage

sort_tpch — base (merge-base)

Metric Value
Wall time 20.0s
Peak memory 2.4 GiB
Avg memory 1.1 GiB
CPU user 61.0s
CPU sys 2.9s
Peak spill 0 B

sort_tpch — branch

Metric Value
Wall time 20.0s
Peak memory 2.4 GiB
Avg memory 1.1 GiB
CPU user 61.3s
CPU sys 2.9s
Peak spill 0 B

File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c6575615365-4235-qjzgn 6.12.105+ #1 SMP Sat Sep 19 07:55:25 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 codex/sort-small-batches (b5358e6) to d4c2a32 (merge-base) diff

Run configuration
run benchmark sort
env:
  BENCH_FILTER: "small_batches"

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 codex/sort-small-batches (b5358e6) to d4c2a32 (merge-base) diff

Run configuration
run benchmark sort_tpch10
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

Comparing HEAD and codex_sort-small-batches
--------------------
Benchmark sort_tpch10.json
--------------------
┏━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Query ┃       HEAD ┃ codex_sort-small-batches ┃    Change ┃
┡━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ Q1    │ 1450.49 ms │               1432.97 ms │ no change │
│ Q2    │ 1333.83 ms │               1340.04 ms │ no change │
│ Q3    │ 8560.07 ms │               8475.08 ms │ no change │
│ Q4    │ 2159.33 ms │               2197.81 ms │ no change │
│ Q5    │ 2375.65 ms │               2360.95 ms │ no change │
│ Q6    │ 2517.28 ms │               2503.34 ms │ no change │
│ Q7    │ 4404.44 ms │               4455.25 ms │ no change │
│ Q8    │ 3387.31 ms │               3505.72 ms │ no change │
│ Q9    │ 3666.40 ms │               3787.59 ms │ no change │
│ Q10   │ 5720.92 ms │               5839.88 ms │ no change │
│ Q11   │ 3374.16 ms │               3391.51 ms │ no change │
└───────┴────────────┴──────────────────────────┴───────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Benchmark Summary                       ┃            ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ Total Time (HEAD)                       │ 38949.87ms │
│ Total Time (codex_sort-small-batches)   │ 39290.14ms │
│ Average Time (HEAD)                     │  3540.90ms │
│ Average Time (codex_sort-small-batches) │  3571.83ms │
│ Queries Faster                          │          0 │
│ Queries Slower                          │          0 │
│ Queries with No Change                  │         11 │
│ Queries with Failure                    │          0 │
└─────────────────────────────────────────┴────────────┘

Distribution per query (min / mean ±stddev / max):

Comparing HEAD and codex_sort-small-batches
--------------------
Benchmark sort_tpch10.json
--------------------
┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Query ┃                                   HEAD ┃               codex_sort-small-batches ┃    Change ┃
┡━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ Q1    │   1450.49 / 1460.51 ±9.14 / 1476.01 ms │   1432.97 / 1439.61 ±4.26 / 1443.96 ms │ no change │
│ Q2    │  1333.83 / 1355.96 ±18.46 / 1386.06 ms │  1340.04 / 1367.90 ±27.19 / 1400.37 ms │ no change │
│ Q3    │  8560.07 / 8601.90 ±36.67 / 8658.35 ms │  8475.08 / 8605.00 ±87.95 / 8733.75 ms │ no change │
│ Q4    │  2159.33 / 2183.75 ±22.12 / 2218.83 ms │  2197.81 / 2227.14 ±32.52 / 2289.97 ms │ no change │
│ Q5    │  2375.65 / 2391.36 ±12.08 / 2407.75 ms │  2360.95 / 2376.59 ±11.81 / 2395.34 ms │ no change │
│ Q6    │   2517.28 / 2523.60 ±7.67 / 2538.44 ms │   2503.34 / 2513.98 ±8.34 / 2525.20 ms │ no change │
│ Q7    │  4404.44 / 4419.00 ±10.47 / 4432.37 ms │  4455.25 / 4491.74 ±34.11 / 4552.18 ms │ no change │
│ Q8    │  3387.31 / 3451.11 ±66.41 / 3569.38 ms │  3505.72 / 3571.06 ±66.88 / 3665.26 ms │ no change │
│ Q9    │  3666.40 / 3744.84 ±52.72 / 3823.49 ms │  3787.59 / 3822.71 ±27.84 / 3856.21 ms │ no change │
│ Q10   │ 5720.92 / 6147.93 ±224.25 / 6324.45 ms │ 5839.88 / 6197.65 ±200.93 / 6386.56 ms │ no change │
│ Q11   │  3374.16 / 3405.88 ±45.06 / 3493.59 ms │  3391.51 / 3411.60 ±23.71 / 3456.72 ms │ no change │
└───────┴────────────────────────────────────────┴────────────────────────────────────────┴───────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Benchmark Summary                       ┃            ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ Total Time (HEAD)                       │ 39685.85ms │
│ Total Time (codex_sort-small-batches)   │ 40024.98ms │
│ Average Time (HEAD)                     │  3607.80ms │
│ Average Time (codex_sort-small-batches) │  3638.63ms │
│ Queries Faster                          │          0 │
│ Queries Slower                          │          0 │
│ Queries with No Change                  │         11 │
│ Queries with Failure                    │          0 │
└─────────────────────────────────────────┴────────────┘

Resource Usage

sort_tpch10 — base (merge-base)

Metric Value
Wall time 200.0s
Peak memory 18.7 GiB
Avg memory 7.2 GiB
CPU user 816.6s
CPU sys 72.1s
Peak spill 0 B

sort_tpch10 — branch

Metric Value
Wall time 205.0s
Peak memory 18.9 GiB
Avg memory 7.1 GiB
CPU user 820.5s
CPU sys 72.5s
Peak spill 0 B

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 codex/sort-small-batches (b5358e6) to d4c2a32 (merge-base) diff

Run configuration
run benchmark sort
env:
  BENCH_FILTER: "small_batches"
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                                   codex_sort-small-batches
-----                     ----                                   ------------------------
small_batches/two_keys    6.55  1211.5±23.73ms        ? ?/sec    1.00    184.9±2.28ms        ? ?/sec

Resource Usage

sort — base (merge-base)

Metric Value
Wall time 545.1s
Peak memory 347.9 MiB
Avg memory 9.6 MiB
CPU user 21.4s
CPU sys 3.0s
Peak spill 0 B

sort — branch

Metric Value
Wall time 525.1s
Peak memory 200.0 MiB
Avg memory 4.4 MiB
CPU user 18.1s
CPU sys 0.5s
Peak spill 0 B

File an issue against this benchmark runner

@geoffreyclaude
geoffreyclaude force-pushed the codex/sort-small-batches branch from b5358e6 to 1563663 Compare November 2, 2026 13:27
@geoffreyclaude

Copy link
Copy Markdown
Contributor Author

This is a great PR , @geoffreyclaude are you planning to move on this?

@comphead Yes definitely, I just need to find time to do a proper self-review of the AI code first. There are also two other follow-up PRs (still very much in AI draft too):

@geoffreyclaude
geoffreyclaude force-pushed the codex/sort-small-batches branch from 1563663 to 97f342f Compare November 4, 2026 19:43
@geoffreyclaude
geoffreyclaude force-pushed the codex/sort-small-batches branch from 97f342f to 808bfdb Compare November 4, 2026 19:59
@geoffreyclaude
geoffreyclaude marked this pull request as ready for review November 4, 2026 20:28
@geoffreyclaude

Copy link
Copy Markdown
Contributor Author

This is a great PR , @geoffreyclaude are you planning to move on this?

@comphead I've finally opened the PR for review. I believe it's best to focus on this one first as it brings the clearest benefit.

The following two should be #26930 (code simplification, removes 186 LOC) and #26933 (yield every batch of rows).

@comphead comphead 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.

Nice speedup on small_batches/two_keys. Most of my comments are about memory on the spill path, which this PR now coalesces too.

  • Benchmarks: sort_tpch reads 8192-row batches, so the SF1/SF10 no change results show that the untouched path stays untouched. They don't exercise the new path. The biggest behavior change is coalescing before a spill, so a memory-limited small-batch run (peak RSS and spill count, before and after) would cover it.
  • Design question: did you consider coalescing at ingest in insert_batch, for example with the BatchCoalescer that LimitedBatchCoalescer already wraps? Each small batch would be copied once as it arrives and charged through the existing reserve_memory_for_batch_and_maybe_spill. That removes the fallback and the realign pass. The costs are an extra copy for inputs that end up on the in-place path, and the shared-slice dedup no longer applying to coalesced batches.
  • Row target: was a larger target measured for multi-key sorts? batch_size sits between the old one-run-per-batch rule and the single-key byte target. The removed comment said many small runs beat a few large lexsorts, and this PR's numbers contradict it, so the right size is an open question.
  • Nit: the run rule is described in four places (the SortExec docs, the in_mem_sort_stream docs, the comment before the call, and the coalesce_in_mem_batches_into_runs docs). Keeping it on coalesce_in_mem_batches_into_runs would be enough.
  • Nit: in sort_spill_reservation, is with_target_partitions(1) needed? The scenario already has one partition and the plan is asserted.
  • Nit: coalesce_small_runs_preserves_rows_and_full_batches is missing the test_ prefix that the surrounding tests use.

let target = self.sort_in_place_threshold_bytes.max(1);
let mut runs: Vec<RecordBatch> = Vec::new();
let mut group: Vec<RecordBatch> = Vec::new();
let mut group: Vec<&RecordBatch> = Vec::new();

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.

group now borrows from batches so that the whole input can be returned on failure. That keeps every original batch alive until the last group has been copied. On main, each group's originals were dropped right after its concat_batches, so the transient peak was one group, not the whole buffer.

The copies are also not reserved before they are made. On the spill path the pool is full by construction. Any growth in footprint (for example the aliased columns in test_small_run_coalescing_falls_back_when_memory_is_full) is only caught by the final try_resize. By then the whole buffer has been copied, and all of it is thrown away.

Owning the group and falling back per group bounds both:

let flush = |group: &mut Vec<RecordBatch>, runs: &mut Vec<RecordBatch>| {
    if group.len() > 1
        && let Ok(run) = concat_batches(&schema, group.iter())
    {
        group.clear();
        runs.push(run);
    } else {
        runs.append(group);
    }
};

If the group's copy is reserved with try_grow before concatenating, and the group is kept as is when that fails, the final realign becomes a shrink. This also removes both return Ok(batches) paths and the pop().cloned().unwrap().

if group_rows >= self.batch_size
&& (!coalesce_large_runs || group_bytes.saturating_add(bytes) > target)
{
if flush(&mut group, &mut runs, &self.schema).is_err() {

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.

Two points on this fallback:

  1. is_err() swallows every error, not only the offset and dictionary-key limits named in the comment above. Matching the Arrow overflow variants (DictionaryKeyOverflowError, OffsetOverflowError) would keep real failures visible.
  2. The in-place path in in_mem_sort_stream still calls concat_batches on all buffered batches. With the default sort_in_place_threshold_bytes (1 MiB), the input from test_small_run_coalescing_falls_back_on_dictionary_overflow takes that path and fails with Arrow error: Dictionary key bigger than the key type. I checked this on main, and this PR doesn't change that path. The test only passes because it sets the threshold to 0. Either fall through to the run path when that concat fails, or narrow the dictionary claim in the description and leave the rest as a follow-up.

Comment on lines +906 to +907
if group_rows >= self.batch_size
&& (!coalesce_large_runs || group_bytes.saturating_add(bytes) > target)

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.

A full batch that follows a short group gets absorbed and copied. With batch_size = 4, [2] [4] becomes one 6-row run, built by copying the 4-row batch. Outside large-run mode, pushing batches with num_rows() >= batch_size straight to runs avoids that copy. Run order doesn't matter, since equal keys have no ordering guarantee.

With that change, the all(..) pre-scan at the top is no longer needed either. An all-full input copies nothing, so a copied flag that skips the realign pass at the end covers the same fast path in one traversal.

adriangb pushed a commit to adriangb/datafusion that referenced this pull request Nov 12, 2026
… limit (apache#26953)

## Which issue does this PR close?

- Closes apache#26940.
- Closes apache#16979.

## Rationale for this change

A sort under a memory limit can fail with `ResourcesExhausted` for
`ExternalSorterMerge` instead of spilling when its merge uses the row
format, as with several sort keys. The merge holds the row-format keys
of every sorted run at once. When the keys are most of each row, that
needs more than the 2x the sort reserves for each buffered batch. The
issue has the details and a reproducer.

## What changes are included in this PR?

With a memory limit, `ExternalSorter` sorts a run that feeds a merge in
the row format into batches of an eighth of `batch_size`, but at least
1024 rows, when the run's encoded keys may be larger than the run, so
the merge holds the keys of one such batch per run. Smaller keys fit
next to the sorted copy, since the sort reserves about twice the run.
That covers several keys and a single key without a specialized cursor,
such as `FixedSizeBinary`. A computed key is sized from its type if that
has a fixed width, and otherwise counts as larger. Split runs compact
their view arrays, so each batch is charged only for its own strings.
Runs whose views share bytes, as dictionary-encoded Parquet strings do,
keep `batch_size`, because compacting them would copy the shared bytes
once per row. Spilling also compacts view arrays with several data
buffers, since each buffer read back from the spill is a slice of one
IPC message and is charged for all of it. The merge output keeps
`batch_size`, and sorts without a memory limit are unchanged. This adds
a `batch_size` parameter to `sort_run`.

## What is the testing strategy for this PR?

`test_merge_keys_larger_than_batches` (current-thread and multi-thread)
sorts in a pool that holds 16 buffered batches, or 8 for the clustered
keys. Two `Int32` keys and a single `FixedSizeBinary(16)` key fail on
`main` with `ResourcesExhausted` and pass here. Two `Int32` keys with a
`Utf8View`, a dictionary or a dictionary-backed `Utf8View` payload check
that splitting does not charge each batch for the shared buffers, and
two clustered `Utf8View` keys check the spilled buffers.
`test_merge_run_batch_size_follows_key_size` checks that only runs whose
keys outgrow them are split. The reproducer from the issue and two
Parquet sorts from review, with `datafusion-cli`:

| | `main` | This PR |
|---|---|---|
| 30M rows, 1 partition, `-m 512M` | fails | 2 spills |
| 30M rows, default partitions, `-m 1G` | fails | 16 spills |
| 28M rows, 1 partition, `-m 1G` | fails | no spill |
| dictionary-encoded strings, 1 partition, `-m 256M` | 3 spills | 3
spills |
| clustered unique strings, 1 partition, `-m 32M` | 507 spills | 507
spills |

With `dfbench`, `sort-tpch` Q5 from apache#16979 (SF1, 1 partition,
`--memory-limit 100M`) fails on `main` and passes here. With the same
settings in `datafusion-cli`, it also fails on `main` at 50M and 200M
and passes here with 6 and 2 spills.

The criterion `sort` benchmark has no memory limit and is unchanged.
With the split forced on at `batch_size = 8192`, its multi-column
in-memory cases took 2 to 12% longer, which is the cost under a memory
limit. No-spill Parquet sorts at `-m 16G` with integer or computed
integer keys and a `Utf8View` payload are within 4% of `main` in time
and peak RSS.

## Are there any user-facing changes?

No. Known limitations: runs of at most 1024 rows are not split. That
covers sorts with a `batch_size` of 1024 or less, and small input
batches, which apache#26931 would coalesce into larger runs. Sorts with
dictionary, list view or nested view columns, and runs whose views share
bytes, are not split, since each batch would be charged for the shared
buffers. The key size counts the bytes of variable-length keys without
the row format's padding, so string keys that only slightly outgrow
their run are not split. Runs sorted by a computed key without a fixed
width, such as a string expression, are split under a memory limit even
when their keys fit. Each run's existing one-batch read-ahead now holds
one of the smaller batches with its reservation already released.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Core DataFusion crate physical-plan Changes to the physical-plan crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants