콘텐츠로 이동

(KO) CUBRID XASL Generator — 최적화된 plan 트리를 서버 측 실행 트리로 컴파일하기

목차

옵티마이저가 plan 을 골랐다고 해서 그것이 곧 데이터베이스 엔진이 실행하는 대상이 되는 것은 아니다. Database Internals(Petrov, 12장 Query Processing) 가 그 간극을 명시적으로 짚는다. 옵티마이저는 plan 을 만들고, 실행기는 실행 가능한 형태(executable form) 를 소비한다. 둘 사이를 누군가는 걸어서 메워 줘야 한다는 점이다. plan은 선언적 산출물이다. “테이블 A에는 인덱스 X 를 쓰고, 그다음 nested-loop 조인으로 B에 들어가고, 마지막에 C로 정렬한다” — 비용 추정과 접근 경로 선택으로 만들어진다. 실행 가능한 형태는 절차적 산출물이다. operator 트리이며 각 operator는 자신을 open / fetch / close 하는 방법을 알고 있다. 구체적인 버퍼, 술어 평가기, tuple 레이아웃이 모두 결선되어 있다는 뜻이다. Codd 의 관계 대수와 Selinger 의 System R 비용 모델 모두 plan 경계에서 멈춘다. 즉 σ(p)(R) 이 어떻게 CPU가 호출할 수 있는 함수로 변하는지에 대해서는 아무 말도 하지 않는다.

이 간극에는 교과서적 문제 셋이 살고 있다. 그리고 그 셋이 모든 관계형 엔진의 plan-to-IR 레이어 모양을 결정한다.

  1. 실행 모델의 선택. Database Internals 12장은 iterator(Volcano) 모델을 설명한다. 이 모델에서 각 operator는 open() / next() / close() 를 노출하고, 부모는 자식으로부터 tuple 을 끌어 온다. 더 새로운 엔진들은 push 기반 실행(벡터화, MonetDB 풍)이나 push-down 컴파일(HyPer, LLVM-IR JIT) 을 위에 얹기도 한다. 어느 쪽을 고르는가가 IR이 무엇을 담느냐를 바꾼다. 순수 iterator IR은 operator 당 하나의 open/next/close 트리플을 부호화한다. 벡터화 IR은 batch 단위 루프를 부호화한다. 컴파일된 IR은 생성된 코드 조각을 부호화한다.
  2. plan 안의 데이터 이름 붙이기. plan에 들어 있는 술어와 projection은 여전히 parser 레벨의 객체(컬럼 이름, 표현식 트리, host variable)를 참조한다. IR은 이것들을 실행기가 런타임에 읽을 수 있는 메모리 위치로 역참조해야만 한다. “클래스 oid X 의 attribute id 3을 이 DB_VALUE 슬롯으로 fetch하라” 라고 말해 주는 디스크립터가 필요하다는 점이다. 런타임 변수 전용 하위 IR은 보편적이다. PostgreSQL의 Var/Const/Param/Aggref 노드, MySQL의 Item 트리, Oracle의 row source expression, CUBRID의 REGU_VARIABLE 이 모두 같은 모양이다.
  3. 클라이언트-서버 실행을 위한 IR 직렬화. 많은 엔진이 최적화는 클라이언트에서 돌리고 실행은 서버에서 돌린다. CUBRID도 그중 하나다. xasl_generation.c 는 클라이언트에서 컴파일되고, query_executor.c 는 서버에서 실행된다. 따라서 IR은 바이트 스트림으로 round-trip 해야만 한다. 포인터를 그대로 와이어로 보낼 수는 없으니 직렬화기는 순환적이고 공유가 많은 IR을 자기 기술적인 버퍼로 평탄화해야 한다. offset 기반 참조와 이미 본 주소 테이블 을 동원해서다. Designing Data-Intensive Applications(Kleppmann) 4장은 이를 positional encoding 이라 부르고, 인코더가 공유 서브트리를 한 번만 방문하고 이후 참조는 offset으로 발행해야 한다는 요구 사항을 짚는다. 그렇지 않으면 와이어 크기가 지수적으로 부풀어 오르기 때문이다.

이렇게 셋이 명명되고 나면, 본 문서의 모든 CUBRID 고유 구조는 셋 중 하나를 구현하거나 결과 자료구조를 와이어 위에서 효율적으로 만드는 일을 하고 있다는 점이 보인다.

SQL을 절차적 plan 트리로 떨어뜨리는 모든 관계형 엔진은 대수 위에 같은 한 줌의 패턴을 채택한다. Selinger 의 원래 System R 논문에는 들어 있지 않다. 옵티마이저가 고른 plan과 그것을 실행하는 런타임 사이에 사는 공학적 어휘다.

PostgreSQL의 nodes/plannodes.hPlan 을 추상 부모로 정의하고, 그 아래 SeqScan, IndexScan, BitmapHeapScan, NestLoop, MergeJoin, HashJoin, Agg, Sort, Limit, Append, SetOp, ModifyTable 같은 물리 operator 마다 한 개의 C struct를 둔다. 모두 lefttreerighttree 포인터로 연결되어 일반적인 binary plan 트리가 된다. 실행기는 시작 시점에 이 트리를 top-down으로 걷고 (ExecInitNode), 런타임에는 bottom-up으로 tuple을 끌어 올린다 (ExecProcNode). MySQL 8.x 의 iterator 실행기도 RowIteratorAccessPath(sql/access_path.h) 클래스로 같은 모양이다. SQL Server 의 showplan XML 은 같은 구조의 텍스트 표현이다. 패턴은 보편적이다. 물리 operator 당 한 개의 트리 노드, 조인 다리나 입력 소스마다 자식 링크, 그리고 이 노드가 어떤 종류인지를 적어 놓는 공통 헤더.

CUBRID는 더 압축된 형태를 고른다. XASL_NODE 라는 단일 struct가 모든 operator 종류를 담는다. PROC_TYPE type 이라는 디스크리미네이터 와 그 변종을 덮는 태그드 union proc 이 그 역할을 한다. PostgreSQL 이 Plan 아래 30개 이상의 struct 타입을 두는 자리에 CUBRID는 17개의 PROC_TYPE 값을 가진 단일 struct를 둔다. 각 proc은 union arm 안에 자기 특화 필드를 들고 있다. 공통 필드(술어, 출력 리스트, sort spec, limit, sub-pointer)는 xasl_node 위에 직접 살면서 proc 사이에서 재사용된다. 타입 안전성을 메모리 지역성과 맞바꾼 셈이다. 그리고 직렬화기 코드를 단순하게 만든다. 디스크리미네이터 당 process 함수 하나로 끝나기 때문이다.

IR 의 모든 leaf operator는 물리 access path 를 참조해야 한다. heap scan, index scan, list-file scan, 또는 구성된 값들의 집합이다. 이름은 다양하다. PostgreSQL의 Path, MySQL의 AccessPath, Spark의 SparkPlan — 그러나 목적은 동일하다. tuple stream 이 어디서 오는가 를 그것으로 무엇을 할 것인가 와 분리해서 부호화한다는 점이다. CUBRID는 이를 xasl->spec_list 에 매달린 ACCESS_SPEC_TYPE 사슬로 빼낸다. 그리고 HYBRID_NODE union 으로 종류(class scan, list scan, set scan, JSON-table scan, showstmt, dblink 등) 를 고른다. index scan 의 경우 access spec은 INDX_INFO 를 가리키고, 거기에 range 종류 (R_KEY, R_RANGE, R_KEYLIST, …), btree id, key range가 들어 있다. heap scan 의 경우 클래스 oid 와 attribute 디스크립터를 들고 다닌다.

옵티마이저를 통과해도 술어와 projection은 parser 레벨 이름을 참조하는 표현식 트리 그대로 살아남는다. 실행기는 parser 이름 위에서 t.col1 = 7 을 평가할 수 없다. 표현식 IR이 필요하다는 뜻이다. 이 IR의 leaf는 tuple 버퍼 안의 슬롯이고, 상수는 미리 바인딩된 DB_VALUE 이며, 노드는 자기 연산 종류를 string lookup 없이도 안다. PostgreSQL의 ExprState(런타임 평가기) 는 parse-tree 표현식으로부터 ExecInitExpr 이 생성한다. MySQL은 파싱된 Item 트리를 그대로 두되 평가마다 해소된 필드 위치를 캐시한다. CUBRID의 REGU_VARIABLE 이 등가물이다. 상수 DB_VALUE, offset 을 가진 attribute id, 산술 노드의 결과, 함수 호출, tuple 리스트 안의 위치 라는 디스크리미네이션 union 이다. XASL generator 의 pt_to_regu_* 가족이 번역기이고, 실행기의 fetch_peek_dbval 이 소비자다.

서브쿼리 배치 — 중첩 plan 은 어디에 사는가

섹션 제목: “서브쿼리 배치 — 중첩 plan 은 어디에 사는가”

SQL 의 서브쿼리는 그 자체로 하나의 plan 트리가 된다. 그리고 부모 plan 은 이걸 언제 실행할지 알아야 한다. 정통적인 전략은 셋이다.

  • outer plan 안으로 끌어올려 join 으로 만든다. 서브쿼리가 query rewrite 단계에서 join 으로 바뀌고 IR 에는 서브쿼리 노드가 아예 남지 않는다 (PostgreSQL의 PLANNER 는 대부분의 비상관 IN 서브쿼리를 convert_ANY_sublink 로 이렇게 처리한다).
  • 한 번 미리 계산해서 입력으로 넣는다. 서브쿼리는 outer scan 이전에 실행되고 그 결과 list-file 이 입력으로 바인딩된다 (PostgreSQL의 InitPlan). 비상관 서브쿼리에만 가능하다는 점이다. IR 은 부모 위에 init-plan 리스트를 들고 다닌다.
  • outer 행마다 다시 평가한다. 상관 서브쿼리는 outer 컬럼을 참조하는 술어를 갖기 때문에 outer 행마다 다시 돌아야 한다. IR 은 이것을 outer 에 붙은 sub-plan 으로 들고 다닌다 (PostgreSQL의 SubPlan).

