콘텐츠로 이동

(KO) CUBRID Query Executor — XASL 해석, 이터레이터 모델, 그리고 힙/인덱스 스캔 연산자

목차

질의 실행기(query executor) 란 물리 계획 트리(physical plan tree)를 결과 튜플(tuple)의 스트림으로 바꿔 주는 런타임을 의미한다. Database Internals(Petrov, 12장 Query Execution)와 Garcia-Molina/Ullman/Widom Database Systems: The Complete Book(15장 “Query Execution, 16장 The Query Compiler”)는 이 문제를 단일한 설계 문제로 보고, 두 갈래 해법 가족이 서로 경쟁한다고 정리한다. 한쪽은 풀(pull) 기반 이터레이터 모델(Volcano)이다. 각 연산자에게 한 번에 한 튜플씩을 요청하는 방식이다. 다른 쪽은 푸시(push) 기반 또는 벡터화 모델(MonetDB/X100, 현대 HyPer)이다. 각 연산자가 다음 연산자로 묶음(batch)을 흘려 보내는 방식이다.

Goetz Graefe의 Volcano — An Extensible and Parallel Query Evaluation System(TKDE 1994)이 이터레이터 모델의 정본(canonical reference)이다. 모든 연산자가 구현해야 하는 세 메서드 계약(three-method contract)이 이론의 핵심이다.

  1. open() — 스캔 상태를 초기화하고, 버퍼를 할당하며, 하위 파일과 자식 연산자를 재귀적으로 연다.
  2. next() — 다음 튜플(혹은 스트림 끝)을 반환한다. 필요할 때 자식 연산자에게서 끌어올린다.
  3. close() — 스캔 상태를 풀고, 자식 연산자를 재귀적으로 닫는다.

이 계약에서 세 가지 구조적 결과가 따라 나온다. CUBRID를 포함해 모든 Volcano 스타일 엔진에 공통으로 나타나는 결과들이다.

  • 합성(composition)은 그래프가 아니라 트리로 일어난다. 각 연산자는 자식들을 직접 명명한다. 옵티마이저가 만든 트리 모양이 실행기가 그대로 걷는 모양이다.
  • 비차단(non-blocking) 연산자는 자동으로 파이프라이닝된다. Index 위에 Scan, 그 위에 Filter 가 얹히면 중간 결과를 실체화(materialise)하지 않는다. next() 호출이 그 트리를 그대로 내려갈 뿐이다.
  • 차단(blocking) 연산자(sort, hash-build, group-by, top-N)는 실체화한다. open()(혹은 첫 next()) 안에서 입력을 모두 소진하고, 임시 파일이나 메모리 구조에 적은 뒤, 그 위쪽에서 보면 잎(leaf)처럼 동작한다.

모든 계획 트리의 잎에 자리하는 교과서 스캔 연산자는 셋이다.

  • Heap scan — 행 지향 힙 파일을 순차 순회한다. 술어가 비선택적이며 사용 가능한 인덱스가 없을 때 가장 저렴한 잎이다. CUBRID의 힙 스캔은 HEAP_CHAIN.next_vpid를 따라 페이지를 걷고, 슬롯마다 record_type(REC_HOME / REC_RELOCATION / REC_BIGONE) 으로 분기한다. cubrid-heap-manager.md를 참조한다.
  • Index scan — B+-tree 범위 순회로 (key, OID) 쌍을 얻고, OID 마다 힙 점-조회(point-read)를 한 번씩 한다. 커버링(covering) 인덱스라면 — 참조하는 모든 컬럼이 키 안에 들어 있다면 — 힙 조회 자체를 생략한다.
  • List scan — 이미 실체화해 둔 튜플 스트림을 다시 읽어 들인다. Sort나 서브 블록(sub-block)을 open() 한 결과는 list file이며, 부모 연산자는 그것을 list scan으로 읽는다. List scan은 계층적 XASL 트리를 동작시키는 핵심 장치다. 모든 실체화 서브 XASL은 list 하나를 만들어 내고, 부모는 그 list를 다른 어떤 릴레이션처럼 소비한다.

풀 기반 모델은 잘 알려진 비용 하나와 잘 알려진 이득 하나를 동시에 가진다. 비용은 튜플마다 발생하는 가상 호출 오버헤드(per-tuple virtual call overhead) 다. 이득은 합성이 사실상 자유라는 점(trivial composition) 이다. CUBRID는 이 비용을 치른다(연산자 당, 튜플 당 함수 포인터 한 번 — scan_next_scanscan_next_scan_localscan_next_<type>_scan 경로). 그 대가로 약 600 KB의 query_executor.c 글루(glue)만으로 SQL 표면 전체를 처리한다. 상관 서브쿼리, GROUP BY, 분석 윈도우(analytic window), 계층 질의, CTE, 파티션 클래스가 모두 이 안에서 처리된다. 벡터화 대안을 도입하려면 모든 연산자의 인터페이스를 다시 짜야 한다. 그 작업은 일어나지 않았다. 이 문서가 추적하는 대상은 따라서 현재 시점의 계획 트리 이터레이터 모델 이다.

DBMS 공통 설계 패턴 (Common DBMS Design)

섹션 제목: “DBMS 공통 설계 패턴 (Common DBMS Design)”

이터레이터 모델 실행기는 PostgreSQL이든 MySQL/InnoDB든, Oracle이든, SQL Server든, CUBRID든 — Graefe의 세 메서드 계약 위에 같은 패턴들을 얹는다. 이 어휘를 먼저 명명해 두면, 다음 섹션 ## CUBRID의 구현 에서 설명하는 CUBRID의 선택은 발명 이 아니라 공유 설계 공간 위에 놓인 다이얼 조합(dial settings) 으로 읽힌다.

각 연산자는 open / next / close 호출을 가로질러 살아 있는 상태 객체(state object)를 가진다. PostgreSQL은 불변 plan(Plan)과 실행별 plan state(PlanState)를 분리하며, ExecProcNode(PlanState *)type 을 읽어 ExecInitNode 시점에 설치된 함수 포인터로 연산자별 next(ExecSeqScan, ExecIndexScan, ExecHashJoin, …)로 점프한다. MySQL의 핸들러 API(handler::rnd_init / rnd_next / rnd_end)는 스토리지 측 스캔을 같은 역할을 한다. CUBRID의 계획은 XASL 노드 트리(xasl_node)다. 스캔의 연산자별 상태는 SCAN_ID 라는 태그 유니언(tagged union)이다(HEAP_SCAN_ID, INDX_SCAN_ID, LLIST_SCAN_ID, SHOWSTMT_SCAN_ID 등 중 하나). 디스 패치는 qexec_execute_mainblock_internal 안의 switch (xasl->type) 이며, BUILDLIST_PROC / BUILDVALUE_PROC / UNION_PROC / MERGELIST_PROC / UPDATE_PROC / DELETE_PROC / INSERT_PROC / MERGE_PROC / CONNECTBY_PROC / CTE_PROC / DO_PROC 등에 분기 한다. 행을 만들어 내는 타입은 공통의 풀 인터프리터 qexec_intprt_fnc 로 떨어진다.

Sort, hash-build, group-by, 그리고 서브 블록 경계는 스트리밍이 불가능하다. 표준 구현은 차단 연산자에 백킹(backing) 임시 파일을 주고, 그 파일을 스캔 하는 결과를 출력으로 두는 방식이다. PostgreSQL은 Tuplesortstate / Tuplestorestate를 쓴다. CUBRID는 list file(QFILE_LIST_ID, list_file.c)을 쓰며, 부모는 그것을 list scan(scan_open_list_scan)으로 다시 읽는다. 모든 BUILDLIST XASL은 xasl->list_id 안으로 실체화된다. 질의의 최종 결과는 최상위 XASL의 list file이며 qexec_execute_query가 이를 반환한다.

서브쿼리 배치 — 세 개의 포인터

섹션 제목: “서브쿼리 배치 — 세 개의 포인터”

서브쿼리가 연산자 트리에서 어디에 매달리는지가 언제 실행되는지 를 결정한다. PostgreSQL은 InitPlan(비상관, 단발 실행)과 SubPlan(상관, 행마다 실행)을 구분한다. CUBRID는 같은 구분을 XASL 노드 위에서 직접 인코딩한다.

  • xasl->aptr_list — 비상관 서브쿼리. 외측 스캔 이전에 한 번만 실행한다. 결과는 dbvalptr이 실체화된 튜플을 가리키는 CONSTANT 타입의 REGU_VARIABLE으로 참조된다.
  • xasl->dptr_list — 상관 서브쿼리. 외측의 상관 컬럼이 바인딩된 뒤, qexec_intprt_fnc 안에서 외측 행마다 한 번씩 실행된다.
  • xasl->scan_ptr — 외측이 그 위에서 반복하는 체인 스캔 연산자. 다중 테이블 조인은 outer.scan_ptr = middle, middle.scan_ptr = inner 식의 체인으로 풀린다.

