Repository navigation
Conversation
Signed-off-by: Stefan Wang <1fannnw@gmail.com>
Signed-off-by: Stefan Wang <1fannnw@gmail.com>
Signed-off-by: Stefan Wang <1fannnw@gmail.com>
Both sides used the same name for their synthetic row number column, so the sides were always requalified as `left` and `right`, and the final projection rebuilt every output column as `left.<name>`. Queries that referred to the original qualifier after the set operation failed, and two columns with the same name from different relations on one side collapsed into duplicate `left.x` outputs. Give each side its own row number name so requalification only happens when the user's columns conflict, join on the columns the requalified schemas actually contain, and alias the output back to the left input's qualifiers and names. Signed-off-by: Stefan Wang <1fannnw@gmail.com>
Document that `LogicalPlanBuilder::intersect` and `except` with `is_all = true` do not preserve duplicate counts and point callers at `intersect_all` and `except_all`. Name the operation in the planning error raised when `row_number` is not registered, and add an upgrade guide entry for custom context providers and session states. The Substrait `intersect_rel` helper is now only used for distinct intersections, so drop its `is_all` parameter. Signed-off-by: Stefan Wang <1fannnw@gmail.com>
Add sqllogictest cases for min and max duplicate counts with NULLs, rows that are NULL in only some columns, INTERSECT ALL binding tighter than EXCEPT ALL, and the two qualifier regressions. Record the +0.0 / -0.0 case as a known issue: window partitioning puts the two values in separate partitions, so the counts are wrong until apache#25766 is fixed. Signed-off-by: Stefan Wang <1fannnw@gmail.com>
Count each distinct row on both sides, join the counts, and repeat each row min(m, n) or m - n times with unnest(range(..)). This replaces the per-row row_number window, which was slow when most rows are distinct. Grouping also treats 0.0 and -0.0 as equal, so those counts are now right. Signed-off-by: Stefan Wang <1fannnw@gmail.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #25767 +/- ##
========================================
Coverage 82.84% 82.85%
========================================
Files 1151 1151
Lines 455961 456290 +329
Branches 455961 456290 +329
========================================
+ Hits 377760 378064 +304
+ Misses 55245 55240 -5
- Partials 22956 22986 +30 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Signed-off-by: 1fanwang <1fannnw@gmail.com>
Signed-off-by: 1fanwang <1fannnw@gmail.com>
Signed-off-by: 1fanwang <1fannnw@gmail.com>
Signed-off-by: 1fanwang <1fannnw@gmail.com>
Signed-off-by: 1fanwang <1fannnw@gmail.com>
kosiew
left a comment
There was a problem hiding this comment.
Thanks for working on this. The grouped-count approach looks promising.
I found one correctness issue with mixed-type join keys that needs to be addressed before approval. I've also included two non-blocking suggestions about Substrait export compatibility and regression coverage.
| )? | ||
| .build() | ||
| }; | ||
| let left_plan = count_rows(left_plan, &left_columns, &left_count)?; |
There was a problem hiding this comment.
Both inputs are grouped before join-key coercion, so distinct values can become equal after casting and their counts are processed separately. For example, (VALUES (1), (1)) EXCEPT ALL (VALUES ('1'), ('01')) returns two rows instead of zero, and INTERSECT ALL can similarly overproduce rows. Could we coerce both inputs to their common comparison types before grouping, as UNION does, and add mixed-type regression tests for SQL and another API? This may change the left output type, so that behavior should be handled explicitly.
There was a problem hiding this comment.
Done in bc0acb6. Both sides are now cast to one common type before grouping.
| // Uses **LeftSemi** join (returns rows from left that exist in right) | ||
| // The requalification ensures no duplicate field name errors | ||
| assert_expected_plan( | ||
| assert_unnest_not_supported( |
There was a problem hiding this comment.
The new Unnest-based plans cannot be exported to Substrait because the producer does not support LogicalPlan::Unnest. The previous export produced incorrect multiplicities, so returning an error is preferable, but the upgrade guide does not mention this compatibility change. Could we document the limitation in the 56.0.0 upgrade guide and link a follow-up issue for proper Substrait serialization support?
|
|
||
| # INTERSECT ALL keeps min(m, n) copies of each row and EXCEPT ALL keeps | ||
| # max(m - n, 0), with NULLs compared as equal. | ||
| statement ok |
There was a problem hiding this comment.
Could we add tests for an empty left input, an empty right input, and both inputs empty for INTERSECT ALL and EXCEPT ALL? In particular, EXCEPT ALL with an empty right input should retain duplicate left rows. The current behavior appears correct, but these tests would protect the new aggregation, outer-join, and expansion paths against regressions.
There was a problem hiding this comment.
Done in 7bed929. Empty left, empty right and both empty are now covered.
…pared in The inputs were grouped before the join cast mismatched columns to a common type, so values that are equal only after the cast, like the strings '1' and '01' and the integer 1, were matched count by count instead of added. Cast mismatched columns to the type = compares them in before grouping. The result takes that type instead of the left type. Signed-off-by: 1fanwang <1fannnw@gmail.com>
Cover an empty left input, an empty right input and two empty inputs, written as an empty table and as a filter that removes every row. EXCEPT ALL with an empty right input keeps every left row, duplicates and NULLs included. Signed-off-by: 1fanwang <1fannnw@gmail.com>
… to Substrait The Substrait producer cannot serialize Unnest, which these plans now contain. Document the failure in the 56.0.0 upgrade guide and link apache#26372 for supporting it. Signed-off-by: 1fanwang <1fannnw@gmail.com>
Which issue does this PR close?
Related to #12955 and #25766. Depends on #25742.
Rationale for this change
INTERSECT ALL and EXCEPT ALL are about 20x slower than INTERSECT on mostly distinct rows with the duplicate-count fix, as kita-renji measured in the parent review. Grouped counts avoid the per-row window partitions.
Grouping each side before the join also miscounted rows when the input column types differ. The join compares groups in a common type, where distinct groups of one side can be equal, such as the strings '1' and '01' against the integer 1. EXCEPT ALL of the integers 1, 1 and the strings '1', '01' returned two rows instead of none.
What changes are included in this PR?
Each side groups equal rows and counts them. A NULL-safe join matches the counts, and range plus unnest emits min(m, n) copies for INTERSECT ALL or max(m - n, 0) for EXCEPT ALL. Grouping also treats signed zero as equal.
Columns whose types differ are first cast to the type equality compares them in, so values equal there count as one row, and the result has that type. Columns of the same type are untouched, and a timestamp with a time zone against one without is left to type coercion, which knows the session time zone.
What is the testing strategy for this PR?
SQL and DataFrame regressions cover duplicate counts, NULLs, column names and qualifiers. The intersection.slt and negative_zero.slt cases cover SQL execution, signed zero and empty inputs. Mixed-type inputs run through SQL and the DataFrame API in both operand orders, checked against the same operation on inputs cast to the common type first.
Testing Done
The mixed-type queries ran with datafusion-cli, dev profile:
Reproducer source: mixed.sql
The correct results are no rows, one row, one row and one row. The strings '1' and '01' are both the integer 1, and 9007199254740992 and 9007199254740993 are both 9007199254740992.0 as doubles. The output is:
The benchmark below was measured before this change. Its queries use inputs of the same type, which are planned as before, and a builder unit test pins that equal types get no extra projection. Casting mismatched inputs adds one projection below the aggregate of each side that needs a cast, and I did not benchmark that cost.
The benchmark ran with datafusion-cli, release-nonlto, and 18 cores:
Reproducer source: bench.sql
Row counts match. EXPLAIN ANALYZE on 84ef8f7:
With this PR:
Signed zero:
Are there any user-facing changes?
INTERSECT ALL and EXCEPT ALL run faster. Custom sessions must register count and range, as documented in the upgrade guide. The unreleased LogicalPlanBuilder helpers take these functions instead of row_number.
With different input column types, the result has the type equality compares them in, not the left type, as UNION ALL does. INTERSECT and EXCEPT without ALL still return the left type.
Substrait consumption still works, but exporting these plans now fails with "Unsupported plan type: Unnest", as the self_referential_*_all tests assert. #26372 tracks supporting it.