(KO) CUBRID 질의 재작성 — 최적화 이전의 트리 변환과 LIMIT 절 사례 연구
목차
학술적 배경
섹션 제목: “학술적 배경”질의 재작성은 파싱(문법만 강제) 과 비용 기반 최적화(물리 계획을 열거하고 가격을 매김) 사이에 위치하는 질의 처리 계층이다. 이 계층의 임무는 논리적이다. 사용자가 작성한 파스 트리를 받아, 최적화기가 계획에 점수를 매기기 전에, 동치이지만 더 다루기 쉬운 파스 트리로 변환한다. Database Internals (Petrov, 12장 “Query Planning and Optimization) 는 이 단계를 rule-based” 단계로 묶고, Database System Concepts (Silberschatz 외, 16장) 는 “관계 표현 변환(transformation of relational expressions)“이라 부른다. 어느 쪽이든 계약은 동일하다 — 동치성을 보존하는 구문 단위 수술.
Goetz Graefe의 The Cascades Framework for Query Optimization
(IEEE Data Eng. Bull., 1995) 는 이 이분법을 명시화했다. 변환
규칙(transformation rules) 은 항상 더 좋아지는 재작성으로 무조건
적용되는 것이고, 구현 규칙(implementation rules) 은 최적화기가
비용을 매겨야 하는 대안들이다. 술어 푸시다운은 변환 규칙의 정본이다
— 더 일찍 거르는 게 더 나쁠 수 없기 때문에 재작성 계층에 산다.
조인 순서 열거는 구현 규칙의 정본이다 — 비용을 매겨야 하므로
최적화기 본체에 들어간다. CUBRID도 이 분리를 따른다.
rewriter/ 디렉토리는 변환을, query_planner.c 와
plan_generation.c 는 열거와 비용 산정을 담당한다.
전형적인 변환 카탈로그는 다음과 같다.
- 술어 푸시다운(Predicate pushdown) — WHERE 결합항을 스키마가 허용하는 한 스캔에 가깝게 옮긴다. 파이프라인 더 앞쪽에서 튜플을 줄여 준다.
- 뷰 인라이닝(View inlining, CUBRID에서는 mq_translate) — 뷰 참조를 그 정의 질의로 치환해 후속 재작성에 실 테이블을 노출한다.
- 서브쿼리 평탄화(Subquery flattening) — 비상관
IN (SELECT ...)을 파생 테이블과의 조인으로 바꿔서, 안쪽 테이블이 조인 열거의 대상에 들어가게 만든다. - 외부 조인 축소(Outer-join reduction) — 옵션 측을 null-rejecting 술어가 위쪽에 걸려 있으면 left/right outer join을 inner join으로 승격한다. Database Internals §12.3 이 왜 이게 안전한지 짚어 준다.
- CNF 정규화 — WHERE 필터를 합접 정규형(conjunctive normal form) 으로 바꾼다. 그래야 각 결합항이 푸시다운과 인덱스 기반 평가의 후보가 된다.
- 상수 접기 / 동등성 축약 —
WHERE x = 5 AND ... x ...에서 그 결합항이 지배하는 모든x자리를5로 치환해, 추가 접기를 위한 상수를 노출한다. - 불필요 조인 제거(Redundant-join elimination) — 컬럼이 참조 되지 않고 조인 조건이 FK→PK 인 조인 테이블을 제거한다. 즉, 그 조인이 행을 늘리지도 줄이지도 못하기 때문이다.
- LIMIT 푸시다운 / 행 카운팅 술어로의 내림(lowering) — 본 문서의
주제다. 사용자에게 보이는
LIMIT n을 스캔 측의 행 번호 매김 술어로 만들어, 새로운 n개 후 정지 계획 연산자 없이 기존 술어 기계 안에서 조기 종료가 표현되게 한다.
마지막 항목이 흥미로운 이유는 순수 푸시다운이 아니기 때문이다.
정렬되지 않은 스캔 위의 LIMIT n 은 스캔 튜플 카운터에 대한
INST_NUM() <= n 로 자명히 표현된다. 그러나 정렬 뒤 의 LIMIT n
은 스캔 측 술어가 아니다 — 카운터가 잘 정의되는 정렬 출력 위에
앉아야 한다. GROUP BY 뒤의 LIMIT n 도 마찬가지로 그룹 출력 위에
앉아야 한다. CUBRID는 이 선택을 세 가지 번호 매김 연산자
(PT_INST_NUM, PT_ORDERBY_NUM, PT_GROUPBY_NUM)로 인코딩하고,
주변 절에 따라 LIMIT를 적절한 술어 슬롯(where, orderby_for,
having) 으로 라우팅한다. 이 내림은 결정 트리이며, 의미 검사
시점에 한 번 걸어가고 일부 모양에 대해서는 mq_rewrite 시점에 다시
들여다본다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”PostgreSQL의 subquery_planner (src/backend/optimizer/plan/ planner.c) 는 정해진 순서의 재작성 패스 시퀀스로 구성되어 있다.
pull_up_sublinks → inline_set_returning_functions →
pull_up_subqueries → flatten_simple_union_all →
reduce_outer_joins → remove_useless_result_rtes →
preprocess_expression (상수 접기, canonicalize_qual,
eval_const_expressions) → preprocess_qual_conditions 의 순서다.
LIMIT는 parse->limitOffset / parse->limitCount 로 살아 있고,
실행기의 ExecLimit 노드가 소비한다 — Postgres는 재작성 시점에
LIMIT를 행 카운팅 술어로 내리지 않는다. 계획 트리 최상단의
연산자로 끝까지 남겨 둔다.
MySQL의 최적화기 진입점 JOIN::optimize 는 optimize_cond (CNF +
상수 접기 패스), prune_partitions, make_join_statistics,
optimize_keyuse 를 차례로 부르고 마지막에 비용 기반 조인 순서를
잡는다. MySQL의 LIMIT n 도 계획 최상단의 연산자다 (SELECT_LEX
의 limit 필드). 재작성 계층이 LIMIT를 들여다보는 알려진 예외는
두 가지뿐이다 — 인덱스 조건 푸시다운과, 인덱스 위 MIN/MAX 에
대한 loose-index-scan 최적화. 두 경우 모두 LIMIT가 작을 때 스캔을
조기 종료시킬 수 있다. LIMIT를 카운팅 술어로 일반화하여 내리는
경로는 없다.
Oracle의 뷰 병합(10g에서 비용 기반 변형이 추가되기 전의 휴리스틱
판본)이 CUBRID mq_translate 의 가장 가까운 선례다. 인라인 가능한
뷰가 부모 질의에 치환되어 안쪽 테이블이 최적화기에 노출된다.
Oracle의 _complex_view_merging 파라미터와 MERGE / NO_MERGE
힌트가 운영자에게 보이는 손잡이다.
CUBRID의 선택 — 의미 검사 단계에서 LIMIT를 행 카운팅 술어로 내리고, 그 결과 표현 위에서 재작성 패스를 한 번 더 도는 — 은 Postgres (LIMIT는 구조적으로 유지된다) 와 인덱스 스캔 내부 LIMIT 최적화 (LIMIT가 계획의 바닥에 영향을 준다) 의 사이 어딘가에 위치한다. 이 선택의 결과는 둘이다.
- LIMIT는 추가 비용 없이 CNF, 상수 접기, 자동 파라미터화에
참여한다. 이들 패스가 돌 시점에는 LIMIT가 이미 우변
PT_VALUE를 가진PT_LE표현이기 때문이다. - 반대로 최적화기는 multi-range 인덱스 스캔 LIMIT 최적화를
적용하려 할 때 내려진 형태를 다시 알아보아야 한다. CUBRID는
이 작업을
qo_check_iscan_for_multi_range_opt(plan_generation.c) 에서 수행하며, 내림 이후 슬롯인query->info.query.orderby_for를 검사해, LIMIT가 스캔 시점에 multi-range 최적화를 구동할 수 있는 모양인지 판단한다.
CUBRID의 구현
섹션 제목: “CUBRID의 구현”재작성을 둘러싼 질의 처리 파이프라인은 SQL 텍스트에서 실행 가능한 XASL까지 가는 6단계 경로다. 재작성은 한 패스에 국지화되어 있지 않고 3단계와 5단계에 분산되어 있다.
flowchart LR SQL["SQL 텍스트"] --> LX["렉서<br/>csql_lexer.l"] LX --> PR["파서<br/>csql_grammar.y<br/>→ PT_NODE 트리"] PR --> NR["이름 해석<br/>pt_resolve_names"] NR --> SC["의미 검사<br/>pt_semantic_check<br/>타입 검사 + LIMIT 내림<br/>(pt_eval_type_pre)"] SC --> VT["뷰 인라이닝<br/>mq_translate"] VT --> RW["재작성 패스<br/>mq_rewrite (qo_rewrite_queries)<br/>CNF, 술어 푸시다운,<br/>서브쿼리 평탄화,<br/>잔존 LIMIT 내림,<br/>자동 파라미터화"] RW --> OPT["비용 최적화기<br/>qo_optimize_statement"] OPT --> XG["XASL 생성<br/>xasl_generation.c"] XG --> EX["실행<br/>query_executor.c"] classDef rw fill:#fff5d6,stroke:#a07000; class SC,VT,RW rw;
노란 단계가 모두 파스 트리를 건드린다. 재작성 계층 본체는 세 번째
단계 (mq_rewrite) 다. 앞의 두 단계가 의미를 갖는 이유는 (a) 그
시점에 LIMIT가 번호 매김 술어로 내려지고, (b) 뷰 참조가 정의로
치환된 트리가 만들어지기 때문이다. 둘 다 mq_rewrite 보다 먼저
일어나므로, 재작성 계층은 대부분의 경우 LIMIT가 이미 술어이고
실 테이블만 보인다고 가정할 수 있다.
드라이버는 src/compat/db_vdb.c 에 있다.
// db_compile_statement_local — src/compat/db_vdb.c (condensed)intdb_compile_statement_local (DB_SESSION * session){ // ... parser already populated session->statements ...
/* prefetch and lock classes to avoid deadlock */ (void) pt_class_pre_fetch (parser, statement);
/* semantic check + LIMIT lowering happens inside */ statement_result = pt_compile (parser, statement);
/* view substitution + post-translate rewrites */ statement_result = mq_translate (parser, statement); // ... mq_translate calls mq_rewrite internally for SELECTs ...
/* re-prefetch translated real classes */ (void) pt_class_pre_fetch (parser, statement);
// ... do_prepare_statement → XASL generation ...}pt_compile 이 의미 검사의 진입점이고, mq_translate 가 뷰
인라이닝 + 인라이닝 후 재작성의 진입점이다 (그 결과 SELECT들마다
mq_rewrite 를 부른다). 이 두 호출 사이에서 모든 파스 트리
모양 변화가 끝난다 — XASL 생성은 그 뒤다.
의미 검사 단계의 LIMIT 내림
섹션 제목: “의미 검사 단계의 LIMIT 내림”LIMIT 절이 처음 받는 재작성은 pt_eval_type_pre
(type_checking.c) 안에서 일어난다. 이 함수는 트리의 모든 노드마다
pt_semantic_type 의 pre-walk 콜백으로 호출된다. 이 결정 트리가
LIMIT 사례의 핵심이다.
flowchart TD
S["LIMIT 절<br/>node.info.query.limit != NULL<br/>flag.rewrite_limit == 1"]
S -->|node_type| K{node_type?}
K -->|PT_SELECT| SEL{ORDER BY<br/>존재?}
K -->|PT_UNION/<br/>PT_INTERSECTION/<br/>PT_DIFFERENCE| UNI{ORDER BY<br/>존재?}
K -->|PT_DELETE| DEL["INST_NUM 내림 →<br/>delete_.search_cond"]
K -->|PT_UPDATE| UPD{ORDER BY<br/>존재?}
SEL -->|있음,<br/>order_siblings 아님| OBN["ORDERBY_NUM 내림 →<br/>query.orderby_for"]
SEL -->|없음,<br/>GROUP BY 있음| GBN["GROUPBY_NUM 내림 →<br/>q.select.having"]
SEL -->|GBY 없음,<br/>DISTINCT 있음| OBN2["ORDERBY_NUM 내림 →<br/>query.orderby_for"]
SEL -->|위 어떤 것도 아님| INST["INST_NUM 내림 →<br/>q.select.where"]
UNI -->|있음| OBN3["ORDERBY_NUM 내림 →<br/>query.orderby_for"]
UNI -->|없음| RESID["mq_rewrite 시점으로 미룸<br/>(qo_rewrite_queries 참조)"]
UPD -->|있음| OBN4["ORDERBY_NUM 내림 →<br/>update.orderby_for"]
UPD -->|없음| INST2["INST_NUM 내림 →<br/>update.search_cond"]
분기 순서가 결정적이다. ORDER BY > GROUP BY > DISTINCT > 그 외
순이며, 처음 매치되는 분기 하나만 적용된다. 즉 GROUP BY와 ORDER BY를
모두 갖는 SELECT는 GROUPBY_NUM 이 아니라 ORDERBY_NUM 을 쓴다.
치트시트의 사례 열거가 소스와 일대일로 맞아 떨어진다. PT_SELECT
처리부는 다음과 같다.
// pt_eval_type_pre (PT_SELECT arm) — src/parser/type_checking.ccase PT_SELECT: /* rewrite limit clause as numbering expression and add it to the corresponding predicate */ if (node->info.query.limit && node->info.query.flag.rewrite_limit) { PT_NODE *limit, *t_node; PT_NODE **expr_pred;
if (node->info.query.order_by && !node->info.query.flag.order_siblings) { expr_pred = &node->info.query.orderby_for; limit = pt_limit_to_numbering_expr (parser, node->info.query.limit, PT_ORDERBY_NUM, false); } else if (node->info.query.q.select.group_by) { expr_pred = &node->info.query.q.select.having; limit = pt_limit_to_numbering_expr (parser, node->info.query.limit, PT_LAST_OPCODE, true); } else if (node->info.query.all_distinct == PT_DISTINCT) { expr_pred = &node->info.query.orderby_for; limit = pt_limit_to_numbering_expr (parser, node->info.query.limit, PT_ORDERBY_NUM, false); } else { expr_pred = &node->info.query.q.select.where; limit = pt_limit_to_numbering_expr (parser, node->info.query.limit, PT_INST_NUM, false); }
if (limit != NULL) { /* append `limit` to the chosen predicate slot */ // ... condensed: walk expr_pred to its tail, splice in `limit` ... node->info.query.flag.rewrite_limit = 0; } } break;이 처리부에서 직관에 잘 안 와 닿는 두 가지가 있다. 첫째,
order_siblings (CONNECT BY 계층 수정자) 는 ORDER BY 분기를 의도적
으로 거부한다 — siblings 정렬은 레벨 단위이지 전역이 아니므로,
ORDERBY_NUM 은 sibling 경계를 가로질러 카운트해 호출자의 의도를
초과해 버리기 때문이다. 둘째, GROUP BY 처리부는 연산자로
PT_LAST_OPCODE (센티넬) 를 넘기고 is_gby_num = true 를 함께
넘긴다 — 헬퍼는 이를 PT_GROUPBY_NUM 함수 노드를 만들라는
의미로 해석한다. PT_EXPR 연산자가 아니라 함수 노드인 이유는
GROUPBY_NUM이 파스 트리 분류상 함수이기 때문이다.
node->info.query.flag.rewrite_limit 플래그는 LIMIT가 존재할 때
문법이 1 로 세트하고 (csql_grammar.y 의 SELECT/UPDATE/DELETE
프로덕션이 기록한다), 내림이 성공하면 0 으로 클리어된다. 클리어
이후에도 LIMIT 표현은 진단 출력을 위해 node->info.query.limit 에
남아 있지만, 실행에는 더 이상 참여하지 않는다 — 실제 술어는
선택된 술어 슬롯에 들어 있는 내려진 번호 매김 표현이다.
parser_print_tree 는 flag.rewrite_limit 을 검사해 플래그가
0일 때 LIMIT 출력을 억제한다. 즉 내림이 끝난 후에는 인쇄된 질의
에 LIMIT 절이 없고, 동치인 WHERE inst_num() <= n (또는 그
형제) 만 보인다. 치트시트의 PT_CONVERT_RANGE 포맷팅 변형이 내려진
형태를 명시적으로 노출한다 — select code, name FROM athlete ... LIMIT 1 예제를 원본 분석 자료가 그렇게 인용하는 이유다.
내려진 술어가 어떻게 만들어지는가
섹션 제목: “내려진 술어가 어떻게 만들어지는가”pt_limit_to_numbering_expr (parser_support.c) 는 작은 팩토리다.
사용자에게 보이는 LIMIT 는 최대 두 값을 가진다 — [offset,] row_count. 함수는 (offset 이 없을 때는) lhs <= row_count,
(offset 이 있을 때는) lhs > offset AND lhs <= offset + row_count
중 하나를 짓는다. lhs 는 다음 셋 중 하나다.
op = PT_INST_NUM인PT_EXPR— 스캔 후 행 수 카운트.op = PT_ORDERBY_NUM인PT_EXPR— 정렬 후 행 수 카운트.function_type = PT_GROUPBY_NUM인PT_FUNCTION— 그룹 후 그룹 수 카운트.
// pt_limit_to_numbering_expr — src/parser/parser_support.c (condensed)PT_NODE *pt_limit_to_numbering_expr (PARSER_CONTEXT * parser, PT_NODE * limit, PT_OP_TYPE num_op, bool is_gby_num){ PT_NODE *lhs, *part1, *part2, *node;
/* Build the LHS: GROUPBY_NUM is a function node, the others are expr nodes. */ if (is_gby_num) { lhs = parser_new_node (parser, PT_FUNCTION); lhs->type_enum = PT_TYPE_INTEGER; lhs->info.function.function_type = PT_GROUPBY_NUM; } else { lhs = parser_new_node (parser, PT_EXPR); lhs->info.expr.op = num_op; /* PT_INST_NUM or PT_ORDERBY_NUM */ }
if (limit->next == NULL) { /* `LIMIT n` → lhs <= n */ node = parser_new_node (parser, PT_EXPR); node->info.expr.op = PT_LE; node->info.expr.arg1 = lhs; node->info.expr.arg2 = parser_copy_tree (parser, limit); } else { /* `LIMIT off, n` → lhs > off AND lhs <= off + n */ part1->info.expr.op = PT_GT; part1->info.expr.arg1 = lhs; part1->info.expr.arg2 = parser_copy_tree (parser, limit); /* offset */
part2->info.expr.op = PT_LE; part2->info.expr.arg1 = parser_copy_tree (parser, lhs); /* part2->arg2 = (offset + row_count); folded if both are PT_VALUE */ // ... condensed: build PT_PLUS sum, fold if both literals ...
node = parser_new_node (parser, PT_EXPR); node->info.expr.op = PT_AND; node->info.expr.arg1 = part1; node->info.expr.arg2 = part2; } return node;}두 인자 경로의 PT_VALUE && PT_VALUE 상수 접기 분기는 작은
이득이다. LIMIT 100, 10 은 inst_num() > 100 AND inst_num() <= 110 으로 직접 만들어지며, 110 은 재작성 시점에 이미 계산되어
있다. 어느 한쪽 경계가 호스트 변수라면 PT_PLUS 표현 노드를
유지하고 나중에 접는다.
mq_rewrite 단계 — 뷰 인라이닝 이후
섹션 제목: “mq_rewrite 단계 — 뷰 인라이닝 이후”mq_translate 는 뷰 참조를 그 정의 질의로 치환한다. 치환 후에는
의미 검사 시점에 존재하지 않던 새 SELECT가 트리에 등장할 수
있고, 그 SELECT들은 뷰 정의에서 온 LIMIT나 mq_translate 가
직접 붙인 LIMIT를 가질 수 있다. 그래서 mq_translate 끝의
mq_rewrite 패스는 다음을 해야 한다.
- 의미 검사에서 내려지지 않은 잔존 LIMIT를 다시 내린다 (주로 UNION 경우).
- 술어를 CNF 정규화한다 (이제는 의미 검사에서 내려진 번호 매김 술어들을 포함한다).
- 안전한 자리로 술어를 내려보낸다.
- 비상관 서브쿼리를 파생 테이블 조인으로 평탄화한다.
- 동등성 항을 축약한다 (등호를 통한 상수 전파).
- 안전한 곳에서 외부 조인을 내부 조인으로 축소한다.
- XASL 계획 캐시를 위해 상수를 자동 파라미터화한다.
드라이버는 mq_rewrite 다. 이 함수는 statement를
parser_walk_tree 를 돌리되, pre 콜백으로 qo_rewrite_queries,
post 콜백으로 qo_rewrite_queries_post 를 사용한다.
// mq_rewrite — src/optimizer/rewriter/query_rewrite.cPT_NODE *mq_rewrite (PARSER_CONTEXT * parser, PT_NODE * statement){ return parser_walk_tree (parser, statement, qo_rewrite_queries, NULL, qo_rewrite_queries_post, NULL);}qo_rewrite_queries 는 세 단계로 구성된 500여 줄 디스패치다.
1단계 — pre-rewrite
섹션 제목: “1단계 — pre-rewrite”각 statement 타입별로, pre-rewrite 단계는 후속 단계가 작동할 술어
슬롯들 (wherep, havingp, orderby_for_p 등) 을 식별하고
모양만 바꾸는 변환을 적용한다.
qo_move_on_of_explicit_join_to_where— 명시적 조인의 ON-절 술어를 WHERE 리스트로 옮기되, 원래 위치 태그를 함께 단다. post 콜백은 외부 조인을 이 태그를 보고 옮김을 되돌린다. 내부 조인은 옮김을 그대로 유지하고, 외부 조인은 COPYPUSH 항으로 변환된 것이 아닌 한 술어가 ON 절로 복원된다.qo_rewrite_index_hints— 사라진 테이블을 가리키는 힌트를 버린다 (뷰 인라이닝 이후 힌트가 더 이상 적용되지 않을 수 있다).PT_UNION/PT_DIFFERENCE/PT_INTERSECTION의 경우, 의미 검사가 미뤄 둔 사례인 잔존 LIMIT 내림 이 여기서 일어난다. ORDER BY가 없는 UNION에 LIMIT가 붙은 경우다.
// qo_rewrite_queries (PT_UNION arm, residual LIMIT) — query_rewrite.ccase PT_UNION:case PT_DIFFERENCE:case PT_INTERSECTION: // ... condensed: rewrite hidden columns of arg1, arg2 as derived ... if (node->info.query.limit && node->info.query.flag.rewrite_limit) { limit = pt_limit_to_numbering_expr (parser, node->info.query.limit, PT_INST_NUM, false); if (limit != NULL) { node->info.query.flag.rewrite_limit = 0;
/* Push limit into UNION arms when safe (no ORDER BY at top, no DISTINCT, no NO_PUSH_PRED hint). */ if (node->info.query.order_by == NULL && !qo_check_distinct_union (parser, node) && !qo_check_hint_union (parser, node, PT_HINT_NO_PUSH_PRED)) { node = qo_push_limit_to_union (parser, node, limit_node); } /* Wrap the UNION as a derived table and apply the lowered predicate on top: SELECT * FROM (UNION) T WHERE INST_NUM() <= n. */ derived = mq_rewrite_query_as_derived (parser, node); derived->info.query.q.select.where = limit; node = derived; } } orderby_for_p = &node->info.query.orderby_for; break;여기서 두 변환이 겹친다. 하나는 arm 단위 LIMIT 푸시다.
qo_push_limit_to_union 은 UNION 트리의 각 가지를 걸어가며
LIMIT를 (또는 LIMIT offset+row_count 를 단일 상한으로 합산해)
각 arm에 복사한다. 그러면 각 arm이 충분한 행 수에 다다르면 독립적
으로 멈춘다. 다른 하나는 파생 테이블로 감싸기다. UNION 자체를
합성된 외곽 SELECT 의 FROM 절로 만들고, 그 외곽의 WHERE에 내려진
INST_NUM() <= n 을 두어 파이프라인 최상단의 캡도 표현한다. 두
변환의 조합으로, LIMIT를 가진 UNION 은 각 arm 을 독립적으로 조기
종료할 수 있고 병합 파이프라인도 멈출 수 있다.
2단계 — 최적화 재작성
섹션 제목: “2단계 — 최적화 재작성”2단계는 OPTIMIZATION_ENABLED(level) 이 참일 때만 돈다 (보통의
질의에서는 참이며, 디버깅용으로 optimization_level 파라미터로
끌 수 있다).
// qo_rewrite_queries (phase 2 sketch) — query_rewrite.cif (OPTIMIZATION_ENABLED (level)) { if (node->node_type == PT_SELECT) { int continue_walk, idx = 0; qo_rewrite_subqueries (parser, node, &idx, &continue_walk); }
/* convert predicate lists to CNF */ if (*wherep) *wherep = pt_cnf (parser, *wherep); if (*havingp) *havingp = pt_cnf (parser, *havingp); if (*startwithp) *startwithp = pt_cnf (parser, *startwithp); if (*connectbyp) *connectbyp = pt_cnf (parser, *connectbyp); if (*aftercbfilterp)*aftercbfilterp= pt_cnf (parser, *aftercbfilterp); if (*orderby_for_p) *orderby_for_p = pt_cnf (parser, *orderby_for_p); // ... merge_upd_wherep, merge_ins_wherep, merge_del_wherep ...
/* in HAVING with GROUP BY, move non-aggregate terms to WHERE */ if (PT_IS_SELECT (node) && node->info.query.q.select.group_by && *havingp) { // ... condensed: HAVING-to-WHERE pushdown for non-aggregate terms ... }
/* replace `WHERE x = const AND ... x ...` with `const` everywhere */ if (*wherep) { if (PT_IS_SELECT (node)) parser_walk_tree (parser, node, NULL, NULL, qo_reduce_equality_terms_post, NULL); else QO_CHECK_AND_REDUCE_EQUALITY_TERMS (parser, node, wherep); } if (*havingp) QO_CHECK_AND_REDUCE_EQUALITY_TERMS (parser, node, havingp);
/* per-clause term rewrites: range merging, OR→IN promotion, etc. */ qo_rewrite_terms (parser, spec, wherep); qo_rewrite_terms (parser, spec, havingp); qo_rewrite_terms (parser, spec, startwithp); qo_rewrite_terms (parser, spec, connectbyp); qo_rewrite_terms (parser, spec, aftercbfilterp); qo_rewrite_terms (parser, spec, merge_upd_wherep); qo_rewrite_terms (parser, spec, merge_ins_wherep); qo_rewrite_terms (parser, spec, merge_del_wherep);
/* outer-join reduction, predicate copy, redundant-join elimination */ if (node->node_type == PT_SELECT) { if (!qo_rewrite_select_queries (parser, &node, wherep, &seqno)) return node; } }GROUP BY가 있는 HAVING 줄의 HAVING-to-WHERE 이동은 더 자세히 볼 가치가 있다. 집계 함수도, 그룹화되지 않은 컬럼도 참조하지 않는 HAVING 결합항은 그룹화 전에 평가될 수 있다 — 이를 WHERE로 옮기는 것은 교과서적인 술어 푸시다운이다. CUBRID는 두 가드를 더 둔다. 결합항이 가짜 컬럼(pseudocolumn) 을 포함해서는 안 되고 (CONNECT BY 가짜 컬럼들은 레벨에 민감하다), GROUP BY 에 ROLLUP이 있어서도 안 된다 (ROLLUP 그룹은 집계 이전 행 수에 의존하므로, 행을 미리 제거 하면 결과가 바뀐다).
qo_rewrite_subqueries 는 서브쿼리 평탄화 패스다. WHERE 결합항이
attr op (SELECT ...) 형태이고, 연산자가 =, IN, =SOME,
>SOME, >=SOME, <SOME, <=SOME 중 하나이며, 서브쿼리 컬럼이
인덱싱 가능하고, 서브쿼리가 비상관 (correlation_level == 0) 일
때, 패스는 다음을 한다.
- 서브쿼리를 파생 테이블 spec 으로 감싸 FROM 리스트에 덧붙인다.
- 연산자를
=,>,>=,<,<=중 하나로 바꾼다. *SOME변형의 경우, 서브쿼리 select 리스트를 원래 컬럼의MIN()이나MAX()로 교체한다 —> SOME은> MIN,< SOME은< MAX가 된다.
이 변환은 외곽 행마다 한 번씩 도는 상관 lookup (서브쿼리 실행) 을 파생 테이블 조인 한 번으로 바꿔 버린다. 그러면 조인 열거기가 인덱스로 구동할 수 있다.
qo_rewrite_select_queries (query_rewrite_select.c) 는 세 가지
일을 순서대로 한다. (i) 옵션 측에 null-rejecting 술어가 지배하는
외부 조인을 내부 조인으로 재작성한다 (qo_rewrite_outerjoin).
(ii) 조인이 유일 키이고 참조되지 않을 때 외부 조인된 테이블을
줄인다 (qo_reduce_outer_joined_tbls). (iii) 컬럼이 사영되지 않은
FK→PK 조인 테이블을 제거한다 (qo_reduce_joined_tbls_ref_by_fk).
뒤의 둘이 불필요 조인 제거 패스다.
3단계 — 자동 파라미터화
섹션 제목: “3단계 — 자동 파라미터화”모든 구조적 재작성이 끝나면 qo_auto_parameterize 가 각 술어
리스트를 걸어가며 리터럴 상수를 입력 호스트 변수 (입력 마커) 로
교체한다. 동기는 XASL 계획 캐시 다. 내려진 파스 트리에 키를 둔
캐시 계획은 리터럴 값만 다른 실행들 사이에서 재사용 가능하므로
캐시 hit률이 극적으로 좋아진다. 이 패스는 prm_get_integer_value (PRM_ID_XASL_CACHE_MAX_ENTRIES) > 0 이고
node->flag.cannot_prepare == 0 일 때 게이팅되며, 파서가 정적
SQL 모드도 skip-auto-parameterize 모드도 아닌 경우에만 돈다.
// qo_auto_parameterize_limit_clause — query_rewrite_auto_parameterize.c (condensed)voidqo_auto_parameterize_limit_clause (PARSER_CONTEXT * parser, PT_NODE * node){ PT_NODE *limit_offsetp, *limit_row_countp;
/* Pull (offset, row_count) out of the per-statement-type slot. */ // ... condensed: switch on node_type, set *_offsetp, *_row_countp ...
/* Replace each literal with `pt_rewrite_to_auto_param` (creates an input host var). */ if (limit_offsetp && pt_is_const_not_hostvar (limit_offsetp)) new_limit_offsetp = pt_rewrite_to_auto_param (parser, limit_offsetp); if (limit_row_countp && pt_is_const_not_hostvar (limit_row_countp)) new_limit_row_countp = pt_rewrite_to_auto_param (parser, limit_row_countp);
/* Splice back into the slot. */ // ... condensed: switch on node_type again to write back ...}주목할 점은 qo_auto_parameterize_limit_clause 가 원래의
node->info.query.limit 슬롯을 작동한다는 것이다 — 내려진
번호 매김 술어가 이미 만들어져 있는데도 그렇다. 재작성 이후 두
사본이 모두 존재한다. limit 슬롯에는 진단·인쇄 용도의 호스트
변수화된 LIMIT ? 가 남고, 내려진 INST_NUM() <= ? (역시
WHERE 리스트를 도는 qo_auto_parameterize 에 의해 호스트 변수화된)
는 XASL이 실제로 평가하는 값이다. 원본 분석의 “NOTE: Dummy로 가지고
있고…” 주석이 가리키는 것이 이 부분이다 — 더미 LIMIT 보존이
옳은 선택인지에 대한 질문은 거기서도 열려 있고, 본 문서에서도
열려 있다.
예제, 처음부터 끝까지
섹션 제목: “예제, 처음부터 끝까지”원본 분석은 예제
SELECT code, name INTO c, m FROM athlete WHERE gender = 'M' AND nation_code = 'KOR' LIMIT 1 을 세 단계에 걸쳐 추적한다.
검증된 소스로 다시 돌리면 다음과 같다.
1단계 — 파싱. SELECT ... LIMIT 1 의 문법 프로덕션이
node->info.query.limit = PT_VALUE(1) 과
node->info.query.flag.rewrite_limit = 1 을 기록한다. 원래의
gender = 'M' AND nation_code = 'KOR' 술어는 q.select.where 에
들어간다.
2단계 — 의미 검사 (pt_eval_type_pre, PT_SELECT 처리부).
ORDER BY, GROUP BY, DISTINCT가 모두 없으므로 fall-through 처리부가
실행된다.
expr_pred = &node->info.query.q.select.where;limit = pt_limit_to_numbering_expr (parser, node->info.query.limit, PT_INST_NUM, false);pt_limit_to_numbering_expr 는 PT_LE(PT_INST_NUM(), PT_VALUE(1))
을 돌려주고, 이는 q.select.where 에 덧붙는다. 기존의 gender,
nation_code 결합항과 새 inst_num() <= 1 결합항이 이제 모두
WHERE 리스트에 있다. rewrite_limit 플래그는 클리어된다.
PT_CONVERT_RANGE 포맷팅으로 트리를 인쇄하면 다음이 나온다.
SELECT [public.athlete].code, [public.athlete].[name]FROM [public.athlete] [public.athlete]WHERE (inst_num() <= 1) AND [public.athlete].gender = 'M' AND [public.athlete].nation_code = 'KOR'3단계 — mq_rewrite. CNF 정규화 (이미 CNF), 동등성 축약 (여기 선 기회 없음), 그리고 모든 리터럴의 자동 파라미터화.
SELECT [public.athlete].code, [public.athlete].[name]FROM [public.athlete] [public.athlete]WHERE (inst_num() <= ?:0) AND [public.athlete].gender = ?:1 AND [public.athlete].nation_code = ?:2이제 계획 캐시 키는 1, ‘M’, ‘KOR’ 같은 구체 값에 무감각해진다.
계획 생성 시점의 multi-range LIMIT 최적화
섹션 제목: “계획 생성 시점의 multi-range LIMIT 최적화”내려진 LIMIT는 최적화기가 두 가지 방식으로 소비한다. 첫째는
일반적인 방식이다 — INST_NUM() <= n 이 스캔 술어 평가에 참여
하고, 충분한 행이 모이면 실행기가 스캔 호출을 멈춘다. 둘째는
ORDER BY 가 있는 인덱스 스캔 의 특수 케이스다. 정렬이 인덱스의
prefix 와 일치하고 LIMIT가 작을 때, CUBRID는 인덱스 범위 우선
순위 큐 로 스캔을 구동해, 각 범위에서 LIMIT를 만족시킬 만큼만
평가하고 멈출 수 있다. 이 최적화가 multi-range optimization
(MRO) 이며 qo_check_iscan_for_multi_range_opt
(plan_generation.c) 에 산다.
// qo_check_iscan_for_multi_range_opt — src/optimizer/plan_generation.c (condensed)boolqo_check_iscan_for_multi_range_opt (QO_PLAN * plan){ if (!qo_is_iscan (plan)) return false; if (QO_NODE_IS_CLASS_HIERARCHY (plan->plan_un.scan.node)) return false;
query = QO_ENV_PT_TREE (env); if (!PT_IS_SELECT (query)) return false; if (query->info.query.q.select.hint & PT_HINT_NO_MULTI_RANGE_OPT) return false; if (pt_has_aggregate (parser, query)) return false; /* CBRD-22696 */ if (query->info.query.order_by == NULL) return false; if (query->info.query.all_distinct == PT_DISTINCT) return false; if (query->info.query.orderby_for == NULL) return false; /* must have lowered LIMIT */
/* the order_by columns must occupy consecutive index positions, in matching * direction (or all reversed), starting at first_col_idx_pos */ // ... build orderby_nodes from select_list using sort_spec.pos_descr.pos_no ... qo_check_plan_index_for_multi_range_opt (orderby_nodes, order_by, plan, &can_optimize, &first_col_idx_pos, &reverse);
/* scan terms / key filter terms must allow MRO; subqueries must not depend on the LIMIT */ qo_check_terms_for_multiple_range_opt (plan, first_col_idx_pos, &can_optimize); can_optimize = qo_check_subqueries_for_multi_range_opt (plan, first_col_idx_pos);
/* upper bound must be < PRM_ID_MULTI_RANGE_OPT_LIMIT system parameter */ if (!pt_check_ordby_num_for_multi_range_opt (parser, query, &env->multi_range_opt_candidate, NULL)) return false;
return true;}MRO 술어는 내림 이후 모양을 명시적으로 검사한다. orderby_for
가 비어 있지 않다는 것은 pt_eval_type_pre 가 LIMIT를 ORDERBY_NUM
슬롯으로 내렸다는 뜻이고, 이 슬롯이 MRO가 작동할 수 있는 유일한
슬롯이기 때문이다. 뒤집어 말하면, INST_NUM 과 GROUPBY_NUM 내림에
대해서는 MRO가 죽어 있다 — 인덱스 기반 정렬 위에 LIMIT가 앉을
때만 적용된다는 점이다.
pt_check_ordby_num_for_multi_range_opt 술어 자체
(parser_support.c:9743) 는 술어 모양을 검사한다. 상한이
prm_get_integer_value (PRM_ID_MULTI_RANGE_OPT_LIMIT)
(기본값은 system_parameter.c 에 있다) 보다 작아야 하고, 술어가
ORDERBY_NUM <= n 이나 ORDERBY_NUM < n, n > ORDERBY_NUM,
n >= ORDERBY_NUM 의 AND 결합 형태여야 한다. 하한은 허용되지
않는다 — MRO는 우선 순위 큐의 앞쪽 행을 효율적으로 건너뛸 방법이
없기 때문이다.
왜 재작성 시점에 내리는가가 중요한가
섹션 제목: “왜 재작성 시점에 내리는가가 중요한가”LIMIT가 mq_rewrite 시점에 이미 술어라는 사실로부터 세 가지
파급 효과가 떨어져 나온다.
- CNF와 동등성 축약이 추가 비용 없이 LIMIT에 작동한다.
INST_NUM() <= n결합항은 평범한 CNF 항이다. 사용자가LIMIT 1을 썼고 앞선 재작성이 다른 결합항을 항진식으로 접어 버렸다면, CNF 단순화기는 살아남은 것을 특별한 LIMIT 절이 아니라 단지inst_num() <= 1로 본다. - 자동 파라미터화도 추가 비용 없이 LIMIT에 작동한다. 리터럴
n은 사용자가 쓴 다른 상수들을 처리하는 동일 walk 가 인식해 호스트 변수 마커로 교체한다. - 인덱스 조건 계획에는 LIMIT가 그저 또 하나의 술어로 보인다. 인덱스 기반 평가를 식별하는 최적화기의 일반 기계는 LIMIT 전용 코드 경로가 필요 없다. LIMIT-인지 가 필요한 곳은 MRO 기계 하나뿐이며, 그것도 옵트인이다.
이 설계의 대가는 앞서 언급한 이중성이다. 원본 info.query.limit
필드가 보존된다 (인쇄용, 그리고 keylimit 등 하류 코드 경로가 쓰는
offset/row_count 호스트 변수의 자동 파라미터화 용도). 내려진 후에
는 사실상 더미인데도 그렇다. 치트시트는 이를 미해결 질문으로
표시했고, 소스도 답을 주지 않는다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”심볼명을 anchor 로 삼는다 — 라인번호가 아니다. CUBRID 소스는 변한다. 본 섹션 끝의 위치 표는 문서의
updated:시점에만 유효하다.
파스 트리 슬롯과 플래그
섹션 제목: “파스 트리 슬롯과 플래그”pt_query_info::limit(parse_tree.h) — 사용자가 작성한LIMIT [offset,] row_count체인.PT_VALUE/PT_HOST_VAR/PT_EXPR노드의 리스트. 내림 이후에도 살아남는다.pt_query_info::flag.rewrite_limit:1(parse_tree.h) — LIMIT 존재 시 문법이 세트하고,pt_eval_type_pre의 내림 처리부 (혹은 UNION-without-ORDER-BY 사례에서는qo_rewrite_queries) 가 클리어한다.pt_delete_info::limit/pt_delete_info::rewrite_limit:1(parse_tree.h) — DELETE 측의 동일한 짝.pt_update_info::limit/pt_update_info::rewrite_limit:1(parse_tree.h) — UPDATE 측의 동일한 짝.PT_INST_NUM/PT_ORDERBY_NUM(parse_tree.h) —PT_OP_TYPEenum 안의 두 번호 매김 연산자.PT_GROUPBY_NUM(parse_tree.h) —FUNC_TYPEenum 안의 번호 매김 함수 (함수와 연산자는 PT_NODE 모양이 다르며, GROUPBY_NUM 은 그룹화 후 평가되므로 함수다).PT_EXPR_INFO_GROUPBYNUM_LIMIT(parse_tree.h) — GROUPBY_NUM 기반으로 내려진 LIMIT 의 각 결합항에 세트되는 플래그. HAVING-to- WHERE 이동 로직이 이를 보고 옮기지 않는다.
문법 측 LIMIT 처리
섹션 제목: “문법 측 LIMIT 처리”csql_grammar.y—flag.rewrite_limit = 1을 기록하는 세 프로덕션 — SELECT/UNION limit 프로덕션, UPDATE limit 프로덕션 (update.rewrite_limit = 1세트), DELETE limit 프로덕션 (delete_.rewrite_limit = 1세트).
의미 검사 진입점
섹션 제목: “의미 검사 진입점”pt_compile(compile.c) — 최상위 진입;pt_semantic_check를 부르고 돌아온다. 파스 시점 에러용 longjmp 환경을 소유한다.pt_semantic_check/pt_check_with_info(semantic_check.c) — 노드 타입별 의미 검사.pt_resolve_names,pt_check_where,pt_semantic_type으로 디스패치.pt_semantic_type(type_checking.c) — 트리를pt_eval_type_pre와pt_eval_type을 도는 pre/post walk 의 소유자. LIMIT 내림이 사는 곳.pt_eval_type_pre(type_checking.c) — pre-walk 콜백; LIMIT 내림 결정 트리를 담고 있다 (PT_SELECT, PT_UPDATE, PT_DELETE, PT_UNION/PT_DIFFERENCE/PT_INTERSECTION 처리부).pt_limit_to_numbering_expr(parser_support.c) —lhs <= row_count또는lhs > offset AND lhs <= offset + row_count를 짓는 내림 팩토리.
뷰 인라이닝과 재작성 드라이버
섹션 제목: “뷰 인라이닝과 재작성 드라이버”mq_translate(view_transform.c) — 최상위 뷰 인라이닝 + 인라이닝 후 재작성 진입. SELECT 형태 statement 를mq_rewrite를 부른다.mq_rewrite(query_rewrite.c) — 재작성 계층의 parser_walk_tree 드라이버.qo_rewrite_queries(pre) 와qo_rewrite_queries_post(post) 를 감싼다.qo_rewrite_queries(query_rewrite.c) — 3단계 재작성 디스패처 (pre-rewrite, 최적화 재작성, 자동 파라미터화). 500여 줄의 본체.qo_rewrite_queries_post(query_rewrite.c) — 외부 조인의 ON 절 술어를 조인 spec 으로 되돌리고, COPYPUSH 항을 제거한다.
서브쿼리 평탄화
섹션 제목: “서브쿼리 평탄화”qo_rewrite_subqueries(query_rewrite_subquery.c) — WHERE 의 비상관 서브쿼리를 파생 테이블 조인으로 평탄화.qo_rewrite_hidden_col_as_derived(query_rewrite_subquery.c) — 서브쿼리의 ORDER BY 가 외곽 사영 측에서 hidden 컬럼을 잃을 경우, 정렬 보존을 위해 파생 테이블로 감싼다.qo_add_limit_clause(query_rewrite_subquery.c) — EXISTS 재작성에서 사용하는 주입. 서브쿼리가LIMIT 1의 이득을 본다면 붙여 준다.rewrite_limit을 1로 다시 올려, 잔존 UNION-arm 경로가 그것을 집어 가도록 만든다.mq_rewrite_query_as_derived(view_transform.c) — 질의를 합성 외곽 SELECT 의 FROM 리스트 파생 테이블로 감싼다.mq_make_derived_spec(view_transform.c) — 파생 테이블용 PT_SPEC 노드와 새 속성 이름을 짓는다.
CNF 와 동등성 축약
섹션 제목: “CNF 와 동등성 축약”pt_cnf(cnf.c) — 술어 리스트를 합접 정규형으로 변환, 태깅 가능한 항에 태그.pt_do_cnf(cnf.c) — statement 의 모든 술어 보유 노드를 도는 최상위 CNF 래퍼.qo_reduce_equality_terms/qo_reduce_equality_terms_post(query_rewrite_term.c) —WHERE x = 5 AND p(x)→WHERE x = 5 AND p(5).QO_CHECK_AND_REDUCE_EQUALITY_TERMS(query_rewrite.h) — 같은 노드를 축약을 재실행하지 않게 막는 가드 매크로.qo_rewrite_terms(query_rewrite_term.c) — 절별 범위 병합, OR-to-IN 승격, 중복 disjunct 제거.
외부 조인 축소와 불필요 조인 제거
섹션 제목: “외부 조인 축소와 불필요 조인 제거”qo_rewrite_select_queries(query_rewrite_select.c) — SELECT 전용 구조 재작성의 드라이버.qo_rewrite_outerjoin(query_rewrite_select.c) — null-rejecting 술어가 지배할 때 외부 조인을 내부 조인으로 승격.qo_rewrite_innerjoin(query_rewrite_select.c) — 대칭적 정리.qo_reduce_outer_joined_tbls(query_rewrite_select.c) — 유일 키이고 사영되지 않은 외부 조인 테이블 제거.qo_reduce_joined_tbls_ref_by_fk(query_rewrite_select.c) — 부모 컬럼이 참조되지 않는 FK→PK 조인 제거.qo_reduce_predicate_for_parent_spec(query_rewrite_select.c) — 조인이 줄어든 후 살아남는 술어를 보정하는 동반자.
LIMIT 전용 헬퍼
섹션 제목: “LIMIT 전용 헬퍼”qo_push_limit_to_union(query_rewrite_set.c) — 최상위에 ORDER BY 도 DISTINCT 도 NO_PUSH_PRED 힌트도 없을 때 UNION 의 각 가지에 LIMIT 를 복사.qo_check_distinct_union/qo_check_hint_union(query_rewrite_set.c) —qo_push_limit_to_union의 안전성 술어들.qo_auto_parameterize_limit_clause(query_rewrite_auto_parameterize.c) — 보존된 LIMIT 호스트 변수의 자동 파라미터화.qo_auto_parameterize_keylimit_clause(query_rewrite_auto_parameterize.c) — 인덱스별 keylimit 힌트의 자동 파라미터화.qo_auto_parameterize(query_rewrite_auto_parameterize.c) — CNF 항에 대한 일반 리터럴 → 호스트 변수 패스.pt_rewrite_to_auto_param(parse_tree_cl.c) — 개별 리터럴 → 호스트 변수 변환.
Multi-range LIMIT 최적화 (계획 생성 시점)
섹션 제목: “Multi-range LIMIT 최적화 (계획 생성 시점)”qo_check_iscan_for_multi_range_opt(plan_generation.c) — 이 인덱스 스캔이 MRO를 쓸 수 있는가 술어;query.orderby_for비공허 (즉 내림 후 모양) 를 게이트.qo_check_plan_index_for_multi_range_opt(plan_generation.c) — ORDER BY 컬럼이 일치 방향으로 연속된 인덱스 위치를 차지하는지 검사.pt_check_ordby_num_for_multi_range_opt(parser_support.c) — 내려진ORDERBY_NUM ≤ n술어 모양과PRM_ID_MULTI_RANGE_OPT_LIMIT시스템 파라미터 상한 검사.qo_plan_multi_range_opt(plan_generation.c) — 완성된 계획 에 MRO 가 실제로 적용되었는지 여부를 알려 주는 boolean.PRM_ID_MULTI_RANGE_OPT_LIMIT(system_parameter.h) — MRO 자격을 위한n의 상한.
문장별 컴파일 드라이버
섹션 제목: “문장별 컴파일 드라이버”db_compile_statement_local(db_vdb.c) — 파이프라인 최상단 드라이버 — 파서 →pt_class_pre_fetch→pt_compile→mq_translate→pt_class_pre_fetch재실행 →do_prepare_statement. 재작성 계층이 더 큰 컴파일 흐름의 어디에 끼어드는지 보여 준다.
이 개정 시점의 위치 힌트 (2026-04-30 기준)
섹션 제목: “이 개정 시점의 위치 힌트 (2026-04-30 기준)”| 심볼 | 파일 | 라인 |
|---|---|---|
pt_query_info::flag.rewrite_limit:1 | src/parser/parse_tree.h | 2141 |
pt_delete_info::rewrite_limit:1 | src/parser/parse_tree.h | 2850 |
pt_update_info::rewrite_limit:1 | src/parser/parse_tree.h | 2972 |
PT_INST_NUM / PT_ORDERBY_NUM enumerator | src/parser/parse_tree.h | 1399 |
pt_compile | src/parser/compile.c | 381 |
pt_class_pre_fetch | src/parser/compile.c | 432 |
pt_eval_type_pre (PT_SELECT LIMIT 처리부) | src/parser/type_checking.c | 7179 |
pt_eval_type_pre (PT_UNION LIMIT 처리부) | src/parser/type_checking.c | 7138 |
pt_eval_type_pre (PT_DELETE LIMIT 처리부) | src/parser/type_checking.c | 7242 |
pt_eval_type_pre (PT_UPDATE LIMIT 처리부) | src/parser/type_checking.c | 7274 |
pt_limit_to_numbering_expr | src/parser/parser_support.c | 4567 |
pt_check_ordby_num_for_multi_range_opt | src/parser/parser_support.c | 9743 |
pt_cnf | src/parser/cnf.c | 941 |
pt_do_cnf | src/parser/cnf.c | 1183 |
mq_translate | src/parser/view_transform.c | (top) |
mq_rewrite | src/optimizer/rewriter/query_rewrite.c | 736 |
qo_rewrite_queries | src/optimizer/rewriter/query_rewrite.c | 44 |
qo_rewrite_queries_post | src/optimizer/rewriter/query_rewrite.c | 586 |
qo_rewrite_queries (PT_UNION 잔존 LIMIT 분기) | src/optimizer/rewriter/query_rewrite.c | 173 |
qo_rewrite_subqueries | src/optimizer/rewriter/query_rewrite_subquery.c | 40 |
qo_rewrite_hidden_col_as_derived | src/optimizer/rewriter/query_rewrite_subquery.c | 331 |
qo_add_limit_clause | src/optimizer/rewriter/query_rewrite_subquery.c | 473 |
qo_push_limit_to_union | src/optimizer/rewriter/query_rewrite_set.c | 40 |
qo_check_distinct_union | src/optimizer/rewriter/query_rewrite_set.c | 101 |
qo_check_hint_union | src/optimizer/rewriter/query_rewrite_set.c | 132 |
qo_rewrite_select_queries | src/optimizer/rewriter/query_rewrite_select.c | 62 |
qo_rewrite_index_hints | src/optimizer/rewriter/query_rewrite_select.c | 151 |
qo_move_on_of_explicit_join_to_where | src/optimizer/rewriter/query_rewrite_select.c | 516 |
qo_reduce_outer_joined_tbls | src/optimizer/rewriter/query_rewrite_select.c | 1909 |
qo_reduce_joined_tbls_ref_by_fk | src/optimizer/rewriter/query_rewrite_select.c | 2105 |
qo_rewrite_outerjoin | src/optimizer/rewriter/query_rewrite_select.c | 3382 |
qo_rewrite_nonnull_count_select_list | src/optimizer/rewriter/query_rewrite_select.c | 3777 |
qo_auto_parameterize | src/optimizer/rewriter/query_rewrite_auto_parameterize.c | 41 |
qo_auto_parameterize_limit_clause | src/optimizer/rewriter/query_rewrite_auto_parameterize.c | 129 |
qo_auto_parameterize_keylimit_clause | src/optimizer/rewriter/query_rewrite_auto_parameterize.c | 293 |
qo_check_iscan_for_multi_range_opt | src/optimizer/plan_generation.c | 4030 |
qo_check_plan_index_for_multi_range_opt | src/optimizer/plan_generation.c | 4199 |
qo_plan_multi_range_opt | src/optimizer/plan_generation.c | 3141 |
db_compile_statement_local | src/compat/db_vdb.c | 531 |
PRM_ID_MULTI_RANGE_OPT_LIMIT | src/base/system_parameter.h | 295 |
소스 검증
섹션 제목: “소스 검증”- 원본 분석은
qo_optimize_queries가 재작성 단계를 돌린다고 쓴다. 그 심볼은 더 이상 존재하지 않는다. 실제 드라이버는mq_rewrite(query_rewrite.c:736) 이고, 이는qo_rewrite_queries(query_rewrite.c:44) 를 parser_walk_tree 의 pre 콜백으로 부른다. 함수명이 PDF 시점의 원래 optimize 프레이밍이 있었던 것이며, 소스는 이후 rewrite 로 이름을 바꾸었다 (최적화기 본체는 이제query_planner.c와plan_generation.c에 산다). - 치트시트의
qo_do_auto_parameterize는 이제qo_auto_parameterize다. WHERE 워커와 LIMIT 절 헬퍼 모두 재작성 계층이src/optimizer/rewriter/로 모듈화되면서 이름이 바뀌었다. 동작은 동일하다. - 치트시트는
is_parsing_static_sql/is_skip_auto_parameterize파라미터 검사를 LIMIT 절 헬퍼에만 적는다. 현재 소스는 일관되게 적용한다 — WHERE 워킹 변형은qo_rewrite_queries의call_auto_parameterize가 게이트하며, 같은 조건을 평가한다. - 원본 분석은 LIMIT 내림을 의미 검사 안의 단일 단계로 다룬다.
SELECT, UPDATE, DELETE 에 대해서는 사실이다.
UNION/DIFFERENCE/INTERSECTION 은 갈라진다 — ORDER BY 가 있는
경우는
pt_eval_type_pre에서 내려지지만, ORDER BY 없는 경우는qo_rewrite_queries에 미뤄진다 (PT_UNION 처리부가 받아서 각 arm 에 limit 를 푸시까지 한다). 치트시트는 UNION 의 “ORDER BY 가 있는 경우” 분기를 인정하지만 잔존 처리는 다루지 않는다. - PDF 는 SELECT 를
pt_eval_type안의where_type()/eval_expr_type()워커들을 인용한다.pt_eval_type_prepre-callback 에서 발화되는 LIMIT 내림 결정 트리는 그 워커들 전에 돈다 — 즉 주변 WHERE 를 타입 평가가 시작될 시점에는 LIMIT 가 이미 번호 매김 술어다. - 원본 분석이 제기한 Dummy LIMIT 호스트 변수 질문 —
내림 후 원래 LIMIT 를 유지할 것인가 — 은 소스에서도 여전히
미해결이다. 의미 검사 처리부와
qo_auto_parameterize_limit_ clause가 모두 원본info.query.limit을 보존한다. 가장 그럴 듯한 이유는 parser_print_tree 다 — 연산자가 진단용으로 원본 LIMIT 를 dump 한다. 그러나 자동 파라미터화 이후 어떤 진단 모드도 이 필드를 읽지 않는다면 그 필드는 죽은 것이다. 이를 확정하려면mq_rewrite이후info.query.limit의 모든 reader 를 감사해야 한다.
미해결 질문
섹션 제목: “미해결 질문”mq_rewrite가 정확히mq_translate안에서 어떻게 호출 되는가? 원본 치트시트는mq_translate가 SELECT 를mq_rewrite를 부른다고만 스케치한다. 하지만mq_translate에는 타입별 변형이 여럿 있다 (mq_translate_select,mq_translate_update,mq_translate_delete,mq_translate_merge). 각 변형이mq_rewrite로 들어가는지 추적하면, DELETE 와 UPDATE 의 LIMIT 가qo_rewrite_queries에 다시 진입하는지, 아니면 의미 검사 시점 내림만으로 끝나는지 가 분명해진다.- 센티넬로서의
PT_LAST_OPCODE. GROUP BY 처리부는num_op로PT_LAST_OPCODE를 넘긴다. 헬퍼는is_gby_num이 true 일 때num_op를 무시하므로 (함수 노드를 만들지 expr 노드를 만들지 를 결정하기 때문에) 값 자체는 무해하다. 그러나 이 센티넬이 어딘가 로 새어 — 예컨대 복사된 표현 안으로 — 하류의op스위치를 혼란시킬 가능성이 있는가? - 두 가지 LIMIT 표현 트레이드오프. 왜
qo_auto_parameterize_limit_clause가 원본info.query.limit을 번호 매김 술어 경로와 별도로 파라미터화 하는가? 내려진 술어는 이미 값을 들고 있다 (WHERE 워커가 자동 파라미터화한다). 인쇄, keylimit, 또는 원본 분석이 식별하지 못한 다른 경로 때문에 이 이중 표현이 여전히 필요한가? - CONNECT BY / 계층 질의와의 상호작용. ORDER BY 처리부의
order_siblings거부는ORDERBY_NUM으로의 내림을 막아 LIMIT 가info.query.limit에 남게 한다.qo_rewrite_queries가 이를 다시 집어 가는가, 아니면 내림되지 않은 LIMIT 인 채로 XASL 생성까지 흘러가서 다른 메커니즘으로 평가되는가? 치트시트는 CONNECT BY 를 분석 생략 으로 다루었다 — 소스 경로 추적이 필요하다. - MRO 와
PRM_ID_MULTI_RANGE_OPT_LIMIT의 기본값. 이 시스템 파라미터의 기본값이 MRO 의 이득을 누릴 수 있는 LIMIT 의 크기를 결정한다. 기본값은system_parameter.c엔트리 (3197 라인) 에서 확인이 필요하다.LIMIT 1000이 들어오는 워크로드는 기본값 에 따라 MRO 를 트리거할 수도 있고 못할 수도 있다. - DBLINK 와 원격 DML 의 LIMIT.
db_compile_statement_local는 DBLINK DML 질의 (PT_IS_DBLINK_DML_QUERY) 에서mq_translate를 건너뛴다. LIMIT 도 그 경로에서는 내려지지 않는다 — 원격 서버가 직접 파싱·실행할 것을 기대하기 때문이다. 그러나 로컬 측 최적화를 위해 LIMIT 내림이 필요한 DBLINK SELECT 를 로컬 재작성 계층이 보게 되는 경우가 있는가? DBLINK SELECT 의 문법 프로덕션과pt_rewrite_for_dblink호출이 단서다.
원본 분석 (raw/code-analysis/cubrid/query-processing/)
섹션 제목: “원본 분석 (raw/code-analysis/cubrid/query-processing/)”code_analysis_Query_rewrite-LIMIT_clause.pdf— LIMIT 절 재작성에 초점을 맞춘 한국어 분석. statement-type 별 결정 트리와select ... LIMIT 1추적 예제를 담고 있다.code_analysis_Query_Processing_Cheat_Sheet.pdf— 파서·의미 검사 전역 지도. LIMIT 내림이pt_eval_type_pre안에 있고 자동 파라미터화가 (그 자료가 부르는 이름인)qo_optimize_queries안에 있음을 위치시킨다._converted/codeanalysisqueryrewrite-limitclause.pdf.md— LIMIT PDF 의 markitdown 추출._converted/codeanalysisqueryprocessingcheatsheet.pdf.md— 치트 시트 PDF 의 markitdown 추출.
형제 문서
섹션 제목: “형제 문서”knowledge/code-analysis/cubrid/cubrid-btree.md— 내려진INST_NUM/ORDERBY_NUM술어가 가드하는 인덱스 스캔. MRO 가 같은 계획 생성 이웃에 산다.knowledge/code-analysis/cubrid/cubrid-mvcc.md— 내려진 술어를 통과한 각 행을 스캔이 적용하는 가시성 검사.
교과서 챕터 / 논문
섹션 제목: “교과서 챕터 / 논문”- Database Internals (Petrov), 12장 “Query Planning and Optimization” — 규칙 기반 vs 비용 기반 분리, 술어 푸시다운, 뷰 인라이닝.
- Database System Concepts (Silberschatz, Korth, Sudarshan, 7판), 16장 Query Optimization — 동치 규칙, 관계 대수 차원의 변환 카탈로그.
- Goetz Graefe, The Cascades Framework for Query Optimization, IEEE Data Eng. Bull. 18(3), 1995 — 변환 규칙(항상 개선되는 재작성)과 구현 규칙(비용을 매겨야 하는 대안)의 형식적 분리.
- Goetz Graefe, Query Evaluation Techniques for Large Databases, ACM Computing Surveys 25(2), 1993 — CUBRID MRO 가 계승하는 인덱스 기반 LIMIT 최적화의 역사적 맥락.
CUBRID 소스 (/data/hgryoo/references/cubrid/)
섹션 제목: “CUBRID 소스 (/data/hgryoo/references/cubrid/)”src/parser/csql_grammar.y— SELECT/UNION/UPDATE/DELETE LIMIT 절에rewrite_limit = 1을 세트하는 문법 프로덕션.src/parser/parse_tree.h— LIMIT 보유 struct 들과PT_INST_NUM/PT_ORDERBY_NUM/PT_GROUPBY_NUM열거형.src/parser/compile.c—pt_compile진입; 의미 검사 드라이버.src/parser/type_checking.c—pt_semantic_type와pt_eval_type_pre; LIMIT 내림 결정 트리.src/parser/parser_support.c—pt_limit_to_numbering_expr내림 팩토리;pt_check_ordby_num_for_multi_range_optMRO 술어 모양 검사.src/parser/cnf.c—pt_cnf,pt_do_cnf.src/parser/view_transform.c—mq_translate(뷰 인라이닝) 과mq_rewrite_query_as_derived/mq_make_derived_spec(서브쿼리 평탄화 및 UNION-with-LIMIT 가 사용하는 파생 테이블 래퍼).src/optimizer/rewriter/query_rewrite.c—mq_rewrite와qo_rewrite_queries; 재작성 계층 드라이버.src/optimizer/rewriter/query_rewrite_subquery.c—qo_rewrite_subqueries,qo_rewrite_hidden_col_as_derived,qo_add_limit_clause.src/optimizer/rewriter/query_rewrite_set.c—qo_push_limit_to_union,qo_check_distinct_union,qo_check_hint_union.src/optimizer/rewriter/query_rewrite_select.c—qo_rewrite_select_queries, 외부 조인 축소 및 불필요 조인 헬퍼들;qo_rewrite_index_hints.src/optimizer/rewriter/query_rewrite_term.c—qo_reduce_equality_terms,qo_rewrite_terms.src/optimizer/rewriter/query_rewrite_auto_parameterize.c—qo_auto_parameterize,qo_auto_parameterize_limit_clause,qo_auto_parameterize_keylimit_clause.src/optimizer/plan_generation.c—qo_check_iscan_for_multi_range_opt와 내려진 ORDERBY_NUM 술어를 소비하는 MRO 기계.src/base/system_parameter.{c,h}—PRM_ID_MULTI_RANGE_OPT_LIMIT와 관련 파라미터.src/compat/db_vdb.c—db_compile_statement_local. 파서 → 의미 검사 → 뷰 인라이닝 → 재작성 → prepare 까지 엮는 statement 단위 컴파일 드라이버.