잎이 인덱스 스캔일 때 술어는 셋으로 갈린다. range 는 인덱스 순회의 범위를 정한다. key 는 순회 도중 인덱스 안의 컬럼에 적용된다. data 는 힙 페치 이후에 적용된다. CUBRID의 INDX_SCAN_ID 는 이를 range_pred / key_pred / scan_pred로 저장하며, 각각이 자기 regu_listpr_eval_fnc를 가진다. MySQL의 range/ref/Using where나 PostgreSQL의 IndexQual/IndexFilter/ Filter도 같은 분할이다.

힙 스캔은 페치한 레코드마다 mvcc_satisfies_snapshot을 호출한다 (heap_nextheap_get_visible_version_internal 경로). 인덱스 스캔은 mvcc_select_lock_needed가 켜져 있을 때 (SELECT ... FOR UPDATE) locator_lock_and_get_object_with_evaluation 으로 한 번 더 검증한다.

이론 (Theory)CUBRID 명칭
연산자 계획 노드xasl_node / XASL_NODE (src/xasl/xasl_node.hpp)
계획 상태 / 실행별 상태xasl_state + spec별 SCAN_ID (src/query/scan_manager.h)
연산자 타입에 의한 디스패치qexec_execute_mainblock_internalswitch (xasl->type) (query_executor.c)
일반 풀 루프 인터프리터qexec_intprt_fnc (query_executor.c)
연산자 open()qexec_open_scan (query_executor.c) → scan_open_<type>_scan (scan_manager.c)
연산자 next()scan_next_scanscan_next_scan_localscan_next_<type>_scan (scan_manager.c)
연산자 close()scan_end_scan + scan_close_scan (scan_manager.c)
실체화 출력QFILE_LIST_ID (list_file.c); XASL마다 xasl->list_id
비상관 서브쿼리xasl->aptr_list (스캔 전 단발 실행)
상관 서브쿼리xasl->dptr_list (외측 행마다 실행)
조인 / 중첩 연산자 체인xasl->scan_ptr (단일 링크 체인, 위에서 아래로 순회)
인덱스 range / key / data 술어INDX_SCAN_ID::range_pred / key_pred / scan_pred
술어 평가 함수 포인터eval_fnc(query_evaluator.c)가 설정한 SCAN_PRED::pr_eval_fnc
외측 블록 반복 진입qexec_next_scan_block_iterations (query_executor.c)
외측 블록 결과 기록qexec_end_one_iteration (query_executor.c)
Group-byqexec_groupby / qexec_hash_gby_* (query_executor.c)
Order-by + DISTINCTqexec_orderby_distinct (query_executor.c)
분석 함수qexec_execute_analytic (query_executor.c)
서버 측 질의 진입xqmgr_execute_queryqmgr_process_queryqexec_execute_query
Heap-scan 연산자S_HEAP_SCAN SCAN_ID 타입, scan_open_heap_scan이 연다
Index-scan 연산자S_INDX_SCAN SCAN_ID 타입, scan_open_index_scan이 연다
List-scan 연산자S_LIST_SCAN SCAN_ID 타입, scan_open_list_scan이 연다
병렬 힙 스캔S_PARALLEL_HEAP_SCAN (scan_open_parallel_heap_scan, query/parallel/)
스캔별 힙 상태HEAP_SCANCACHE (캐시된 page latch + 스냅샷 — cubrid-heap-manager.md 참조)
레코드별 디코더HEAP_CACHE_ATTRINFO (heap_attrinfo.h; heap_attrinfo_read_dbvalues)

실행기가 하는 일은 네 가지다. (1) XASL 트리를 걷고 각 노드를 xasl->type으로 디스패치한다. (2) 행을 만드는 타입에 대해서는 spec의 SCAN_ID에 대한 균일한 open/next/close 루프를 돌린다. (3) 차단 노드(blocking node)는 출력을 list file로 실체화하고, list scan으로 다시 읽는다. (4) 경계 노드(group-by, order-by, analytic)는 메인 루프 이후의 별도 단계에서 구동한다. 이 넷은 순차로 일어나는 것이 아니라 실행 모델 안에서 교차한다. 이하는 실제 질의가 그것들을 통과하는 순서대로 따라 간다.

서버 측 진입 — xqmgr_execute_query에서 qexec_execute_mainblock_internal까지

섹션 제목: “서버 측 진입 — xqmgr_execute_query에서 qexec_execute_mainblock_internal까지”

질의는 query manager를 거쳐 실행기에 도달한다. xqmgr_execute_queryXASL_ID로 캐시된 XASL을 찾고, 호스트 변수(host variable) 버퍼를 풀어낸 뒤 qmgr_process_query를 호출한다. 이쪽이 (이미 트리가 아니라면) XASL 스트림을 풀어 트리로 만들어 qexec_execute_query로 넘긴다. 후자는 호스트 변수와 타임존을 담은 VAL_DESCR을 들고 있는 XASL_STATE를 초기화한 뒤, qexec_execute_mainblock으로 디스패치 한다. 이 함수가 재귀 진입점이며, 이후 모든 서브쿼리·조인 블록·CTE가 다시 들어오는 자리이기도 하다.

// qexec_execute_mainblock — src/query/query_executor.c (condensed)
int
qexec_execute_mainblock (THREAD_ENTRY * thread_p, xasl_node * xasl,
xasl_state * xstate,
UPDDEL_CLASS_INSTANCE_LOCK_INFO * p_class_lock_info)
{
// ... recursion-depth guard, trace timer setup ...
thread_inc_recursion_depth (thread_p);
error = qexec_execute_mainblock_internal (thread_p, xasl, xstate, p_class_lock_info);
// ... trace timer stop, perfmon stats accumulate ...
thread_dec_recursion_depth (thread_p);
return error;
}

qexec_execute_mainblock_internal이 연산자 타입 디스패치를 한다.

// qexec_execute_mainblock_internal — src/query/query_executor.c (condensed)
static int
qexec_execute_mainblock_internal (THREAD_ENTRY * thread_p, XASL_NODE * xasl,
XASL_STATE * xasl_state,
UPDDEL_CLASS_INSTANCE_LOCK_INFO * p_lock_info)
{
// ... limit-clause early exit ...
switch (xasl->type)
{
case UPDATE_PROC: error = qexec_execute_update (thread_p, xasl, false, xasl_state); break;
case DELETE_PROC: error = qexec_execute_delete (thread_p, xasl, xasl_state); break;
case INSERT_PROC: error = qexec_execute_insert (thread_p, xasl, xasl_state, false); break;
case MERGE_PROC: error = qexec_execute_merge (thread_p, xasl, xasl_state); break;
case CTE_PROC: error = qexec_execute_cte (thread_p, xasl, xasl_state); break;
case DO_PROC: error = qexec_execute_do_stmt(thread_p, xasl, xasl_state); break;
case CONNECTBY_PROC: /* fall through to special hierarchical interpreter */ break;
/* BUILDLIST_PROC / BUILDVALUE_PROC / UNION_PROC / ... fall through to pull-loop below */
}
// ... pre-processing (open scan, run aptr)
// ... processing (qexec_intprt_fnc loop)
// ... post-processing (group-by, order-by, analytic)
}

DML proc 타입이 별도 함수로 라우팅되는 이유는, 외측 스캔을 구동하면서 동시에 적절한 잠금을 걸어 변경을 수행해야 하기 때문이다. 행을 만들어 내는 타입은 공통의 풀 인터프리터를 공유한다.

XASL 트리 모양 — aptr / dptr / scan_ptr

섹션 제목: “XASL 트리 모양 — aptr / dptr / scan_ptr”

XASL 노드 하나에는 주변을 잡아 주는 다른 XASL을 가리키는 포인터가 셋 매달려 있다.

flowchart TB
  subgraph X["xasl_node"]
    T["type:<br/>BUILDLIST / BUILDVALUE /<br/>UNION / SCAN / ..."]
    SP["spec_list<br/>(ACCESS_SPEC_TYPE 체인 —<br/>이 블록의 테이블/인덱스/list-file)"]
    OP["outptr_list<br/>(REGU_VARIABLE 리스트 — 출력 프로젝션)"]
    A["aptr_list →<br/>비상관 서브쿼리<br/>(스캔 전, 한 번만 실행)"]
    D["dptr_list →<br/>상관 서브쿼리<br/>(외측 행마다 실행)"]
    S["scan_ptr →<br/>체인의 다음 연산자<br/>(조인 블록 / 중첩 스캔)"]
    L["list_id →<br/>실체화 출력<br/>(QFILE_LIST_ID)"]
  end
  A -. recurse .-> X1["aptr xasl"]
  D -. recurse .-> X2["dptr xasl"]
  S -. recurse .-> X3["scan_ptr xasl"]

v0.7 deck의 예시가 이 관계를 구체적으로 보여 준다.

