jayzhan211 opened a new pull request, #26015:
URL: https://github.com/apache/datafusion/pull/26015
## Which issue does this PR close?
- Part of #24768. Related to #1599 and #12952.
This is an interim, sort-based path, not the hybrid hash join #24768
proposes.
Supersedes #25217, split into smaller PRs for review: this is the core, and
the
dynamic-filter bounds and the `hash_join_max_build_size` option follow
separately (linked below). The review comment there about probe-side ordering
is handled here (the fallback is declined when the join advertises an
ordering;
covered by `sort_merge_fallback_honors_a_promised_probe_ordering`).
## Rationale for this change
A partitioned hash join whose build side does not fit in the memory pool
fails
the query with `Resources exhausted`, even though the same join completes as
a
sort-merge join, because sorts spill. Today the only workaround is
`prefer_hash_join = false`, which pays for a sort on every join, including
the
ones that fit.
With this PR the choice is made at runtime, per partition: a join stays a
hash
join while its build side fits, and only a partition that runs out of memory
sorts both inputs (spilling) and finishes as a sort-merge join.
Reproducer in `datafusion-cli` (12-core laptop, `release-nonlto` builds of
this
PR and of its base commit, median of 5-10 interleaved runs):
```sql
-- datafusion-cli --mem-pool-type fair -m 256m
SET datafusion.execution.target_partitions = 4;
SELECT count(*), sum(l.v), sum(r.w)
FROM (SELECT value AS k, value AS v FROM generate_series(1, 20000000)) l
JOIN (SELECT value AS k, value * 2 AS w FROM generate_series(1, 20000000)) r
ON l.k = r.k;
```
| | main | this PR |
|---|---|---|
| `--mem-pool-type fair -m 256m` | `Resources exhausted ...
HashJoinInput[3]` (8/8 runs) | 0.50 s (8/8 runs),
`sort_merge_fallback_count=4`, 621 MB spilled |
| same, `prefer_hash_join = false` (today's workaround) | 0.30 s | 0.30 s |
| no memory limit | 0.144 s | 0.143 s |
Main also fails at `-m 768m`. With the default greedy pool this PR completes
4-5 runs out of 6 at 256-512 MB: the failures are in a sort's merge phase,
which the greedy pool cannot make sibling sorters spill for, as with any
multi-partition sort; 768 MB completes 6/6.
## What changes are included in this PR?
- `HashJoinExec` (`PartitionMode::Partitioned` only): when the join's own
reservation fails with `ResourcesExhausted`, either while collecting build
batches or while building the hash table over batches that fit, the
partition
sorts its build side with `ExternalSorter`, sorts its probe side, and
finishes
through the existing sort-merge join stream (`sort_merge_join_stream`,
already
shared since #25490). New module `hash_join/sort_merge_fallback.rs`, new
stream
state `SortMergeFallback`.
- The table-building half of `collect_left_input` moves into
`build_in_memory`
(body unchanged) so the build rows are still available when it fails.
- New metric `sort_merge_fallback_count`; the fallback's spills are reported
in
the join's spill metrics.
- A partition that falls back reports itself to the dynamic filter like a
canceled partition: its probe rows are not filtered (no hash table to test
against). Keeping bounds pruning for it is a follow-up.
- `ExternalSorter` and a few of its methods become `pub(crate)`. No public
API
change.
The fallback is declined (today's behavior is kept) when: disk spilling is
disabled, the join is `CollectLeft`, it is a null-aware `NOT IN` join
(#25546),
it advertises a probe-side output ordering (a merge emits key order), or a
key
type is not supported by the row format.
Known limitation: each fallback sort pre-reserves
`sort_spill_reservation_bytes`
(10 MB default) and a falling-back partition runs two, so a budget smaller
than
that floor still fails, now inside the sort. See the module docs.
Follow-ups (stacked on this PR, each small):
- #TODO keep bounds pruning for fallen-back partitions
(`PushdownStrategy::Unknown`)
- #TODO `datafusion.execution.hash_join_max_build_size`, a trigger that works
without a memory limit
## What is the testing strategy for this PR?
- `hash_join/exec.rs`: all ten join types, with and without a join filter, at
two budgets: one that runs out while collecting the build batches and one
that
only runs out building the hash table (each verified by disabling the other
trigger). Each asserts the same rows as the in-memory join, that it fell
back,
and that it spilled. Plus projection and `fetch`, the dynamic filter
staying
permissive, and the probe-ordering gate.
- `core/tests/memory_limit/hash_join_sort_merge_fallback.rs`: `config_matrix`
runs one SQL join under four configurations (no limit, limit with spilling,
limit without spilling, large limit) and checks both the answer and which
algorithm ran.
- Two existing tests that expect a memory error now disable the disk manager.
## Are there any user-facing changes?
Queries that failed with `Resources exhausted` in a partitioned hash join now
complete when disk spilling is enabled and the budget covers the sorts.
Nothing
changes for joins that fit in memory. No new configuration options.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]