CUBRID는 이 셋 모두를 XASL_NODE 한 개에 부호화한다. aptr_list (ahead pointer) 는 부모보다 먼저 한 번 실행되는 비상관 서브쿼리를 담는다. dptr_list(driving pointer) 는 outer tuple 마다 다시 도는 상관 서브쿼리를 담는다. scan_ptr 은 현재 scan 에 사슬로 묶이는 조인의 inner 를 담는다. 두 종류의 서브쿼리와 다중 join 을 모두 가진 SELECT 하나는 단일 XASL 로 컴파일되며, 그 자식 슬롯이 세 역할을 한 struct 안에서 모두 덮는다는 점이다.

클라이언트-서버 실행을 위한 직렬화

섹션 제목: “클라이언트-서버 실행을 위한 직렬화”

최적화는 클라이언트에서 돌고 실행은 서버에서 돈다면, IR 은 바이트로 실려 가야 한다. 이렇게 하는 시스템은 모두 같은 세 문제를 마주한다. 공유 서브트리는 한 번만 발행되어야 한다 (그렇지 않으면 인코딩이 부풀어 오른다). 포인터는 offset 으로 다시 쓰여야 한다. 수신 측은 unpack 전에 연속된 arena 를 할당해야 한다. PostgreSQL의 outfuncs.c / readfuncs.c 는 공유 트리를 (@1) 같은 back-reference 를 가진 텍스트 인코딩을 쓴다. CUBRID 의 xts_map_xasl_to_stream / stx_map_stream_to_xasl 은 방문 포인터 해시 테이블(XTS_VISITED_PTR) 과 단일 xts_Stream_buffer 안의 offset 을 쓰는 바이너리 스트림을 쓴다. 거기에 더해, 본문이 시작되기 전에 클래스 oid 와 lock 요구를 적어 놓는 작은 헤더가 따로 붙는다. 그래야 서버가 XASL 을 할당하기 전에 lock 을 먼저 잡을 수 있다는 점이다.

이론적 개념 (Theoretical concept)CUBRID 명칭 (CUBRID name)
Plan 트리 노드XASL_NODE (src/query/xasl.h)
물리 operator 디스크리미네이터PROC_TYPE { UNION_PROC, DIFFERENCE_PROC, INTERSECTION_PROC, OBJFETCH_PROC, BUILDLIST_PROC, BUILDVALUE_PROC, SCAN_PROC, MERGELIST_PROC, HASHJOIN_PROC, UPDATE_PROC, DELETE_PROC, INSERT_PROC, CONNECTBY_PROC, DO_PROC, MERGE_PROC, BUILD_SCHEMA_PROC, CTE_PROC } (xasl.h)
Operator 별 페이로드xasl_node::proc 태그드 union (buildlist, buildvalue, union_, mergelist, hashjoin, update, insert, delete_, connect_by, merge, cte, fetch)
Access pathxasl->spec_listACCESS_SPEC_TYPE 사슬, HYBRID_NODE union이 class / list / set / showstmt / json-table / dblink / method / regu-value 중 하나로 분기
런타임 표현식 IRREGU_VARIABLE (regu_var.hpp), TYPE_CONSTANT, TYPE_ATTR_ID, TYPE_POSITION, TYPE_INARITH, TYPE_FUNC
술어 IRPRED_EXPR (xasl/xasl_predicate.hpp), T_PRED / T_EVAL_TERM / T_NOT_TERM 디스크리미네이터
출력 projectionxasl->outptr_list 에 매달린 OUTPTR_LIST / REGU_VARIABLE_LIST 사슬
spec 별 컬럼 버퍼QPROC_DB_VALUE_LIST 노드의 VAL_LIST. 같은 컬럼을 이름하는 형제 regu 들 사이에서 재사용
미리 계산되는 (비상관) 서브쿼리xasl->aptr_list
상관 서브쿼리xasl->dptr_list
현재 scan 에 사슬로 묶이는 조인의 innerxasl->scan_ptr
Path-fetch (OBJFETCH) sub-planxasl->bptr_list, xasl->fptr_list
인덱스 access 메타데이터ACCESS_SPEC_TYPE::indexptr 가 가리키는 INDX_INFO (R_KEY, R_RANGE, R_KEYLIST, R_RANGE_LIST, …)
Scan 안의 술어 자리 (수직 / 수평 / 힙)ACCESS_SPEC_TYPE 위의 where_range, where_key, where_pred
PT_NODE → XASL_NODE 워크의 최상위 진입점parser_generate_xasl (xasl_generation.c)
SELECT 별 plan 컴파일러pt_plan_query (xasl_generation.c)
다행 vs 단행 select 디스패치pt_to_buildlist_proc / pt_to_buildvalue_proc (xasl_generation.c)
spec/scan/서브쿼리 슬롯을 채우는 plan walkerqo_to_xaslgen_outer / gen_inner (optimizer/plan_generation.c)
Parser 트리 → 표현식 IRpt_to_regu_variable, pt_to_pred_expr, pt_to_outlist, pt_to_val_list, pt_to_index_info, pt_to_spec_list (xasl_generation.c)
Aptr / dptr 주입기pt_set_aptr, pt_set_dptr (xasl_generation.c)
XASL 할당기regu_xasl_node_alloc (xasl_regu_alloc.cpp)
직렬화 드라이버xts_map_xasl_to_stream (query/xasl_to_stream.c)
XASL 별 pack 함수xts_save_xasl_node, xts_process_xasl_node (query/xasl_to_stream.c)
방문 포인터 offset 테이블xts_get_offset_visited_ptr, xts_mark_ptr_visited (query/xasl_to_stream.c)
서버 측 unpackerstx_init_xasl_unpack_info, stream_to_xasl (query/stream_to_xasl.c, xasl/xasl_stream.cpp)

XASL generator 에는 네 개의 이동 부품이 있다. 진입 워크 가 query 형태의 PT_NODE 를 XASL proc 트리로 바꾼다. plan 주도의 내부 워크 (gen_outer / gen_inner) 가 spec list, scan pointer, 서브쿼리 슬롯 을 채운다. 하위 IR 빌더(pt_to_regu_variable, pt_to_pred_expr, pt_to_outlist, pt_to_val_list, pt_to_index_info, pt_to_spec_list) 가 표현식, 술어, projection, access spec 을 컴파일 한다. 직렬화기(xts_map_xasl_to_stream 가족) 가 만들어진 포인터 트리를 서버가 unpack 할 수 있는 평탄한 바이트 스트림으로 바꾼다. 이 순서대로 본다.

flowchart LR
  subgraph CLIENT["클라이언트 (xasl_generation.c, plan_generation.c)"]
    PT["PT_NODE 트리<br/>(이름 해소,<br/>타입 검사,<br/>view 재작성 완료)"]
    QO["QO_PLAN 트리<br/>(옵티마이저 출력:<br/>scan / join / sort / follow)"]
    PGX["parser_generate_xasl<br/>· parser_generate_xasl_post<br/>(parser_walk_tree)"]
    PPQ["pt_plan_query<br/>PT_SELECT 마다"]
    BLP["pt_to_buildlist_proc<br/>(다행)"]
    BVP["pt_to_buildvalue_proc<br/>(단행)"]
    POL["pt_to_outlist<br/>pt_to_val_list"]
    PSA["pt_set_aptr"]
    PGOP["pt_gen_optimized_plan"]
    QTX["qo_to_xasl<br/>gen_outer / gen_inner"]
    AAS["add_access_spec<br/>add_scan_proc<br/>add_subqueries"]
    PSD["pt_set_dptr"]
    XASL["XASL_NODE 트리<br/>(클라이언트, 인메모리)"]
    XTS["xts_map_xasl_to_stream<br/>xts_save_xasl_node<br/>xts_process_xasl_node"]
    STREAM["XASL stream<br/>(packed bytes)"]
  end
  subgraph SERVER["서버 (stream_to_xasl.c, query_executor.c)"]
    STX["stx_init_xasl_unpack_info<br/>stream_to_xasl"]
    XASLS["XASL_NODE 트리<br/>(서버 측, 재구성)"]
    EXEC["query_executor.c<br/>qexec_execute_mainblock"]
  end
  PT --> PGX
  PGX --> PPQ
  PPQ --> BLP
  PPQ --> BVP
  BLP --> POL
  BLP --> PSA
  BLP --> PGOP
  PGOP --> QTX
  QTX --> AAS
  AAS --> XASL
  BLP --> PSD
  PSD --> XASL
  QO --> PGOP
  XASL --> XTS
  XTS --> STREAM
  STREAM --> STX
  STX --> XASLS
  XASLS --> EXEC

이 그림은 세 경계를 부호화한다. (parser ↔ plan) PT_NODE 측은 src/parser/xasl_generation.c 가 소유한다. QO_PLAN 측은 src/optimizer/plan_generation.c 가 소유한다. 둘은 pt_gen_optimized_plan 안에서 만난다. 부모 XASL 을 할당하고 그 outptr_listaptr_list 를 시드한 다음 qo_to_xasl 을 호출하는 지점이다. (client ↔ server) XASL generator 자체는 모두 클라이언트 측에 산다. 그 출력이 서버에 도달하는 유일한 경로가 직렬화다. (buildlist ↔ buildvalue) 다행 select 는 BUILDLIST_PROC 으로 컴파일된다. 이는 임시 list-file 로 materialise 한다는 뜻이다. 단행이 보장된 select 는 BUILDVALUE_PROC 으로 컴파일된다. 이는 임시 파일 없이 단일 tuple 슬롯에 결과를 쓴다는 뜻이다.

이 노드는 넓다. 공통 필드가 모든 proc 종류를 덮고, proc 별 부가 필드는 proc union 안에 숨는다.

