jayzhan211 opened a new pull request, #26853:
URL: https://github.com/apache/datafusion/pull/26853

   ## Which issue does this PR close?
   
   - None. This is a refactor of how `EquivalenceProperties` checks sort 
requirements; it fixes no reported bug.
   
   ## Rationale for this change
   
   The planner removes a sort when it can prove that its input is already 
sorted the required way. For a
   requirement like `[a, b]`, it first checks `a`. Then, since `b` only has to 
be sorted among rows that tie on `a`,
   it treats `a` as a constant: it clones the `EquivalenceProperties`, adds `a` 
with `add_constants`, and checks `b`
   against the clone.
   
   This has three costs:
   
   - **Planning time.** Every check with more than one key clones the 
properties. Checks with four keys are 2-3x
     faster without the clone (benchmarks below).
   - **Plans depend on hash map order.** The clone finds orderings that follow 
from the satisfied key through a side
     effect of `add_constants` (it strips constants from the stored orderings 
and re-runs ordering discovery once,
     over the leading expressions in hash map order). For example, with 
ordering `[k, x, y, z]`,
     `x = CAST(y AS BIGINT)` and `y = CAST(z AS INT)`, the requirement `[k, z]` 
holds. `main` proves it in about half
     of the runs when a second ordering `[y, w]` exists, and never without it, 
so the same query can plan with or
     without a sort.
   - **The trick is written three times.** `ordering_satisfy_requirement` and 
`common_sort_prefix_length` are two
     copies of the same loop, and `find_longest_permutation` does its own clone 
plus `add_constants`. A "constant"
     that only means "satisfied so far" is hard to tell apart from a real 
constant when reading the code.
   
   ## What changes are included in this PR?
   
   - First commit: benchmark cases in 
`datafusion/physical-expr/benches/equivalence_properties.rs` for
     `find_longest_permutation`, window-shaped orderings and a deeply nested 
sort key, so they can run on `main` too.
   - New `properties/walk.rs` with `Walk`: one pass along the keys that *pins* 
each satisfied key (and its
     equivalence class), and keeps, for each ordering, its *head*: the first 
expression that is not fixed (pinned,
     constant, or computed only from fixed expressions). After each pin, the 
walk adds the orderings that the new
     heads imply, until none is new.
   - `ordering_satisfy_requirement` and `common_sort_prefix_length` both call 
`Walk::sorted_prefix_len`.
     `find_longest_permutation` pins found expressions in a `Walk` instead of 
cloning and adding constants (and skips
     the pins after its last round, which nothing reads). `get_expr_properties` 
reads through a `Walk` with nothing
     pinned.
   - `sorted_prefix_len` accepts a key if any head is that key in the required 
direction. `main` only looked at the
     first ordering the key heads, in its stored order (see the plan 
differences).
   - Removed: `add_satisfied_key_constants` and the free function 
`update_properties` (now a method of `Walk`).
   - The body of `discover_new_orderings` is now `implied_ordering`, shared 
with the walk, with a doc that says why the
     implied ordering holds. The body of 
`OrderingEquivalenceClass::is_expr_partial_const` is now `is_partial_const`,
     shared with the walk.
   - `OrderingEquivalenceClass::get_options` and `is_expr_partial_const` are no 
longer called inside DataFusion; they
     stay as public API.
   - Last commit: `is_partial_const` now says what it checks, an expression 
leading one ordering in some direction
     and another in the exact reverse, instead of removing `(descending, 
nulls_first)` tuples from two hash sets.
     Its doc says why this exists: a whole-partition window aggregate, such as 
`SUM(x) OVER (PARTITION BY k)`,
     records that it is constant per `k` by adding `[k, w]` in all four 
directions of `w`.
   
   The `EquivalenceProperties` the walk reads never changes during a check.
   
   ### Plan differences
   
   No plan changes in the existing tests (see testing). On hand-built inputs, 
this PR proves an ordering where
   `main` does not:
   
   | Input | `main` | This PR |
   |---|---|---|
   | The chain of casts above | satisfied in some runs (hash map order) | 
satisfied |
   | Column `x` is the constant `5`, ordering `[k, b % 5]`, 
`find_longest_permutation([5, k, b % 5])` (e.g. `GROUP BY 5, k, b % 5`) | `5, 
k`: pinning the literal `5` adds it to `x`'s class, so `b % 5` normalizes to `b 
% x` and no longer matches | `5, k, b % 5` |
   | `a` leads orderings in two directions that are not opposite, such as `a 