SELECT (select col1 from tab c where col1 = a.col1) -- DPTR (correlated)
FROM tab a, tab b -- SCAN_PTR chain
WHERE a.col2 = b.col2
AND a.col1 = (select col1 from tab d); -- APTR (uncorrelated)
flowchart LR
  TopXASL["BUILDLIST<br/>(SELECT)<br/>spec=tab a"]
  AptrXASL["BUILDLIST<br/>spec=tab d<br/>flag=XASL_LINK_TO_REGU_VARIABLE"]
  DptrXASL["BUILDLIST<br/>spec=tab c<br/>flag=XASL_LINK_TO_REGU_VARIABLE"]
  ScanPtrXASL["SCAN_PROC<br/>spec=tab b"]
  TopXASL -- aptr_list --> AptrXASL
  TopXASL -- dptr_list --> DptrXASL
  TopXASL -- scan_ptr --> ScanPtrXASL

계획 평가 순서: aptr → 외측 스캔 시작 → 외측 행마다 → dptr(행마다 한 번 실행) → scan_ptr(조인). 그 후 end_one_iteration이 프로젝션된 행을 외측의 list_id에 적는다.

BUILDLIST / BUILDVALUE / SCAN_PROC 노드에 대해서는 실행기가 일반 인터프리터를 돌린다.

// qexec_intprt_fnc — src/query/query_executor.c (condensed)
static SCAN_CODE
qexec_intprt_fnc (THREAD_ENTRY * thread_p, XASL_NODE * xasl,
XASL_STATE * xasl_state, QFILE_TUPLE_RECORD * tplrec,
XASL_SCAN_FNC_PTR next_scan_fnc)
{
while ((xb_scan = qexec_next_scan_block_iterations (thread_p, xasl)) == S_SUCCESS)
{
while ((ls_scan = scan_next_scan (thread_p, &xasl->curr_spec->s_id)) == S_SUCCESS)
{
/* per-row: bptr (path expressions) */
for (xptr = xasl->bptr_list; qualified && xptr; xptr = xptr->next)
qexec_execute_obj_fetch (thread_p, xptr, xasl_state, scan_op);
/* per-row: dptr (correlated subqueries) */
for (xptr = xasl->dptr_list; xptr; xptr = xptr->next)
qexec_execute_mainblock (thread_p, xptr, xasl_state, NULL);
/* per-row: scan_ptr (joined block, recursive) */
if (xasl->scan_ptr)
qexec_execute_scan (thread_p, xasl->scan_ptr, xasl_state, tplrec, next_scan_fnc);
/* per-row: predicates not pushed to the scan */
if (xasl->if_pred)
ev_res = eval_pred (thread_p, xasl->if_pred, &xasl_state->vd, NULL);
/* write the qualified row to xasl->list_id */
if (qualified)
qexec_end_one_iteration (thread_p, xasl, xasl_state, tplrec);
}
}
}

세 겹의 중첩 루프다. 가장 바깥은 스캔 블록(scan block)을 도는 루프 이며(next_scan_block이 호출될 때마다 새 access spec 조합을 위해 SCAN_ID를 리셋한다. 아래 블록 반복 항목 참조), 가운데는 현재 블록 안에서 튜플을 끌어올리는 scan_next_scan 루프, 가장 안은 행마다 해야 할 일(경로 표현식, 상관 서브쿼리, 조인 체인, 잔여 술어)을 차례로 처리한다. 모양 자체는 정확히 Volcano의 그것이며, 가드 링이 하나가 아니라 셋이라는 차이뿐이다.

Open / Next / Close — SCAN_ID 가 연산자의 상태

섹션 제목: “Open / Next / Close — SCAN_ID 가 연산자의 상태”

SCAN_ID는 CUBRID가 지원하는 모든 스캔 종류를 덮는 태그 유니언이다. type 필드가 유니언 가운데 어느 가지가 살아 있는지를 고른다.

// SCAN_ID — src/query/scan_manager.h (sketch)
typedef enum {
S_HEAP_SCAN, S_HEAP_SCAN_RECORD_INFO, S_HEAP_PAGE_SCAN, S_HEAP_SAMPLING_SCAN,
S_INDX_SCAN, S_INDX_KEY_INFO_SCAN, S_INDX_NODE_INFO_SCAN,
S_LIST_SCAN,
S_CLASS_ATTR_SCAN,
S_SET_SCAN, S_JSON_TABLE_SCAN,
S_VALUES_SCAN,
S_DBLINK_SCAN,
S_PARALLEL_HEAP_SCAN,
S_SHOWSTMT_SCAN,
...
} SCAN_TYPE;
struct scan_id_struct
{
SCAN_TYPE type;
SCAN_OPERATION_TYPE scan_op_type;
bool fixed; /* PEEK ok? (page latch held across calls) */
bool grouped; /* iterating in groups (UPDATE/DELETE join helper) */
bool mvcc_select_lock_needed;
S_DIRECTION direction; /* S_FORWARD / S_BACKWARD */
S_POSITION position; /* S_BEFORE / S_ON / S_AFTER */
S_STATUS status; /* S_OPENED / S_STARTED / S_ENDED / S_CLOSED */
QPROC_QUALIFICATION qualification;
VAL_LIST *val_list; /* output projection target */
VAL_DESCR *vd;
union {
HEAP_SCAN_ID hsid;
INDX_SCAN_ID isid;
LLIST_SCAN_ID llsid; /* list scan */
SET_SCAN_ID ssid;
REGU_VALUES_SCAN_ID rvsid; /* VALUES (...), VALUES (...) */
PARALLEL_HEAP_SCAN_ID phsid;
/* ... */
} s;
};

SCAN_ID의 생애 주기는 Heap scan deck이 명시한 다섯 단계(textbook five-step) 그대로다.

stateDiagram-v2
  [*]            --> S_OPENED   : scan_open_<type>_scan
  S_OPENED       --> S_STARTED  : scan_start_scan (초기 페이지 fix, 캐시 할당)
  S_STARTED      --> S_STARTED  : scan_next_scan 루프
  S_STARTED      --> S_ENDED    : scan_end_scan (블록별 자원 해제)
  S_ENDED        --> S_OPENED   : (다음 access-spec / 블록 반복)
  S_ENDED        --> [*]        : scan_close_scan (file/list/temp 해제)

start/endopen/close가 분리된 이유는 블록 반복(block iterations) 때문이다. 질의가 access spec을 여러 개 가진다면 — 예를 들어 두 테이블의 UNION-ALL이거나, 서브 클래스를 여럿 거느린 파티션 클래스라면 — 각 블록은 start_scan → next_scan*loop → end_scan을 돈다. 그동안에도 파일 수준 자원(open_scan 시점에 할당)은 블록 경계를 넘어 살아 있다가 close_scan에서야 풀린다.

scan_open_heap_scan은 access spec의 regu list로부터 HEAP_SCAN_ID를 짠다.

// scan_open_heap_scan — src/query/scan_manager.c (condensed)
int
scan_open_heap_scan (THREAD_ENTRY * thread_p, SCAN_ID * scan_id,
bool mvcc_select_lock_needed, SCAN_OPERATION_TYPE op_type,
int fixed, int grouped, QPROC_SINGLE_FETCH single_fetch,
DB_VALUE * join_dbval, val_list_node * val_list, VAL_DESCR * vd,
OID * cls_oid, HFID * hfid,
regu_variable_list_node * regu_list_pred, PRED_EXPR * pr,
regu_variable_list_node * regu_list_rest, ...)
{
HEAP_SCAN_ID *hsidp = &scan_id->s.hsid;
scan_id->type = scan_type; /* S_HEAP_SCAN */
scan_init_scan_id (scan_id, mvcc_select_lock_needed, op_type, fixed, grouped, ...);
COPY_OID (&hsidp->cls_oid, cls_oid);
hsidp->hfid = *hfid;
UT_CAST_TO_NULL_HEAP_OID (&hsidp->hfid, &hsidp->curr_oid); /* iterator cursor */
/* attach scan predicate + its compiled evaluator (eval_fnc picks the fastest
* pr_eval_fnc for this PRED_EXPR shape) */
scan_init_scan_pred (&hsidp->scan_pred, regu_list_pred, pr,
((pr) ? eval_fnc (thread_p, pr, &single_node_type) : NULL));
scan_init_scan_attrs (&hsidp->pred_attrs, num_attrs_pred, attrids_pred, cache_pred);
hsidp->rest_regu_list = regu_list_rest;
scan_init_scan_attrs (&hsidp->rest_attrs, num_attrs_rest, attrids_rest, cache_rest);
hsidp->scancache_inited = false; /* HEAP_SCANCACHE allocated lazily in start_scan */
hsidp->scanrange_inited = false;
return NO_ERROR;
}

스캔 타입을 가리지 않고 모양은 동일하다. 옵티마이저가 만든 access spec에서 식별자와 술어를 SCAN_ID의 유니언으로 복사하고, 컴파일된 술어 평가기를 설치한 뒤, 실제 페이지 fix와 latch 획득은 scan_start_scan까지 미룬다. eval_fnc(query_evaluator.c)는 연산자 융합(operator fusion)의 진입점이기도 하다. PRED_EXPR의 모양을 들여다보고 최적화된 함수 포인터(단일 비교, conjunction, disjunction, between, …) 가운데 하나를 돌려 준다. 그 결과, 행마다 안쪽 루프가 일반 술어 트리를 재귀로 풀어내는 대신 간접 호출 한 번만 치르면 된다.