// xasl_node — src/query/xasl.h
struct xasl_node
{
XASL_NODE_HEADER header; /* xasl_flag + id, packed first on the wire */
XASL_NODE *next; /* sibling XASL block (UNION_PROC chain etc.) */
PROC_TYPE type; /* discriminator: BUILDLIST_PROC, SCAN_PROC, … */
int flag;
QFILE_LIST_ID *list_id; /* materialised result handle */
// ... condensed: sort, limit, instnum/ordbynum bookkeeping ...
OUTPTR_LIST *outptr_list; /* projection (select list) */
ACCESS_SPEC_TYPE *spec_list; /* leaf access specs (class / list / set / …) */
VAL_LIST *val_list; /* per-tuple column buffer for spec_list */
XASL_NODE *aptr_list; /* CTEs + uncorrelated subqueries (run once, ahead) */
XASL_NODE *bptr_list; /* OBJFETCH_PROC list (path expressions) */
XASL_NODE *dptr_list; /* correlated subqueries (run per outer tuple) */
XASL_NODE *fptr_list; /* OBJFETCH after dptr */
XASL_NODE *scan_ptr; /* inner side of join chained off current scan */
XASL_NODE *connect_by_ptr; /* CONNECT BY xasl */
PRED_EXPR *during_join_pred; /* predicate evaluated during outer join match */
PRED_EXPR *after_join_pred; /* predicate evaluated after the join */
PRED_EXPR *if_pred; /* WHERE clause residual */
PRED_EXPR *instnum_pred; /* INST_NUM() < N */
union {
UNION_PROC_NODE union_; /* UNION/DIFFERENCE/INTERSECTION */
FETCH_PROC_NODE fetch; /* OBJFETCH_PROC */
BUILDLIST_PROC_NODE buildlist; /* multi-row result */
BUILDVALUE_PROC_NODE buildvalue; /* single-row aggregate result */
MERGELIST_PROC_NODE mergelist;
HASHJOIN_PROC_NODE hashjoin;
UPDATE_PROC_NODE update;
INSERT_PROC_NODE insert;
DELETE_PROC_NODE delete_;
CONNECTBY_PROC_NODE connect_by;
MERGE_PROC_NODE merge;
CTE_PROC_NODE cte;
} proc;
/* XASL cache + serialization metadata */
OID creator_oid;
int n_oid_list;
OID *class_oid_list;
int *class_locks;
int *tcard_list;
// ...
};

네 개의 포인터 슬롯(aptr_list, bptr_list, dptr_list, scan_ptr) 에 next 까지 더하면, 단일 XASL_NODE 구조체가 SELECT가 만들어 낼 수 있는 네 가지 중첩 plan 을 모두 부호화하게 된다. union 사슬은 next 로, outer-then-inner 조인 사슬은 scan_ptr 로, 부모 이전에 materialise 되는 비상관 서브쿼리는 aptr_list 로, outer tuple 마다 다시 도는 상관 서브쿼리는 dptr_list 로 들어간다는 점이다. 분석 자료의 표어 XASL(aptr, dptr, scan_ptr) 가 곧 실행기의 멘탈 모델이다. outer 행마다 dptr 들을 돌리고, 그다음 scan_ptr 사슬을 내려간다.

컴파일러는 재작성 후의 PT_NODE 위를 도는 순서가 있는 워크 다. 한 번에 끝내는 번역이 아니다. 새 symbol-info 프레임을 밀어 넣는 pre-order 훅(parser_generate_xasl_pre) 과 XASL 을 bottom-up 으로 만드는 post-order 훅(parser_generate_xasl_post) 을 가진 parser_walk_tree 로 동작한다. 그래서 안쪽 서브쿼리가, 그것을 참조하는 바깥 query 보다 먼저 XASL 이 된다.

// parser_generate_xasl — src/parser/xasl_generation.c (condensed)
XASL_NODE *
parser_generate_xasl (PARSER_CONTEXT * parser, PT_NODE * node)
{
XASL_NODE *xasl = NULL;
// ... condensed: check abort, save next, walk for is_system_generated_stmt ...
switch (node->node_type)
{
case PT_SELECT:
case PT_UNION:
case PT_DIFFERENCE:
case PT_INTERSECTION:
node->info.query.is_subquery = (PT_MISC_TYPE) 0;
if (node) node = meth_translate (parser, node); /* method calls */
if (node)
{
xasl_Supp_info.query_list = parser_new_node (parser, PT_SELECT);
xasl_Supp_info.query_list->info.query.xasl = NULL;
pt_init_xasl_supp_info ();
/* the actual XASL build, bottom-up over subqueries */
node = parser_walk_tree (parser, node,
parser_generate_xasl_pre, NULL,
parser_generate_xasl_post, &xasl_Supp_info);
}
if (node && !pt_has_error (parser))
{
xasl = (XASL_NODE *) node->info.query.xasl;
}
break;
}
/* fill in XASL cache info: creator oid, class_oid_list, locks, tcard */
if (xasl)
{
// ... condensed: oid + locks + tcard arrays from xasl_Supp_info ...
}
return xasl;
}

pre-order 훅 parser_generate_xasl_pre 는 묵은 info.query.xasl 을 지운다(parse 트리는 prepared statement 재실행 시 재사용된다는 점에서 중요하다). 그리고 pt_push_symbol_info 로 이 query 레벨에 새 symbol 스코프를 연다. 심볼은 pt_to_val_list 가 spec 별 컬럼 버퍼를 만들 때 소비하는 table_info 를 들고 다닌다.

post-order 훅 parser_generate_xasl_post 가 실제 작업이 일어나는 자리다. 자식들로부터 제어가 돌아왔을 때 모든 서브쿼리는 이미 자기 XASL 을 info.query.xasl 에 매달아 두었다. 그래서 부모의 parser_generate_xasl_proc 호출은 그것들을 결선하기만 하면 된다.

// parser_generate_xasl_post — src/parser/xasl_generation.c (condensed)
static PT_NODE *
parser_generate_xasl_post (PARSER_CONTEXT * parser, PT_NODE * node, void *arg, int *continue_walk)
{
XASL_NODE *xasl;
XASL_SUPP_INFO *info = (XASL_SUPP_INFO *) arg;
switch (node->node_type)
{
case PT_SELECT:
case PT_UNION:
case PT_DIFFERENCE:
case PT_INTERSECTION:
assert (node->info.query.xasl == NULL);
xasl = parser_generate_xasl_proc (parser, node, info->query_list);
pt_pop_symbol_info (parser); /* matched with the pre-order push */
// ... condensed: walk select.from for class oid list ...
break;
case PT_CTE:
xasl = parser_generate_xasl_proc (parser, node, info->query_list);
break;
}
return node;
}

parser_generate_xasl_proc 는 query 노드 타입별로 디스패치한다 (PT_SELECTpt_plan_query, PT_UNION 등 → set-op 빌더). 결과는 node->info.query.xasl 에 다시 들어가서 부모의 post-hook 이 찾을 수 있게 된다.

각 SELECT 의 컴파일은 두 단계다. 옵티마이저에 plan 을 요청한 다음, parse 트리와 plan 을 함께 XASL 로 번역한다. BUILDLIST_PROC 을 쓸지 BUILDVALUE_PROC 을 쓸지는 실행기가 한 행을 만들지 여러 행을 만들지에 달려 있다.

// pt_plan_query — src/parser/xasl_generation.c (condensed)
static XASL_NODE *
pt_plan_query (PARSER_CONTEXT * parser, PT_NODE * select_node)
{
XASL_NODE *xasl;
QO_PLAN *plan = NULL;
if (select_node->node_type != PT_SELECT)
return NULL;
/* 1) cost-based optimization */
plan = qo_optimize_query (parser, select_node);
/* if hint-driven optimization fails, retry without hints */
if (!plan && select_node->info.query.q.select.hint != PT_HINT_NONE)
{
// ... condensed: clear all hint sublists, retry qo_optimize_query ...
plan = qo_optimize_query (parser, select_node);
}
/* 2) translate plan + select_node into XASL */
if (pt_is_single_tuple (parser, select_node))
{
xasl = pt_to_buildvalue_proc (parser, select_node, plan); /* aggregate w/o GROUP BY */
}
else
{
xasl = pt_to_buildlist_proc (parser, select_node, plan); /* normal SELECT */
}
// ... condensed: dump plan + cache plan text on xasl ...
return xasl;
}

pt_is_single_tuple 은 select list 가 단일 aggregate 이고 (SELECT SUM(x) FROM t) GROUP BY 가 없을 때 true 다. 이는 분석 자료가 BUILDVALUE_PROC 의 자리라고 짚는 정확히 그 경우다. 그 밖의 모든 경우는 BUILDLIST_PROC 이고 list-file 을 만든다.

buildlist 컴파일러는 파일에서 가장 긴 함수다 (aggregate, group-by, having, order-by, limit, window 함수, connect-by 가 모두 들어 있어 수천 줄에 이른다). 그러나 골격이 본질이다. 할당하고, 부모 컨텍스트 를 세팅하고, outptr 을 만들고, aptr 을 세팅하고, 최적화된 plan 을 생성하고(여기서 spec_list/scan_ptr 이 채워진다), dptr 을 세팅한다. 순서가 중요하다. outptr 이 aptr 보다 먼저인 이유는 select list 에서 비상관 서브쿼리를 참조할 수 있기 때문이다. aptr 이 optimized-plan 보다 먼저인 이유는 plan 안의 spec 별 서브쿼리가 이미 끌어올려진 게 무엇인지 알아야 하기 때문이다. dptr 이 마지막인 이유는 상관 서브쿼리 가 leaf scan 에 매달려야 하는데, 그 leaf 는 gen_outer 가 끝난 뒤에야 존재하기 때문이다.

// pt_to_buildlist_proc — src/parser/xasl_generation.c (condensed)
static XASL_NODE *
pt_to_buildlist_proc (PARSER_CONTEXT * parser, PT_NODE * select_node, QO_PLAN * qo_plan)
{
XASL_NODE *xasl, *save_parent_proc_xasl;
SYMBOL_INFO *symbols = parser->symbols;
PT_NODE *from = select_node->info.query.q.select.from;
if (symbols == NULL || from == NULL || select_node->node_type != PT_SELECT)
return NULL;
/* 1) allocate the BUILDLIST_PROC shell */
xasl = regu_xasl_node_alloc (BUILDLIST_PROC);
if (xasl == NULL) return NULL;
/* 2) tell child compiles who their parent is — pt_set_aptr/pt_set_dptr will read this */
save_parent_proc_xasl = parser->parent_proc_xasl;
parser->parent_proc_xasl = xasl;
/* 3) limit / inst_num / ordby_num bookkeeping */
// ... condensed ...
/* 4) build select list as outptr */
if (pt_has_aggregate (parser, select_node))
{
/* group key + aggregate exprs become outptr; aggregates collected separately */
xasl->outptr_list = pt_to_outlist (parser, group_out_list, NULL, UNBOX_AS_VALUE);
// ... condensed: aggregate list, having, group-by sort spec ...
}
else
{
xasl->outptr_list = pt_to_outlist (parser, select_node->info.query.q.select.list,
&xasl->selected_upd_list, UNBOX_AS_VALUE);
}
/* 5) hoist uncorrelated subqueries into aptr_list before plan generation */
pt_set_aptr (parser, select_node, xasl);
/* 6) walk QO_PLAN to fill spec_list / scan_ptr / per-leaf subqueries */
xasl = pt_gen_optimized_plan (parser, select_node, qo_plan, xasl);
/* 7) attach correlated subqueries that reference outer columns */
/* pt_gen_optimized_plan already calls pt_set_dptr on the deepest leaf via qo_to_xasl */
parser->parent_proc_xasl = save_parent_proc_xasl;
return xasl;
}

