my-ship-it commented on issue #1867:
URL: https://github.com/apache/cloudberry/issues/1867#issuecomment-5113691196

   ## Root Cause Analysis
   
   Thanks @adnanhamdussalam for the reproducer and @liang8283 for narrowing it 
down. I dug into this — below is the full analysis.
   
   ### Summary
   
   This is not a wrong-results bug and not an ORCA optimizer failure per se: it 
is a **GPORCA planner fallback** triggered on **every UPDATE of an AO / AOCO / 
PAX table whose SET list does not touch a distribution (or partition) key 
column**. ORCA aborts DXL→PlStmt translation, and the whole statement — joins, 
aggregates and all — is re-planned by the Postgres planner. The spill files and 
the slowdown reported here come from that fallback plan.
   
   The defect was introduced by a CBDB-specific patch that forces `isSplit = 
true` for append-optimized tables in the *translator*, while the *optimizer* 
has already produced a plan with no Split node. The two ends disagree, and the 
translator then looks up a column that nothing produces.
   
   ### Reproduction (verified on current `main`)
   
   ```sql
   CREATE TABLE t (a int, b int) WITH (appendonly=true) DISTRIBUTED BY (a);
   SET optimizer_trace_fallback = on;
   EXPLAIN UPDATE t SET b = 1;
   INFO:  GPORCA failed to produce a plan, falling back to Postgres-based 
planner
   DETAIL:  DXL-to-PlStmt Translation: Attribute number 7 not found in project 
list
   ```
   
   | Case (`optimizer=on`) | Result |
   |---|---|
   | heap, non-distribution column | GPORCA (in-place update) |
   | **AO row, non-distribution column** | **fallback** |
   | **AOCO, non-distribution column** | **fallback** |
   | **AO, `DISTRIBUTED RANDOMLY`** | **fallback** |
   | AO, distribution column (`SET a = 1`) | GPORCA (Split Update) |
   | AO, DELETE | GPORCA |
   
   The number in the message is an ORCA **colid**, not a `pg_attribute.attnum`. 
It follows `n_columns + n_updated_columns + 4` (2 cols / 1 updated → 7; 3 / 1 → 
8; 3 / 2 → 9; 4 / 1 → 9), i.e. it is always the **last colref allocated — the 
DML action column**. For the reporter's 52+ column table this lands on 72, 
matching the title.
   
   ### Mechanism
   
   Three code sites, in order:
   
   **1. The optimizer downgrades the split update, ignoring storage type.**
   `src/backend/gporca/libgpopt/src/operators/CExpressionPreprocessor.cpp:3232` 
(`ConvertSplitUpdateToInPlaceUpdate`, invoked unconditionally at `:3612`) turns 
`CLogicalUpdate(fSplit=true)` into `fSplit=false` whenever no distribution or 
partition key column is modified. Storage type is never consulted.
   
   **2. The action column becomes an orphan.**
   `src/backend/gporca/libgpopt/src/xforms/CXformUpdate2DML.cpp:99` allocates 
`pcrAction` unconditionally, but only the `fSplit == true` branch (`:102`) 
builds the `CLogicalSplit` that projects it. `CTranslatorExprToDXL.cpp:5590` 
then writes that colid into the DXL `ActionCol` attribute regardless. Harmless 
as long as nobody resolves it.
   
   **3. The translator resurrects `isSplit` and resolves it.**
   `src/backend/gpopt/translate/CTranslatorDXLToPlStmt.cpp`:
   
   ```c
   BOOL isSplit = phy_dml_dxlop->FSplit();          // :5257  -> false in the 
DXL
   ...
   if (md_rel->IsNonBlockTable())                   // :5290  -> AO / AOCO / PAX
   {
       isSplit = true; // AO tables are always use split updates
   }
   ...
   if (m_cmd_type == CMD_UPDATE && isSplit)         // :5364
       AddJunkTargetEntryForColId(&dml_target_list, &child_context,
                                  phy_dml_dxlop->ActionColId(), "DMLAction");
   ```
   
   The lookup misses the child translate context and 
`CMappingColIdVarPlStmt.cpp:201` raises `ExmiDXL2PlStmtAttributeNotFound`, 
which ORCA reports as a fallback.
   
   ### The premise behind the override is incorrect
   
   The comment asserts that AO tables always require a split update. The 
