hhr293 commented on PR #58424: URL: https://github.com/apache/spark/pull/58424#issuecomment-5568013778
> **I think this optimization is worth having in Spark: it recognizes when an expensive join is only being used to answer a simple yes/no question, and replaces it with a much cheaper calculation.** The technical idea is sound. The main tradeoff is maintaining substantial correctness machinery for a fairly narrow set of queries. > > I rechecked the PR: its head is still `15aa88d1`, the commit we reviewed. Our review found no actionable correctness defect. It remains a draft, and the author has announced additional fixes and test improvements before marking it ready. [Author’s update](https://github.com/apache/spark/pull/58424#issuecomment-5543195034) > > Consider the question: **“Which orders contain items from more than one warehouse?”** > > One way to answer it is to compare the sales table with itself. For each order, find two rows whose warehouses differ: > > ```sql > SELECT a.order_id > FROM sales a > JOIN sales b > ON a.order_id = b.order_id > AND a.warehouse_id <> b.warehouse_id > ``` > > If one order contains 50 rows from warehouse A and 50 from warehouse B, that join has **5,000 matching ordered pairs**. But when this result is used inside `IN (...)`, all those pairs communicate the same fact: that order qualifies. > > The optimization instead calculates: > > ```sql > SELECT order_id > FROM sales > WHERE order_id IS NOT NULL > GROUP BY order_id > HAVING COUNT(DISTINCT warehouse_id) > 1 > ``` > > It processes the group’s 100 input rows and its distinct warehouse values without generating those pairs. That is a meaningful algorithmic improvement. > > The restriction to membership is essential. If the surrounding query actually needs the pairs, their counts, or columns from both rows, this replacement would change the answer. It also must preserve duplicate rows in the query **outside** the `IN`. > > The implementation handles this carefully: > > 1. **Find a supported membership subquery.** It runs before Spark converts `IN` into join operators, while that context remains explicit. It supports a direct self-join and a self-join nested inside another inner join. > 2. **Prove that the two inputs are interchangeable.** Matching table names is insufficient. The rule checks equivalent plans, corresponding column positions, deterministic expressions, and supported sources and operators. > 3. **Build the aggregate while preserving SQL semantics.** Null grouping keys are removed because ordinary equality joins never match them. `COUNT(DISTINCT)` already ignores null warehouse values, matching the behavior of `<>`. > 4. **Repair column references.** Removing one side of a join changes which columns exist. The rule rebuilds projections and updates Spark’s internal column identities, including references from the surrounding join. > > Most of the implementation’s complexity comes from proving and preserving these conditions. The actual aggregate construction is small. [Implementation](https://github.com/apache/spark/blob/15aa88d1f7567c2219aeacf0c0e2e194f167f947/sql/core/src/main/scala/org/apache/spark/sql/execution/RewriteSelfJoinInequalityToAggregate.scala) > > The important tradeoffs are: > > Choice Benefit Cost or limitation > Replace pair generation with aggregation Avoids potentially quadratic join work and removes one copy of the self-join input Distinct aggregation can still shuffle, consume memory, and spill > Restrict the rewrite to membership queries Makes collapsing duplicate pairs valid Excludes many ordinary inequality joins > Explicit source, operator, expression, and type allowlists Unproven cases keep their existing plans Many potentially valid queries receive no benefit; the allowlists need maintenance > Preserve surrounding joins and projections Covers more useful queries, including the targeted Q95 shapes Requires considerable column-reference bookkeeping > Disable the rule by default Allows controlled evaluation Merging the PR alone gives existing users no automatic speedup > One further performance consideration: **`COUNT(DISTINCT)` calculates more information than the predicate needs.** Once two different values exist, the answer is already yes, but the implementation uses Spark’s general distinct-count machinery. A future implementation could investigate a bounded “two distinct values” aggregate, or an extrema-based formulation where ordering and equality semantics permit it. That is an improvement opportunity; the current approach benefits from reusing established execution operators. > > The reported performance result is encouraging: the author measured **about 46% lower runtime, or 1.87× speedup, for TPC-DS Q95**, using vanilla JVM Spark, SF300, `local[12]`, and the median of three measured runs after warm-up. Results were checked for equality. This supports substantial benefit for the target query, although I did not independently reproduce that benchmark. [Benchmark details](https://github.com/apache/spark/pull/58424) > > For a stronger performance case, I would want measurements across different group sizes and distinct-value counts, including small groups where aggregation’s overhead matters. Shuffle volume, spill, and intermediate row counts would help explain the gain and its limits. The current evidence supports a targeted optimization; it does not establish a general workload speedup. > > The correctness evidence is reasonably strong for this draft: > > * Our earlier review covered five independent scopes and found no actionable correctness defect. > * The new suite’s **31 tests passed in CI** at the reviewed commit. [CI run](https://github.com/hhr293/spark/actions/runs/33352322392) > * Those 31 tests also passed in our adapted local setup, and additional checks supported preservation of tuple membership and duplicate outer rows. > * The local setup used cached Spark classes with compatibility adaptations. We did **not** complete a clean native build of the PR. > > There are still worthwhile refinements. Several test helpers compare sets, which can conceal duplicate-count regressions; those assertions should preserve multiplicity. Comments promising broad ANSI error parity also need care: our probing found an evaluation difference consistent with existing filter-pushdown behavior, rather than a demonstrated query-result defect. > > The announced Gluten follow-ups include preserving alias metadata and adding focused tests that prevent accidental changes to the number and ordering of subquery output columns. Those are sensible improvements to incorporate before reviewing the final Spark commit. [Metadata review](https://github.com/apache/gluten/pull/12756#discussion_r3930589969), [output-shape tests](https://github.com/apache/gluten/pull/12756#discussion_r3930589952) > > The source restriction limits applicability: the implementation accepts the exact stock Parquet file-format class. Normal Delta scans use a different class and are therefore excluded—even when their SQL has the right shape. Supporting Delta would require a separate correctness assessment; enabling this flag does not make Delta scans eligible. [Source restriction](https://github.com/apache/spark/blob/15aa88d1f7567c2219aeacf0c0e2e194f167f947/sql/core/src/main/scala/org/apache/spark/sql/execution/RewriteSelfJoinInequalityToAggregate.scala#L659-L685) > > My recommendation is to **support the upstream direction, retain the opt-in default, and review the author’s remaining changes before treating the draft as finished**. I would prioritize stronger multiplicity tests and reproducible performance evidence over expanding the rule’s scope immediately. > > **Why it deserves a place in Spark:** a query pattern can be narrow yet disproportionately expensive. This rule lets Spark recognize the underlying question, avoid unnecessary pair generation, and improve existing SQL without requiring every application author to discover and implement the rewrite themselves. The reported Q95 improvement shows that this can have meaningful value. Its strongest justification is the size of the avoidable work when it applies; benefit across diverse workloads remains to be demonstrated. Thanks @sunchao for the thorough review. I agree with the main points and will incorporate them before marking the PR ready: Strengthen tests to preserve row multiplicity and port the output-shape / metadata follow-ups from apache/gluten#12756. Add broader performance coverage beyond Q95, especially smaller groups where aggregation overhead may dominate. Narrow the ANSI/error-parity comments to what the tests actually guarantee. Keep the current stock-Parquet source boundary explicit; Delta and other sources can be considered separately. Keep COUNT(DISTINCT) for this PR and treat a bounded ">= 2 distinct" aggregate as a possible follow-up. I'll keep the rule opt-in for now, update the tests/docs/performance validation, and ping you and @LuciferYang once the next revision is ready. -- 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]
