콘텐츠로 이동

(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.cplan_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 시점에 다시 들여다본다.

PostgreSQL의 subquery_planner (src/backend/optimizer/plan/ planner.c) 는 정해진 순서의 재작성 패스 시퀀스로 구성되어 있다. pull_up_sublinksinline_set_returning_functionspull_up_subqueriesflatten_simple_union_allreduce_outer_joinsremove_useless_result_rtespreprocess_expression (상수 접기, canonicalize_qual, eval_const_expressions) → preprocess_qual_conditions 의 순서다. LIMIT는 parse->limitOffset / parse->limitCount 로 살아 있고, 실행기의 ExecLimit 노드가 소비한다 — Postgres는 재작성 시점에 LIMIT를 행 카운팅 술어로 내리지 않는다. 계획 트리 최상단의 연산자로 끝까지 남겨 둔다.

MySQL의 최적화기 진입점 JOIN::optimizeoptimize_cond (CNF + 상수 접기 패스), prune_partitions, make_join_statistics, optimize_keyuse 를 차례로 부르고 마지막에 비용 기반 조인 순서를 잡는다. MySQL의 LIMIT n 도 계획 최상단의 연산자다 (SELECT_LEXlimit 필드). 재작성 계층이 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가 계획의 바닥에 영향을 준다) 의 사이 어딘가에 위치한다. 이 선택의 결과는 둘이다.

  1. LIMIT는 추가 비용 없이 CNF, 상수 접기, 자동 파라미터화에 참여한다. 이들 패스가 돌 시점에는 LIMIT가 이미 우변 PT_VALUE 를 가진 PT_LE 표현이기 때문이다.
  2. 반대로 최적화기는 multi-range 인덱스 스캔 LIMIT 최적화를 적용하려 할 때 내려진 형태를 다시 알아보아야 한다. CUBRID는 이 작업을 qo_check_iscan_for_multi_range_opt (plan_generation.c) 에서 수행하며, 내림 이후 슬롯인 query->info.query.orderby_for 를 검사해, LIMIT가 스캔 시점에 multi-range 최적화를 구동할 수 있는 모양인지 판단한다.

재작성을 둘러싼 질의 처리 파이프라인은 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)
int
db_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 절이 처음 받는 재작성은 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.c
case 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_treeflag.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_NUMPT_EXPR — 스캔 후 행 수 카운트.
  • op = PT_ORDERBY_NUMPT_EXPR — 정렬 후 행 수 카운트.
  • function_type = PT_GROUPBY_NUMPT_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, 10inst_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 패스는 다음을 해야 한다.

  1. 의미 검사에서 내려지지 않은 잔존 LIMIT를 다시 내린다 (주로 UNION 경우).
  2. 술어를 CNF 정규화한다 (이제는 의미 검사에서 내려진 번호 매김 술어들을 포함한다).
  3. 안전한 자리로 술어를 내려보낸다.
  4. 비상관 서브쿼리를 파생 테이블 조인으로 평탄화한다.
  5. 동등성 항을 축약한다 (등호를 통한 상수 전파).
  6. 안전한 곳에서 외부 조인을 내부 조인으로 축소한다.
  7. XASL 계획 캐시를 위해 상수를 자동 파라미터화한다.

드라이버는 mq_rewrite 다. 이 함수는 statement를 parser_walk_tree 를 돌리되, pre 콜백으로 qo_rewrite_queries, post 콜백으로 qo_rewrite_queries_post 를 사용한다.

// mq_rewrite — src/optimizer/rewriter/query_rewrite.c
PT_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여 줄 디스패치다.

각 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.c
case 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단계는 OPTIMIZATION_ENABLED(level) 이 참일 때만 돈다 (보통의 질의에서는 참이며, 디버깅용으로 optimization_level 파라미터로 끌 수 있다).

// qo_rewrite_queries (phase 2 sketch) — query_rewrite.c
if (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) 일 때, 패스는 다음을 한다.

  1. 서브쿼리를 파생 테이블 spec 으로 감싸 FROM 리스트에 덧붙인다.
  2. 연산자를 =, >, >=, <, <= 중 하나로 바꾼다.
  3. *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). 뒤의 둘이 불필요 조인 제거 패스다.