두 헬퍼 pt_set_aptrpt_set_dptr 은 parse 트리를 돌면서 post-order 워크가 info.query.xasl 을 채워 둔 PT_NODE 들을 찾는다. 비상관이면 xasl->aptr_list 에 prepend, 상관이면 xasl->dptr_list 에 prepend 한다는 점이다.

Plan 워크 — pt_gen_optimized_planqo_to_xasl

섹션 제목: “Plan 워크 — pt_gen_optimized_plan 과 qo_to_xasl”

pt_gen_optimized_plan 은 얇은 래퍼다. qo_to_xasl 을 호출한 다음, query hint(USE_IDX_DESC, NO_IDX_DESC, NLJ_KEEP_HEAP_PAGE_PINNED) 가 결정하는 몇 개의 인덱스 방향 플래그를 패치한다. 재귀적인 plan 워크 자체는 qo_to_xasl 과 그 상호 헬퍼 gen_outer, gen_inner 에 산다.

// qo_to_xasl — src/optimizer/plan_generation.c (condensed)
xasl_node *
qo_to_xasl (QO_PLAN * plan, xasl_node * xasl)
{
QO_ENV *env;
XASL_NODE *lastxasl;
if (plan && xasl && (env = plan->info->env))
{
xasl = gen_outer (env, plan, &EMPTY_SET, NULL, NULL, xasl);
/* find the deepest scan node — that's where correlated subqueries attach */
lastxasl = xasl;
while (lastxasl)
{
if (lastxasl->scan_ptr) lastxasl = lastxasl->scan_ptr;
else if (lastxasl->fptr_list) lastxasl = lastxasl->fptr_list;
else break;
}
pt_set_dptr (env->parser, env->pt_tree->info.query.q.select.list, lastxasl, MATCH_ALL);
xasl = preserve_info (env, plan, xasl);
}
return xasl;
}

pt_set_dptrlastxasl 로 호출되는 데에는 이유가 있다. dptr 서브쿼리는 outer 행마다 발화되므로 가장 안쪽 scan(루트에서 도달 가능한 가장 깊은 scan_ptr)에 매달려야 한다는 점이다. 거기가 매 행이 materialise 되는 자리이기 때문이다. 만약 루트에 매달면 전체 query 의 결과 행마다 한 번씩 실행된다. 잘못이다. 두 번째 위에 매달면 그 위의 조인이 단 한 번도 평가되지 않은 채 materialise 된다. 역시 잘못이다.

gen_outer 는 plan 을 타입별로 번역하는 재귀다.

// gen_outer — src/optimizer/plan_generation.c (condensed)
static XASL_NODE *
gen_outer (QO_ENV * env, QO_PLAN * plan, BITSET * subqueries,
XASL_NODE * inner_scans, XASL_NODE * fetches, XASL_NODE * xasl)
{
// ... condensed: bitset bookkeeping for predicates and subqueries ...
switch (plan->plan_type)
{
case QO_PLANTYPE_SCAN:
/* leaf — attach the access spec, then accumulated inner scans + fetches + correlated subqs */
xasl = add_access_spec (env, xasl, plan);
xasl = add_scan_proc (env, xasl, inner_scans);
xasl = add_fetch_proc (env, xasl, fetches);
xasl = add_subqueries (env, xasl, &new_subqueries);
break;
case QO_PLANTYPE_SORT:
/* if we're inside a join leg, materialise the sub-plan into a list-file
and rescan it; otherwise just recurse and add a sort spec */
if (inner_scans != NULL || plan->plan_un.sort.sort_type == SORT_LIMIT)
{
listfile = make_buildlist_proc (env, namelist);
listfile = gen_outer (env, plan->plan_un.sort.subplan,
&EMPTY_SET, NULL, NULL, listfile);
listfile = add_sort_spec (env, listfile, plan, xasl->ordbynum_val, false);
xasl = add_uncorrelated (env, xasl, listfile);
xasl = init_list_scan_proc (env, xasl, listfile, namelist,
&(plan->sarged_terms), NULL);
// ... condensed ...
}
else
{
xasl = gen_outer (env, plan->plan_un.sort.subplan, &new_subqueries,
inner_scans, fetches, xasl);
xasl = add_sort_spec (env, xasl, plan, NULL, true /* add instnum pred */);
}
break;
case QO_PLANTYPE_JOIN:
outer = plan->plan_un.join.outer;
inner = plan->plan_un.join.inner;
switch (plan->plan_un.join.join_method)
{
case QO_JOINMETHOD_NL_JOIN:
case QO_JOINMETHOD_IDX_JOIN:
/* build the inner side as a SCAN_PROC, then recurse on the outer
passing the inner as inner_scans */
inner_scans = gen_inner (env, inner, &predset, &new_subqueries, inner_scans);
xasl = gen_outer (env, outer, &EMPTY_SET, inner_scans, fetches, xasl);
break;
case QO_JOINMETHOD_MERGE_JOIN:
// ... condensed: build both sides as listfiles, attach via mergelist proc ...
break;
}
break;
// ... condensed: FOLLOW (path expr), WORST cases ...
}
// ... condensed: free bitsets ...
return xasl;
}

이 재귀 구조가 분석 자료의 표어 “조인 순서: A → B → C 는 BUILDLIST(A) → SCAN(B) → SCAN(C) 로 컴파일된다” 의 출처다. outer 자체가 다시 조인인 QO_PLANTYPE_JOIN 은 outer 쪽을 따라 재귀하다가 QO_PLANTYPE_SCAN 에 도달하면 거기가 access spec 을 가진 BUILDLIST_PROC 이 된다. 거꾸로 올라오는 길에서 각 층의 inner 는 SCAN_PROC 으로 감싸져 (gen_innermake_buildlist_proc 으로) outer 의 scan_ptr 에 사슬로 묶인다. 결과는 루트가 list-file 로 materialise 되고 그 scan_ptr 후손들은 순수 파이프라인인 left-deep XASL 사슬이다.

add_access_spec 은 leaf 컴파일러다. parse 트리의 class spec 을 ACCESS_SPEC_TYPE 으로 변환하고, 술어를 key/access 종류로 쪼개고, spec 별 val_list 를 바인딩한다.

// add_access_spec — src/optimizer/plan_generation.c (condensed)
static XASL_NODE *
add_access_spec (QO_ENV * env, XASL_NODE * xasl, QO_PLAN * plan)
{
PARSER_CONTEXT *parser = QO_ENV_PARSER (env);
PT_NODE *class_spec = QO_NODE_ENTITY_SPEC (plan->plan_un.scan.node);
PT_NODE *key_pred = NULL;
PT_NODE *access_pred = NULL;
PT_NODE *if_pred = NULL;
PT_NODE *instnum_pred = NULL;
QO_XASL_INDEX_INFO *info = qo_get_xasl_index_info (env, plan);
/* 1) split predicates: key (index range), access (heap-side residual), if/instnum */
make_pred_from_plan (env, plan, &key_pred, &access_pred, info, NULL);
/* 2) build the access spec — class oid, index info, range/key/pred regu lists */
xasl->spec_list = pt_to_spec_list (parser, class_spec, key_pred, access_pred,
plan, info, NULL, NULL);
/* 3) per-spec column buffer */
xasl->val_list = pt_to_val_list (parser, class_spec->info.spec.id);
/* 4) attach if-predicate and inst_num predicate to the parent xasl */
if_pred = make_if_pred_from_plan (env, plan);
instnum_pred = make_instnum_pred_from_plan (env, plan);
xasl = add_if_predicate (env, xasl, if_pred);
xasl = pt_to_instnum_pred (parser, xasl, instnum_pred);
// ... condensed: free pointer lists, free index info ...
return xasl;
}

where_range / where_key / where_pred 로의 분할(분석 자료가 수직 / 수평 / 힙 축이라 부르는 것) 은 pt_to_index_infopt_to_spec_list 안에서 일어난다. 인덱스의 leading prefix 와 매칭 되는 술어가 where_range 를 끌고 간다. 나머지 인덱스 컬럼 위의 술어가 where_key 가 된다(인덱스 워크 도중 leaf 레벨 필터링). 인덱스 가 닿지 않는 컬럼을 참조하는 술어는 where_pred 가 된다(힙 행을 fetch 한 뒤 데이터 페이지에서 평가).

하위 IR — REGU_VARIABLE, OUTPTR_LIST, VAL_LIST

섹션 제목: “하위 IR — REGU_VARIABLE, OUTPTR_LIST, VAL_LIST”

SELECT 의 projection, 술어, 런타임 상수는 모두 REGU_VARIABLE 트리 로 떨어진다. pt_to_regu_variable 이 모든 PT_NODE 모양을 regu 로 사상하는 큰 switch 다. 그리고 실행기는 그것을 fetch_peek_dbval 로 다시 읽는다.