DESC NULLS FIRST` declared before `a ASC NULLS FIRST`, from the start or after 
a satisfied `k`; requirement `[a ASC NULLS FIRST]` or `[k, a ASC NULLS FIRST]` 
| depends on the order it stores orderings in (4 of the 6 declaration orders in 
one example) | satisfied |
   
   Each extra ordering really holds, so these can only remove sorts.
   
   One case is worse: with orderings like the last row's, a requirement on an 
expression *computed from* `a`, such as
   `[k, CAST(a AS BIGINT) ASC NULLS FIRST]`. Both versions give `a` one 
direction, from whichever ordering comes
   first: `main` in its stored order, this PR in declaration order. In 2 of the 
6 declaration orders of one example
   `main` proves it and this PR does not, so a sort is kept. The reverse never 
happens.
   
   ## What is the testing strategy for this PR?
   
   - New unit tests in `walk.rs`: an ordering found only after pinning 
(`ordering_found_after_pinning`), the chain of
     casts above (`chained_orderings_found_after_pinning`, fails on `main`), 
and the two-direction case
     (`any_head_in_required_direction`). New unit test 
`test_is_expr_partial_const` in `ordering.rs`; before it, no
     test failed when `is_partial_const` always returned `false`.
   - All existing tests pass unchanged.
   - Checked locally, not part of this PR:
     - Side by side: with the old algorithms kept next to the walk, every call 
to the four entry points ran both and
       logged any difference. The only difference was the chain-of-casts unit 
test above, over the
       `datafusion-physical-expr`, `datafusion-physical-plan` and 
`datafusion-physical-optimizer` unit tests,
       `core_integration`, all sqllogictests, and the equivalence fuzz tests 
(`--features extended_tests`).
     - Against data: a randomized test built small sorted tables, declared only 
properties that hold on them
       (orderings, cast equalities, constants, window-style orderings), and 
checked every answer of the four entry
       points against the rows. Over 9.6 million cases, neither version claimed 
an ordering the rows do not have, and
       both returned the same errors. The cases where `main` and this PR differ 
are the ones listed above.
     - The old and new `is_partial_const` agree on every sequence of up to 6 
sort directions over two expressions
       (299,593 sequences).
   
   Benchmarks: `cargo bench -p datafusion-physical-expr --bench 
equivalence_properties` with the first commit's cases on
   both sides, `main` vs this PR, 5 alternating rounds, medians, time of this 
PR / time of `main` (below 1 is faster).
   Columns are the number of equivalence classes.
   
   | Check | 2 | 8 | 32 |
   |---|---|---|---|
   | `ordering_satisfy`, 1 key | 0.81 | 0.82 | 0.81 |
   | `ordering_satisfy`, 1 key, unsatisfied | 0.83 | 0.84 | 0.83 |
   | `ordering_satisfy`, 4 keys | 0.37 | 0.35 | 0.29 |
   | `ordering_satisfy_requirement`, 1 key | 0.77 | 0.76 | 0.76 |
   | `ordering_satisfy_requirement`, 4 keys | 0.37 | 0.35 | 0.30 |
   | `extract_common_sort_prefix`, 4 keys | 0.38 | 0.36 | 0.30 |
   
   Other shapes:
   
   | Case | Ratio |
   |---|---|
   | `find_longest_permutation`, 1 expression (2 / 32 classes) | 0.13 / 0.07 |
   | `find_longest_permutation`, 5 expressions, one found per round (2 / 32 
classes) | 0.44 / 0.38 |
   | `W` window aggregates over `PARTITION BY k`, i.e. `[k, w_i]` in four 
directions per window; requirement `[k, w_1]` (`W` = 4 / 16 / 32) | 0.20 / 0.11 
/ 0.08 |
   | Requirement `[c0, e, c1]` with `e = c1 + 1 + ... + 1` nested `d` deep (`d` 
= 4 / 16 / 32) | 0.81 / 1.20 / 1.41 |
   
   The last row is the one shape that is slower. For every node of `e`, both 
versions check whether it is constant by
   looking each sub-expression up in a hash map, which hashes the whole 
sub-expression, so the cost grows with the
   cube of the depth. This PR also looks each one up in its set of pinned 
expressions, which `main` does not have.
   Sort keys nested this deep are rare: at depth 4 this PR is faster.
   
   ## Are there any user-facing changes?
   
   No public API change. Plans differ only in the cases listed above.
   


-- 
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]

Reply via email to