모든 구조적 재작성이 끝나면 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)
void
qo_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_exprPT_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)
bool
qo_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 시점에 이미 술어라는 사실로부터 세 가지 파급 효과가 떨어져 나온다.

  1. CNF와 동등성 축약이 추가 비용 없이 LIMIT에 작동한다. INST_NUM() <= n 결합항은 평범한 CNF 항이다. 사용자가 LIMIT 1 을 썼고 앞선 재작성이 다른 결합항을 항진식으로 접어 버렸다면, CNF 단순화기는 살아남은 것을 특별한 LIMIT 절이 아니라 단지 inst_num() <= 1 로 본다.
  2. 자동 파라미터화도 추가 비용 없이 LIMIT에 작동한다. 리터럴 n 은 사용자가 쓴 다른 상수들을 처리하는 동일 walk 가 인식해 호스트 변수 마커로 교체한다.
  3. 인덱스 조건 계획에는 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_TYPE enum 안의 두 번호 매김 연산자.
  • PT_GROUPBY_NUM (parse_tree.h) — FUNC_TYPE enum 안의 번호 매김 함수 (함수와 연산자는 PT_NODE 모양이 다르며, GROUPBY_NUM 은 그룹화 평가되므로 함수다).
  • PT_EXPR_INFO_GROUPBYNUM_LIMIT (parse_tree.h) — GROUPBY_NUM 기반으로 내려진 LIMIT 의 각 결합항에 세트되는 플래그. HAVING-to- WHERE 이동 로직이 이를 보고 옮기지 않는다.
  • csql_grammar.yflag.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_prept_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 노드와 새 속성 이름을 짓는다.
  • 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) — 조인이 줄어든 후 살아남는 술어를 보정하는 동반자.
  • 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_fetchpt_compilemq_translatept_class_pre_fetch 재실행 → do_prepare_statement. 재작성 계층이 더 큰 컴파일 흐름의 어디에 끼어드는지 보여 준다.

이 개정 시점의 위치 힌트 (2026-04-30 기준)