// pt_to_regu_variable — src/parser/xasl_generation.c (condensed signature + skeleton)
REGU_VARIABLE *
pt_to_regu_variable (PARSER_CONTEXT * parser, PT_NODE * node, UNBOX unbox)
{
REGU_VARIABLE *regu = NULL;
DB_VALUE *val = NULL;
if (node == NULL)
{
/* default: empty varchar constant — used as a placeholder */
regu_alloc (val);
db_value_domain_init (val, DB_TYPE_VARCHAR, DB_DEFAULT_PRECISION, DB_DEFAULT_SCALE);
regu = pt_make_regu_constant (parser, val, DB_TYPE_VARCHAR, NULL);
}
else if (PT_IS_POINTER_REF_NODE (node))
{
/* TYPE_CONSTANT pointing into another node's etc */
// ... condensed: domain resolution, regu->type = TYPE_CONSTANT ...
}
else
{
switch (node->node_type)
{
case PT_DOT_: /* path expr — resolves to TYPE_ATTR_ID via pt_attribute_to_regu */
case PT_NAME: /* column reference — TYPE_ATTR_ID or TYPE_POSITION */
case PT_VALUE: /* literal — TYPE_CONSTANT with a pre-bound DB_VALUE */
case PT_HOST_VAR: /* parameter — TYPE_CONSTANT with the host_var slot */
case PT_EXPR: /* arith/func — TYPE_INARITH with an ARITH_TYPE child */
case PT_FUNCTION: /* built-in or aggregate — TYPE_FUNC */
case PT_METHOD_CALL: /* method — special-cased */
// ... condensed: each shape allocates a regu and fills the union ...
}
}
return regu;
}

WHERE col1 IN (1,2) 술어에 대한 분석 자료의 예시는 따라가 볼 만하다. IN 리스트는 두 개의 REGU_VARIABLE 로 떨어진다. 타입은 TYPE_POS_VALUE 이고 val_pos = 0, val_pos = 1 로 상수 1, 2 를 들고 있는 공유 VAL_DESCR 배열을 가리킨다. IN 자체는 T_PRED 타입의 PRED_EXPR 이 되고, 그 좌우는 T_EVAL_TERM 비교다. 이 레이아웃의 장점은 인덱스 range scan 이 키마다 position 값을 다시 바인딩할 수 있고, 힙 측 잔여 술어가 다시 파싱 없이 같은 PRED_EXPR 을 재사용할 수 있다는 점이다.

pt_to_outlist 는 select list 위를 걸으며 OUTPTR_LIST(개수와 REGU_VARIABLE_LIST 노드 사슬) 를 만든다. pt_to_val_list 는 spec id 에 대한 SYMBOL_INFO 의 table_info 위를 걸으며 VAL_LIST 를 만들고, 그 엔트리는 outptr 의 regu 들과 같은 컬럼을 참조할 때 공유 된다는 점이다. 이 공유가 “한 번 컬럼을 fetch 해서 여러 자리에 projection” 을 런타임에 싸게 만든다.

서브쿼리 배치 — aptr / dptr / scan_ptr 한 그림으로

섹션 제목: “서브쿼리 배치 — aptr / dptr / scan_ptr 한 그림으로”
flowchart TB
  Q["SELECT a.col1<br/>FROM tab a, tab b<br/>WHERE a.col1 = b.col1<br/>  AND a.col1 = (SELECT col1 FROM tab d)<br/>  /* 비상관 */<br/>  AND EXISTS<br/>    (SELECT 1 FROM tab c WHERE c.k = a.k)<br/>  /* 상관 */"]
  R["XASL_NODE 루트<br/>type=BUILDLIST_PROC<br/>(driver = tab a)"]
  AP["XASL_NODE<br/>type=BUILDLIST_PROC<br/>(비상관 서브쿼리: tab d)"]
  DP["XASL_NODE<br/>type=BUILDLIST_PROC<br/>(상관 서브쿼리: tab c, a.k 참조)"]
  SP["XASL_NODE<br/>type=SCAN_PROC<br/>(inner: tab b)"]
  Q --> R
  R -. aptr_list .-> AP
  R -. dptr_list .-> DP
  R -. scan_ptr  .-> SP
  AP -. R 이전 1회 .-> R
  DP -. outer 행마다 .-> R
  SP -. outer 행마다 중첩 .-> R

실행기의 루프는 정확히 이 순서로 슬롯을 읽는다.

run aptr_list once (materialises into a list-file, bound by LIST_SPEC_NODE)
for each outer tuple of R:
run dptr_list (correlated subq evaluated against the new outer)
descend scan_ptr chain (joined inner scan)
project R's outptr into list-file (BUILDLIST) or single value (BUILDVALUE)

aptr_list 는 CTE 가 들어가는 자리이기도 하다(분석 자료가 CTE_PROC 을 언급하고, xasl.h 의 코멘트는 “CTEs are guaranteed always before the subqueries” 라고 적혀 있다는 점이다). 이것이 CTE 가 실행기의 관점에서 비상관 서브쿼리와 똑같이 읽히는 이유다. 재귀 플래그를 빼면 사실상 같다.

INDEX(col1, col2, col3) 와 query WHERE col1 = 1 AND col3 = 2 AND col4 = 2 가 있으면, 옵티마이저는 각 conjunct 를 인덱스의 leading-key prefix 와의 위치 관계로 분류한다.

flowchart LR
  WHERE["WHERE 절"]
  RANGE["where_range<br/>col1 = 1<br/>(B-tree 수직 하강)"]
  KEY["where_key<br/>col3 = 2<br/>(B-tree leaf 수평 스캔,<br/> 비-leading 인덱스 컬럼)"]
  PRED["where_pred<br/>col4 = 2<br/>(힙 페이지 평가,<br/> 비-인덱스 컬럼)"]
  IDX["INDEX(col1, col2, col3)"]
  WHERE --> RANGE
  WHERE --> KEY
  WHERE --> PRED
  IDX -.- RANGE
  IDX -.- KEY

pt_to_index_infowhere_range 가 소비할 INDX_INFO(range 종류, btree id, key info 사슬) 를 만든다. 잔여는 pt_to_pred_expr 가 만든 PRED_EXPR 트리 형태로 where_key / where_pred 에 들어간다는 점이다. 분석 자료가 짚듯 PRED_EXPR 은 “PT_NODE 보다 가벼운 struct” 다. 이유는 parse 트리가 소스 위치, 타입 도출 이력, 재작성 상태를 들고 다니지만 실행기에는 이게 필요 없기 때문이다. 술어 IR 은 연산 종류, 좌우 regu 포인터, short-circuit 플래그만 보유한다.

XASL_NODE 트리가 만들어지고 나면, 그것을 서버로 실어 보내야 한다. 송신자가 xts_map_xasl_to_stream 이다.

// xts_map_xasl_to_stream — src/query/xasl_to_stream.c (condensed)
int
xts_map_xasl_to_stream (const XASL_NODE * xasl_tree, XASL_STREAM * stream)
{
int offset, header_size, body_size;
char *p;
if (!xasl_tree || !stream) return ER_QPROC_INVALID_XASLNODE;
/* Header: dbval_cnt + creator_oid + n_oid_list + class_oid_list + locks + tcard */
header_size = sizeof (int) + sizeof (OID) + sizeof (int)
+ sizeof (OID) * xasl_tree->n_oid_list
+ sizeof (int) * xasl_tree->n_oid_list
+ sizeof (int) * xasl_tree->n_oid_list;
/* layout: [size of header][header data][size of body][body data...] */
offset = sizeof (int) + header_size + sizeof (int);
offset = xasl_stream_make_align (offset);
xts_reserve_location_in_stream (offset);
xts_id_serial = 0;
/* recursive walk: each xts_save_* checks the visited table first,
emits the body once, returns the offset for subsequent references */
if (xts_save_xasl_node (xasl_tree) == ER_FAILED) goto end;
/* now backfill the header */
p = or_pack_int (xts_Stream_buffer, header_size);
p = or_pack_int (p, xasl_tree->dbval_cnt);
p = or_pack_oid (p, (OID *) (&xasl_tree->creator_oid));
p = or_pack_int (p, xasl_tree->n_oid_list);
for (i = 0; i < xasl_tree->n_oid_list; i++) p = or_pack_oid (p, &xasl_tree->class_oid_list[i]);
for (i = 0; i < xasl_tree->n_oid_list; i++) p = or_pack_int (p, xasl_tree->class_locks[i]);
for (i = 0; i < xasl_tree->n_oid_list; i++) p = or_pack_int (p, xasl_tree->tcard_list[i]);
body_size = xts_Free_offset_in_stream - offset;
p = or_pack_int (p, body_size);
stream->buffer = xts_Stream_buffer;
stream->buffer_size = xts_Free_offset_in_stream;
end:
xts_free_visited_ptrs ();
return xts_Xasl_errcode;
}

헤더는 클래스 oid 리스트와 oid 별 lock 모드 리스트를 들고 다닌다. 서버는 본문을 unpack 하기 전에 이것을 읽고 필요한 lock 을 먼저 잡는다는 점이다. lock 획득에 실패하면 서버는 XASL 트리를 한 번도 materialise 하지 않은 채 query 를 거절할 수 있다. tcard_list 는 옵티마이저가 사용한 클래스별 table-card 힌트다. 서버가 plan 의 신선도를 검증할 수 있게 해 준다. 클래스의 카디널리티가 plan 시점 대비 의미 있게 변했으면, 캐시된 XASL 은 무효화된다.

xts_save_xasl_node 는 공유 서브트리 처리의 핫 패스다.

// xts_save_xasl_node — src/query/xasl_to_stream.c (condensed)
static int
xts_save_xasl_node (const XASL_NODE * xasl)
{
int offset, size;
if (xasl == NULL) return NO_ERROR;
/* deduplication: if we've packed this node before, return the prior offset */
offset = xts_get_offset_visited_ptr (xasl);
if (offset != ER_FAILED) return offset;
size = xts_sizeof_xasl_node (xasl);
offset = xts_reserve_location_in_stream (size);
xts_mark_ptr_visited (xasl, offset);
/* pack the body into a temp buffer, then memcpy into the stream */
buf = xts_process_xasl_node (buf_p, xasl);
// ... condensed: copy into xts_Stream_buffer at offset ...
return offset;
}

방문 포인터 검사가 인코딩을 유일 노드 수에 비례하게 만든다(경로 수가 아니라). 이게 없으면 UNION 의 두 가지에서 참조되는 CTE 가 두 번 packed 된다. xts_process_xasl_node 는 노드 별 필드-by-필드 pack 이다.

