(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)이 이론의 핵심이다.
open()— 스캔 상태를 초기화하고, 버퍼를 할당하며, 하위 파일과 자식 연산자를 재귀적으로 연다.next()— 다음 튜플(혹은 스트림 끝)을 반환한다. 필요할 때 자식 연산자에게서 끌어올린다.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_scan → scan_next_scan_local
→ scan_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
로 떨어진다.
list file 형태의 실체화 출력
섹션 제목: “list file 형태의 실체화 출력”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_list와 pr_eval_fnc를 가진다. MySQL의
range/ref/Using where나 PostgreSQL의 IndexQual/IndexFilter/
Filter도 같은 분할이다.
힙 스캔은 페치한 레코드마다 mvcc_satisfies_snapshot을 호출한다
(heap_next → heap_get_visible_version_internal 경로). 인덱스
스캔은 mvcc_select_lock_needed가 켜져 있을 때
(SELECT ... FOR UPDATE) locator_lock_and_get_object_with_evaluation
으로 한 번 더 검증한다.
이론 ↔ CUBRID 명칭 매핑
섹션 제목: “이론 ↔ CUBRID 명칭 매핑”| 이론 (Theory) | CUBRID 명칭 |
|---|---|
| 연산자 계획 노드 | xasl_node / XASL_NODE (src/xasl/xasl_node.hpp) |
| 계획 상태 / 실행별 상태 | xasl_state + spec별 SCAN_ID (src/query/scan_manager.h) |
| 연산자 타입에 의한 디스패치 | qexec_execute_mainblock_internal의 switch (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_scan → scan_next_scan_local → scan_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-by | qexec_groupby / qexec_hash_gby_* (query_executor.c) |
| Order-by + DISTINCT | qexec_orderby_distinct (query_executor.c) |
| 분석 함수 | qexec_execute_analytic (query_executor.c) |
| 서버 측 질의 진입 | xqmgr_execute_query → qmgr_process_query → qexec_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) |
CUBRID의 구현
섹션 제목: “CUBRID의 구현”실행기가 하는 일은 네 가지다. (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_query는
XASL_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)intqexec_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 intqexec_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에 적는다.
풀 루프 — qexec_intprt_fnc
섹션 제목: “풀 루프 — qexec_intprt_fnc”BUILDLIST / BUILDVALUE / SCAN_PROC 노드에 대해서는 실행기가 일반 인터프리터를 돌린다.
// qexec_intprt_fnc — src/query/query_executor.c (condensed)static SCAN_CODEqexec_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/end와 open/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)intscan_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_CODEscan_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_dbvalues는
pred_attrs만 디코드한다. rest_attrs 디코더는 행이 자격을 통과한
뒤에야 돈다.
heap_next 자체는 실행기가 부르는 힙 측 이터레이터다.
// heap_next — src/storage/heap_file.c (entry; condensed)SCAN_CODEheap_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_CODEheap_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_RELOCATION → REC_NEWHOME / REC_BIGONE
→ overflow)가 heap_next_internal 안에서 일어나는 양상은
cubrid-heap-manager.md를 참조한다.
인덱스 스캔 경로 — 술어 분할 + 힙 점-조회
섹션 제목: “인덱스 스캔 경로 — 술어 분할 + 힙 점-조회”scan_open_index_scan은 INDX_SCAN_ID를 만든다. 그 안의 세 술어
슬롯 — range_pred, key_pred, scan_pred — 은 각각 자기 regu list와
eval_fnc로 컴파일된 평가기를 가진다. scan_next_index_scan은
btree_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_scan은 qfile_scan_list_next로 list file을 걷고,
실체화된 튜플에 scan_pred를 적용한 뒤, rest_regu_list로 프로젝션
한다. 모양은 힙 스캔과 같으며, 다른 점은 heap_next 자리에
qfile_scan_list_next가 들어간다는 것과 MVCC 단계가 없다는 것뿐이다
(list file은 이미 프로젝션된 DB_VALUE를 들고 있다).
scan_next_scan — 외측 디스패처
섹션 제목: “scan_next_scan — 외측 디스패처”인터프리터는 스캔 타입별 next를 직접 부르지 않는다. 항상
scan_next_scan을 거친다.
// scan_next_scan — src/query/scan_manager.c (condensed)SCAN_CODEscan_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_CODEscan_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 추적
섹션 제목: “종단 간 — 구체적인 SELECT 추적”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 항목을 더할
뿐이다.
블록 반복 — (a ∪ b) ⋈ (c ∪ d)
섹션 제목: “블록 반복 — (a ∪ b) ⋈ (c ∪ d)”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_CODEqexec_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_ptr—xasl->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— 자격을 통과한 행 기록, 메인 블록 스캔 닫기.
DML과 특별 proc 타입 핸들러
섹션 제목: “DML과 특별 proc 타입 핸들러”qexec_execute_update/qexec_execute_delete/qexec_execute_insert/qexec_execute_merge— BUILDLIST 행 소스에locator_*/heap_*의 변경을 짝지운다.qexec_execute_dblink_query—TARGET_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_listhash 누적기 spill / 다시 적재.qexec_orderby_distinct/qexec_orderby_distinct_by_sorting—sort_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_TYPEenum이 모든 스캔 종류를 나열한다(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_scan→scan_handle_single_scan→scan_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들.
힙 측 헬퍼 (src/storage/heap_file.c)
섹션 제목: “힙 측 헬퍼 (src/storage/heap_file.c)”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 변환.
술어 평가기, 페처, list I/O
섹션 제목: “술어 평가기, 페처, list I/O”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_type—BUILDLIST_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_TYPE—TARGET_CLASS/TARGET_LIST/TARGET_REGUVAL_LIST/TARGET_SET/TARGET_DBLINK/TARGET_JSON_TABLE/TARGET_VALUES_LIST/TARGET_SHOWSTMT.ACCESS_METHOD—ACCESS_METHOD_SEQUENTIAL/ACCESS_METHOD_INDEX/ACCESS_METHOD_SEQUENTIAL_RECORD_INFO/..._SAMPLING_SCAN.
이 개정 시점의 위치 힌트 (2026-04-30)
섹션 제목: “이 개정 시점의 위치 힌트 (2026-04-30)”| 심볼 | 파일 | 라인 |
|---|---|---|
xqmgr_execute_query | query_manager.c | 1299 |
qmgr_process_query | query_manager.c | 1177 |
qmgr_get_query_entry | query_manager.c | 543 |
qexec_execute_query | query_executor.c | 16292 |
qexec_execute_mainblock | query_executor.c | 14900 |
qexec_execute_mainblock_internal | query_executor.c | 15058 |
qexec_intprt_fnc | query_executor.c | 9092 |
qexec_open_scan | query_executor.c | 7303 |
qexec_execute_scan | query_executor.c | 8300 |
qexec_execute_scan_ptr | query_executor.c | 8261 |
qexec_next_scan_block | query_executor.c | 7851 |
qexec_next_scan_block_iterations | query_executor.c | 7979 |
qexec_end_one_iteration | query_executor.c | 1130 |
qexec_end_mainblock_iterations | query_executor.c | 14701 |
qexec_groupby | query_executor.c | 5247 |
qexec_groupby_index | query_executor.c | 20526 |
qexec_hash_gby_agg_tuple | query_executor.c | 4537 |
qexec_hash_gby_get_next | query_executor.c | 4778 |
qexec_hash_gby_put_next | query_executor.c | 4793 |
qexec_orderby_distinct | query_executor.c | 3936 |
qexec_orderby_distinct_by_sorting | query_executor.c | 4000 |
qexec_execute_analytic | query_executor.c | 21007 |
qexec_execute_obj_fetch | query_executor.c | 13182 |
qexec_execute_connect_by | query_executor.c | 16716 |
qexec_execute_cte | query_executor.c | 17472 |
scan_open_heap_scan | scan_manager.c | 2846 |
scan_open_index_scan | scan_manager.c | 3067 |
scan_open_list_scan | scan_manager.c | 3715 |
scan_start_scan | scan_manager.c | 4136 |
scan_next_scan_block | scan_manager.c | 4606 |
scan_end_scan | scan_manager.c | 4749 |
scan_close_scan | scan_manager.c | 4882 |
scan_next_scan_local | scan_manager.c | 5193 |
scan_next_heap_scan | scan_manager.c | 5361 |
scan_next_index_scan | scan_manager.c | 5909 |
scan_next_list_scan | scan_manager.c | 6647 |
scan_next_scan | scan_manager.c | 7329 |
heap_next_internal | storage/heap_file.c | 7902 |
heap_next | storage/heap_file.c | 19427 |
heap_first | storage/heap_file.c | 8483 |
heap_last | storage/heap_file.c | 8511 |
heap_attrinfo_read_dbvalues | storage/heap_file.c | 10683 |
heap_get_visible_version | storage/heap_file.c | 25456 |
heap_get_visible_version_internal | storage/heap_file.c | 25577 |
heap_prepare_object_page | storage/heap_file.c | 25856 |
소스 검증 (2026-04-30 기준)
섹션 제목: “소스 검증 (2026-04-30 기준)”각 항목은 현재 소스에 대한 사실이며, 원본 분석 자료를 함께 보지 않아도 그 자체로 읽힌다. 끝의 부연은 어떻게 검증되었는지와, 관련이 있을 때 역사적 드리프트(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_PROC과BUILDVALUE_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_PROC과CTE_PROC은 별도로 디스패치된다. 어느 쪽도 deck에 없다. CTE 재귀 반복 카운터 (xasl->max_iterations)는qexec_intprt_fnc안에서cte_offset_read_tuple과 손을 맞추며 동작하는데, 이 메커니즘은 deck보다 나중의 것이다. -
병렬 힙 스캔.
S_PARALLEL_HEAP_SCAN과scan_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의 설명도 현재 코드 본문과 일치 한다.
미해결 질문
섹션 제목: “미해결 질문”-
튜플당 가상 호출 비용.
scan_next_scan→scan_handle_single_scan→scan_next_scan_local→scan_next_<type>_scan은 튜플마다 최소 세 번의 간접 호출을 한다.pr_eval_fnc는 그 위에 또 한 번 얹힌다. 콜드 캐시에서의 TPC-H 모양 스캔이라면, 간접 호출 타깃의 캐시 미스 비용이 유력한 병목 후보다. 추적 경로: 지속 스캔 워크로드에서scan_next_heap_scan를 PMU 프로파일링을 하고, 벡터 배치(vector-batched) 대안의 비용을 추정해 본다. -
MVCC 재페치 아래에서의
cache_last_fix_page불변식.mvcc_select_lock_needed == true이고 잠금을 잡은 채 행을 다시 페치해야 할 때, page LSA는 이미 전진해 있을 수 있다 (PGBUF_IS_PAGE_CHANGED). 그 시점에 코드는 PEEK을 COPY로 떨어 뜨리고 재시작한다. 반복적인SELECT FOR UPDATE아래에서 격하게 업데이트되는 힙 페이지처럼, 이 루프가 수렴하지 않을 수 있는 경우가 있는가? -
Hash group-by spill 임계값의 기본값.
PRM_ID_GBY_HASH_TO_SCAN_THRESHOLD는 인메모리 hash가 spill하는 시점을 통제한다. 현재 기본값은 현대적 메모리 크기에 맞춰 재 튜닝되지 않았다. 벤치마크 스윕을 돌리면 더 나은 기본값(혹은 워크로드 적응형 정책의 근거)이 떠오를 것이다. -
재귀 CTE에 대한
xasl->max_iterations. 인터프리터는recursive_iterations >= xasl->max_iterations일 때 재귀 CTE를 종료한다(qexec_intprt_fnc). 옵티마이저는 이 값을 무엇으로 설정하는가? 사용자 튜닝이 가능한가, 아니면 하드 상한인가? 추적 경로:xasl_generation/에서xasl_node::max_iterations에 대한 모든 대입을 추적한다. -
count_star_with_iscan_opt의 정확성. 사용 가능한 인덱스가 있을 때SELECT COUNT(*) FROM t를 키별 반복을 건너뛰는 최적화는specp->s_id.s.isid.need_count_only = true와 스캔 블록 당 한 번의oids_count누적에 의존한다. 이를 조용히 우회해 잘못된 카운트를 만들어 내는 필터 모양이 있는가? -
병렬 힙 스캔 폴백 경계.
scan_open_parallel_heap_scan은 힙이 작거나 파티션이 너무 작아 병렬화의 경제성이 떨어질 때 단일 스레드로 폴백한다. 정확한 경계는 내부 구현에 있다. 그 술어와 옵티마이저 비용 모델과의 상호작용을 문서화해 둘 가치가 있다. -
S_DBLINK_SCAN푸시다운. 로컬 인덱스를 보통 key/scan/ data로 갈리는 술어 가운데, 어느 비율이 원격으로 밀려 가는가? 조용히 로컬에서 재평가되는 술어 모양(NLS 비교, JSON 연산 등)은 없는가? -
SELECT_PROC대BUILDLIST_PROC의 역사. 한때 별도의SELECT_PROCproc 타입이 있다가 BUILDLIST에 흡수된 것일까? v0.7 deck이 모호하다. git history로 결판이 난다. (xasl/에 대한git log --diff-filter=D -S 'SELECT_PROC'가 답을 줄 것이다.) -
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.md—mvcc_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.h—SCAN_ID,SCAN_TYPE,S_STATUS정의.src/query/fetch.c—fetch_val_list/fetch_peek_dbvalregu- variable 실체화.src/query/query_evaluator.c—eval_fnc,eval_data_filter, 모양별 술어 빠른 경로.src/query/list_file.c/list_file.h—QFILE_LIST_ID,qfile_scan_list_next,sort_listfile.src/query/parallel/— 병렬 힙 스캔 워커 풀 구현.src/storage/heap_file.c—heap_next/heap_next_internal과scan_next_heap_scan이 부르는 힙 측 스캔 헬퍼.src/xasl/xasl_node.hpp—xasl_node, proc 타입 상수,ACCESS_SPEC_TYPE.