Skip to content

[CBRD-27442] Respect an outer join's ON clause when ordering joins and when skipping terms - #7970

Draft
youngjinj wants to merge 2 commits into
CUBRID:developfrom
youngjinj:CBRD-27442
Draft

youngjinj wants to merge 2 commits into
CUBRID:developfrom
youngjinj:CBRD-27442

Conversation

@youngjinj

@youngjinj youngjinj commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

http://jira.cubrid.org/browse/CBRD-27442

Purpose

외부 조인을 그것이 의존하는 테이블보다 앞에 두는 조인 순서가 후보로 남아, 보존되어야 할 행이 사라진다. 원인은 두 곳이다. ON 절이 참조하는 노드를 의존 관계로 기록하지 않거나, 성립하지 않는 등식을 이미 평가된 항의 근거로 삼는다.

옵티마이저는 조인 순서를 정할 때 QO_NODE_OUTER_DEP_SET 을 쓴다. 이 집합은 그 노드보다 먼저 조인해야 하는 노드들을 담고, planner_visit_node() 가 검사해 만족하지 않는 조합을 버린다. 아래 (1)은 그 집합이 비는 문제이고, (2)는 조인 항을 건너뛸지 판단하는 근거의 문제다.

Implementation

(1) ON 절 술어가 조인 상대가 아닌 테이블을 읽는 경우

qo_analyze_term() 의 분류 처리가 끝난 자리에 ON 절 조건이 참조하는 노드를 모두 QO_NODE_OUTER_DEP_SET 에 넣는 블록을 추가한다. 분류를 따지지 않고 적용해, 두 테이블을 참조해 QO_TC_OTHER 로 강등된 조건과 한 테이블만 참조해 QO_TC_SARG 로 분류된 조건을 함께 덮는다. 기존 QO_TC_JOIN 처리 블록은 전자만, 그것도 tail_node 가 ON 절 소유 노드와 같을 때만 다룬다.

QO_ENV_NODE() 는 노드 배열을 경계 검사 없이 인덱싱하므로 QO_ASSERT 세 개를 함께 넣었다. 주소를 만들기 전에 locationenv->nnodes 안에 있는지 확인하고, 꺼낸 노드의 location 이 그 값과 같은지 보고, 반복문 안에서는 ON 절이 자기가 속한 조인보다 앞선 노드만 읽는지 검사한다.

(2) 외부 조인 ON 절의 등식을 이미 성립한 근거로 쓰는 경우

qo_check_skip_term() 이 동치류가 이미 연결되었다는 근거를 모을 때 외부 조인 ON 절의 등식을 제외한다. 그 등식은 짝이 맞은 행에서만 성립하고 널로 채워진 행에서는 성립하지 않는다.

qo_equivalence() 에서 외부 조인 ON 항을 빼는 대안은 택하지 않았다. 동치류를 읽는 코드 네 군데 가운데 세 군데는 병합 조인의 정렬 순서로만 쓰고, 외부 조인 ON 등식에도 유효하다. 결함은 동치류 자체가 아니라 qo_check_skip_term() 이 동치류에 얹은 해석에 있다. 또 qo_equivalence() 호출이 QO_TERM_NOMINAL_SEG 대입과 같은 if 안에 있어, 빼면 홑 동치류가 생겨 외부 조인이 낀 다중 등가 조인이 정렬 순서를 공유하지 못하는 플랜 퇴행이 난다.