// xts_process_xasl_node — src/query/xasl_to_stream.c (condensed)
static char *
xts_process_xasl_node (char *ptr, const XASL_NODE * xasl)
{
int offset, cnt;
ACCESS_SPEC_TYPE *access_spec;
/* 1) header: assigned a fresh id_serial so the executor can identify nodes */
((XASL_NODE *) xasl)->header.id = xts_id_serial++;
ptr = xts_process_xasl_header (ptr, xasl->header);
ptr = or_pack_int (ptr, xasl->type);
ptr = or_pack_int (ptr, xasl->flag);
/* 2) every pointer becomes an offset (or 0 for NULL) */
offset = xts_save_list_id (xasl->list_id); ptr = or_pack_int (ptr, offset);
offset = xts_save_sort_list (xasl->after_iscan_list); ptr = or_pack_int (ptr, offset);
offset = xts_save_sort_list (xasl->orderby_list); ptr = or_pack_int (ptr, offset);
offset = xts_save_pred_expr (xasl->ordbynum_pred); ptr = or_pack_int (ptr, offset);
offset = xts_save_db_value (xasl->ordbynum_val); ptr = or_pack_int (ptr, offset);
offset = xts_save_regu_variable (xasl->orderby_limit); ptr = or_pack_int (ptr, offset);
// ... condensed: limit_offset, limit_row_count, single_tuple, outptr_list,
// selupd_list, val_list, merge_val_list ...
/* 3) access spec list: pack count then each spec */
for (cnt = 0, access_spec = xasl->spec_list; access_spec; access_spec = access_spec->next, cnt++) ;
ptr = or_pack_int (ptr, cnt);
for (access_spec = xasl->spec_list; access_spec; access_spec = access_spec->next)
ptr = xts_process_access_spec_type (ptr, access_spec);
/* 4) recurse into the four pointer slots */
offset = xts_save_xasl_node (xasl->aptr_list); ptr = or_pack_int (ptr, offset);
offset = xts_save_xasl_node (xasl->bptr_list); ptr = or_pack_int (ptr, offset);
offset = xts_save_xasl_node (xasl->dptr_list); ptr = or_pack_int (ptr, offset);
// ... condensed: fptr_list, scan_ptr, connect_by_ptr, predicates, instnum/orderby_num,
// proc-union (per type), creator_oid, class oid list, ... ...
return ptr;
}

두 가지 인코딩 규칙을 짚어 둔다. (1) Pointers-as-offsets. C 에서 포인터인 모든 필드는 xts_save_* 헬퍼가 반환한 offset 의 or_pack_int 가 된다. offset 0 은 NULL 을 의미한다. stream_to_xasl.c 의 수신자는 각 pack 을 or_unpack_intxts_get_addr_unpack_info 스타일 lookup 으로 짝지어 포인터를 재수화한다. (2) Pack-by-discriminator. proc-union 은 type 으로 디스패치되어 활성 arm 만 packed 된다는 점이다. 수신자도 동일하게 동작한다. 그래서 BUILDVALUE_PROC 은 가장 큰 변종이 아니라 BUILDVALUE_PROC_NODE 의 바이트만큼만 비용이 든다.

unpack 측은 대칭이다. stx_init_xasl_unpack_info 가 서버 측 arena 를 할당한다 (unpack_info->packed_size * UNPACK_SCALE. 여기서 UNPACK_SCALE = 3 은 align 과 포인터 재수화 때문에 deserialised 트리가 와이어 크기의 약 3배가 되는 것을 감안한 값이다). 그다음 stream_to_xasl 이 재귀적으로 각 offset 을 읽고, 이미 unpack 된 표 를 lookup 해서 캐시된 포인터를 반환하거나 본문을 unpack 해서 삽입 한다.

한데 모아 보기 — 한 SELECT 컴파일의 처음부터 끝까지

섹션 제목: “한데 모아 보기 — 한 SELECT 컴파일의 처음부터 끝까지”
sequenceDiagram
  participant CT as 클라이언트 스레드 (PT walker)
  participant QO as qo_optimize_query
  participant PB as pt_to_buildlist_proc
  participant QX as qo_to_xasl / gen_outer
  participant XTS as xts_map_xasl_to_stream
  participant SVR as 서버
  CT->>CT: parser_walk_tree pre-order:<br/>query 마다 pt_push_symbol_info
  CT->>CT: 안쪽 서브쿼리부터 post-order<br/>(parser_generate_xasl_proc)
  CT->>QO: qo_optimize_query(parser, select_node)
  QO-->>CT: QO_PLAN 트리 (scan/join/sort/follow)
  CT->>PB: pt_to_buildlist_proc(parser, select_node, plan)
  PB->>PB: regu_xasl_node_alloc(BUILDLIST_PROC)
  PB->>PB: pt_to_outlist  → xasl->outptr_list
  PB->>PB: pt_set_aptr   → xasl->aptr_list
  PB->>QX: pt_gen_optimized_plan
  QX->>QX: gen_outer 재귀<br/>scan / join / sort / follow
  QX->>QX: add_access_spec → spec_list, val_list,<br/>          where_range / where_key / where_pred
  QX->>QX: add_scan_proc   → scan_ptr 사슬
  QX->>QX: add_subqueries  → leaf 별 aptr/dptr
  QX-->>PB: 채워진 xasl
  PB->>PB: lastxasl(가장 깊은 scan)에 pt_set_dptr
  PB-->>CT: xasl
  CT->>CT: parser_generate_xasl 마무리:<br/>class_oid_list, locks, tcard, creator_oid 채움
  CT->>XTS: xts_map_xasl_to_stream(xasl, stream)
  XTS->>XTS: 헤더 pack (dbval_cnt, oids, locks, tcard)
  XTS->>XTS: xts_save_xasl_node 재귀<br/>방문 표가 공유 서브트리 dedup
  XTS-->>CT: stream->buffer + buffer_size
  CT->>SVR: prepare/execute RPC 가 stream 운반
  SVR->>SVR: stx_init_xasl_unpack_info (arena = size * UNPACK_SCALE)
  SVR->>SVR: stream_to_xasl 가 트리를 재수화
  SVR->>SVR: qexec_execute_mainblock 가 실행

분석 자료가 짚는 sub-plan 처리 순서

섹션 제목: “분석 자료가 짚는 sub-plan 처리 순서”

분석 자료의 XASL generator 진행 절차 슬라이드는 buildlist 케이스 의 명시적 순서를 준다. 이는 소스와 정확히 일치한다.

pt_to_buildlist_proc (PT_SELECT, QO_PLAN)
pt_to_outlist (select.list) — outlist (projection)
pt_set_aptr (select) — aptr (uncorrelated subqueries)
pt_gen_optimized_plan (select, plan, xasl)
qo_to_xasl (plan, xasl)
gen_outer (env, plan, ...)
case QO_PLANTYPE_SCAN:
add_access_spec — spec_list (range/key/pred, indexptr)
add_scan_proc — scan_ptr
add_subqueries — aptr + dptr per leaf
case QO_PLANTYPE_JOIN:
inner_scan = gen_inner (inner)
gen_outer (outer, ..., inner_scan)
pt_set_dptr (select.list, lastxasl) — dptr on deepest leaf

이 호출들 가운데 둘이 미묘하다. pt_set_aptrpt_gen_optimized_plan 전에 돌아야 한다. 왜냐하면 add_subqueries 가 각 서브쿼리의 PT_NODE 위에 이미 붙은 XASL 을 lookup 하기 때문이다. 사전 부착이 바로 pt_set_aptr 의 비상관 PT_SELECT 워크에서 일어난다. pt_set_dptr 는 재귀 워크 후에 돌아야 한다. 왜냐하면 상관 서브쿼리는 가장 깊은 scan 의 val_list 슬롯으로만 존재하는 outer 컬럼을 참조하기 때문이다. 더 일찍 바인딩하면 잘못된 tuple 버퍼를 가리키게 된다.

라인 번호가 아니라 심볼 이름을 닻으로 삼는다. CUBRID 소스는 움직인다. 문서 끝의 위치 표는 본 문서의 updated: 날짜에만 유효한 힌트다.

최상위 진입과 query 별 디스패치

섹션 제목: “최상위 진입과 query 별 디스패치”
  • parser_generate_xasl (xasl_generation.c) — prepare/execute 경로 에서 호출되는 가장 바깥 진입점. 두-패스 워크를 돌리고 캐시 메타데이터를 채운다.
  • parser_generate_xasl_pre / parser_generate_xasl_post (xasl_generation.c) — pre-order 가 symbol info 를 푸시, post-order 가 XASL 을 bottom-up 으로 구성.
  • parser_generate_xasl_proc (xasl_generation.c) — query 별 디스패처. SELECT → pt_plan_query, set op → pt_to_union_proc 가족.
  • pt_plan_query (xasl_generation.c) — qo_optimize_query 호출 후 pt_to_buildlist_proc / pt_to_buildvalue_proc 호출.

Buildlist / buildvalue / set-op 컴파일러