scan_next_heap_scan이 튜플 단위 next()다. 구조는 Heap scan deck이 설명한 그것과 정확히 일치한다.

// scan_next_heap_scan — src/query/scan_manager.c (condensed)
static SCAN_CODE
scan_next_heap_scan (THREAD_ENTRY * thread_p, SCAN_ID * scan_id)
{
HEAP_SCAN_ID *hsidp = &scan_id->s.hsid;
FILTER_INFO data_filter = { &hsidp->scan_pred, &hsidp->pred_attrs,
NULL, NULL, scan_id->val_list, scan_id->vd,
&hsidp->cls_oid, ... };
while (1)
{
/* 1. Pull the next visible tuple from the heap. */
sp_scan = heap_next (thread_p, &hsidp->hfid, &hsidp->cls_oid, &hsidp->curr_oid,
&recdes, &hsidp->scan_cache, is_peeking);
if (sp_scan != S_SUCCESS)
return (sp_scan == S_END) ? S_END : S_ERROR;
/* 2. Evaluate the WHERE-pushed predicate against the tuple's pred_attrs. */
ev_res = eval_data_filter (thread_p, p_current_oid, &recdes,
&hsidp->scan_cache, &data_filter);
if (ev_res != V_TRUE) continue; /* not qualified — try next */
/* 3. If FOR UPDATE / non-MVCC class, lock and re-fetch — re-evaluate predicate. */
if (scan_id->mvcc_select_lock_needed)
{
sp_scan = locator_lock_and_get_object_with_evaluation (...);
if (mvcc_reev_data.filter_result == V_FALSE) continue;
}
/* 4. Fetch the rest of the columns (those not needed for the predicate). */
if (hsidp->rest_regu_list)
fetch_val_list (thread_p, hsidp->rest_regu_list, scan_id->vd,
&hsidp->cls_oid, p_current_oid, NULL, COPY);
return S_SUCCESS;
}
}

네 단계의 안쪽 모양 — 다음 객체 → 술어 → 필요시 잠금 → 잔여 값 — 은 술어-속성 분할(predicate-attribute split) 의 실행기 측 증폭기다. 술어 평가 전에는 pred_attrs만 디코드되며, WHERE 절에서 떨어지는 행은 프로젝션 컬럼을 디코드하는 비용을 결코 치르지 않는다. eval_data_filter 안에서 호출되는 heap_attrinfo_read_dbvaluespred_attrs만 디코드한다. rest_attrs 디코더는 행이 자격을 통과한 뒤에야 돈다.

heap_next 자체는 실행기가 부르는 힙 측 이터레이터다.

// heap_next — src/storage/heap_file.c (entry; condensed)
SCAN_CODE
heap_next (THREAD_ENTRY * thread_p, const HFID * hfid, OID * class_oid,
OID * next_oid, RECDES * recdes, HEAP_SCANCACHE * scan_cache,
int ispeeking)
{
return heap_next_internal (thread_p, hfid, class_oid, next_oid, recdes,
scan_cache, ispeeking, false /*reversed*/, NULL, NULL);
}
// heap_next_internal — src/storage/heap_file.c (condensed)
static SCAN_CODE
heap_next_internal (THREAD_ENTRY * thread_p, const HFID * hfid, OID * class_oid,
OID * next_oid, RECDES * recdes, HEAP_SCANCACHE * scan_cache,
bool ispeeking, bool reversed_direction, ...)
{
while (true)
{
while (true)
{
/* Reuse the cached page latch if it covers oid; otherwise unfix
* the old one and fix the new vpid via heap_scan_pb_lock_and_fetch. */
if (scan_cache->page_watcher.pgptr != NULL && !VPID_EQ (&vpid, vpidptr_incache))
pgbuf_replace_watcher (thread_p, &scan_cache->page_watcher, &old_page_watcher);
if (scan_cache->page_watcher.pgptr == NULL)
scan_cache->page_watcher.pgptr =
heap_scan_pb_lock_and_fetch (thread_p, &vpid, OLD_PAGE_PREVENT_DEALLOC,
S_LOCK, scan_cache, &scan_cache->page_watcher);
/* Walk to next slot on the page; if we ran off the page, follow
* HEAP_CHAIN.next_vpid and continue. */
...
}
/* Apply mvcc_satisfies_snapshot and follow REC_RELOCATION / REC_BIGONE
* chains to materialise the visible record body into recdes. */
...
}
}

cache_last_fix_page 패턴 — 연속한 next-OID가 같은 페이지에 떨어지는 한 page latch를 heap_next 호출 사이에 유지 한다. 이 힙 스캔에 튜플당 평균(amortised) O(1)의 page-fix 비용을 부여한다. 레코드 타입 디스패치(REC_HOME / REC_RELOCATIONREC_NEWHOME / REC_BIGONE → overflow)가 heap_next_internal 안에서 일어나는 양상은 cubrid-heap-manager.md를 참조한다.

인덱스 스캔 경로 — 술어 분할 + 힙 점-조회

섹션 제목: “인덱스 스캔 경로 — 술어 분할 + 힙 점-조회”

scan_open_index_scanINDX_SCAN_ID를 만든다. 그 안의 세 술어 슬롯 — range_pred, key_pred, scan_pred — 은 각각 자기 regu list와 eval_fnc로 컴파일된 평가기를 가진다. scan_next_index_scanbtree_range_search 아래에서 B+-tree를 걷는다. 순회 도중 range/key 술어를 평가하고, 살아남은 OID마다 둘 중 하나를 한다. (a) 인덱스가 커버링 이라 프로젝션이 키 안에서 모두 만족된다면 인덱스에서 곧장 돌려준다. (b) 그렇지 않다면 locator_attribute_info_force / heap_get_visible_version으로 힙 행을 가져와 scan_pred를 그 위에 적용한다.

flowchart LR
  Spec["ACCESS_SPEC<br/>(TARGET_CLASS, ACCESS_METHOD_INDEX)"] --> Open["scan_open_index_scan"]
  Open --> SID["INDX_SCAN_ID<br/>{range_pred, key_pred, scan_pred,<br/>cls_regu_list_rest, indx_cov, ...}"]
  SID --> Start["scan_start_scan"]
  Start --> Next["scan_next_index_scan"]
  Next -->|btree_range_search| Btree["B+-tree leaf 순회"]
  Btree --> KeyEval["key_pred 평가"]
  KeyEval -- pass --> CovTest{커버링<br/>인덱스?}
  CovTest -- yes --> ListWrite["인덱스 키에서 직접<br/>val_list로 복사"]
  CovTest -- no --> Heap["heap_get_visible_version (oid)"]
  Heap --> ScanEval["scan_pred 평가"]
  ScanEval -- pass --> ListWrite
  ListWrite --> Next

이렇게 술어를 잘게 밀어 두는 이득은 워크로드에 따라 달라진다. key_pred가 인덱스 범위의 90 %를 떨궈 내는 질의라면, 그만큼의 키마다 힙 페치 비용을 한 푼도 치르지 않고 B+-tree leaf 비교만으로 끝 난다.

리스트 스캔 경로 — 실체화된 자식이 부모를 먹이는 방식

섹션 제목: “리스트 스캔 경로 — 실체화된 자식이 부모를 먹이는 방식”

옵티마이저가 서브 블록을 만들어 내는 경우 — 파생 테이블, 정렬된 중간 결과, 인라인이 불가능한 aptr, 집계 결과 등 — 그 서브 블록의 XASL은 자기 자신이 BUILDLIST_PROC이며 xasl->list_id로 실체화한다. 부모의 access spec은 TARGET_LIST이고 그 서브 XASL을 가리킨다.

SELECT a.col1
FROM (select col1 from tab a where col2 = 3) a, tab b
WHERE a.col1 = b.col1;
flowchart LR
  TopXASL["외측 BUILDLIST<br/>spec_list=[TARGET_LIST(a), TARGET_CLASS(b)]"]
  AptrSub["서브 XASL<br/>BUILDLIST(tab a)<br/>실체화 → list_id"]
  TopXASL -- aptr_list --> AptrSub
  TopXASL --> ScanA["a.list_id 위의 scan_open_list_scan"]
  TopXASL --> ScanB["tab b 위의 scan_open_heap_scan"]
  ScanA --> Next1["scan_next_list_scan 튜플"]
  ScanB --> Next2["scan_next_heap_scan 튜플"]

scan_next_list_scanqfile_scan_list_next로 list file을 걷고, 실체화된 튜플에 scan_pred를 적용한 뒤, rest_regu_list로 프로젝션 한다. 모양은 힙 스캔과 같으며, 다른 점은 heap_next 자리에 qfile_scan_list_next가 들어간다는 것과 MVCC 단계가 없다는 것뿐이다 (list file은 이미 프로젝션된 DB_VALUE를 들고 있다).

인터프리터는 스캔 타입별 next를 직접 부르지 않는다. 항상 scan_next_scan을 거친다.