섹션 제목: “이 개정 시점의 위치 힌트 (2026-04-30 기준)”
심볼파일라인
pt_query_info::flag.rewrite_limit:1src/parser/parse_tree.h2141
pt_delete_info::rewrite_limit:1src/parser/parse_tree.h2850
pt_update_info::rewrite_limit:1src/parser/parse_tree.h2972
PT_INST_NUM / PT_ORDERBY_NUM enumeratorsrc/parser/parse_tree.h1399
pt_compilesrc/parser/compile.c381
pt_class_pre_fetchsrc/parser/compile.c432
pt_eval_type_pre (PT_SELECT LIMIT 처리부)src/parser/type_checking.c7179
pt_eval_type_pre (PT_UNION LIMIT 처리부)src/parser/type_checking.c7138
pt_eval_type_pre (PT_DELETE LIMIT 처리부)src/parser/type_checking.c7242
pt_eval_type_pre (PT_UPDATE LIMIT 처리부)src/parser/type_checking.c7274
pt_limit_to_numbering_exprsrc/parser/parser_support.c4567
pt_check_ordby_num_for_multi_range_optsrc/parser/parser_support.c9743
pt_cnfsrc/parser/cnf.c941
pt_do_cnfsrc/parser/cnf.c1183
mq_translatesrc/parser/view_transform.c(top)
mq_rewritesrc/optimizer/rewriter/query_rewrite.c736
qo_rewrite_queriessrc/optimizer/rewriter/query_rewrite.c44
qo_rewrite_queries_postsrc/optimizer/rewriter/query_rewrite.c586
qo_rewrite_queries (PT_UNION 잔존 LIMIT 분기)src/optimizer/rewriter/query_rewrite.c173
qo_rewrite_subqueriessrc/optimizer/rewriter/query_rewrite_subquery.c40
qo_rewrite_hidden_col_as_derivedsrc/optimizer/rewriter/query_rewrite_subquery.c331
qo_add_limit_clausesrc/optimizer/rewriter/query_rewrite_subquery.c473
qo_push_limit_to_unionsrc/optimizer/rewriter/query_rewrite_set.c40
qo_check_distinct_unionsrc/optimizer/rewriter/query_rewrite_set.c101
qo_check_hint_unionsrc/optimizer/rewriter/query_rewrite_set.c132
qo_rewrite_select_queriessrc/optimizer/rewriter/query_rewrite_select.c62
qo_rewrite_index_hintssrc/optimizer/rewriter/query_rewrite_select.c151
qo_move_on_of_explicit_join_to_wheresrc/optimizer/rewriter/query_rewrite_select.c516
qo_reduce_outer_joined_tblssrc/optimizer/rewriter/query_rewrite_select.c1909
qo_reduce_joined_tbls_ref_by_fksrc/optimizer/rewriter/query_rewrite_select.c2105
qo_rewrite_outerjoinsrc/optimizer/rewriter/query_rewrite_select.c3382
qo_rewrite_nonnull_count_select_listsrc/optimizer/rewriter/query_rewrite_select.c3777
qo_auto_parameterizesrc/optimizer/rewriter/query_rewrite_auto_parameterize.c41
qo_auto_parameterize_limit_clausesrc/optimizer/rewriter/query_rewrite_auto_parameterize.c129
qo_auto_parameterize_keylimit_clausesrc/optimizer/rewriter/query_rewrite_auto_parameterize.c293
qo_check_iscan_for_multi_range_optsrc/optimizer/plan_generation.c4030
qo_check_plan_index_for_multi_range_optsrc/optimizer/plan_generation.c4199
qo_plan_multi_range_optsrc/optimizer/plan_generation.c3141
db_compile_statement_localsrc/compat/db_vdb.c531
PRM_ID_MULTI_RANGE_OPT_LIMITsrc/base/system_parameter.h295
  • 원본 분석은 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.cplan_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_queriescall_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_pre pre-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 를 감사해야 한다.
  1. 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 에 다시 진입하는지, 아니면 의미 검사 시점 내림만으로 끝나는지 가 분명해진다.
  2. 센티넬로서의 PT_LAST_OPCODE. GROUP BY 처리부는 num_opPT_LAST_OPCODE 를 넘긴다. 헬퍼는 is_gby_num 이 true 일 때 num_op 를 무시하므로 (함수 노드를 만들지 expr 노드를 만들지 를 결정하기 때문에) 값 자체는 무해하다. 그러나 이 센티넬이 어딘가 로 새어 — 예컨대 복사된 표현 안으로 — 하류의 op 스위치를 혼란시킬 가능성이 있는가?
  3. 두 가지 LIMIT 표현 트레이드오프. 왜 qo_auto_parameterize_limit_clause원본 info.query.limit 을 번호 매김 술어 경로와 별도로 파라미터화 하는가? 내려진 술어는 이미 값을 들고 있다 (WHERE 워커가 자동 파라미터화한다). 인쇄, keylimit, 또는 원본 분석이 식별하지 못한 다른 경로 때문에 이 이중 표현이 여전히 필요한가?
  4. CONNECT BY / 계층 질의와의 상호작용. ORDER BY 처리부의 order_siblings 거부는 ORDERBY_NUM 으로의 내림을 막아 LIMIT 가 info.query.limit 에 남게 한다. qo_rewrite_queries 가 이를 다시 집어 가는가, 아니면 내림되지 않은 LIMIT 인 채로 XASL 생성까지 흘러가서 다른 메커니즘으로 평가되는가? 치트시트는 CONNECT BY 를 분석 생략 으로 다루었다 — 소스 경로 추적이 필요하다.
  5. MRO 와 PRM_ID_MULTI_RANGE_OPT_LIMIT 의 기본값. 이 시스템 파라미터의 기본값이 MRO 의 이득을 누릴 수 있는 LIMIT 의 크기를 결정한다. 기본값은 system_parameter.c 엔트리 (3197 라인) 에서 확인이 필요하다. LIMIT 1000 이 들어오는 워크로드는 기본값 에 따라 MRO 를 트리거할 수도 있고 못할 수도 있다.
  6. 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.cpt_compile 진입; 의미 검사 드라이버.
  • src/parser/type_checking.cpt_semantic_typept_eval_type_pre; LIMIT 내림 결정 트리.
  • src/parser/parser_support.cpt_limit_to_numbering_expr 내림 팩토리; pt_check_ordby_num_for_multi_range_opt MRO 술어 모양 검사.
  • src/parser/cnf.cpt_cnf, pt_do_cnf.
  • src/parser/view_transform.cmq_translate (뷰 인라이닝) 과 mq_rewrite_query_as_derived / mq_make_derived_spec (서브쿼리 평탄화 및 UNION-with-LIMIT 가 사용하는 파생 테이블 래퍼).
  • src/optimizer/rewriter/query_rewrite.cmq_rewriteqo_rewrite_queries; 재작성 계층 드라이버.
  • src/optimizer/rewriter/query_rewrite_subquery.cqo_rewrite_subqueries, qo_rewrite_hidden_col_as_derived, qo_add_limit_clause.
  • src/optimizer/rewriter/query_rewrite_set.cqo_push_limit_to_union, qo_check_distinct_union, qo_check_hint_union.
  • src/optimizer/rewriter/query_rewrite_select.cqo_rewrite_select_queries, 외부 조인 축소 및 불필요 조인 헬퍼들; qo_rewrite_index_hints.
  • src/optimizer/rewriter/query_rewrite_term.cqo_reduce_equality_terms, qo_rewrite_terms.
  • src/optimizer/rewriter/query_rewrite_auto_parameterize.cqo_auto_parameterize, qo_auto_parameterize_limit_clause, qo_auto_parameterize_keylimit_clause.
  • src/optimizer/plan_generation.cqo_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.cdb_compile_statement_local. 파서 → 의미 검사 → 뷰 인라이닝 → 재작성 → prepare 까지 엮는 statement 단위 컴파일 드라이버.