섹션 제목: “Buildlist / buildvalue / set-op 컴파일러”
  • pt_to_buildlist_proc (xasl_generation.c) — 다행 메인 컴파일러. aggregate, group-by, having, order-by, limit, window, connect-by 모두.
  • pt_to_buildvalue_proc (xasl_generation.c) — 단행 aggregate 컴파일 러 (GROUP BY 없음).
  • pt_to_union_proc (xasl_generation.c) — UNION_PROC, DIFFERENCE_PROC, INTERSECTION_PROC 빌더.
  • pt_to_cte_proc (xasl_generation.c) — CTE_PROC 빌더.
  • pt_make_aptr_parent_node (xasl_generation.c) — aptr 가 자식 query 를 들고 있는 부모 XASL 을 할당하는 공통 헬퍼 (INSERT/UPDATE/DELETE/MERGE 가 사용).
  • pt_to_xasl_for_dblink (xasl_generation.c) — dblink 형태 XASL.
  • pt_gen_optimized_plan (xasl_generation.c) — qo_to_xasl 을 호출 하고 인덱스 힌트를 패치하는 래퍼.
  • pt_gen_simple_plan (xasl_generation.c) — QO_PLAN 이 없을 때의 폴백 (예: 최적화 없이 시스템이 생성한 stmt).
  • qo_to_xasl (optimizer/plan_generation.c) — 재귀 워크의 진입점.
  • gen_outer (optimizer/plan_generation.c) — SCAN / JOIN / SORT / FOLLOW 의 outer-side 재귀.
  • gen_inner (optimizer/plan_generation.c) — inner-side 빌더. outer 의 scan_ptr 에 사슬로 묶이는 SCAN_PROC 을 만든다.
  • make_buildlist_proc (optimizer/plan_generation.c) — sub-plan 의 list-file materialisation.
  • make_sort_limit_proc (optimizer/plan_generation.c) — Top-N sort 변종.
  • add_access_spec (optimizer/plan_generation.c) — leaf scan 컴파일러.
  • add_scan_proc / add_fetch_proc / add_uncorrelated / add_subqueries (optimizer/plan_generation.c) — XASL_NODE 의 네 포인터 슬롯에 사슬 삽입.
  • add_sort_spec (optimizer/plan_generation.c) — orderby / groupby sort spec 구성.
  • make_pred_from_plan (optimizer/plan_generation.c) — 잔여 술어를 key / access 종류로 분할.
  • qo_get_xasl_index_info (optimizer/plan_generation.c) — 선택된 인덱스에 대한 QO_XASL_INDEX_INFO 추출.

하위 IR 빌더 (parse 트리 → XASL 조각)

섹션 제목: “하위 IR 빌더 (parse 트리 → XASL 조각)”
  • pt_to_outlist (xasl_generation.c) — select list 를 OUTPTR_LIST 로.
  • pt_to_val_list (xasl_generation.c) — table_info 를 VAL_LIST 로.
  • pt_to_regu_variable (xasl_generation.c) — 일반 표현식을 REGU_VARIABLE 로 (큰 switch).
  • pt_to_pred_expr (xasl_generation.c) — 술어 트리를 PRED_EXPR 로.
  • pt_to_index_info (xasl_generation.c) — 인덱스 access 를 INDX_INFO 로 (range 종류, key info, key1/key2 regu).
  • pt_to_class_spec_list / pt_to_spec_list (xasl_generation.c) class spec 을 ACCESS_SPEC_TYPE 으로. key/pred/range regu 리스트 포함.
  • pt_set_aptr / pt_set_dptr (xasl_generation.c) — 비상관 / 상관 서브쿼리 부착.
  • pt_set_numbering_node_etc (xasl_generation.c) — INST_NUM / ORDERBY_NUM DB_VALUE 를 parse-tree 참조에 결선.
  • pt_to_pos_descr (xasl_generation.c) — 이름을 outptr 안의 position 디스크립터로.
  • pt_attribute_to_regu (xasl_generation.c) — 컬럼 참조를 TYPE_ATTR_ID regu 로.
  • pt_make_regu_constant (xasl_generation.c) — 리터럴 / host_var 를 TYPE_CONSTANT regu 로.
  • regu_xasl_node_alloc (xasl_regu_alloc.cpp) — XASL_NODE 할당기 (per-query parser arena, regu_alloc 가족).
  • regu_alloc (xasl_regu_alloc.hpp) — 일반 typed 할당기.
  • pt_init_xasl_supp_info (xasl_generation.c) — query 사이에 부가 정보 누적기를 비운다.
  • xts_map_xasl_to_stream (query/xasl_to_stream.c) — 최상위 pack 진입점. XASL_STREAM { buffer, buffer_size } 출력.
  • xts_save_xasl_node (query/xasl_to_stream.c) — 방문 검사 + 본문 pack + 스트림으로 memcpy.
  • xts_process_xasl_node (query/xasl_to_stream.c) — 노드 별 필드 pack.
  • xts_sizeof_xasl_node (query/xasl_to_stream.c) — 사전 사이즈 추정 (버퍼 예약용).
  • xts_save_outptr_list / xts_save_regu_variable / xts_save_pred_expr / xts_save_arith_type / xts_save_indx_info / xts_save_db_value / xts_save_val_list / xts_save_sort_list / xts_save_aggregate_type / xts_save_function_type / xts_save_analytic_type (query/xasl_to_stream.c) — 하위 IR 별 pack 함수. 모두 방문 표를 사용.
  • xts_get_offset_visited_ptr / xts_mark_ptr_visited / xts_free_visited_ptrs (query/xasl_to_stream.c) — 방문 포인터 표.
  • xts_reserve_location_in_stream (query/xasl_to_stream.c) — 버퍼 성장.
  • xts_map_filter_pred_to_stream / xts_map_func_pred_to_stream (query/xasl_to_stream.c) — 필터/함수 술어용 보조 진입점 (트리거와 함수 인덱스가 사용).
  • stx_init_xasl_unpack_info (xasl/xasl_stream.cpp) — 서버 arena 할당기 (packed_size * UNPACK_SCALE).
  • stream_to_xasl (query/stream_to_xasl.c) — 최상위 unpack 진입점.
  • stx_get_xasl_node / stx_get_regu_variable / 등 (query/stream_to_xasl.c) — xts_save_* 가족과 대칭.
  • xasl_stream_get_ptr_block / xasl_stream_make_align (xasl/xasl_stream.cpp) — 송신과 수신이 공유하는 align / block 관리.
  • xasl_stream_compare (xasl/xasl_stream.cpp) — packed XASL 조각의 동등성 (XASL 캐시 lookup 이 사용).
심볼파일Line
xasl_node structsrc/query/xasl.h1075
PROC_TYPE enumsrc/query/xasl.h188
XASL_NODE_HEADER structsrc/query/xasl.h73
parser_generate_xaslsrc/parser/xasl_generation.c22460
parser_generate_xasl_presrc/parser/xasl_generation.c22322
parser_generate_xasl_postsrc/parser/xasl_generation.c22373
pt_plan_querysrc/parser/xasl_generation.c17680
pt_to_buildlist_procsrc/parser/xasl_generation.c16148
pt_to_buildvalue_procsrc/parser/xasl_generation.c17204
pt_gen_optimized_plansrc/parser/xasl_generation.c14714
pt_gen_simple_plansrc/parser/xasl_generation.c14812
pt_make_aptr_parent_nodesrc/parser/xasl_generation.c18519
pt_to_xasl_for_dblinksrc/parser/xasl_generation.c18807
pt_to_outlistsrc/parser/xasl_generation.c13830
pt_to_val_listsrc/parser/xasl_generation.c13237
pt_set_aptrsrc/parser/xasl_generation.c13388
pt_set_dptrsrc/parser/xasl_generation.c13370
pt_to_regu_variablesrc/parser/xasl_generation.c7577
pt_to_pred_exprsrc/parser/xasl_generation.c2194
pt_to_index_infosrc/parser/xasl_generation.c11852
pt_to_class_spec_listsrc/parser/xasl_generation.c12290
pt_to_spec_listsrc/parser/xasl_generation.c13167
pt_to_pos_descrsrc/parser/xasl_generation.c5377
pt_to_pos_descr_groupbysrc/parser/xasl_generation.c24103
qo_to_xaslsrc/optimizer/plan_generation.c2915
gen_outersrc/optimizer/plan_generation.c1955
gen_innersrc/optimizer/plan_generation.c2759
add_access_specsrc/optimizer/plan_generation.c895
add_scan_procsrc/optimizer/plan_generation.c978
add_subqueriessrc/optimizer/plan_generation.c1066
make_buildlist_procsrc/optimizer/plan_generation.c728
regu_xasl_node_allocsrc/parser/xasl_regu_alloc.cpp40
xts_map_xasl_to_streamsrc/query/xasl_to_stream.c276
xts_save_xasl_nodesrc/query/xasl_to_stream.c1682
xts_process_xasl_nodesrc/query/xasl_to_stream.c2810
xts_sizeof_xasl_nodesrc/query/xasl_to_stream.c5941
xts_save_aggregate_typesrc/query/xasl_to_stream.c498
xts_save_function_typesrc/query/xasl_to_stream.c634
xts_save_analytic_typesrc/query/xasl_to_stream.c702
xts_save_arith_typesrc/query/xasl_to_stream.c970
xts_save_indx_infosrc/query/xasl_to_stream.c1038
xts_save_outptr_listsrc/query/xasl_to_stream.c1106
xts_save_pred_exprsrc/query/xasl_to_stream.c1242
xts_save_regu_variablesrc/query/xasl_to_stream.c1310
xts_save_sort_listsrc/query/xasl_to_stream.c1394
xts_save_val_listsrc/query/xasl_to_stream.c1462
xts_save_db_valuesrc/query/xasl_to_stream.c1614
xts_save_filter_pred_nodesrc/query/xasl_to_stream.c1767
xts_save_func_predsrc/query/xasl_to_stream.c1835
xts_save_update_infosrc/query/xasl_to_stream.c2109
stx_init_xasl_unpack_infosrc/xasl/xasl_stream.cpp74
xasl_stream_make_alignsrc/xasl/xasl_stream.cpp231

분석 자료와 소스 사이의 어긋남