// scan_next_scan — src/query/scan_manager.c (condensed)
SCAN_CODE
scan_next_scan (THREAD_ENTRY * thread_p, SCAN_ID * s_id)
{
return scan_handle_single_scan (thread_p, s_id, scan_next_scan_local);
}

scan_handle_single_scan은 (a) SELECT-INTO 류의 질의를 위한 single_fetch 의미론, (b) 튜플 단위 통계 카운터, (c) qualification 해소 루프를 처리하는 래퍼를 더한다. 실제 디스패치는 scan_next_scan_local이 한다.

// scan_next_scan_local — src/query/scan_manager.c (condensed)
static SCAN_CODE
scan_next_scan_local (THREAD_ENTRY * thread_p, SCAN_ID * scan_id)
{
switch (scan_id->type)
{
case S_HEAP_SCAN:
case S_HEAP_SCAN_RECORD_INFO:
case S_HEAP_SAMPLING_SCAN:
return scan_next_heap_scan (thread_p, scan_id);
case S_INDX_SCAN:
return scan_next_index_scan (thread_p, scan_id);
case S_LIST_SCAN:
return scan_next_list_scan (thread_p, scan_id);
case S_CLASS_ATTR_SCAN:
return scan_next_class_attr_scan (thread_p, scan_id);
case S_SHOWSTMT_SCAN:
return scan_next_showstmt_scan (thread_p, scan_id);
case S_SET_SCAN:
return scan_next_set_scan (thread_p, scan_id);
case S_JSON_TABLE_SCAN:
return scan_next_json_table_scan (thread_p, scan_id);
case S_VALUES_SCAN:
return scan_next_value_scan (thread_p, scan_id);
case S_DBLINK_SCAN:
return scan_next_dblink_scan (thread_p, scan_id);
case S_HEAP_PAGE_SCAN:
return scan_next_heap_page_scan (thread_p, scan_id);
case S_INDX_KEY_INFO_SCAN:
return scan_next_index_key_info_scan (thread_p, scan_id);
/* ... S_PARALLEL_HEAP_SCAN: returned by a different path, see below ... */
default:
return S_ERROR;
}
}

새 스캔 연산자를 추가하는 작업은 닫힌 수술이다. SCAN_TYPE을 늘리고, 유니언 가지를 추가하고, open/next/start/end/close 한 벌을 더한 뒤, 이 switch에 case를 한 줄 붙이면 된다.

SELECT col1, col2 FROM t1 WHERE col1 = 1;

계획은 t1 위의 TARGET_CLASS access spec 하나를 가진 단일 BUILDLIST_PROC다. 옵티마이저가 ACCESS_METHOD_SEQUENTIAL을 선택했다고 가정한다(쓸 만한 인덱스가 없거나, seqscan 비용이 더 낮다). 런타임 경로는 다음과 같다.

sequenceDiagram
  participant CL as Client
  participant QM as qmgr_process_query
  participant QE as qexec_execute_query
  participant MB as qexec_execute_mainblock_internal
  participant IPF as qexec_intprt_fnc
  participant SC as scan_next_scan / scan_next_heap_scan
  participant HF as heap_next_internal
  participant LF as list_file
  CL->>QM: xqmgr_execute_query (xasl_id, host_vars)
  QM->>QE: qexec_execute_query (xasl_p)
  QE->>MB: qexec_execute_mainblock_internal
  Note over MB: 전처리(Pre-processing)
  MB->>MB: qexec_open_scan(spec=t1) → scan_open_heap_scan
  MB->>MB: aptr 루프 (없음)
  MB->>IPF: BUILDLIST_PROC 디스패치
  Note over IPF: 처리(Processing) — 풀 루프
  loop while qexec_next_scan_block_iterations == S_SUCCESS
    IPF->>SC: scan_start_scan + scan_next_scan
    loop while scan_next_scan == S_SUCCESS
      SC->>HF: heap_next (다음 OID, RECDES 페치)
      HF-->>SC: RECDES + page latch (캐시됨)
      SC->>SC: heap_attrinfo_read_dbvalues (pred_attrs)
      SC->>SC: pr_eval_fnc(scan_pred) — col1 = 1 ?
      alt qualified
        SC->>SC: fetch_val_list (rest_regu_list — col2)
        SC-->>IPF: S_SUCCESS
        IPF->>IPF: dptr 루프, scan_ptr 루프, if_pred
        IPF->>LF: qexec_end_one_iteration → 튜플 기록
      else not qualified
        SC->>SC: continue (rest 페치 없음)
      end
    end
    SC->>SC: scan_end_scan
  end
  Note over MB: 후처리(Post-processing)
  MB->>MB: groupby? 없음. orderby? 없음. analytic? 없음.
  MB->>MB: qexec_close_scan
  MB-->>QE: NO_ERROR
  QE-->>QM: xasl->list_id
  QM-->>CL: 복제된 QFILE_LIST_ID

deck이 짚어 준 점선 같은 성질은 이거다. 이 루프는 조인이라고 해서 달라지지 않는다. 조인은 단지 xasl->scan_ptr을 NULL이 아니게 만들 뿐이며, qexec_intprt_fnc의 행별 본문이 내부 블록을 qexec_execute_scan을 재귀적으로 부른다. 마찬가지로 상관 서브쿼리는 자격을 통과한 외측 행마다 한 번씩 도는 dptr_list 항목을 더할 뿐이다.

access spec이 다중 타깃을 덮는 경우 — 보통 파티션 클래스(ALL super_class)나 OID-list spec — spec은 ACCESS_SPEC_TYPE 노드 체인을 들고 있다. 인터프리터는 이를 qexec_next_scan_block_iterations로 처리한다. 외측 블록이 리셋될 때마다, spec 체인을 위에서 아래로 전진시키고 모든 조인 블록 스캔을 함께 리셋한다.

flowchart LR
  Out0["외측 access_spec<br/>= (a, b)"] -- 블록 1: a --> InRing0
  Out0 -- 블록 2: b --> InRing1
  subgraph InRing0[" "]
    direction LR
    In0a["내측 access_spec<br/>= (c, d)<br/>블록 1: c"] --> In0b["내측 블록 2: d"]
  end
  subgraph InRing1[" "]
    direction LR
    In1a["내측 access_spec<br/>= (c, d)<br/>블록 1: c (리셋)"] --> In1b["내측 블록 2: d"]
  end

네 블록 전개 (a∪b)⋈(c∪d) = (a⋈c)∪(a⋈d)∪(b⋈c)∪(b⋈d) 는, 가장 바깥의 XASL에서 qexec_next_scan_block으로 외측 블록 셀렉터를 돌리고, 그 아래에서 모든 scan_ptr 체인을 v0.7 deck이 보여 준 루프로 재귀 리셋하면 자연히 떨어진다.

// qexec_next_scan_block_iterations — src/query/query_executor.c (sketch)
static SCAN_CODE
qexec_next_scan_block_iterations (THREAD_ENTRY * thread_p, XASL_NODE * xasl)
{
/* find the deepest scan_ptr that still has a qualified block */
for (last_xptr = xasl; last_xptr->scan_ptr; last_xptr = last_xptr->scan_ptr)
if (!last_xptr->next_scan_block_on || (...))
break;
/* advance that scan_ptr to its next block */
xs_scan = qexec_next_scan_block (thread_p, last_xptr);
/* reset every scan_ptr below it back to its first block */
for (xptr2 = last_xptr; xptr2; xptr2 = xptr2->scan_ptr)
qexec_next_scan_block (thread_p, xptr2->scan_ptr);
/* reset every scan above it to re-run on the new combination */
while (last_xptr != xasl) { /* unwind back up */ }
}

서브쿼리 — APTR은 한 번 돌고, DPTR은 행마다 돌며, REGU_VARIABLE이 결과를 묶는다

섹션 제목: “서브쿼리 — APTR은 한 번 돌고, DPTR은 행마다 돌며, REGU_VARIABLE이 결과를 묶는다”

두 종류의 서브쿼리는 모두 별개의 XASL 트리로 끝나고, 재귀적 qexec_execute_mainblock으로 실행된다. 차이는 언제 실행되는가와, 결과가 어떻게 부모에 도달하는가 다.

APTR(xasl->aptr_list)의 경우, 외측의 전처리가 한 번만 qexec_execute_mainblock(aptr)을 돌린다. 결과 list는 value.dbvalptr 가 실행기가 실체화한 튜플의 슬롯을 가리키는 CONSTANT 타입의 REGU_VARIABLE로 참조된다. 그 aptr의 XASL은 XASL_LINK_TO_REGU_VARIABLE 플래그가 설정되어 있어, 나중에 실행기가 aptr_list를 다시 걸을 때 이를 건너뛴다(결과는 이미 실체화·바인딩되어 있다).

DPTR(xasl->dptr_list)의 경우, 행 단위 루프가 외측 행마다 qexec_execute_mainblock(dptr)을 돌린다. dptr의 스캔 안에서 외측의 상관 컬럼은 보인다. 같은 VAL_DESCR(XASL_STATE 경유)를 공유하기 때문이다.