Postgres planner in CBDB does not agree: `check_splitupdate()` 
(`src/backend/optimizer/prep/preptlist.c:129`) requests a split update **only 
when a distribution key column is modified**, independent of storage type. An 
AO in-place UPDATE is executed as visimap-delete plus same-segment append 
inside the table AM, and since the distribution key is unchanged the tuple 
never has to move between segments. That is exactly the plan the fallback 
produces today, and it is correct. So the executor has no such requirement — 
ORCA has been paying for a constraint that does not exist.
   
   ### Provenance and blast radius
   
   ```
   1659ae11ae5  [ORCA] Implemented InPlaceUpdate ... (upstream #13889, 
cherry-picked)
   f573ee1d39b  Fix failure of ORCA non-split update for CBDB   <-- introduces 
the override
   0af03caefc6  [ORCA] Support PAX AM in ORCA                   <-- widens 
IsAORowOrColTable() to IsNonBlockTable(), pulling PAX in
   ```
   
   `f573ee1d39b` is the very next commit after the in-place-update xform; both 
landed in the ORCA sync of 2024-11-01.
   
   - **Affected releases:** `2.0.0-incubating` and later, including current 
`main`. `1.6.0` is not affected.
   - **Affected statements:** UPDATE on AO row, AOCO and PAX tables where the 
SET list touches no distribution/partition key column. INSERT, DELETE, and 
distribution-key UPDATE are unaffected.
   - **Severity:** no incorrect results; ORCA is silently disabled for the 
entire statement. With the default `optimizer_trace_fallback = off` there is no 
diagnostic at all — the only symptom is a plan regression.
   
   ### Why regression tests did not catch it
   
   The same commit rewrote the affected `*_optimizer` expected outputs to match 
the fallback plan: `src/test/regress/expected/ao_locks_optimizer.out` and 
`src/test/isolation2/output/uao/parallel_update_optimizer.source` both went 
from `n segments` to `1 segment`, which is exactly the Postgres planner's 
directly-dispatched in-place update. The regression was baked into the expected 
files.
   
   ### Proposed fix
   
   **Option A (preferred) — drop the override and let ORCA match the Postgres 
planner.**
   
   ```diff
   --- a/src/backend/gpopt/translate/CTranslatorDXLToPlStmt.cpp
   -    if (md_rel->IsNonBlockTable())
   -    {
   -            isSplit = true; // AO tables are always use split updates
   -    }
   ```
   
   Aligns ORCA with `check_splitupdate()`, restores ORCA planning for these 
statements, and yields the better plan (no Split node, no Explicit Redistribute 
Motion, direct dispatch stays available). Requires re-validating the 
AO/AOCO/PAX update suites and reverting the expected-output changes listed 
above.
   
   **Option B (conservative) — teach the optimizer the same rule the translator 
assumes.**
   
   ```diff
   --- a/src/backend/gporca/libgpopt/src/operators/CExpressionPreprocessor.cpp
        BOOL split_update = false;
   +    // AO/AOCO/PAX tables cannot be updated in place
   +    if (tabdesc->IsNonBlockTable())
   +            split_update = true;
   ```
   
   (`CTableDescriptor::IsNonBlockTable()` already exists — 
`CTableDescriptor.h:241`.) This restores pre-2024-11 behaviour and honours the 
original intent with a minimal, low-risk change, at the cost of a plan that is 
strictly worse than what the Postgres planner produces.
   
   Recommendation: land Option A, keeping Option B as the fallback if PAX turns 
out to have a genuine in-place-update restriction. Either way a regression test 
should be added that runs a non-distribution-key UPDATE against AO, AOCO and 
PAX under `optimizer_trace_fallback = on` and asserts that no fallback occurs.
   
   ### Notes for @adnanhamdussalam
   
   - The slow plan you have today *is* the Postgres planner's plan. Adding an 
explicit `SET optimizer = off` around those UPDATEs will not make anything 
slower — it only skips a wasted ORCA planning attempt. Getting an actual ORCA 
plan requires the heap / CTAS workaround @liang8283 described, or this fix.
   - There is no GUC workaround: `ConvertSplitUpdateToInPlaceUpdate` runs 
unconditionally during preprocessing, with no trace flag guarding it.
   


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