섹션 제목: “분석 자료와 소스 사이의 어긋남”
  • 분석 자료의 BUILDLIST_PROC 정의는 맞지만 불완전하다. 자료 는 BUILDLIST 를 ROW 가 여러 건이 될 수 있는 SQL 이라 한다. 소스 도 동의한다. pt_is_single_tuple (BUILDVALUE 를 고르는 함수) 은 GROUP BY 없는 단일 aggregate 를 요구한다. 다만 자료가 짚지 않는 케이스가 하나 더 있다. pt_make_aptr_parent_node 로 UPDATE/ DELETE/INSERT 캐리어 안으로 끌어 올려진 SELECT 는 카디널리티와 무관하게 항상 BUILDLIST 다. 소비자는 사용자가 아니라 outer modify proc 이기 때문이다.

  • SCAN_PROC 에 임시 파일이 없다. 확인됨. 자료의 약식 표현 SCAN_PROC: TEMP 영역을 사용하지 않음 은 의미와 부합한다. scan_ptr 에 매달리는 SCAN_PROC 노드는 부모 루프에 파이프라인되며 디스크에 아무것도 쓰지 않는다. BUILDLIST_PROC 노드는 QFILE_LIST_ID 를 할당하고 materialise 한다. 만약 그것이 scan_ptr 아래에 나타나면 (드물고, join-leg materialisation 을 위해 make_buildlist_proc 으로 만들어진 경우에 한정) materialisation 은 위치와 무관하게 일어난다.

  • aptr_list 는 자료에서 잘못 명명되어 있다. xasl->aptr_list 의 소스 코멘트는 “CTEs and uncorrelated subquery. CTEs are guaranteed always before the subqueries 라고 적혀 있다. 자료는 aptr 를 비상관 서브쿼리” 만으로 다룬다. CTE 도 같은 슬롯을 공유하며, 실행기는 순서대로 평가한다. CTE 가 먼저다.

  • 자료의 PRED_EXPR 는 PT_NODE 보다 가볍다 는 구조적인 이유로 참이다. PT_NODE 는 소스 추적, 재작성 이력, 타입 도출 상태, 그리고 70여 arm 의 info union 을 들고 다닌다. PRED_EXPR 의 union 은 세 arm 이다 (pred, eval_term, not_term). 컴파일 타임에 PT_NODE 는 수백 바이트, PRED_EXPR 은 ~32 바이트다. 5×–10× 의 축소가 행 단위 술어 평가를 싸게 만든다.

  • 다중 테이블 예시에서 자료의 SCAN_PROC 셀 모양은 단순화다. 자료는 조인의 inner 쪽을 SCAN_PROC (tab b) 라고만 그린다. 소스는 실제로는 outer 의 scan_ptr 에 사슬로 묶이는 SCAN_PROC 을 만든다. 거기에 자기 spec_list(tab b 의 access spec 포함) 와 자기 val_list 를 들고 있다. SCAN_PROC 은 부모의 outptr 를 projection forwarding 으로 상속한다. inner 를 완전한 XASL_NODE 로 시각화하면 재귀 구조가 명시적으로 보인다.

  • pt_set_dptr 는 buildlist 루트가 아니라 가장 깊은 scan 에서 돈다. 자료의 의사코드는 qo_to_xasl 반환 후 pt_set_dptr (select.list) 를 보여 준다. 소스는 그 호출을 qo_to_xasl 자체 안에 두고 leaf 에 닿을 때까지 scan_ptrfptr_list 를 내려간다. 이 배치가 옳다. outer.col 을 참조하는 상관 서브쿼리는 outer.col 이 val_list 에 들어간 후에 실행되어야 하고, 그것은 leaf scan 에 서만 일어난다는 점이다.

  • 방문 포인터 표는 pack 단위지 글로벌이 아니다. 자료는 공유 서브트리를 다루지 않지만, 소스의 xts_map_xasl_to_stream 끝에 있는 xts_free_visited_ptrs 가 query 마다 표를 리셋한다는 점을 보여 준다. query 간 공유는 없다. 그러나 단일 query 안에서 outptr 와 where_pred 가 공유하는 REGU_VARIABLE 은 정확히 한 번만 발행 된다.

  1. UNPACK_SCALE = 3 의 근거. 서버 측 unpack arena 는 `packed_size

    • 3으로 사이즈가 정해진다. 이 3 은 어디서 왔는가? 보통의 XASL 를 packed-vs-rehydrated 비를 측정해 보면 3 이 보수적인 값인지, 경험적으로 도출된 값인지, 임의의 값인지 알 수 있을 것이다. 조사 경로:stx_init_xasl_unpack_infodb_private_alloc` 을 계측해서 샘플 워크로드의 실제 사용량을 찍어 본다.
  2. 해시 GROUP BY 아래의 aggregate 배치. 자료는 “BUILDLIST 가 임시 파일을 만든다” 에서 멈춘다. 소스의 pt_to_buildlist_proc 에는 g_hash_eligible 분기가 있다 (PT_HINT_NO_HASH_AGGREGATEpt_is_hash_agg_eligible 워크). 옵티마이저는 언제 해시 aggregation 을 결정하며, 해시 테이블은 XASL_NODE proc-union 안에서 어떻게 자리하는가? 조사 경로: 실행기에서의 BUILDLIST_PROC_NODE::g_hash_eligibleBUILDLIST_PROC_NODE::g_output_first_tuple 의미.

  3. Connect-by 의 connect_by_ptr 와 proc-union connect_by. xasl_nodeconnect_by_ptr (XASL_NODE) 와 proc.connect_by (CONNECTBY_PROC_NODE) 를 모두 들고 다닌다. 왜 둘인가? 아마도 connect-by sub-plan 자체는 XASL_NODE (재귀 scan) 이고, proc-union 은 prior/level 관리 정보를 담기 때문일 것이다. 조사 경로: pt_to_connect_by_proc (그 이름으로 존재한다면) 과 qexec_execute_connect_by 를 따라 둘이 어떻게 상호작용하는지 본다.

  4. xasl_generation 으로부터의 HASHJOIN_PROC 도달 가능성. PROC_TYPE enum 은 HASHJOIN_PROC 을 나열하지만, 자료는 언급하지 않는다. 어디서 인스턴스화되는가? gen_outerQO_JOINMETHOD_HASH_JOIN arm 이 가장 그럴듯한 후보다. 다만 현재 소스에서 그 분기 본문이 다른 proc 타입이나 add_hash_pred 확장으로 옮겨졌을 가능성도 있다. 조사 경로: git grep 으로 regu_xasl_node_alloc (HASHJOIN_PROC)make_hashjoin_proc 을 찾는다.

  5. tcard_list 를 통한 plan 무효화. 헤더가 클래스별 table-card 힌트를 운반한다. 서버 측에서 그것을 현재 카디널리티와 비교해 캐시된 XASL 의 무효화를 결정하는 경로는 자료에서 다루지 않는다. 조사 경로: xasl_cache.cxcache_find_xasl_id 안에서 tcard_list 를 소비하는 lookup.

  6. vfetch_to 를 통한 REGU_VARIABLE 공유. 자료는 REGU_VARIABLE 이 DB_VALUE_LIST 엔트리를 가리키는 vfetch_to 포인터를 들고 있음을 보여 주고, “동일 컬럼의 regu_var일 경우 db_value의 값을 공유함 이라고 적는다. 컴파일러가 같은 컬럼” 을 감지할 때 쓰는 정확한 동등 술어가 무엇인지 — attr_descr.id 에 클래스 oid 를 더한 것인가, 아니면 PT_NODE 포인터 동일성인가? 조사 경로: PT_NAME 에 대한 pt_to_regu_variable 와 (있다면) mq_regu_var_lookup 캐시.

  7. 메소드 호출 regu 의 병렬성과의 상호작용. xasl_node::px_executorexecuted_parallelism 필드가 XASL 을 병렬 인지하게 만든다. 메소드 호출 (JSP/SP) 은 meth_translate 에서 병렬-안전하지 않다고 표시된다. generator 는 query 가 병렬 적격인지 아닌지를 어떻게 결정하며, 그 게이트는 어디인가? 조사 경로: parser_generate_xasl 끝의 scan_check_parallel_heap_scan_possiblecheck_parallel_subquery_possible 호출.

Raw 분석 (raw/code-analysis/cubrid/query-processing/)

섹션 제목: “Raw 분석 (raw/code-analysis/cubrid/query-processing/)”
  • analysis_QP_XASL_generator_1.0.pptx — 본 문서가 닻으로 삼은 분석 자료.
  • _converted/analysisqpxaslgenerator1.0.pptx.md — markitdown 추출본.
  • (계획 중) cubrid-query-optimizer.md — 여기서 소비되는 QO_PLAN 을 만든다.
  • (계획 중) cubrid-query-executor.md — 본 generator 가 만드는 XASL 트리를 소비한다.
  • (계획 중) cubrid-xasl-cache.md — SQL 과 클래스 oid 집합을 키로 packed XASL 스트림을 캐시한다. xts_map_xasl_to_stream 의 헤더가 캐시 키 페이로드다.
  • Database Internals (Petrov), 12장 Query Processing — plan vs executable form 의 구분, iterator(Volcano) 모델.
  • Designing Data-Intensive Applications (Kleppmann), 4장 “Encoding and Evolution” — back-reference 를 가진 positional encoding, 직렬화의 공유 서브트리 문제.
  • Selinger et al., Access Path Selection in a Relational Database Management System, SIGMOD 1979 — plan 선택과 plan 실행의 경계를 그은 System R 논문.
  • Graefe, Volcano—An Extensible and Parallel Query Evaluation System, IEEE TKDE 1994 — XASL 의 open/next/close 후예가 구현하는 iterator 모델.
  • Graefe, Query Evaluation Techniques for Large Databases, ACM Computing Surveys 1993 — 물리 operator 와 plan IR 의 부호화에 대한 정본 격 서베이.

CUBRID 소스 (/data/hgryoo/references/cubrid/)

섹션 제목: “CUBRID 소스 (/data/hgryoo/references/cubrid/)”
  • src/parser/xasl_generation.{c,h} — parser 측 컴파일러의 본체. C 770 KB 가량.
  • src/parser/xasl_regu_alloc.{cpp,hpp} — parser arena 에 묶인 XASL_NODE / regu / 술어 할당기.
  • src/optimizer/plan_generation.c — plan 주도 워크 (gen_outer, gen_inner, add_access_spec, 헬퍼들).
  • src/optimizer/query_planner.h — 여기서 소비되는 QO_PLAN 모양.
  • src/query/xasl.hxasl_node, PROC_TYPE, 모든 proc-union 노드 정의, ACCESS_SPEC / VAL_LIST / OUTPTR_LIST 헤더.
  • src/query/xasl_to_stream.{c,h} — 송신자. 방문 포인터 직렬화기.
  • src/query/stream_to_xasl.{c,h} — 수신자.
  • src/xasl/xasl_stream.{cpp,hpp} — 송신과 수신이 공유하는 align / block 헬퍼와 unpack-info arena.
  • src/xasl/xasl_predicate.hppPRED_EXPR 정의.
  • src/xasl/access_spec.hppACCESS_SPEC_TYPE / hybrid 노드.
  • src/query/regu_var.hppREGU_VARIABLE 정의.
  • src/query/query_executor.c — 소비자. 진입점 qexec_execute_mainblock.