파생 테이블 방식의 FROM 절 서브쿼리의 경우, 외측의 spec은 내측 XASL을 가리키는 TARGET_LIST다. 전처리에서 aptr로 내측을 한 번 돌리고, 외측은 scan_open_list_scan으로 결과를 읽는다.

후처리 — group-by, order-by, analytic

섹션 제목: “후처리 — group-by, order-by, analytic”

메인 풀 루프가 끝나면 실행기는 xasl->list_id를 세 단계의 선택적 후처리를 돌린다.

// post-processing tail — qexec_execute_mainblock_internal (sketch)
{
/* ... main loop drained ... */
/* GROUP BY — sort or hash, then evaluate aggregates per group */
if (xasl->type == BUILDLIST_PROC && xasl->proc.buildlist.groupby_list)
qexec_groupby (thread_p, xasl, xasl_state, &tplrec);
/* analytic functions (window) — separate post-pass over list_id */
if (xasl->type == BUILDLIST_PROC && xasl->proc.buildlist.a_eval_list)
qexec_execute_analytic (thread_p, xasl, xasl_state, ...);
/* ORDER BY / DISTINCT — sort_listfile with SORT_ELIM_DUP or SORT_DUP */
if (xasl->orderby_list || (XASL_IS_FLAGED (xasl, XASL_HAS_DISTINCT)))
qexec_orderby_distinct (thread_p, xasl, query_opt_mode, &tplrec);
}

Group-by는 두 구현 — sort group-by와 hash group-by — 가운데 옵티마이저가 하나를 고른다.

flowchart LR
  subgraph Sort["sort group-by (qexec_groupby)"]
    SR["결과 리스트"] --> SS["sort_listfile"]
    SS --> SI["정렬된 run 순회"]
    SI --> SA["ACC 누적<br/>(qdata_evaluate_aggregate_list)"]
    SA --> SO["결과 리스트"]
  end
  subgraph Hash["hash group-by"]
    HM["행별 평가<br/>(qexec_hash_gby_agg_tuple)"] --> HACC["인메모리 hash ACC"]
    HACC -- spill --> HPL["부분 리스트 (정렬됨)"]
    HPL --> HM2["sort_listfile"]
    HM2 --> HMG["같은 키 그룹 병합<br/>(qdata_aggregate_accumulator_to_accumulator)"]
    HMG --> HSA["잔여 ACC 누적"]
    HSA --> HO["결과 리스트"]
  end

Hash group-by는 인메모리 hash가 설정된 예산 (prm_get_integer_value(PRM_ID_GBY_HASH_TO_SCAN_THRESHOLD))을 넘으면 spill 경로로 전환한다. 흘러내린 부분 리스트는 같은 qdata_aggregate_accumulator_to_accumulator 경로를 거쳐 병합된다. Order-by와 DISTINCT는 둘 다 sort_listfile을 통과하며, DISTINCT는 SORT_ELIM_DUP 플래그를 단다.

병렬 힙 스캔 — 푸시 측으로 잠시 새는 한 갈래

섹션 제목: “병렬 힙 스캔 — 푸시 측으로 잠시 새는 한 갈래”

큰 힙에 대한 순차 힙 스캔에서, CUBRID는 파일을 여러 워커 스레드로 쪼갤 수 있다. qexec_open_scan은 먼저 scan_open_parallel_heap_scan 을 시도한다. 실패할 경우(작은 힙, 분할이 너무 작아 의미 없음) 단일 스레드 scan_open_heap_scan으로 폴백한다. 병렬 구현은 src/query/parallel/에 산다. SCAN_ID의 가지는 S_PARALLEL_HEAP_SCAN 이며, 그 결과를 실행기의 나머지는 마치 list scan이 흘려 주는 것처럼 읽는다.

// scan_open_parallel_heap_scan — src/query/scan_manager.c (sketch via qexec_open_scan)
#if SERVER_MODE && !WINDOWS
error_code = scan_open_parallel_heap_scan (thread_p, s_id,
mvcc_select_lock_needed, fixed,
grouped, vd, curr_spec,
&ACCESS_SPEC_CLS_OID (curr_spec),
&ACCESS_SPEC_HFID (curr_spec),
xasl, query_id);
if (s_id->type == S_PARALLEL_HEAP_SCAN)
{
assert (s_id->s.phsid.manager != nullptr);
}
else
{
/* fallback — small heap; revert to S_HEAP_SCAN */
scan_open_heap_scan (thread_p, s_id, ...);
}
#endif

병렬 힙은 실행기가 잠시나마 순수 풀 모드를 벗어나는 유일한 자리다. 워커들이 부분 결과 튜플을 코디네이터 큐로 민다(push). 부모는 이를 풀 스타일로 읽어 들인다.

심볼명을 anchor로 삼는다. 라인번호는 이 문서의 updated: 시점 기준의 빠른 힌트일 뿐이며, 안정적인 식별자는 심볼명이다.

서버 진입과 디스패치 (src/query/)

섹션 제목: “서버 진입과 디스패치 (src/query/)”
  • xqmgr_execute_query (query_manager.c) — 와이어 측 서버 진입점. 캐시된 XASL을 찾는다.
  • qmgr_process_query (query_manager.c) — 스트림 → XASL 트리로 풀어내고, qexec_execute_query를 호출한 뒤, list id를 복제·반환한다.
  • qexec_execute_query (query_executor.c) — 최상위 진입점. XASL_STATE 를 세팅하고 최상위 QFILE_LIST_ID를 돌려 준다.
  • qexec_execute_mainblock / qexec_execute_mainblock_internal — 재귀 깊이 래퍼 + 연산자 타입 디스패치 switch.
  • qexec_intprt_fnc — 일반 풀 루프 인터프리터(BUILDLIST/BUILDVALUE/ SCAN_PROC).
  • qexec_execute_scan / qexec_execute_scan_ptrxasl->scan_ptr (조인 블록 체인)에 대한 재귀 진입.
  • qexec_open_scan — access spec에서 scan_open_<type>_scan로 팬-아웃.
  • qexec_next_scan_block / qexec_next_scan_block_iterations — 파티션 또는 서브 클래스의 UNION-ALL을 가로질러 access spec 체인을 전진.
  • qexec_end_one_iteration / qexec_end_mainblock_iterations — 자격을 통과한 행 기록, 메인 블록 스캔 닫기.
  • qexec_execute_update / qexec_execute_delete / qexec_execute_insert / qexec_execute_merge — BUILDLIST 행 소스에 locator_* / heap_*의 변경을 짝지운다.
  • qexec_execute_dblink_queryTARGET_DBLINK 잎.
  • qexec_execute_do_stmt — DO 문(결과 없음, 부수효과만).
  • qexec_execute_connect_by — 계층 질의(CONNECT BY).
  • qexec_execute_cte — CTE 실체화/재귀(xasl->max_iterations 상한).
  • qexec_groupby / qexec_groupby_index — sort 및 인덱스 기반 group-by.
  • qexec_hash_gby_agg_tuple / qexec_hash_gby_get_next / qexec_hash_gby_put_next — hash group-by 행별 처리, 커서 래퍼.
  • qdata_evaluate_aggregate_list / qdata_aggregate_accumulator_to_accumulator — 누적기 갱신; hash spill의 병합.
  • qdata_save_agg_htable_to_list / qdata_load_agg_hvalue_in_agg_list hash 누적기 spill / 다시 적재.
  • qexec_orderby_distinct / qexec_orderby_distinct_by_sortingsort_listfile(SORT_ELIM_DUP / SORT_DUP)을 통한 order-by + distinct.
  • qexec_execute_analytic — 분석 함수(윈도우) 후처리.

스캔 관리 (src/query/scan_manager.{c,h})