Remarks

  • QO_TC_JOIN 안의 PT_JOIN_LEFT_OUTER, PT_JOIN_RIGHT_OUTER 가지에 있는 QO_ADD_OUTER_DEP_SET (on_node, head_node) 는 (1)의 새 블록과 결과가 겹친다. 그 가지의 항은 노드가 둘이고 on_nodetail_node 라, 새 블록이 넣는 값이 head_node 로 같기 때문이다. 두 호출을 지우고 빌드해도 회귀 테스트가 그대로 통과하지만, 변경 범위를 좁게 두려고 남겼다.
  • 두 결함이 들어온 경로가 다르다. 결함 (1)의 로직은 2014년 커밋 6522201dab 부터 지금까지 같은 형태로 있고, 11.0 부터 드러난다. 10.2 에서는 qo_add_dummy_join_term()PT_JOIN_LEFT_OUTER 처리에서 조인 항의 유무를 따지지 않고 의존 집합을 채워 주어 같은 순서가 후보에 오르지 못했다. 결함 (2)는 qo_check_skip_term() 을 도입한 커밋 86063bc164 [CBRD-25214] Remove join predicate already logically evaluated. (#4954) (2024-03-26) 으로 들어왔고, 그 커밋이 release/11.4 에 있고 release/11.3 에 없어 11.4 부터 드러난다.
  • 버전별로 두 반례를 돌려 재현 경계를 확인했다. 결함 (1)의 반례는 10.2 에서 정답을 내고 11.0 부터 오답을 낸다. 결함 (2)의 반례는 11.3 까지 정답을 내고 11.4 부터 오답을 낸다. develop 에서 86063bc164 하나만 되돌려 빌드하면 결함 (2)의 반례만 정답으로 돌아오고 결함 (1)의 반례는 그대로 오답이다.
  • Oracle 19c(19.0.0.0.0)와 PostgreSQL 17.9 에서 같은 질의를 돌려 기대값을 확인했다. 두 제품은 LEADING 으로 위험한 순서를 지정해도 그 순서를 채택하지 않는다. PostgreSQL 은 그 조합에 2e10 규모의 페널티 비용을 매겨 원래 순서를 유지하고, Oracle 은 힌트를 받아들이되 계획이 바뀌지 않는다. 수정 후 CUBRID 가 두 제품과 일치한다.

@youngjinj youngjinj self-assigned this Sep 17, 2026
@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown

❌ TC Merge Gate — Merge Blocked

TC changes for this PR are not on TC develop yet.

  • cubrid-testcases: TC PR tc/pr-7970 is open (draft) — merge it, or close it if it carries no change
  • cubrid-testcases-private-ex: TC PR tc/pr-7970 is open (draft) — merge it, or close it if it carries no change
    Steps to unblock:
  1. Merge the TC PRs listed above. Close one only when it carries no change.
  2. Re-run this check: Actions tab → TC Merge Gate → Re-run failed jobs

@youngjinj

Copy link
Copy Markdown
Contributor Author

@greptile 리뷰해줘

@github-actions

Copy link
Copy Markdown

🧪 TC Test Environment Ready

CircleCI Testing:

  • CircleCI will automatically test using the branches below.

TC Repositories & Branches:

Next Steps:

  1. Wait for CircleCI tests to complete
  2. If CircleCI tests failed, please check the test results and fix the issues.
  3. When ready to merge this PR, please merge the TC PR first, then merge this PR.

@greptile-apps

greptile-apps Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Retrigger

결과 정합성을 직접 훼손하는 문제는 확인되지 않아 병합 가능해 보이지만, cross-partition ON dependency가 유발하는 비차단성 최적화 fallback은 개선하는 것이 좋습니다.

Reviews (1) · Last reviewed commit: "Join every table an outer join's ON clau..."

{
/* an ON clause only reads nodes that precede the join it belongs to */
QO_ASSERT (env, t < QO_NODE_IDX (on_node));
QO_ADD_OUTER_DEP_SET (on_node, QO_ENV_NODE (env, t));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 조인 파티션 의존성 누락

FROM a, b LEFT JOIN c ON a.k = b.k에서는 ON term이 QO_TC_OTHER로 강등되고 dummy edge는 b-c만 연결하므로 a가 별도 파티션에 남을 수 있습니다. 이 줄은 c.outer_dep_seta를 추가하지만, 파티션 구성은 edge만 사용합니다. 따라서 {b,c} 파티션을 탐색할 때 방문할 수 없는 a가 선행조건으로 남아 파티션 탐색이 실패하고 전체 CBO 계획이 fallback됩니다. 결과 정합성에는 직접적인 영향이 없더라도 합법적인 쿼리가 최적화 경로에서 항상 탈락하므로, 이 outer dependency를 파티션 구성이나 파티션 로컬 선행조건 판정에도 일관되게 반영해 주세요.

Knowledge Base Used: Query optimization

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의존 집합에 담긴 노드가 다른 파티션에 남을 수 있다는 지적은 맞습니다. 다만 예로 드신 질의는 옵티마이저에 도달하지 않으며, 결과의 형태도 다릅니다.

쉼표 조인은 이름 해석에서 거부됩니다

쉼표 조인에서는 ON 절이 쉼표 너머의 테이블을 참조할 수 없습니다. develop 과 이 PR 은 FROM a, b LEFT JOIN c ON a.k = b.k 에 똑같이 아래 오류를 냅니다.

ERROR: '[dba.a].k' in join condition is not defined.

CROSS JOIN 으로는 재현됩니다

CREATE TABLE a (k INT, v INT); CREATE TABLE b (k INT, v INT); CREATE TABLE c (k INT, v INT);
INSERT INTO a VALUES (1,1),(2,2); INSERT INTO b VALUES (1,1),(2,2); INSERT INTO c VALUES (1,1);
UPDATE STATISTICS ON a, b, c;

SELECT /*+ RECOMPILE */ COUNT(*) FROM a CROSS JOIN b LEFT JOIN c ON a.k = b.k;

a CROSS JOIN b 의 결과가 4행이고, LEFT OUTER JOIN 이 그 4행을 보존하므로 정답은 4 입니다.

빌드 결과
develop 11.5.0.2608-9ee8bac 2 (오답)
이 PR 11.5.0.2603-1f10dba Outer join query optimization failed.

이 PR 에서는 최적화 단계의 fallback 이 아니라 질의가 실패하는 오류가 납니다.

CBRD-27483 에서 다룹니다

원인은 두 가지입니다. CROSS JOIN 과 쉼표 조인이 머지된 뷰가 조인 그래프에 간선을 남기지 않고, qo_discover_partitions() 가 간선 정보를 바탕으로 파티션을 결정합니다. 간선을 잇는 수로 분리했습니다.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞습니다. 제가 든 쉼표 조인 예시는 CUBRID의 이름 해석 규칙상 옵티마이저에 도달하지 않으므로 부적절했습니다.

제기한 파티션 분리 시나리오는 CROSS JOIN에서 재현되며, 이 경우 직접 원인은 이 PR의 의존 집합 추가가 아니라 CROSS JOIN/쉼표 조인이 뷰 병합 후 조인 그래프에 간선을 남기지 않는 점입니다. 또한 증상도 단순한 CBO fallback이 아니라 Outer join query optimization failed입니다.

따라서 해당 지적은 이 PR의 결함으로 유지하지 않겠습니다. 간선 연결 문제는 CBRD-27483에서 별도로 다루는 것이 맞습니다.

@youngjinj
youngjinj force-pushed the CBRD-27442 branch 5 times, most recently from 314d582 to a3f2f21 Compare September 22, 2026 09:59
youngjinj and others added 2 commits September 22, 2026 19:10
When a LEFT OUTER JOIN's ON clause carries a predicate over a table that is
not the join partner, qo_analyze_term () never recorded that table in the
outer-join node's dependency set. The optimizer was then free to place the
outer join ahead of that table. In that order the predicate cannot be
evaluated where the outer join needs it, so rows that LEFT OUTER JOIN must
preserve with NULL-padded columns were dropped instead.

Whether the optimizer picks that order is decided by cost, so the same query
returns the right answer on one data set and the wrong one on another.

Two predicate shapes reach the defect through different classifications:

  - a predicate over two tables is classified QO_TC_JOIN and then demoted to
    QO_TC_OTHER, because its tail node differs from the ON-clause owner
  - a predicate over one table is classified QO_TC_SARG from the start and
    never enters the QO_TC_JOIN handling at all

Both leave the dependency set empty, so record the dependency once the
classification is settled, whatever class the predicate was given.

QO_ENV_NODE () indexes the node array without a bound check and never returns
NULL, and this change widens the access to QO_TC_SARG and QO_TC_OTHER terms,
so assert the location is in range and that it agrees with the node index.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RwDdyHWR9thhoJHJtuCNYX
qo_check_skip_term () gathers evidence that an equivalence class is
already connected, and counts an equality carried in an outer join's ON
clause among it.  That equality holds only on the rows the join matched;
on a row it preserved with NULL padding the two sides differ, so it says
nothing about the class.  A later term the class covers is then dropped
and the query returns values the join never matched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jfLrX4jqiAkaPiTdNe5rs
@youngjinj youngjinj changed the title [CBRD-27442] Join order ignores tables the ON clause depends on, so rows that should be preserved are lost [CBRD-27442] Join order and term skipping ignore what an outer join's ON clause requires, so its result comes out wrong Sep 22, 2026
@youngjinj youngjinj changed the title [CBRD-27442] Join order and term skipping ignore what an outer join's ON clause requires, so its result comes out wrong [CBRD-27442] Respect an outer join's ON clause when ordering joins and when skipping terms Sep 22, 2026
@youngjinj

Copy link
Copy Markdown
Contributor Author

/run all

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant