morningman opened a new pull request, #66435:
URL: https://github.com/apache/doris/pull/66435

   ### What problem does this PR solve?
   
   Issue Number: close #xxx
   
   Related PR: #xxx
   
   Problem Summary:
   
   An Iceberg `MERGE INTO` that is semantically equivalent to a 14s `UPDATE` 
ran for **18 minutes** on TPC-DS 10TB. `explain verbose` showed the target-side 
scan reading the whole table with **no predicates and no runtime filters**, 
caused by two planner defects:
   
   **1. A mark join blocks runtime filters (general optimizer issue, not 
MERGE-specific).**
   
   When an `IN`/`EXISTS` subquery is written inside a join `ON` clause, 
subquery unnesting produces a **mark join**. Predicate push-down later moves 
the mark conjunct down to the target branch as a bare filter conjunct — but the 
join itself stays a mark join, and `RuntimeFilterGenerator` refuses to generate 
runtime filters on mark joins (`RuntimeFilterGenerator.java`, the 
`isMarkJoin()` check). The decisive evidence: the `UPDATE` plan and the `MERGE` 
plan contain the *same* `LEFT SEMI JOIN(BROADCAST)` on `ss_sold_date_sk = 
d_date_sk` over the same tables — the only difference is `isMarkJoin: false` vs 
`true`, and only the former produces RFs, so only the `UPDATE` prunes the 
target scan from ~6 years of data down to 1 year.
   
   The same condition written in `WHERE` is fine; written in `ON` it degrades — 
plain `SELECT`s are affected too.
   
   **2. Both MERGE paths build `source LEFT OUTER JOIN target`.**
   
   Doris builds the hash table on the right child, so the structurally wide 
side — the target must carry *all* columns plus the row identity (for Iceberg: 
`struct<file_path, row_position, ...>` with a full S3 URI per row) — always 
became the build side. Worse, `LEFT_OUTER_JOIN` is in `DENIED_JOIN_TYPES`, so 
the merge join could never produce runtime filters at all. The internal OLAP 
path (`MergeIntoCommand`) used `LEFT_OUTER_JOIN` unconditionally, even without 
`WHEN NOT MATCHED` clauses (the external path already had the INNER 
optimization). For reference, Trino plans MERGE as `target RIGHT JOIN source` 
and only allows dynamic filters on `INNER || RIGHT` — exactly complementary to 
Doris's denied list.
   
   **Changes:**
   
   1. **Add the `ELIMINATE_MARK_JOIN` rewrite rule** (the `RuleType` enum entry 
existed but was never implemented). When a `LEFT SEMI` mark join's mark slot is 
consumed only as a bare conjunct of the filter directly above it, requiring 
`TRUE` discards `NULL` exactly like `FALSE`, so the pair degenerates to a plain 
`LEFT SEMI JOIN` — which may produce runtime filters. A literal-`TRUE` alias 
keeps the mark slot's `ExprId` alive for any upper references (after the filter 
the mark can only be `TRUE`); column pruning removes it when unused. Registered 
in `PUSH_DOWN_FILTERS` next to `EliminateOuterJoin`; can be disabled with `set 
disable_nereids_rules='ELIMINATE_MARK_JOIN'`. Out of scope by design: mark 
slots consumed inside `OR`/projections, and `NOT IN` (anti mark joins).
   
   2. **Extract `MergeUtils.buildMergeJoin()`**, shared by `MergeIntoCommand` 
(internal OLAP MOW) and `ExternalRowLevelMergePlanBuilder` (external): the 
target now stays on the **left (probe)** side. Without `WHEN NOT MATCHED` the 
join is `INNER`; with them it is `target RIGHT OUTER JOIN source`, which 
preserves exactly the same unmatched source rows as the previous shape while 
allowing runtime filters built from the (narrow) source side to prune the 
target scan — `RIGHT_OUTER_JOIN` is not in `DENIED_JOIN_TYPES`, and BE fully 
supports it. The internal path also gains the `INNER` optimization it 
previously lacked. `JoinCommute` remains in the search space, so the CBO can 
still swap sides when statistics justify it.
   
   After this PR, the pathological MERGE plan gets: a non-mark semi join 
producing RFs onto the target scan (partition pruning), the merge join eligible 
for RFs, and the narrow source side as the hash-table build side.
   
   ### Release note
   
   Improve MERGE INTO (and queries with IN/EXISTS subqueries in join ON 
clauses) performance: eliminate filter-consumed mark joins to unlock runtime 
filters, and put the MERGE target on the probe side of the join.
   
   ### Check List (For Author)
   
   - Test <!-- At least one of them must be included. -->
       - [ ] Regression test
       - [x] Unit Test
       - [ ] Manual test (add detailed scripts or steps below)
       - [ ] No need to test or manual test. Explain why:
           - [ ] This is a refactor/code format and no logic has been changed.
           - [ ] Previous test can cover this change.
           - [ ] No code files have been changed.
           - [ ] Other reason <!-- Add your reason?  -->
   
   - Behavior changed:
       - [ ] No.
       - [x] Yes. Query results are unchanged; plan shapes change (`explain` 
output): filter-consumed mark joins become plain semi joins, and the MERGE base 
join becomes `INNER` / `target RIGHT OUTER JOIN source` instead of `source LEFT 
OUTER JOIN target`.
   
   - Does this need documentation?
       - [x] No.
       - [ ] Yes. <!-- Add document PR link here. eg: 
https://github.com/apache/doris-website/pull/1214 -->
   
   ### Check List (For Reviewer who merge this PR)
   
   - [ ] Confirm the release note
   - [ ] Confirm test cases
   - [ ] Confirm document
   - [ ] Add branch pick label <!-- Add branch pick label that this PR should 
merge into -->
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)
   


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