섹션 제목: “스캔 관리 (src/query/scan_manager.{c,h})”
  • SCAN_ID — 연산자 상태(태그 유니언). SCAN_TYPE enum이 모든 스캔 종류를 나열한다(S_HEAP_SCAN, S_INDX_SCAN, S_LIST_SCAN, S_CLASS_ATTR_SCAN, S_SET_SCAN, S_JSON_TABLE_SCAN, S_VALUES_SCAN, S_SHOWSTMT_SCAN, S_DBLINK_SCAN, S_PARALLEL_HEAP_SCAN, S_HEAP_PAGE_SCAN, S_INDX_KEY_INFO_SCAN).
  • S_STATUS 생애주기(S_OPENED / S_STARTED / S_ENDED / S_CLOSED); S_DIRECTION, S_POSITION.
  • scan_init_scan_id / scan_init_scan_pred / scan_init_scan_attrs 공통 초기화기.
  • scan_open_heap_scan / scan_open_index_scan / scan_open_list_scan 세 주요 open.
  • scan_open_class_attr_scan / scan_open_set_scan / scan_open_values_scan / scan_open_json_table_scan / scan_open_dblink_scan / scan_open_showstmt_scan / scan_open_heap_page_scan — 부수적 잎들.
  • scan_open_parallel_heap_scan — 워커 풀 기반 힙 스캔. 단일 스레드 scan_open_heap_scan로 폴백.
  • scan_start_scan / scan_end_scan / scan_close_scan — 블록별 / 파일 수준 자원 생애주기.
  • scan_next_scan_block — SCAN_ID 하나 안에서 access spec 체인을 전진.
  • scan_next_scanscan_handle_single_scanscan_next_scan_local 외측 디스패처 체인.
  • scan_next_heap_scan / scan_next_index_scan / scan_next_list_scan 세 주요 next.
  • scan_next_class_attr_scan / scan_next_set_scan / scan_next_value_scan / scan_next_json_table_scan / scan_next_dblink_scan / scan_next_heap_page_scan / scan_next_index_key_info_scan — 부수적 next들.
  • heap_next / heap_prev — 정/역방향 이터레이터. heap_next_internal 위의 얇은 래퍼(page-fix, slot 순회, MVCC 가시성, REC_RELOCATION / REC_BIGONE 체인).
  • heap_next_sampling / heap_next_record_info — 샘플링과 레코드 정보 변종.
  • heap_first / heap_last — single-fetch / 위치 인덱스 진입.
  • heap_get_visible_version / heap_get_visible_version_internal / heap_prepare_object_page — 스냅샷 평가와 버전 체인 순회를 동반한 점-조회.
  • heap_attrinfo_start / heap_attrinfo_end / heap_attrinfo_read_dbvalues — 디코더 캐시 생애주기, RECDES → DB_VALUE 변환.
  • eval_data_filter / eval_fnc / eval_pred* (query_evaluator.c) FILTER_INFO 브리지, 모양별 컴파일된 술어, 빠른 경로.
  • fetch_val_list / fetch_peek_dbval (fetch.c) — REGU_VARIABLE → DB_VALUE 실체화.
  • qexec_execute_obj_fetch — bptr(경로 표현식) 페처.
  • QFILE_LIST_ID, qfile_open_list / qfile_close_list, qfile_scan_list_next, qfile_save_tuple, qfile_clone_list_id, sort_listfile (list_file.c) — list-file I/O.

XASL 노드 타입 (src/xasl/xasl_node.hpp)

섹션 제목: “XASL 노드 타입 (src/xasl/xasl_node.hpp)”
  • xasl_node — 연산자/계획 노드.
  • proc_typeBUILDLIST_PROC / BUILDVALUE_PROC / UNION_PROC / INTERSECTION_PROC / DIFFERENCE_PROC / MERGELIST_PROC / OBJFETCH_PROC / SCAN_PROC / UPDATE_PROC / DELETE_PROC / INSERT_PROC / MERGE_PROC / CONNECTBY_PROC / CTE_PROC / DO_PROC.
  • ACCESS_SPEC_TYPETARGET_CLASS / TARGET_LIST / TARGET_REGUVAL_LIST / TARGET_SET / TARGET_DBLINK / TARGET_JSON_TABLE / TARGET_VALUES_LIST / TARGET_SHOWSTMT.
  • ACCESS_METHODACCESS_METHOD_SEQUENTIAL / ACCESS_METHOD_INDEX / ACCESS_METHOD_SEQUENTIAL_RECORD_INFO / ..._SAMPLING_SCAN.

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

섹션 제목: “이 개정 시점의 위치 힌트 (2026-04-30)”
심볼파일라인
xqmgr_execute_queryquery_manager.c1299
qmgr_process_queryquery_manager.c1177
qmgr_get_query_entryquery_manager.c543
qexec_execute_queryquery_executor.c16292
qexec_execute_mainblockquery_executor.c14900
qexec_execute_mainblock_internalquery_executor.c15058
qexec_intprt_fncquery_executor.c9092
qexec_open_scanquery_executor.c7303
qexec_execute_scanquery_executor.c8300
qexec_execute_scan_ptrquery_executor.c8261
qexec_next_scan_blockquery_executor.c7851
qexec_next_scan_block_iterationsquery_executor.c7979
qexec_end_one_iterationquery_executor.c1130
qexec_end_mainblock_iterationsquery_executor.c14701
qexec_groupbyquery_executor.c5247
qexec_groupby_indexquery_executor.c20526
qexec_hash_gby_agg_tuplequery_executor.c4537
qexec_hash_gby_get_nextquery_executor.c4778
qexec_hash_gby_put_nextquery_executor.c4793
qexec_orderby_distinctquery_executor.c3936
qexec_orderby_distinct_by_sortingquery_executor.c4000
qexec_execute_analyticquery_executor.c21007
qexec_execute_obj_fetchquery_executor.c13182
qexec_execute_connect_byquery_executor.c16716
qexec_execute_ctequery_executor.c17472
scan_open_heap_scanscan_manager.c2846
scan_open_index_scanscan_manager.c3067
scan_open_list_scanscan_manager.c3715
scan_start_scanscan_manager.c4136
scan_next_scan_blockscan_manager.c4606
scan_end_scanscan_manager.c4749
scan_close_scanscan_manager.c4882
scan_next_scan_localscan_manager.c5193
scan_next_heap_scanscan_manager.c5361
scan_next_index_scanscan_manager.c5909
scan_next_list_scanscan_manager.c6647
scan_next_scanscan_manager.c7329
heap_next_internalstorage/heap_file.c7902
heap_nextstorage/heap_file.c19427
heap_firststorage/heap_file.c8483
heap_laststorage/heap_file.c8511
heap_attrinfo_read_dbvaluesstorage/heap_file.c10683
heap_get_visible_versionstorage/heap_file.c25456
heap_get_visible_version_internalstorage/heap_file.c25577
heap_prepare_object_pagestorage/heap_file.c25856

각 항목은 현재 소스에 대한 사실이며, 원본 분석 자료를 함께 보지 않아도 그 자체로 읽힌다. 끝의 부연은 어떻게 검증되었는지와, 관련이 있을 때 역사적 드리프트(version drift) 또는 검증의 한계를 적는다.

원본 분석(analysis_QP_executor_0.7.pptx, 작성 2020-03-16)은 명시적 버전 0.7 — 초기 탐색 단계에서 만들어진 것이며, CUBRID 10.x / 초기 11.x 시점의 실행기를 반영한다. 동반 자료인 Heap scan.pdf는 더 최근 자료로, 현재 소스와 잘 맞는다. v0.7 deck이 작성된 이후 몇몇 지점이 이동했다. deck을 현재 트리에 대고 읽을 때 다음 차이를 의식해야 한다.

  • deck의 서술 진입점은 qexec_execute_mainblock()이다. 현재 코드는 _internal 한 단계를 더한다. qexec_execute_mainblock은 재귀 깊이 + perfmon 래퍼일 뿐이며, 디스패치 switch는 한 프레임 안쪽으로 들어가 있다. deck에서 인용한 심볼은 여전히 해소되지만, 분기는 한 단 더 들어간 자리다.

  • SELECT_PROC은 더 이상 별도 proc 타입이 아니다. deck의 “EXECUTOR (qexec_execute_mainblock())” 슬라이드는 SELECT_PROC을 암묵적으로 가정한 채 pre/processing/post 흐름만을 코드 경로로 보여 준다. 현재 코드는 BUILDLIST_PROCBUILDVALUE_PROC (그리고 여러 다른 타입)을 쓰고, 같은 qexec_intprt_fnc 인터프리터로 라우팅한다. proc 타입은 옵티마이저가 적어 두는 정보이지, 실행기가 최상위에서 분기하는 대상이 아니다.

  • DML proc 타입(UPDATE_PROC/DELETE_PROC/INSERT_PROC/ MERGE_PROC). deck의 흐름도는 SELECT 경로만 보인다. 현재 qexec_execute_mainblock_internal은 DML proc 타입에 대한 전용 분기를 가지며 각각 qexec_execute_update / _delete / _insert / _merge로 라우팅한다. v0.7 서술은 이를 다루지 않는다.

  • 계층/CTE proc 타입. CONNECTBY_PROCCTE_PROC은 별도로 디스패치된다. 어느 쪽도 deck에 없다. CTE 재귀 반복 카운터 (xasl->max_iterations)는 qexec_intprt_fnc 안에서 cte_offset_read_tuple과 손을 맞추며 동작하는데, 이 메커니즘은 deck보다 나중의 것이다.

  • 병렬 힙 스캔. S_PARALLEL_HEAP_SCANscan_open_parallel_heap_scan은 비교적 최근 추가다(SERVER_MODE && !WINDOWS에서만). deck의 힙 스캔 흐름은 단일 스레드 S_HEAP_SCAN을 가정한다. 인덱스가 없는 큰 힙에 대해서는 qexec_open_scan이 먼저 병렬을 시도하고 단일 스레드로 폴백한다.

  • S_JSON_TABLE_SCAN / S_DBLINK_SCAN / S_VALUES_SCAN / S_SHOWSTMT_SCAN. v0.7 deck에 없는 현대적 잎 스캔 타입들. 같은 open/start/next/end/close 모양이 적용되지만, 각자 자기 scan_open_* / scan_next_* 한 벌을 가진다.

  • Hash group-by spill 메커니즘. deck의 다이어그램은 부분 리스트 병합 로직을 보여 주지만 PRM_ID_GBY_HASH_TO_SCAN_THRESHOLD나 정확한 spill 조건을 명명하지는 않는다. 현재 코드는 예산이 있는 인메모리 hash를 쓰고, 예산을 넘으면 qdata_save_agg_htable_to_list로 spill 한다. 병합은 같은 키의 누적기를 합치는 qdata_aggregate_accumulator_to_accumulator로 일어난다.

  • SCAN_ID 위의 grouped / single_fetch 의미. deck은 이 플래 그들을 자세히 다루지 않지만, 이들은 scan_next_heap_scan(grouped 아래에서의 fixed page latch + PEEK 의미)과 상위 래퍼 scan_handle_single_scan(SELECT-INTO 단일 튜플)을 의미 있게 바꾼다.

  • MVCC 가시성이 재작업되었다. deck은 현재 형태의 heap_get_visible_version / heap_get_visible_version_internal 이전 시점이다. 특히 너무 새로운 버전을 undo 로그를 거슬러 올라가는 prev_version_lsa 체인 순회(cubrid-mvcc.md / cubrid-heap-manager.md 참조)는 deck의 짧은 MVCC 스냅샷 언급과 실질 적으로 다르다.

  • Heap scan PDF의 다섯 단계 생애주기(scan_open / scan_start / scan_next / scan_end / scan_close)는 현재 소스 기준으로도 맞다. scan_next_heap_scan의 네 단계 안쪽 루프(다음 객체 → 술어 → 필요시 잠금 → 잔여)에 대한 PDF의 설명도 현재 코드 본문과 일치 한다.

  1. 튜플당 가상 호출 비용. scan_next_scanscan_handle_single_scanscan_next_scan_localscan_next_<type>_scan은 튜플마다 최소 세 번의 간접 호출을 한다. pr_eval_fnc는 그 위에 또 한 번 얹힌다. 콜드 캐시에서의 TPC-H 모양 스캔이라면, 간접 호출 타깃의 캐시 미스 비용이 유력한 병목 후보다. 추적 경로: 지속 스캔 워크로드에서 scan_next_heap_scan를 PMU 프로파일링을 하고, 벡터 배치(vector-batched) 대안의 비용을 추정해 본다.

  2. MVCC 재페치 아래에서의 cache_last_fix_page 불변식. mvcc_select_lock_needed == true이고 잠금을 잡은 채 행을 다시 페치해야 할 때, page LSA는 이미 전진해 있을 수 있다 (PGBUF_IS_PAGE_CHANGED). 그 시점에 코드는 PEEK을 COPY로 떨어 뜨리고 재시작한다. 반복적인 SELECT FOR UPDATE 아래에서 격하게 업데이트되는 힙 페이지처럼, 이 루프가 수렴하지 않을 수 있는 경우가 있는가?

  3. Hash group-by spill 임계값의 기본값. PRM_ID_GBY_HASH_TO_SCAN_THRESHOLD는 인메모리 hash가 spill하는 시점을 통제한다. 현재 기본값은 현대적 메모리 크기에 맞춰 재 튜닝되지 않았다. 벤치마크 스윕을 돌리면 더 나은 기본값(혹은 워크로드 적응형 정책의 근거)이 떠오를 것이다.

  4. 재귀 CTE에 대한 xasl->max_iterations. 인터프리터는 recursive_iterations >= xasl->max_iterations일 때 재귀 CTE를 종료한다(qexec_intprt_fnc). 옵티마이저는 이 값을 무엇으로 설정하는가? 사용자 튜닝이 가능한가, 아니면 하드 상한인가? 추적 경로: xasl_generation/에서 xasl_node::max_iterations에 대한 모든 대입을 추적한다.

  5. count_star_with_iscan_opt의 정확성. 사용 가능한 인덱스가 있을 때 SELECT COUNT(*) FROM t를 키별 반복을 건너뛰는 최적화는 specp->s_id.s.isid.need_count_only = true와 스캔 블록 당 한 번의 oids_count 누적에 의존한다. 이를 조용히 우회해 잘못된 카운트를 만들어 내는 필터 모양이 있는가?

  6. 병렬 힙 스캔 폴백 경계. scan_open_parallel_heap_scan은 힙이 작거나 파티션이 너무 작아 병렬화의 경제성이 떨어질 때 단일 스레드로 폴백한다. 정확한 경계는 내부 구현에 있다. 그 술어와 옵티마이저 비용 모델과의 상호작용을 문서화해 둘 가치가 있다.

  7. S_DBLINK_SCAN 푸시다운. 로컬 인덱스를 보통 key/scan/ data로 갈리는 술어 가운데, 어느 비율이 원격으로 밀려 가는가? 조용히 로컬에서 재평가되는 술어 모양(NLS 비교, JSON 연산 등)은 없는가?

  8. SELECT_PROCBUILDLIST_PROC의 역사. 한때 별도의 SELECT_PROC proc 타입이 있다가 BUILDLIST에 흡수된 것일까? v0.7 deck이 모호하다. git history로 결판이 난다. (xasl/에 대한 git log --diff-filter=D -S 'SELECT_PROC'가 답을 줄 것이다.)

  9. XASL_LINK_TO_REGU_VARIABLE 생애주기. aptr의 결과가 부모의 REGU_VARIABLE에 바인딩될 때, 같은 XASL의 재실행(예: UNION-ALL spec 안에서)에서는 aptr이 다시 돌아서는 안 된다. 플래그는 재실 행을 억제하지만, 묶인 DB_VALUE의 소유권 의미가 미묘하다. aptr이 SET 타입 값을 돌려주는 코너 케이스를 들여다 볼 가치가 있다.

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

섹션 제목: “원본 분석 (raw/code-analysis/cubrid/query-processing/)”
  • analysis_QP_executor_0.7.pptx — 초기(2020-03) 실행기 개관 deck. 메인 블록 흐름, 서브쿼리 배치, group-by 스케치.
  • Heap scan.pdf — 힙 스캔 심층 자료. 다섯 단계 스캔 생애주기, 네 단계 안쪽 루프, HEAP_SCANCACHE/HEAP_CACHE_ATTRINFO 로컬 캐시 프레이밍.
  • _converted/analysisqpexecutor0.7.pptx.md — PPTX의 markitdown 추출 (위 본문에 인라인 인용).
  • _converted/heap-scan.pdf.md — PDF의 markitdown 추출.
  • knowledge/code-analysis/cubrid/cubrid-heap-manager.md — 레코드 타입 어휘(REC_HOME / REC_RELOCATION / REC_BIGONE), heap_next 의 페이지 순회, 그리고 HEAP_SCANCACHE가 참여하는 다섯 캐시.
  • knowledge/code-analysis/cubrid/cubrid-mvcc.mdmvcc_satisfies_snapshot, prev_version_lsa 체인, 그리고 가시성이 힙 이터레이터에 어떻게 끼워지는가.
  • Database Internals (Petrov), 12장 Query Execution — pull vs push, 연산자 상태, 실체화 전략.
  • Database Systems: The Complete Book (Garcia-Molina/Ullman/Widom), 15장 “Query Execution + 16장 The Query Compiler” — Volcano 모델 프레이밍, sort/hash/group 연산자 의사코드.
  • Graefe, Goetz. Volcano — An Extensible and Parallel Query Evaluation System, IEEE TKDE 6(1), 1994 — 이터레이터 모델의 정본 논문. open/next/close와 exchange 연산자를 도입.
  • Graefe, Goetz. Query Evaluation Techniques for Large Databases, ACM Computing Surveys 25(2), 1993 — 차단 연산자 설계와 조인 알고리즘을 포함한 더 넓은 서베이.

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

섹션 제목: “CUBRID 소스 (/data/hgryoo/references/cubrid/)”
  • src/query/query_executor.c — 실행기(~28 KLOC). 진입점: qexec_execute_mainblock_internal.
  • src/query/query_manager.c — 서버 측 질의 배관. 진입점: xqmgr_execute_query.
  • src/query/scan_manager.c — 튜플 단위 이터레이터 디스패처. 진입점: scan_next_scan.
  • src/query/scan_manager.hSCAN_ID, SCAN_TYPE, S_STATUS 정의.
  • src/query/fetch.cfetch_val_list / fetch_peek_dbval regu- variable 실체화.
  • src/query/query_evaluator.ceval_fnc, eval_data_filter, 모양별 술어 빠른 경로.
  • src/query/list_file.c / list_file.hQFILE_LIST_ID, qfile_scan_list_next, sort_listfile.
  • src/query/parallel/ — 병렬 힙 스캔 워커 풀 구현.
  • src/storage/heap_file.cheap_next / heap_next_internalscan_next_heap_scan이 부르는 힙 측 스캔 헬퍼.
  • src/xasl/xasl_node.hppxasl_node, proc 타입 상수, ACCESS_SPEC_TYPE.