(KO) CUBRID Scan Manager — SCAN_ID 디스패치, open/next/close 프로토콜, 그리고 액세스 메서드 카탈로그
목차
- 학술적 배경
- DBMS 공통 설계 패턴 (Common DBMS Design)
- CUBRID의 구현
- 소스 코드 가이드
- 교차 검증 노트 (Cross-check Notes)
- 미해결 질문 (Open Questions)
- 출처
학술적 배경
섹션 제목: “학술적 배경”관계형 executor가 하는 일은 연산자(operator)들을 트리로 엮은 뒤, 그 잎(leaf)에다 다음 튜플을 달라고 묻는 것이다. 그 잎이 곧 액세스 메서드(access method) 다. 힙 스캔, 인덱스 스캔, list-file 스캔, 생성된 행(generated rows), 외부 데이터 스캔(foreign-data scan), 가상 카탈로그(virtual catalogue) 같은 것들이 모두 잎 자리에 들어간다. 그 위로 필터, 프로젝션, 조인, 정렬, group-by, 그리고 나머지 연산자 대수(operator algebra)가 쌓인다. 위쪽의 모든 연산자가 “그냥 어떤 균일한 테이블과 이야기하는 것처럼” 살 수 있게 만들어 주는 런타임 계약(contract), 그것이 본 문서가 말하는 scan manager 다.
이 설계를 떠받치는 이론적 기둥은 셋이다.
첫째는 액세스 경로 선택(access-path selection) 이다. Selinger 등의
1979년 논문 Access Path Selection in a Relational DBMS (System R 논문)
이 옵티마이저/executor 분리의 출발점이다. 옵티마이저는 관계마다. 순차
힙 스캔, 선택된 인덱스 위의 인덱스 스캔, 또는 합성 — 액세스 경로를
CPU·I/O 비용 추정값에 기반해 고른다. executor는 그렇게 선택된 경로를
충실히 실행한다. 이 분리가 살아남은 이유는, 액세스 경로 비용 산정 은
탐색 문제(통계, 조인 순서 열거, 동적 계획법이 필요)인 반면, 액세스 경로
실행 은 기계적인 디스패치 문제(선택된 힙 워크/B+Tree 하강/list reader를
적용)이기 때문이다. System R은 또 하나의 아이디어를 제시했다. 연산자별 책임을 한 단계 위로 숨기는, 공통의 균일한 스캔 핸들 이라는
개념. CUBRID는 이 두 절반을 그대로 물려받았다. 옵티마이저는 선택을
XASL access-spec 노드에 적어 넣고, scan_open_* 와 scan_next_scan 이
그 선택을 실제 페이지 read로 번역한다.
둘째 기둥은 iterator 모델 이다. Goetz Graefe가 Volcano — An
Extensible and Parallel Query Evaluation System (TKDE 1994)에서
형식화하고 Query Evaluation Techniques for Large Databases (ACM
Computing Surveys 1993)에서 개관한 모델이다. 모든 연산자가 open /
next / close 세 메서드를 구현하고, executor는 한 번에 한 튜플씩
끌어당긴다. scan manager 입장에서 중요한 구조적 결과는 두 가지다.
첫째, 모든 잎이 같은 3-메서드 인터페이스를 구현해야 한다. 그래야
위쪽 연산자가 자기 잎이 어떤 종류인지 신경 쓰지 않아도 된다. 이것이
CUBRID가 SCAN_ID 안에 인코딩한 다형성(polymorphism)이다. 둘째, 진행
중인 스캔의 상태는 함수 호출 스택이 아니라 스캔 핸들 안에 데이터로
실려 다닌다. executor가 next 를 반복적으로 들어왔다 나가기
때문이다. 그래서 힙 스캔은 HEAP_SCANCACHE 와 curr_oid 를
HEAP_SCAN_ID 안에 들고 있고, 인덱스 스캔은 BTREE_SCAN, OID 리스트,
curr_oidno 를 INDX_SCAN_ID 안에 들고 있다.
셋째 기둥은 액세스 메서드 카탈로그(access-method catalogue) 다. Hellerstein과 Stonebraker의 Anatomy of a Database System (Red Book 4장)과 Petrov의 Database Internals(1장 Introduction and Overview, 2-3장 B-tree 변형)는 DBMS를 작은 수의 안정된 액세스 메서드 모듈 — 힙 파일, B+Tree, 해시 인덱스, 때로는 비트맵·LSM 변형 — 의 계층 위에 얹힌 시스템으로 묘사한다. 각 모듈은 자기만의 페이지 포맷, 래칭(latching), 동시성 제어를 책임진다. scan manager는 위쪽의 연산자 대수와 아래쪽의 액세스 메서드 모듈 사이의 디스패치 계층이다. Database System Concepts(Silberschatz, Korth, Sudarshan)는 같은 아이디어를 Query Processing 챕터에서 evaluation algorithms로 부른다. 모든 대수 연산자가 하나의 평가 알고리즘(인덱스 스캔, sort-merge join, hash aggregate 등)을 고르고, 잎의 관계 액세스에서 heap-vs-index 를 고르는 일이 곧 스캔 디스패치다.
CUBRID에 특히 관계 깊은 텍스트북적 아이디어가 두 가지 더 있다.
Database Internals 2-3장은 실제 시스템들이 채택한 B-tree 변형들을
정리하는데, 그 안에는 covering index (leaf가 컬럼을 충분히 들고 있어
힙 fetch를 건너뛸 수 있다), index skip scan (술어가 선두 컬럼을
빠뜨릴 때, 그 컬럼의 distinct 값들을 한 바퀴 돌며 검사한다),
index loose scan (DISTINCT/MIN/MAX 질의를 위해 distinct prefix당
한 엔트리만 읽는다)이 포함된다. 이 셋은 모두 INDX_SCAN_ID 안에 플래그
및 인라인 상태로 등장한다. Database System Concepts 의 다중 테이블
조인 챕터는 hash join build-and-probe 패턴을 도입한다. CUBRID는 이
패턴을 별도 연산자가 아니라 list scan 위에 HASH_LIST_SCAN 으로
얹어 두었다. build 측은 list file에서 튜플을 읽어 해싱하고, probe
측은 외부 루프 튜플마다 해시 lookup을 친다. scan manager가 이 계층화를
소유하는 이유는, build/probe 기계가 별도 연산자로 분해되지 않은 채
LLIST_SCAN_ID 에 볼팅되어(bolted on) 있기 때문이다.
스캔 연산자가 연산자 트리에서 어디에 위치하느냐가 그 책임을 결정한다.
위쪽으로는 모든 Volcano 스타일 연산자(필터, 프로젝션, 정렬, hash-build,
group-by, top-N, join driver)가 결국 스캔의 next() 를 호출한다.
아래쪽으로는 액세스 메서드(힙 파일, B+Tree, list 파일)가 더 낮은
레벨의 인터페이스를 노출한다(페이지 단위 iteration, 키-값 검색, 튜플
단위 list read). scan manager 는 바로 이 두 추상화를 연결하는
계층이다. CUBRID에서 Selinger 1979가 함의한 다형성 — “옵티마이저가
이 액세스 경로를 골랐다, executor가 그것을 실행한다” — 이 실제로
C 코드의 SCAN_TYPE 위 switch 로 컴파일되는 유일한 자리다.
DBMS 공통 설계 패턴 (Common DBMS Design)
섹션 제목: “DBMS 공통 설계 패턴 (Common DBMS Design)”iterator 모델 위에 서 있는 모든 관계형 엔진은 액세스 메서드 디스패치 계층을 갖는다. 차이는 그것이 어떻게 구현되어 있는가 에 있다.
PostgreSQL은 액세스 경로마다 plan-state 노드 타입을 따로 둔다.
SeqScanState, IndexScanState, BitmapHeapScanState,
BitmapIndexScanState, IndexOnlyScanState, TidScanState,
SubqueryScanState, FunctionScanState, ValuesScanState,
CteScanState, WorkTableScanState, ForeignScanState 등이 그것이다.
각각은 ScanState 를 확장한 struct이며, ExecProcNode 가 ExecInitNode
가 설정한 함수 포인터로 디스패치한다. 다형성은 C struct 상속과
노드별 함수 포인터 테이블로 구현된다. 비용은 두꺼운 plumbing(스캔
종류마다 initializer 하나, execProc 하나, shutdown 하나)이고, 이득은
C 코드 안에서의 종류별 타입 안전성이다. 인덱스 스캔은 다시 한
계층 더 내려가서, IndexScanDesc 와 액세스 메서드별
(am_beginscan/am_gettuple/am_endscan) 콜백을 둔다. 이 콜백들이
플러그인 가능한 인덱스 액세스 메서드 API로 디스패치된다. 이 덕분에
Postgres는 단일한 executor로 B-tree, hash, GiST, GIN, BRIN, SP-GiST를
모두 지원한다.
MySQL/InnoDB는 디스패치를 한 계층 더 아래로 밀어 넣었다. SQL 계층의
JOIN_TAB / QEP_TAB 는 스토리지 엔진이 무엇인지 전혀 신경 쓰지
않는다. 그 대신 handler API 를 호출한다. 거기서
handler::ha_index_init / ha_index_read_map / ha_index_next /
ha_rnd_init / ha_rnd_next / ha_rnd_end 가 3-메서드 계약 노릇을
한다. 엔진별 handler(ha_innobase, ha_myisam, ha_archive,
ha_federated, ha_partition)가 실제 스캔 로직을 공급한다. 따라서
디스패치가 이중이다. 한 번은 SQL/handler 경계에서, 또 한 번은 엔진
내부에서 일어난다. 이득은 엔진 플러그어빌리티(pluggability)이지만,
대가는 handler API 자체가 시간이 흐르며 수십 개의 메서드로 부풀었다는
점이다(covering index, multi-range read, condition pushdown, parallel
scan 같은 기능들이 경계 너머로 새어 나오면서).
SQL Server의 관계 엔진은 액세스 메서드 row source를 쓴다 (row
source 라는 용어는 Oracle에서 빌려온 것으로, 거기서는
ROWSOURCE_SCAN, ROWSOURCE_INDEX_RANGE 등으로 같은 개념이 등장한다).
질의 plan은 연산자 트리이고, 각 연산자는 Open, GetNext, Close
메서드를 가지며, 잎은 균일한 튜플 인터페이스로 스토리지 엔진을
호출하는 액세스 메서드 row source다. Oracle은 추가로 row source
트리를 위치 독립적인(position-independent) 코드로 컴파일해 커서 사이에
공유하기까지 한다.
CUBRID는 셋 중 어느 것도 아닌 세 번째 디자인 포인트를 골랐다. 하나의 다형 struct, 하나의 함수 테이블, 하나의 switch. SCAN_ID
struct는 SCAN_TYPE 디스크리미네이터 하나와 종류별 서브-struct들의
익명 union 하나(HEAP_SCAN_ID, INDX_SCAN_ID, LLIST_SCAN_ID,
SET_SCAN_ID, REGU_VALUES_SCAN_ID, SHOWSTMT_SCAN_ID,
JSON_TABLE_SCAN_ID, DBLINK_SCAN_ID, METHOD_SCAN_ID,
PARALLEL_HEAP_SCAN_ID, HEAP_PAGE_SCAN_ID, INDEX_NODE_SCAN_ID)를
들고 다닌다. 모든 공개 진입점 — scan_open_<kind>, scan_start_scan,
scan_next_scan, scan_end_scan, scan_close_scan,
scan_reset_scan_block, scan_next_scan_block — 은 단일 함수이며,
그 본체가 switch (scan_id->type) 하나로 종류별 구현으로 분기한다.
다형성은 그러므로 데이터(디스크리미네이터)에 있다. 타입(Postgres)이나
인터페이스(MySQL)에 있는 것이 아니다. CUBRID가 받아들인 트레이드오프는
이렇다. 타입 안전성이 줄어들고(SCAN_TYPE 이 잘못 들어와도 컴파일은
되고, switch의 default 가 런타임 에러를 낸다), 그 대신 메커니즘이
극도로 얇아진다(하나의 open 함수, 하나의 next 함수, 하나의 close
함수가 scan_manager.h 에 들어 있다).
scan_manager.h 의 SCAN_TYPE enum은 CUBRID가 오늘 지원하는 액세스
메서드의 정확한 카탈로그를 열거한다. S_HEAP_SCAN,
S_PARALLEL_HEAP_SCAN, S_CLASS_ATTR_SCAN, S_INDX_SCAN, S_LIST_SCAN,
S_SET_SCAN, S_JSON_TABLE_SCAN, S_METHOD_SCAN, S_VALUES_SCAN,
S_SHOWSTMT_SCAN, S_HEAP_SCAN_RECORD_INFO, S_HEAP_PAGE_SCAN,
S_INDX_KEY_INFO_SCAN, S_INDX_NODE_INFO_SCAN, S_DBLINK_SCAN,
S_HEAP_SAMPLING_SCAN. 이 카탈로그는 컴파일 타임에 고정된다. 새 액세스
경로를 추가한다는 것은 enum 값을 추가하고, union에 서브-struct를
얹고, scan_open_* 함수를 작성하고, scan_start_scan /
scan_next_scan_local / scan_end_scan / scan_close_scan 의 switch
arm을 모두 늘린다는 뜻이다. 이 전체 카탈로그가 scan_manager.c
약 240 KB에 살고 있다.
이론 ↔ CUBRID 명칭 매핑
섹션 제목: “이론 ↔ CUBRID 명칭 매핑”§학술적 배경의 텍스트북 개념과 CUBRID의 명명된 엔티티가 다음과 같이
대응한다. ## CUBRID의 구현 섹션은 이 표의 각 행을 한 단계씩 더
깊이 들여다본다.
| 이론 (Theory) | CUBRID 명칭 |
|---|---|
| 액세스 경로 디스크리미네이터 | SCAN_TYPE enum (scan_manager.h) |
| 다형 스캔 핸들 | SCAN_ID (scan_id_struct, 디스크리미네이터 + 익명 union) |
| 상태 머신 | SCAN_STATUS × SCAN_POSITION × SCAN_DIRECTION |
Volcano open | scan_open_<kind> (종류별 함수) |
Volcano next | scan_next_scan → scan_handle_single_scan → scan_next_scan_local |
Volcano close | scan_end_scan (per-iteration) + scan_close_scan (per-lifetime) |
| 힙 스캔 (테이블 풀 스캔) | S_HEAP_SCAN + HEAP_SCAN_ID (+ HEAP_SCANCACHE) |
| 인덱스 스캔 | S_INDX_SCAN + INDX_SCAN_ID (+ BTREE_SCAN) |
| Covering index | INDX_COV 서브-struct, SCAN_IS_INDEX_COVERED 매크로 |
| Index skip scan | INDEX_SKIP_SCAN 서브-struct, SCAN_IS_INDEX_ISS 매크로 |
| Index loose scan | SCAN_IS_INDEX_ILS 매크로 + ils_prefix_len |
| Multi-range optimization (top-N) | MULTI_RANGE_OPT 서브-struct |
| 머터리얼라이즈된 list 스캔 | S_LIST_SCAN + LLIST_SCAN_ID + QFILE_LIST_ID |
| Hash join build/probe (list 위에 얹힘) | HASH_LIST_SCAN 서브-struct, S_LIST_SCAN 의 분기 |
컬렉션 unnest (SET/MULTISET/SEQUENCE) | S_SET_SCAN + SET_SCAN_ID |
(VALUES …) 인라인 행 | S_VALUES_SCAN + REGU_VALUES_SCAN_ID |
JSON_TABLE(...) 행 생성 | S_JSON_TABLE_SCAN + JSON_TABLE_SCAN_ID (C++ scanner) |
SHOW … 가상 카탈로그 | S_SHOWSTMT_SCAN + SHOWSTMT_SCAN_ID + SHOW_REQUEST 테이블 |
| 외부 SQL (DBLINK) | S_DBLINK_SCAN + DBLINK_SCAN_ID (CCI 클라이언트) |
| 저장 프로시저/메서드 | S_METHOD_SCAN + METHOD_SCAN_ID |
| 병렬 힙 스캔 | S_PARALLEL_HEAP_SCAN + PARALLEL_HEAP_SCAN_ID |
| 데이터 필터 (heap/list/show/dblink) | SCAN_PRED scan_pred |
| 키 필터 / 범위 필터 (인덱스 전용) | SCAN_PRED key_pred, range_pred |
| MVCC 재평가 훅 | MVCC_REEV_DATA + locator_lock_and_get_object_with_evaluation |
| Outer join NULL 패딩 | QPROC_SINGLE_FETCH 모드 4종 + scan_handle_single_scan |
CUBRID의 구현
섹션 제목: “CUBRID의 구현”SCAN_ID — 하나의 struct, 여러 모양
섹션 제목: “SCAN_ID — 하나의 struct, 여러 모양”다형성은 scan_manager.h 의 단일 struct에 실현된다. union 위쪽의
필드들은 모든 스캔 종류에 공통이고, union 자체가 종류별 상태를
인코딩한다.
// scan_id_struct — src/query/scan_manager.hstruct scan_id_struct{ SCAN_TYPE type; /* discriminator */ SCAN_STATUS status; /* S_OPENED|S_STARTED|S_ENDED|S_CLOSED */ SCAN_POSITION position; /* S_BEFORE|S_ON|S_AFTER */ SCAN_DIRECTION direction; /* S_FORWARD|S_BACKWARD */ bool mvcc_select_lock_needed; SCAN_OPERATION_TYPE scan_op_type; /* S_SELECT|S_DELETE|S_UPDATE */ int fixed; int grouped; int qualified_block; QPROC_SINGLE_FETCH single_fetch; int single_fetched; int null_fetched; QPROC_QUALIFICATION qualification; DB_VALUE *join_dbval; val_list_node *val_list; val_descr *vd; union { LLIST_SCAN_ID llsid; HEAP_SCAN_ID hsid; PARALLEL_HEAP_SCAN_ID phsid; HEAP_PAGE_SCAN_ID hpsid; INDX_SCAN_ID isid; INDEX_NODE_SCAN_ID insid; SET_SCAN_ID ssid; DBLINK_SCAN_ID dblid; REGU_VALUES_SCAN_ID rvsid; SHOWSTMT_SCAN_ID stsid; JSON_TABLE_SCAN_ID jtid; METHOD_SCAN_ID msid; } s; SCAN_STATS scan_stats; SCAN_STATS *partition_stats; bool scan_immediately_stop;};union 위쪽의 공통 필드들은 위쪽 연산자 대수가 균일하게 필요로 하는
바로 그것들이다. position / status 가 iterator의 상태 머신을
이루고, single_fetch / single_fetched 는 outer-join NULL 패딩을
인코딩한다(스캔 종류와 무관하게 scan_handle_single_scan 이 읽는다).
mvcc_select_lock_needed / scan_op_type 은 가시성 검사를 살피는
모든 종류로 트랜잭셔널 의도(transactional intent)를 전파한다.
union은 근본적으로 다른 모양의 상태들을 단일한 메모리 레이아웃에
압축한다. heap의 경우 HEAP_SCANCACHE 와 현재 OID, index의 경우
BTREE_SCAN + OID 버퍼 + 세 필터 트리플 + ISS/MRO/covered 상태,
list의 경우 QFILE_LIST_SCAN_ID + 옵셔널 HASH_LIST_SCAN,
JSON_TABLE의 경우 cubscan::json_table::scanner C++ 객체 (C++17의
unrestricted-union 규칙 덕분에 가능)다. 디스크리미네이터 SCAN_TYPE
은 해당 scan_open_* 함수 안에서 정확히 한 번 설정되고, 이후로는
디스패치 switch들로 모든 곳에서 읽힌다.
classDiagram
class SCAN_ID {
SCAN_TYPE type
SCAN_STATUS status
SCAN_POSITION position
SCAN_DIRECTION direction
bool mvcc_select_lock_needed
SCAN_OPERATION_TYPE scan_op_type
QPROC_SINGLE_FETCH single_fetch
union s
SCAN_STATS scan_stats
}
class HEAP_SCAN_ID {
OID curr_oid
OID cls_oid
HFID hfid
HEAP_SCANCACHE scan_cache
SCAN_PRED scan_pred
SCAN_ATTRS pred_attrs
SCAN_ATTRS rest_attrs
sampling_info sampling
}
class INDX_SCAN_ID {
INDX_INFO* indx_info
BTREE_SCAN bt_scan
BTREE_ISCAN_OID_LIST* oid_list
OID* curr_oidp
SCAN_PRED key_pred
SCAN_PRED scan_pred
SCAN_PRED range_pred
INDX_COV indx_cov
MULTI_RANGE_OPT multi_range_opt
INDEX_SKIP_SCAN iss
}
class LLIST_SCAN_ID {
QFILE_LIST_ID* list_id
QFILE_LIST_SCAN_ID lsid
SCAN_PRED scan_pred
HASH_LIST_SCAN hlsid
}
class SET_SCAN_ID {
regu_variable_node* set_ptr
regu_variable_list_node* operand
DB_VALUE set
int set_card
int cur_index
}
class JSON_TABLE_SCAN_ID {
cubscan::json_table::scanner
}
class DBLINK_SCAN_ID {
DBLINK_SCAN_INFO scan_info
SCAN_PRED scan_pred
}
class SHOWSTMT_SCAN_ID {
SHOWSTMT_TYPE show_type
DB_VALUE** arg_values
DB_VALUE** out_values
int cursor
void* ctx
}
class PARALLEL_HEAP_SCAN_ID {
OID curr_oid
HFID hfid
parallel_heap_scan::manager*
accumulative_trace_storage* trace_storage
}
SCAN_ID *-- HEAP_SCAN_ID : type=S_HEAP_SCAN
SCAN_ID *-- INDX_SCAN_ID : type=S_INDX_SCAN
SCAN_ID *-- LLIST_SCAN_ID : type=S_LIST_SCAN
SCAN_ID *-- SET_SCAN_ID : type=S_SET_SCAN
SCAN_ID *-- JSON_TABLE_SCAN_ID : type=S_JSON_TABLE_SCAN
SCAN_ID *-- DBLINK_SCAN_ID : type=S_DBLINK_SCAN
SCAN_ID *-- SHOWSTMT_SCAN_ID : type=S_SHOWSTMT_SCAN
SCAN_ID *-- PARALLEL_HEAP_SCAN_ID : type=S_PARALLEL_HEAP_SCAN
프로토콜 — open, start, next-block, next, end, close
섹션 제목: “프로토콜 — open, start, next-block, next, end, close”다섯 개의 공개 동사가 라이프사이클을 구성한다. 각각은
scan_manager.c 의 얇은 switch이며, 종류별 본체가 그 자리에서
바로 일을 하거나, 형제 모듈(heap_file.c, btree.c, list_file.c,
set_scan.c, show_scan.c, dblink_scan.c, scan_json_table.cpp,
parallel/px_heap_scan/)을 호출한다.
scan_open_<kind> 가 SCAN_ID 를 초기화한다. SCAN_TYPE 마다 인자
리스트가 다르므로 종류마다 open이 하나씩 있다. 모든 open 함수는
먼저 scan_init_scan_id 를 호출해 공통 상태(position=S_BEFORE,
status=S_OPENED, direction=S_FORWARD,
qualification=QPROC_QUALIFIED)를 세팅한 뒤, union 멤버를 채운다.
scan_start_scan 은 S_OPENED → S_STARTED 로 전이하면서 트랜잭셔널
자원을 획득한다. heap과 index에는 heap_scancache_start, MVCC 스냅샷은
logtb_get_mvcc_snapshot, 속성 정보 캐시는 heap_attrinfo_start, list
에는 qfile_open_list_scan + qfile_start_scan_fix 를 부른다.
JSON_TABLE / set / values / method / dblink는 단지 상태만 세팅하고,
show는 showstmt_start_scan 으로 위임한다.
scan_next_scan_block 과 scan_reset_scan_block 은 grouped (블록
단위) 스캔을 위한 것이다. grouped가 아닌 스캔은 이들을 항등 함수로
취급해 한 번 S_SUCCESS 를 반환하고 이후로는 S_END 를 반환한다.
grouped 힙 스캔은 heap_scanrange_to_following /
heap_scanrange_to_prior 를 사용하고, grouped 인덱스 스캔은
scan_get_index_oidset 으로 OID 버퍼를 다시 채운다.
scan_next_scan 은 튜플 단위 진입점이다.
// scan_next_scan — src/query/scan_manager.cSCAN_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 은 single_fetch 의미를 강제하는 2차 래퍼다
(outer-join NULL 패딩, 조인 컬럼이 NULL일 때 조기 종료 등). 진짜
디스패치는 scan_next_scan_local 이다.
// scan_next_scan_local — src/query/scan_manager.c (excerpt)static SCAN_CODEscan_next_scan_local (THREAD_ENTRY * thread_p, SCAN_ID * scan_id){ /* ... trace bookkeeping elided ... */ switch (scan_id->type) { case S_HEAP_SCAN: case S_HEAP_SCAN_RECORD_INFO: case S_HEAP_SAMPLING_SCAN: status = scan_next_heap_scan (thread_p, scan_id); break; case S_PARALLEL_HEAP_SCAN: status = scan_next_parallel_heap_scan (thread_p, scan_id); break; case S_HEAP_PAGE_SCAN: status = scan_next_heap_page_scan (thread_p, scan_id); break; case S_CLASS_ATTR_SCAN: status = scan_next_class_attr_scan (thread_p, scan_id); break; case S_INDX_SCAN: status = scan_next_index_scan (thread_p, scan_id); break; case S_INDX_KEY_INFO_SCAN: status = scan_next_index_key_info_scan (thread_p, scan_id); break; case S_INDX_NODE_INFO_SCAN: status = scan_next_index_node_info_scan (thread_p, scan_id); break; case S_LIST_SCAN: if (scan_id->s.llsid.hlsid.hash_list_scan_type != HASH_METH_NOT_USE) status = scan_next_hash_list_scan (thread_p, scan_id); else status = scan_next_list_scan (thread_p, scan_id); break; case S_SHOWSTMT_SCAN: status = scan_next_showstmt_scan (thread_p, scan_id); break; case S_VALUES_SCAN: status = scan_next_value_scan (thread_p, scan_id); break; case S_SET_SCAN: status = scan_next_set_scan (thread_p, scan_id); break; case S_JSON_TABLE_SCAN: status = scan_next_json_table_scan (thread_p, scan_id); break; case S_METHOD_SCAN: status = scan_next_method_scan (thread_p, scan_id); break; case S_DBLINK_SCAN: status = scan_next_dblink_scan (thread_p, scan_id); break; default: er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, ER_QPROC_INVALID_XASLNODE, 0); return S_ERROR; } /* ... fetch / ioread perfmon delta accumulation elided ... */ return status;}scan_end_scan 과 scan_close_scan 은 scan_start_scan 과
scan_open_* 의 거울상이다. end 는 iteration 단위 자원을 해제한다. HEAP_SCANCACHE 닫기, qfile_scan fix 종료, key copy 버퍼 비우기.
close 는 스캔 라이프타임 단위의 할당을 해제한다. bt_attr_ids,
vstr_ids, OID 버퍼, copy_buf, indx_cov 레코드들,
multi_range_opt 배열, prebuilt_midxkey_domains, hash-list-scan 테이블
등. 두 단계의 해제가 분리된 이유는, 같은 SCAN_ID 가 (nested-loop
조인을 위해) 다시 열지 않은 채 그냥 재시작 되는 경우가 있기
때문이다. end 후 다시 start_scan 하는 것이 풀 reopen보다 싸다.
stateDiagram-v2
[*] --> S_OPENED : scan_open_*
S_OPENED --> S_STARTED : scan_start_scan
S_STARTED --> S_STARTED : scan_next_scan*
S_STARTED --> S_ENDED : scan_end_scan
S_ENDED --> S_STARTED : scan_start_scan (재반복)
S_ENDED --> S_CLOSED : scan_close_scan
S_CLOSED --> [*]
note right of S_STARTED
position 순환
S_BEFORE -> S_ON -> S_AFTER
direction 뒤집힐 수 있음
S_FORWARD <-> S_BACKWARD
end note
scan_next_scan 이 연산자 트리에서 차지하는 자리
섹션 제목: “scan_next_scan 이 연산자 트리에서 차지하는 자리”executor(cubrid-query-executor.md 가 자세히 다룬다)는 XASL 연산자
트리의 모든 잎을 스캔으로 취급한다. qexec_open_scan 은 옵티마이저가
고른 액세스 경로에 따라 적절한 scan_open_<kind> 를 호출하고,
qexec_intprt_fnc 의 pull 루프는 S_END 가 돌아올 때까지
scan_next_scan 을 친다. qexec_end_scan 과 qexec_close_scan 이
라이프사이클을 마무리한다. 이는 곧, 위쪽의 모든 연산자 — 필터,
프로젝션, sort-build, group-by, hash-join build, top-N — 가
scan_next_scan 의 하류(downstream)에 있다는 뜻이다. scan manager는
액세스 메서드 다형성이 풀리는 단일한 길목(chokepoint)이다.
flowchart TB
subgraph "스캔 위 연산자 트리 (Operator Tree above scan)"
GROUPBY[Group-By / Aggregate]
SORT[Sort / TOP-N]
HASHB[Hash-Join Build]
PROJ[Project / rest 패치]
FILT[Filter / scan_pred]
GROUPBY --> SORT
SORT --> HASHB
HASHB --> PROJ
PROJ --> FILT
end
FILT --> NEXT[scan_next_scan]
NEXT --> LOCAL[scan_next_scan_local]
LOCAL --> SWITCH{switch type}
SWITCH -->|S_HEAP_SCAN| HEAP[scan_next_heap_scan]
SWITCH -->|S_INDX_SCAN| IDX[scan_next_index_scan]
SWITCH -->|S_LIST_SCAN| LIST[scan_next_list_scan / scan_next_hash_list_scan]
SWITCH -->|S_SET_SCAN| SET[scan_next_set_scan]
SWITCH -->|S_VALUES_SCAN| VALS[scan_next_value_scan]
SWITCH -->|S_JSON_TABLE_SCAN| JT[scan_next_json_table_scan]
SWITCH -->|S_DBLINK_SCAN| DBL[scan_next_dblink_scan]
SWITCH -->|S_SHOWSTMT_SCAN| SHOW[scan_next_showstmt_scan]
SWITCH -->|S_PARALLEL_HEAP_SCAN| PHS[scan_next_parallel_heap_scan]
HEAP --> HF[heap_next/prev/sampling/record_info]
IDX --> BTS[btree_keyval_search/btree_range_search]
LIST --> QF[qfile_scan_list_next]
JT --> JN[scanner::next_scan]
DBL --> CCI[CCI 통한 dblink_scan_next]
SHOW --> SHF[showstmt_next_scan]
PHS --> WMP[worker_manager 디스패치]
힙 스캔 경로 — S_HEAP_SCAN
섹션 제목: “힙 스캔 경로 — S_HEAP_SCAN”scan_open_heap_scan 은 access spec으로부터 HEAP_SCAN_ID 를
초기화한다. 클래스 OID와 HFID를 비트단위로 복사하고, 현재 OID를
힙 볼륨의 NULL 슬롯으로 리셋하고, 데이터 필터 scan_pred 를
predicate-expression 트리에 묶고 eval_fnc 으로 평가자(evaluator)를
선택한 뒤, pred/rest 속성 캐시를 연결한다. scan_type 은 S_HEAP_SCAN,
S_HEAP_SCAN_RECORD_INFO (cache_recordinfo 배열을 추가해 슬롯별
REC_HOME/REC_RELOCATION/REC_BIGONE/REC_NEWHOME 과 LSA를 노출),
혹은 S_HEAP_SAMPLING_SCAN (sampling_weight = total_pages / NUMBER_OF_SAMPLING_PAGES 계산) 중 하나가 된다.
scan_start_scan 은 grouped면 heap_scanrange_start 를, 아니면
heap_scancache_start 를 호출해 슬롯 iteration 동안 가장 최근 페이지를
핀해 줄 HEAP_SCANCACHE 를 얻는다. 비-루트 클래스에 대해서는
logtb_get_mvcc_snapshot 으로 스레드별 MVCC 스냅샷을 획득하고,
heap_attrinfo_start 로 술어/rest 속성 정보 캐시를 시작한다.
scan_next_heap_scan 이 튜플별 루프다. 정방향에서는 heap_next /
heap_next_sampling / heap_next_record_info 를, 역방향에서는
heap_prev / heap_prev_record_info 를 부른다. 힙이 다음 슬롯을 돌려
주면, eval_data_filter 가 pred_attrs.attr_cache 에 새로 채워진
DB_VALUE들을 pred_expr 트리를 걷는다. 필터를 통과하지 못한
행은 continue. 통과한 행은 heap_attrinfo_read_dbvalues 와
fetch_val_list 로 rest 속성을 가져오거나, mvcc_select_lock_needed
가 켜져 있을 때는 locator_lock_and_get_object_with_evaluation 로
들어가 행 락을 잡고 데이터 필터를 최신 가시 버전 를 다시
검사한다(아래 재평가 훅 참조). 페이지가 PEEK 도중 바뀌는 루프
(PGBUF_IS_PAGE_CHANGED)는, 술어 read와 rest 속성 read 사이에
동시 쓰기가 슬롯의 바이트를 옮겼다면 COPY 의미로 다시 가져온다.
이는 executor 경로에서 heap_next / heap_prev /
heap_next_sampling / heap_next_record_info 의 유일한 소비자다.
이 프리미티브들의 정본(canonical) 문서는 cubrid-heap-manager.md 다.
인덱스 스캔 경로 — S_INDX_SCAN
섹션 제목: “인덱스 스캔 경로 — S_INDX_SCAN”scan_open_index_scan 은 가장 긴 open 함수다(≈ 350줄). 상태 머신이
가장 풍부하기 때문이다. B+Tree root 헤더는 pgbuf_fix +
btree_get_root_header 로 가져와 max_key_len 과 BTID를 얻고,
BTREE_INIT_SCAN 이 커서를 초기화한다. btree_glean_root_header_info
가 BTID_INT.key_type 을 채운다. 스캔이 covering이 아니라면 OID 버퍼는
풀에서 받아 온다(scan_alloc_iscan_oid_buf_list, 용량
ISCAN_OID_BUFFER_COUNT). 세 필터 트리플 — key_pred, range_pred,
scan_pred — 은 각각 자기 regu_list 와 pr_eval_fnc 를 데리고
scan_init_scan_pred 로 묶인다. scan_init_indx_coverage /
scan_init_iss / scan_init_multi_range_optimization 가 옵셔널한
covering-index list-file, ISS 상태, MRO top-N 힙을 각각 셋업한다.
scan_start_scan 은 (기본적으로 힙 fetch가 일어나므로) heap scan cache
를 시작하고, range/key/pred/rest 술어용 속성 캐시들을 시작한 뒤,
oids_count / curr_keyno / curr_oidno / one_range 를 리셋한다.
그러면 scan_next_index_scan 이 두 상태를 번갈아 돈다.
- OID 떨어짐 —
call_get_next_index_oidset(ISS-aware 래퍼)이scan_get_index_oidset을 부르고, 이는btree_keyval_search(R_KEY/R_KEYLIST) 또는btree_range_search(R_RANGE/R_RANGELIST)를 발동시켜 OID 버퍼를max_oid_cnt까지 채운다. - OID 보유 —
curr_oidno를 증가시키고curr_oidp를 그 OID에 맞춘 뒤,scan_next_index_lookup_heap으로 힙 행을 가져오고 데이터 필터를 평가하고 MVCC 재평가를 적용한다.
이 골격 위에 옵티마이저가 운전하는 네 가지 변형이 올라탄다.
- Covering index —
SCAN_IS_INDEX_COVERED(isidp). 키들이scan_dump_key_into_tuple로INDX_COV.list_id에 덤프되고,qfile_scan_list_next가 그것을 스캔 출력으로 읽어 들인다. 힙 fetch 자체가 없다 (Petrov Database Internals 2장). - MRO (multi-range optimization) —
ORDER BY ... LIMIT N를multi_range_opt.use = true가 된다.top_n_items가 가장 좋은 N개 키의 힙이며, 새 키가 들어올 때마다 가장 나쁜 원소와 경쟁한다. 매칭 키 수만큼의 정렬 비용 대신 O(N log N). - ISS (index skip scan) —
iss.skipped_range = &indx_info->iss_range. 술어가 선두 컬럼을 빠뜨릴 때,scan_get_next_iss_value가 B+Tree lookup으로 distinct C1 값들을 한 칸씩 진행시키며 값마다 원래 범위를 재시도한다(Database Internals 3장). - LISS (loose index scan) —
SCAN_IS_INDEX_ILS(isidp)와ils_prefix_len > 0. prefix 중복을 건너뛰며 distinct prefix당 한 엔트리만 반환한다. 그 덕분에SELECT DISTINCT와MAX … GROUP BY가 distinct prefix 수에 비례해서 동작한다.
모든 B+Tree 상태(BTREE_SCAN, BTID_INT, root-header fetch, range/key
val 검색)는 INDX_SCAN_ID.bt_scan 을 거친다. 정본 문서는
cubrid-btree.md 다.
flowchart LR
A[scan_next_index_scan] --> B{OID 보유?}
B -- 아니오 --> C[call_get_next_index_oidset]
C --> D{ISS?}
D -- 예, 비었음 --> E[scan_get_next_iss_value]
E --> C
D -- 아니오/행 있음 --> F[scan_get_index_oidset]
F --> G[btree_keyval_search OR btree_range_search]
G --> H[oid_list 채워짐]
H --> B
B -- 예 --> I{covered?}
I -- 예 --> J[INDX_COV.list_id 통한 scan_dump_key_into_tuple]
J --> K[qfile_scan_list_next] --> L[val_list로 fetch_val_list]
I -- 아니오 --> M[scan_next_index_lookup_heap]
M --> N[heap_get_visible_version]
N --> O[eval_data_filter]
O --> P{mvcc_select_lock_needed?}
P -- 예 --> Q[locator_lock_and_get_object_with_evaluation]
Q --> R[필터 재평가]
P -- 아니오 --> R
R --> S[fetch_val_list rest_regu_list] --> T[S_SUCCESS 반환]
List 스캔 경로 — S_LIST_SCAN
섹션 제목: “List 스캔 경로 — S_LIST_SCAN”list 스캔은 이미 머터리얼라이즈된 튜플 스트림 — 보통 sort, hash-build,
group-by, sub-block의 출력 — 을 읽는다. scan_open_list_scan 은 위에서
온 QFILE_LIST_ID, 데이터 필터 scan_pred, rest regu 리스트, 그리고
(옵셔널하게) hash-list-scan용 build/probe regu 리스트를 묶는다.
is_read_only 가 list-file 페이지를 쓰기 잠금으로 잡을지를 결정한다.
scan_start_scan 은 qfile_open_list_scan + qfile_start_scan_fix 로
커서를 붙이고 첫 페이지를 핀한다.
튜플별 루프는 qfile_scan_list_next (PEEK) 로 한 칸 진행하고,
fetch_val_list (scan_pred.regu_list) 로 술어 속성 DB_VALUE들을 채운
뒤 술어를 평가한다. QPROC_QUALIFIED / NOT_QUALIFIED /
QUALIFIED_OR_NOT 으로 분기하고, 성공하면 fetch_val_list (rest_regu_list) 로 rest 속성을 가져온다. 머터리얼라이즈하는 모든
서브 XASL은 QFILE_LIST_ID 에 쓰고 그 부모는 S_LIST_SCAN 으로 읽는다.
계층적 XASL 트리, derived table, CTE 머터리얼라이제이션, sort-then-scan
이 균일하게 동작하는 이유다.
resolve_domains_on_list_scan 은 루프 맨 위에서 한 번 돈다. 도메인
결정이 미뤄진(host-variable late binding) TYPE_POSITION regu
변수들을 해결하기 위해서다. list-file의 type list로의 위치 인덱스가
가변 도메인 regu 변수를 위한 실제 TP_DOMAIN 을 산출한다.
Hash list 스캔 — hash_list_scan_type != HASH_METH_NOT_USE 인 S_LIST_SCAN
섹션 제목: “Hash list 스캔 — hash_list_scan_type != HASH_METH_NOT_USE 인 S_LIST_SCAN”옵티마이저가 list 스캔이 hash-joinable 모양의 안쪽(inner)이라고
판단하면 hash-list-scan을 켠다. check_hash_list_scan 이 가능성을
검증하고(튜플 수 > 0, build/probe regu 리스트가 null이 아니고 균형이
맞고, 타입이 강제 가능하고, no_hash_list_scan 힌트가 없을 것),
세 메서드 중 하나를 고른다.
HASH_METH_IN_MEM— 튜플 전체를 인메모리 해시 테이블에 보관 (mht_create_hls).HASH_METH_HYBRID— 튜플 위치만 메모리에 두고, probe 때qfile_jump_scan_tuple_position으로 list file에서 다시 읽어 온다. list가 메모리에는 들어가지만 튜플이 두꺼울 때 쓴다.HASH_METH_HASH_FILE— list가PRM_ID_MAX_HASH_LIST_SCAN_SIZE를 넘치면 extendible-hash 파일(fhs_create/fhs_insert/fhs_search)을 쓴다. 정본은cubrid-extendible-hash.md.
build는 scan_open_list_scan 안에서 scan_build_hash_list_scan 으로
실행된다. list를 한 번 걷으면서 build_regu_list 로부터
qdata_build_hscan_key 로 키를 만들고, qdata_hash_scan_key 로 해싱
한 뒤 insert. probe는 scan_next_hash_list_scan 에서 일어난다. 현재
외부 튜플을 probe_regu_list 로 키를 만들고, 해싱한 뒤 lookup.
scan_hash_probe_next 가 충돌 동료(collision peer)를 위해
mht_get_hls 와 mht_get_next_hls 를 모두 걷는다. 하이브리드 변형은
추가로 qfile_jump_scan_tuple_position 을 호출해 기록된 위치의 튜플을
머터리얼라이즈한다.
이것이 CUBRID의 hash-join build/probe 기계다. 별도 연산자가 아니라 list 스캔 위에 얹혀 있다. 안쪽이 이미 list-file일 때만 동작하지만, 그럴 때의 nested-loop 대비 이득은 상당하다.
Set / Values / JSON_TABLE / Show / Dblink / Method 스캔
섹션 제목: “Set / Values / JSON_TABLE / Show / Dblink / Method 스캔”Set 스캔 (S_SET_SCAN). CUBRID 컬렉션(SET / MULTISET /
SEQUENCE)을 행으로 풀어 헤친다. scan_open_set_scan 이 컬렉션을
산출하는 regu 변수(set_ptr)를 기록한다. qproc_next_set_scan
(set_scan.c)이 S_BEFORE 에서는 fetch_copy_dbval 로 컬렉션을
패치하고 원소 0을 산출하고, S_ON 에서는 cur_index 를 진행시키며
다음을 산출하고, S_AFTER 에서는 S_END 를 반환한다. 특수 경로
F_SEQUENCE 는 DB_SET 으로 머터리얼라이즈하지 않고 인라인 operand
리스트를 직접 걷는다.
Values 스캔 (S_VALUES_SCAN). (VALUES (1,'a'),(2,'b'),...) 을
구현한다. scan_open_values_scan 이 REGU_VALUE_LIST 셀들의
valptr_list 를 기록한다. scan_next_value_scan 은 모든 셀의
current_value 를 한 칸씩 같이 진행시키고, 첫 셀이 끝나는 순간
S_END 를 반환한다.
JSON_TABLE 스캔 (S_JSON_TABLE_SCAN). JSON 문서와 path 표현식을
행으로 변환한다. scan_open_json_table_scan 은 임베드된
cubscan::json_table::scanner 의 스캔 술어와 value descriptor만
초기화한다. 무거운 일은 scan_json_table.cpp 가 한다. C++ scanner는
C++17의 unrestricted-union 규칙으로 JSON_TABLE_SCAN_ID 로 C에
노출된다. 깊이별 cursor 스택을 유지한다. 각 커서가 현재 JSON 문서,
이 레벨 배열에 대한 iterator, 현재 자식 인덱스, 행 가져옴 / “진행
필요” / 노드 소진 플래그를 들고 있다. scanner::next_scan 이
scan_next_internal 을 커서 스택 위에서 재귀적으로 호출하면서, 중첩된
경로의 cross-product 확장으로 행을 만들어 낸다(각 NESTED PATH가 자기
배열의 카디널리티만큼 행 수를 곱한다). scanner 로컬 술어 필터는 외부
scan_handle_single_scan 이 아니라 next_scan 안쪽에 산다. 그래야
JSON iterator를 정확하게 진행시키기 위해 자격 미달 행을 내부에서
건너뛸 수 있기 때문이다.
Show 스캔 (S_SHOWSTMT_SCAN). 서버 내부(SHOW VOLUME HEADER,
SHOW THREADS, SHOW PAGE_BUFFER_STATUS, SHOW HEAP CAPACITY,
SHOW INDEX HEADER, SHOW TIMEZONES, SHOW JOB QUEUES,
SHOW TRAN_TABLES 등)를 가상 관계로 노출한다. show_scan.c 는
SHOWSTMT_TYPE 으로 색인되는 SHOW_REQUEST 테이블을 두고, 엔트리당
start_func / next_func / end_func 포인터를 넣는다. 서버 부팅 시
showstmt_scan_init 가 채운다. 가장 흔한 구현 패턴은
showstmt_array_next_scan / showstmt_array_end_scan 다. start_func
가 행을 SHOWSTMT_ARRAY_CONTEXT (DB_VALUE 행렬)에 미리 머터리얼라이즈
하고, 그 다음에 iterate 한다. volume-header / log-header / slotted-page
slots / heap-header는 underlying 서브시스템에서 행을 요청 즉시 패치한다.
Dblink 스캔 (S_DBLINK_SCAN). CCI 드라이버로 원격 SQL 문을
실행한다. scan_open_dblink_scan 이 술어를 기록한 뒤 dblink_open_scan
을 부른다. 그 안에서 qmgr_dblink_find_conn_handle /
cci_connect_with_url_ex 로 CCI 연결을 재사용 또는 생성하고, SQL
텍스트에 cci_prepare 를 걸고, dblink_bind_param 으로 host
변수를 묶고, cci_execute 를 부른 뒤 cci_get_result_info 로 컬럼
메타데이터를 모은다. scan_next_dblink_scan → dblink_scan_next 는
cci_cursor (첫 호출은 CCI_CURSOR_FIRST, 이후는 CCI_CURSOR_CURRENT)
cci_fetch로 진행시키고, 컬럼별로cci_get_data를 부른 뒤dblink_make_cci_value+tp_value_cast_preserve_domain로 변환한다. 결과 끝은CCI_ER_NO_MORE_DATA로 알려진다.dblink_scan_reset이 바깥 루프의 rescan을 위해CCI_CURSOR_FIRST로 다시 위치시킨다(CCI는cci_execute후에도 결과 집합을 유지한다).dblink_close_scan이cci_close_req_handle을, 그리고auto_commit이면cci_disconnect까지 부른다.
병렬 힙 스캔 (S_PARALLEL_HEAP_SCAN). 옵티마이저가 큰 힙 위의
집계나 hash-join build처럼 병렬화로 이득이 있을 때 고른다.
scan_open_parallel_heap_scan 은 parallel_heap_scan::manager <RESULT_TYPE> 템플릿 인스턴스(scan-only / scan-with-aggregation /
scan-as-hash-join-build로 파라미터화)를 만든다.
scan_start_parallel_heap_scan 이 힙을 블록 범위로 파티션하고,
worker manager로 워커별 태스크를 분배한다. scan_next_parallel_heap_scan
은 워커들이 비동기적으로 채워 넣는, 결과 핸들러가 재조립한 스트림에서
읽어 낸다. scan_end_parallel_heap_scan 이 워커에게 정지를 알리고
통계를 합치고, scan_close_parallel_heap_scan 이 매니저를 해제한다.
런타임 substrate는 cubrid-thread-worker-pool.md 가 정본이다. scan
manager는 디스패치만 소유한다.
Method 스캔 (S_METHOD_SCAN). 저장 프로시저(Java PL 또는
PL/CSQL)나 메서드를 호출해 그 결과를 관계로 본다.
scan_open_method_scan 이 METHOD_SCAN_ID::init + open 으로 JNI
브리지나 PL 스택을 셋업한다. scan_next_method_scan 이
METHOD_SCAN_ID::next_scan 을 호출해 다음 튜플을 JNI/메서드 경계
너머로 마샬해 scan_id->val_list 에 넣는다. 정본은
cubrid-pl-javasp.md / cubrid-pl-plcsql.md.
Class-attribute 스캔 (S_CLASS_ATTR_SCAN). 클래스 레벨의 shared
속성 한 행을 반환한다. scan_next_class_attr_scan 이 S_BEFORE 에서
데이터 필터를 한 번 평가하고 곧장 S_AFTER 로 전이한다. 한 행을
넘는 것을 만들지 않는다.
Heap-page / record-info / index-key-info / index-node-info 스캔 —
introspection 변형들. S_HEAP_PAGE_SCAN 은 슬롯으로 내려가지 않고
힙 페이지를 걷는다. S_HEAP_SCAN_RECORD_INFO 는 일반 힙 스캔 위에
얹혀 슬롯별 cache_recordinfo (record_type, record_size, OID-of-home,
MVCC 헤더)를 채운다. S_INDX_KEY_INFO_SCAN 과 S_INDX_NODE_INFO_SCAN
은 B+Tree 리프 키와 노드 구조를 걷는 walker로, SHOW INDEX HEADER /
SHOW INDEX CAPACITY 가 쓴다. S_HEAP_SAMPLING_SCAN 은
sampling_weight 페이지마다 한 페이지씩 방문한다(total_pages / NUMBER_OF_SAMPLING_PAGES 로 계산). 분석기(analyzer)가 싸게 카디널리티를
추정할 때 쓴다(cubrid-statistics.md).
술어 / 범위 / 필터 훅
섹션 제목: “술어 / 범위 / 필터 훅”세 개의 술어 슬롯이, 그것이 의미를 갖는 스캔 종류에 등장한다. key_pred, range_pred, scan_pred. 앞의 둘은 인덱스 전용이고,
scan_pred (데이터 필터)는 heap, list, set, show, dblink에 등장한다.
SCAN_PRED 는 regu_list (평가 전 채워야 할 속성 read 리스트),
pred_expr (술어 트리), pr_eval_fnc (eval_fnc 로 얻은 평가자)를
들고 다닌다. scan_init_scan_pred 가 셋을 묶고, 종류별 scan_open_*
가 슬롯마다 한 번씩 부른다. 평가 시점에 튜플별 루프는 regu_list 를
fetch_val_list 로 술어가 참조하는 DB_VALUE들을 채우고,
pr_eval_fnc(thread_p, pred_expr, vd, current_oid) 로 boolean을 받는다.
eval_data_filter (query_evaluator.c)가 정본 래퍼로, range/key/data
필터를 명명하는 FILTER_INFO 번들, 값 리스트, value descriptor, 클래스
OID, 재평가 훅(스캔 종류마다 일부 슬롯만 채워짐)을 받는다. scan
manager가 술어 평가자에게 말을 거는 단일 통합 훅이다.
재평가 훅
섹션 제목: “재평가 훅”mvcc_select_lock_needed 가 true (UPDATE/DELETE 드라이버 스캔, FOR
UPDATE select)일 때, 스캔은 행 락을 잡은 뒤 데이터 필터를 최신
가시 버전 를 다시 검사해야 한다. 진행 중인 트랜잭션이 스냅샷
read와 락 획득 사이에 컬럼을 바꾸었을 수 있기 때문이다. 훅은
MVCC_REEV_DATA + UPDDEL_MVCC_COND_REEVAL + MVCC_SCAN_REEV_DATA 와
locator_lock_and_get_object_with_evaluation (locator_sr.c)으로
연결된다. 후자는 행 락을 잡고, 최신 버전을 재패치하고, reev 데이터를
평가하고, filter_result 를 보고한다. 재평가가 실패하면 스캔이 그
행을 투명하게 건너뛴다. 같은 기계가 인덱스 스캔에서도
scan_next_index_lookup_heap 으로 쓰인다. 정본은
cubrid-mvcc.md.
Single-fetch / outer-join 의미
섹션 제목: “Single-fetch / outer-join 의미”scan_handle_single_scan 은 임의의 종류별 next 함수를 네 가지
single_fetch 모드로 감싼다. QPROC_NO_SINGLE_INNER (pass-through;
일반 케이스); QPROC_SINGLE_OUTER (행은 최대 하나; join_dbval 이
NULL이면 액세스 메서드를 건드리지 않고 NULL-패딩된 행을 반환 —
left-outer join의 안쪽이 외부 컬럼 NULL일 때 단락 처리);
QPROC_SINGLE_INNER (NULL 패딩 폴백 없는 같은 single-row 계약);
QPROC_NO_SINGLE_OUTER (행 다수 허용; 자격을 갖춘 행이 없으면 외부의
행 수를 보존하기 위해 NULL-패딩된 행 한 개를 반환 — outer-join 행
보존). outer-join 행 보존 로직이 스캔 계층에 사는 덕분에, 위쪽 모든
연산자가 그것을 다시 구현할 필요가 없다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”심볼은 책임별로 묶어 정리한다. 위치 힌트 표의 라인번호는 본 문서가
마지막으로 updated: 된 시점의 관찰값이다. 시간이 가면 어긋나는
힌트로 다루고, 먼저 심볼명으로 grep 하는 것이 권장 lookup 방식이다.
SCAN_ID 타입 plumbing
섹션 제목: “SCAN_ID 타입 plumbing”SCAN_TYPE—scan_manager.h의 디스크리미네이터 enum. 값은S_HEAP_SCAN,S_PARALLEL_HEAP_SCAN,S_CLASS_ATTR_SCAN,S_INDX_SCAN,S_LIST_SCAN,S_SET_SCAN,S_JSON_TABLE_SCAN,S_METHOD_SCAN,S_VALUES_SCAN,S_SHOWSTMT_SCAN,S_HEAP_SCAN_RECORD_INFO,S_HEAP_PAGE_SCAN,S_INDX_KEY_INFO_SCAN,S_INDX_NODE_INFO_SCAN,S_DBLINK_SCAN,S_HEAP_SAMPLING_SCAN.scan_id_struct(SCAN_ID) — 다형 스캔 핸들.scan_init_scan_id— 공통(union 위쪽) 필드를 채운다.scan_init_scan_pred—regu_list/pred_expr/pr_eval_fnc를 SCAN_PRED에 묶는다.scan_init_scan_attrs— 속성 정보 캐시를 SCAN_ATTRS에 묶는다.scan_initialize/scan_finalize— 프로세스 단위 ISCAN OID 버퍼 풀(scan_Iscan_oid_buf_list).scan_alloc_oid_list/scan_free_oid_list— 버퍼 (해제) 할당 프리미티브.scan_alloc_iscan_oid_buf_list/scan_free_iscan_oid_buf_list— INDX_SCAN_ID의 OID 버퍼를 위한 풀-aware 획득/해제.scan_save_scan_pos/scan_jump_scan_pos— list-file 스캔 전용 체크포인트 프리미티브.
공개 라이프사이클 진입점
섹션 제목: “공개 라이프사이클 진입점”scan_open_heap_scan,scan_open_heap_page_scan,scan_open_class_attr_scan— 힙 변형들.scan_open_index_scan,scan_open_index_key_info_scan,scan_open_index_node_info_scan— 인덱스 변형들.scan_open_list_scan— list 스캔, 옵션으로 hash-list-scan 셋업 포함.scan_open_showstmt_scan— show.scan_open_values_scan—(VALUES …).scan_open_set_scan— 컬렉션 unnest.scan_open_json_table_scan— JSON_TABLE.scan_open_method_scan— 메서드/SP 호출.scan_open_dblink_scan— CCI를 통한 외부 데이터.scan_open_parallel_heap_scan(px_heap_scan.cpp) — 병렬 힙.scan_start_scan— iteration 단위 획득.scan_reset_scan_block,scan_next_scan_block— grouped 스캔의 블록 iteration.scan_next_scan,scan_prev_scan— 튜플별 진입점;scan_handle_single_scan의 얇은 래퍼.scan_end_scan— iteration 단위 해제.scan_close_scan— 스캔 라이프타임 단위 해제.
힙 스캔 내부
섹션 제목: “힙 스캔 내부”scan_next_heap_scan, scan_next_heap_page_scan,
scan_next_class_attr_scan. 로컬 enum OBJECT_GET_STATUS 가
scan_next_heap_scan 안에서 lock-acquire / re-fetch 상태 머신을
추적한다.
인덱스 스캔 내부
섹션 제목: “인덱스 스캔 내부”scan_next_index_scan, scan_next_index_lookup_heap,
scan_next_index_key_info_scan, scan_next_index_node_info_scan,
call_get_next_index_oidset, scan_get_index_oidset,
scan_regu_key_to_index_key, scan_dbvals_to_midxkey,
scan_init_iss, scan_save_range_details /
scan_restore_range_details, scan_get_next_iss_value,
scan_init_index_key_limit, scan_init_indx_coverage,
scan_init_multi_range_optimization, scan_dump_key_into_tuple.
매크로 SCAN_IS_INDEX_COVERED / MRO / ISS / ILS. 서브 구조체
INDEX_SKIP_SCAN, MULTI_RANGE_OPT, INDX_COV.
List + hash list 스캔 내부
섹션 제목: “List + hash list 스캔 내부”scan_next_list_scan, resolve_domains_on_list_scan /
resolve_domain_on_regu_operand, scan_build_hash_list_scan,
scan_next_hash_list_scan, scan_hash_probe_next,
check_hash_list_scan. 해시 프리미티브들은 query_hash_scan.c 에 있다. qdata_alloc_hscan_key / hscan_value / hscan_value_OID,
qdata_build_hscan_key, qdata_hash_scan_key, qdata_hscan_key_eq,
qdata_copy_hscan_key / copy_hscan_key_without_alloc, fhs_create /
destroy / insert / search / search_next, in-mem mht_put_hls /
get_hls / get_next_hls / clear_hls / destroy_hls. 매크로
MAKE_TUPLE_POSTION, MAKE_TFTID_TO_TUPLE_POSTION.
Set / values / JSON / show / dblink / parallel
섹션 제목: “Set / values / JSON / show / dblink / parallel”qproc_next_set_scan (set_scan.c), scan_next_set_scan,
scan_next_value_scan, scan_next_json_table_scan. C++의
cubscan::json_table::scanner 와 메서드 open / next_scan / end /
init / clear / scan_next_internal / set_input_document /
init_cursor / set_next_cursor / init_iterators /
reset_ordinality / clear_node_columns / get_tree_height, 그리고
inner 클래스 cursor. showstmt_scan_init, showstmt_start_scan /
next_scan / end_scan, scan_next_showstmt_scan,
showstmt_array_next_scan / array_end_scan,
showstmt_alloc_array_context / free_array_context /
alloc_tuple_in_context, SHOW_REQUEST, thread_start_scan /
thread_scan_mapfunc. dblink_open_scan / close_scan / scan_next /
scan_reset, dblink_bind_param, dblink_make_cci_value /
make_date_time / make_date_time_tz / get_basic_utype,
dblink_execute_query / end_tran. parallel_heap_scan::manager <RESULT_TYPE> 템플릿 (px_heap_scan.hpp), extern-C 진입점들
scan_open_parallel_heap_scan / scan_start_parallel_heap_scan /
scan_next_parallel_heap_scan / scan_end_parallel_heap_scan /
scan_close_parallel_heap_scan / scan_reset_scan_block_parallel_heap_scan,
input_handler_ftabs, result_handler<RESULT_TYPE>, trace_handler,
accumulative_trace_storage.
술어 / single-fetch / 재평가 / 통계
섹션 제목: “술어 / single-fetch / 재평가 / 통계”scan_handle_single_scan, scan_next_scan_local,
scan_prev_scan_local, eval_data_filter (query_evaluator.c),
update_logical_result, FILTER_INFO. MVCC_REEV_DATA /
MVCC_SCAN_REEV_DATA / UPDDEL_MVCC_COND_REEVAL
(query_reevaluation.hpp),
locator_lock_and_get_object_with_evaluation (locator_sr.c),
mvcc_reev_data.filter_result. SCAN_STATS,
scan_print_stats_json / scan_print_stats_text (EXPLAIN 사용).
이 개정 시점의 위치 힌트
섹션 제목: “이 개정 시점의 위치 힌트”심볼명은 안정적이다. 아래의 라인번호는 본 문서의 updated: 일자
시점의 시들기 쉬운 힌트다.
| 심볼 | 파일 | 라인 |
|---|---|---|
SCAN_TYPE enum | src/query/scan_manager.h | 75 |
scan_id_struct | src/query/scan_manager.h | 379 |
HEAP_SCAN_ID | src/query/scan_manager.h | 104 |
INDX_SCAN_ID (indx_scan_id) | src/query/scan_manager.h | 230 |
LLIST_SCAN_ID | src/query/scan_manager.h | 291 |
SET_SCAN_ID | src/query/scan_manager.h | 323 |
SHOWSTMT_SCAN_ID | src/query/scan_manager.h | 303 |
DBLINK_SCAN_ID | src/query/scan_manager.h | 97 |
PARALLEL_HEAP_SCAN_ID | src/query/scan_manager.h | 130 |
INDEX_SKIP_SCAN | src/query/scan_manager.h | 222 |
MULTI_RANGE_OPT | src/query/scan_manager.h | 193 |
INDX_COV | src/query/scan_manager.h | 168 |
SCAN_IS_INDEX_COVERED 매크로 | src/query/scan_manager.h | 421 |
SCAN_IS_INDEX_ISS 매크로 | src/query/scan_manager.h | 424 |
SCAN_IS_INDEX_ILS 매크로 | src/query/scan_manager.h | 425 |
scan_init_iss | src/query/scan_manager.c | 220 |
scan_init_index_scan | src/query/scan_manager.c | 290 |
scan_save_range_details | src/query/scan_manager.c | 320 |
scan_restore_range_details | src/query/scan_manager.c | 349 |
scan_get_next_iss_value | src/query/scan_manager.c | 392 |
scan_init_scan_pred | src/query/scan_manager.c | 639 |
scan_init_scan_attrs | src/query/scan_manager.c | 654 |
scan_init_indx_coverage | src/query/scan_manager.c | 676 |
scan_init_index_key_limit | src/query/scan_manager.c | 812 |
scan_alloc_oid_list | src/query/scan_manager.c | 940 |
scan_alloc_iscan_oid_buf_list | src/query/scan_manager.c | 997 |
scan_dbvals_to_midxkey | src/query/scan_manager.c | 1475 |
scan_regu_key_to_index_key | src/query/scan_manager.c | 1932 |
scan_get_index_oidset | src/query/scan_manager.c | 2224 |
scan_init_scan_id | src/query/scan_manager.c | 2789 |
scan_open_heap_scan | src/query/scan_manager.c | 2846 |
scan_open_heap_page_scan | src/query/scan_manager.c | 2932 |
scan_open_class_attr_scan | src/query/scan_manager.c | 2981 |
scan_open_index_scan | src/query/scan_manager.c | 3067 |
scan_open_index_key_info_scan | src/query/scan_manager.c | 3437 |
scan_open_index_node_info_scan | src/query/scan_manager.c | 3643 |
scan_open_list_scan | src/query/scan_manager.c | 3715 |
scan_open_showstmt_scan | src/query/scan_manager.c | 3849 |
scan_open_values_scan | src/query/scan_manager.c | 3958 |
scan_open_set_scan | src/query/scan_manager.c | 3996 |
scan_open_json_table_scan | src/query/scan_manager.c | 4036 |
scan_open_method_scan | src/query/scan_manager.c | 4071 |
scan_open_dblink_scan | src/query/scan_manager.c | 4109 |
scan_start_scan | src/query/scan_manager.c | 4136 |
scan_reset_scan_block | src/query/scan_manager.c | 4446 |
scan_next_scan_block | src/query/scan_manager.c | 4606 |
scan_end_scan | src/query/scan_manager.c | 4749 |
scan_close_scan | src/query/scan_manager.c | 4882 |
call_get_next_index_oidset | src/query/scan_manager.c | 5118 |
scan_next_scan_local | src/query/scan_manager.c | 5193 |
scan_next_heap_scan | src/query/scan_manager.c | 5361 |
scan_next_heap_page_scan | src/query/scan_manager.c | 5739 |
scan_next_class_attr_scan | src/query/scan_manager.c | 5810 |
scan_next_index_scan | src/query/scan_manager.c | 5909 |
scan_next_index_lookup_heap | src/query/scan_manager.c | 6288 |
scan_next_index_key_info_scan | src/query/scan_manager.c | 6539 |
scan_next_index_node_info_scan | src/query/scan_manager.c | 6595 |
scan_next_list_scan | src/query/scan_manager.c | 6647 |
scan_next_value_scan | src/query/scan_manager.c | 6756 |
scan_next_showstmt_scan | src/query/scan_manager.c | 6820 |
scan_next_set_scan | src/query/scan_manager.c | 6911 |
scan_next_json_table_scan | src/query/scan_manager.c | 7014 |
scan_next_method_scan | src/query/scan_manager.c | 7038 |
scan_next_dblink_scan | src/query/scan_manager.c | 7094 |
scan_handle_single_scan | src/query/scan_manager.c | 7182 |
scan_next_scan | src/query/scan_manager.c | 7329 |
scan_prev_scan_local | src/query/scan_manager.c | 7343 |
scan_prev_scan | src/query/scan_manager.c | 7460 |
scan_save_scan_pos | src/query/scan_manager.c | 7474 |
scan_jump_scan_pos | src/query/scan_manager.c | 7491 |
scan_initialize | src/query/scan_manager.c | 7616 |
scan_finalize | src/query/scan_manager.c | 7659 |
resolve_domains_on_list_scan | src/query/scan_manager.c | 7712 |
resolve_domain_on_regu_operand | src/query/scan_manager.c | 7808 |
scan_init_multi_range_optimization | src/query/scan_manager.c | 7848 |
scan_dump_key_into_tuple | src/query/scan_manager.c | 7921 |
scan_print_stats_json | src/query/scan_manager.c | 7969 |
scan_print_stats_text | src/query/scan_manager.c | 8105 |
scan_build_hash_list_scan | src/query/scan_manager.c | 8263 |
scan_next_hash_list_scan | src/query/scan_manager.c | 8371 |
scan_hash_probe_next | src/query/scan_manager.c | 8478 |
check_hash_list_scan | src/query/scan_manager.c | 8657 |
qproc_next_set_scan | src/query/set_scan.c | 46 |
cubscan::json_table::scanner | src/query/scan_json_table.hpp | 109 |
scanner::open | src/query/scan_json_table.cpp | 230 |
scanner::next_scan | src/query/scan_json_table.cpp | 297 |
scanner::scan_next_internal | src/query/scan_json_table.cpp | 438 |
scanner::cursor | src/query/scan_json_table.cpp | 37 |
showstmt_scan_init | src/query/show_scan.c | 102 |
showstmt_next_scan | src/query/show_scan.c | 252 |
showstmt_start_scan | src/query/show_scan.c | 285 |
showstmt_end_scan | src/query/show_scan.c | 311 |
showstmt_array_next_scan | src/query/show_scan.c | 449 |
showstmt_array_end_scan | src/query/show_scan.c | 479 |
showstmt_alloc_array_context | src/query/show_scan.c | 338 |
dblink_open_scan | src/query/dblink_scan.c | 686 |
dblink_close_scan | src/query/dblink_scan.c | 788 |
dblink_scan_next | src/query/dblink_scan.c | 830 |
dblink_scan_reset | src/query/dblink_scan.c | 1056 |
parallel_heap_scan::manager (template) | src/query/parallel/px_heap_scan/px_heap_scan.hpp | 38 |
scan_next_parallel_heap_scan | src/query/parallel/px_heap_scan/px_heap_scan.cpp | 44 |
scan_reset_scan_block_parallel_heap_scan | src/query/parallel/px_heap_scan/px_heap_scan.cpp | 77 |
scan_end_parallel_heap_scan | src/query/parallel/px_heap_scan/px_heap_scan.cpp | 197 |
scan_close_parallel_heap_scan | src/query/parallel/px_heap_scan/px_heap_scan.cpp | 242 |
scan_open_parallel_heap_scan | src/query/parallel/px_heap_scan/px_heap_scan.cpp | 349 |
scan_start_parallel_heap_scan | src/query/parallel/px_heap_scan/px_heap_scan.cpp | 586 |
교차 검증 노트 (Cross-check Notes)
섹션 제목: “교차 검증 노트 (Cross-check Notes)”cubrid-query-executor.md 와의 교차 검증. executor 문서는
qexec_open_scan 과 qexec_intprt_fnc 를 연산자 트리의 드라이버로
설명한다. 본 문서는 그것이 위에 얹혀 있는 액세스 메서드 디스패치를
다룬다. 두 문서의 인계점은 정확히 다음이다. qexec_open_scan 이
ACCESS_SPEC_TYPE.access 에 따라 본 문서가 열거하는 scan_open_<kind>
중 하나를 부르고, 이어서 qexec_intprt_fnc 가 끌어당긴 튜플마다
scan_next_scan 을 부른다. 두 문서는 SCAN_ID라는 이름을 공유하지만
책임을 깔끔히 나눈다. executor 문서는 연산자 간의 plumbing(aptr/dptr/
scan_ptr 체인, 연산자 사이의 list-file 머터리얼라이제이션,
fetch_val_list, trace + EXPLAIN)을 소유하고, 본 문서는 잎 레벨의
액세스 메서드 기계만 소유한다.
cubrid-query-optimizer.md 와의 교차 검증. 옵티마이저는
SCAN_TYPE 의 선택을 access spec에 ACCESS_SPEC_TYPE.access enum
(SEQUENTIAL / INDEX 등)으로 적어 넣고, xasl_generation 이
그것을 scan_open_<kind> 호출로 번역한다. 옵티마이저/executor 분리는
정확히 그 경계에 산다. 위쪽은 비용 기반 경로 선택, 아래쪽은 기계적
디스패치. 옵티마이저 문서는 비용 모델, 통계 소비, 조인 순서 결정을
다루고, 본 문서는 경로가 선택된 후 일어나는 일을 다룬다.
cubrid-heap-manager.md 와의 교차 검증. 이 문서의 힙 스캔
경로는 heap_next / heap_prev / heap_next_record_info /
heap_next_sampling / heap_get_visible_version /
heap_attrinfo_start / heap_attrinfo_read_dbvalues /
heap_scancache_start / heap_scancache_end /
heap_scanrange_start / heap_scanrange_to_following 의 얇은
클라이언트일 뿐이다. REC_RELOCATION / REC_BIGONE / REC_NEWHOME 같은
슬롯 타입 처리 의미를 포함해, 그 함수들의 의미는 heap-manager 문서가
정본이다. 본 문서는 단지 scan manager가 그것들을 어떻게 부르는지를
설명한다.
cubrid-btree.md 와의 교차 검증. 인덱스 스캔 경로도 마찬가지로
btree_keyval_search / btree_range_search / btree_get_next_key_info
/ btree_get_next_node_info / btree_glean_root_header_info /
btree_get_root_header / BTREE_INIT_SCAN / BTREE_RESET_SCAN /
BTREE_END_OF_SCAN 의 클라이언트다. B+Tree 문서는 하강(descent), 키
인코딩(MIDXKEY), 유일성, 동시성 제어의 정본이다. 본 문서는 executor의
인덱스 스캔 iterator가 그 프리미티브들을 어떻게 감싸는지, 그리고
B+Tree 안이 아니라 scan-manager 계층에 있는 ISS / LISS / MRO / covered
변형들을 다룬다.
cubrid-mvcc.md 와의 교차 검증. 재평가 훅(MVCC_REEV_DATA +
locator_lock_and_get_object_with_evaluation +
mvcc_reev_data.filter_result)은 스캔이 mvcc_select_lock_needed
아래에서 MVCC 가시성 검사에 참여하는 인터페이스다. MVCC 문서는
스냅샷 의미, 버전 체인, 가시성의 정본이다. 본 문서는 스캔이
scan_start_scan 시점에 logtb_get_mvcc_snapshot 으로 스냅샷을
얻고, UPDATE/DELETE 드라이버 스캔에서 락을 잡은 뒤 술어를 재검사하는
방법 을 다룬다.
scan_manager.h 의 타입과 오늘의 executor 사용 사이 드리프트.
enum의 모든 SCAN_TYPE 값은 executor의 qexec_open_scan 과 scan
manager의 switch들 양쪽 모두에 연결되어 있다. 카탈로그는 망라적이고
일관적이다. 일반 옵티마이저 경로가 결코 만들어 내지 않는, introspection
전용 enum 값이 둘 있다. S_HEAP_PAGE_SCAN 과 S_INDX_NODE_INFO_SCAN
은 access spec을 손으로 짜는 SHOW 인프라스트럭처로만 도달한다.
S_HEAP_SCAN_RECORD_INFO 도 record-info SHOW로만 도달한다.
미래의 bitmap heap scan이나 TID scan에 해당하는 enum 값이나
서브-struct는 없다. 둘 다 추가하려면 enum과 union을 동시에 확장해야
한다.
scan_id_struct.s union은 METHOD_SCAN_ID 를 들고 있는데, 그
내부는 scan-manager 모듈 바깥의 method_scan.hpp 에 산다. 이는 의도된
디커플링이다. JNI/PL 브리지가 액세스 메서드 디스패치와 독립적으로
진화할 수 있게 하기 위해서다.
parallel_heap_scan::manager 템플릿 C++ 클래스는 PARALLEL_HEAP_SCAN_ID
C struct 안에 void *manager 포인터로 들어 있다. 매니저 클래스를
직접 멤버로 둘 수 없는 이유는, 그 C struct가 C++17로 컴파일되지만 역사적
으로 C-style POD 레이아웃을 유지해 온 .c 파일에 보여야 하기 때문이다.
parallel_heap_scan::accumulative_trace_storage *trace_storage 가
임베드된 값이 아니라 forward-declared 포인터로 사는 이유도 동일하다.
MVCC_REEV_DATA 기계는 scan_manager.c 가 아니라
query_reevaluation.cpp/hpp 에 산다. scan-manager의 일은 reev 구조체를
채우고 locator_lock_and_get_object_with_evaluation 을 부르는 것까지다.
실제 재평가는 술어 평가자에게 reev 데이터를 흘려 위임된다.
미해결 질문 (Open Questions)
섹션 제목: “미해결 질문 (Open Questions)”-
병렬 인덱스 스캔? 힙 스캔에는
S_PARALLEL_HEAP_SCAN이 존재하지만, 그에 대응하는S_PARALLEL_INDX_SCAN은 없다. B+Tree의 range-search 인터페이스는 현재 워커 사이에 안전하게 파티션할 수 있게 구조화되어 있지 않다. 파티션은 leaf 레벨 링크 리스트(단방향) 를 쪼개거나, 워커 로컬 OID 버퍼를 부모에서 다시 합치는 방식의 sub-range 분할을 요구한다. 병렬 질의 인프라가 더 성숙해질 때 탐구해 볼 가치가 있다. -
적응형 스캔 메서드 전환.
SCAN_TYPE의 선택은 옵티마이저 시점에 고정된다. executor가 실제 선택성이 추정보다 훨씬 나쁘다는 사실을 관찰한 뒤 인덱스 스캔에서 힙 스캔으로 폴백하거나, build 측이 넘치는 것을 보고 인메모리 해시에서 file-hash로 갈아탈 수 있는 메커니즘은 없다. PostgreSQL이 프로토타입을 가진 영역이지만 CUBRID에는 없다. -
재개 가능 커서 / 스캔 상태 체크포인팅.
scan_save_scan_pos와scan_jump_scan_pos는 존재하지만 list-file 전용이다. (힙과 인덱스 스캔 모두에 걸친) 일반 재개 커서가 있다면 broker가 긴 질의를 스캔 중간에 잠시 멈출 수 있다. 비-list 스캔에서는 현재 불가능하다.SCAN_POSstruct는QFILE_TUPLE_POSITION만 들고 있어서, 확장하려면 스캔 종류별 직렬화가 필요하다. -
벡터화(Vectorisation). 모든 스캔 종류가 현재 한 번에 한 튜플씩 생산한다. 벡터화된 next API(
next_batch(thread_p, scan_id, batch_size, batch_out))가 있다면 튜플별pr_eval_fnc호출과 술어 속성 fetch를 여러 튜플에 걸쳐 amortize할 수 있을 것이다. 그러나 비용은 침습적이다. 스캔 위쪽의 모든 연산자가 배치를 처리해야 한다. PostgreSQL이 custom-scan과 JIT 인프라로 현재 헤쳐 나가는 것과 같은 트레이드 오프다. -
플러그인 가능한 액세스 메서드. 오늘 새
SCAN_TYPE을 추가한다는 것은 enum, union,scan_manager.c의 모든 switch, 그리고 executor의qexec_open_scan을 모두 손보는 일이다. handler-API 스타일의 간접화(SCAN_TYPE별 함수 포인터 테이블)가 있다면 카탈로그를 런타임에 확장 가능하게 만들 수 있겠지만, 그 대가는 switch-table의 컴파일 타임 망라성 검사를 잃는 것이다. Postgres는IndexAmRoutine콜백 테이블로 컴파일 타임 확장을 골랐고, MySQL은handlerton으로 런타임 확장을 골랐다. CUBRID는 어느 방향으로도 결단을 내리지 않았다. -
DISTINCT/MIN/MAX 너머의 index loose scan. LISS는 오늘 옵티마이저가 매우 특정한 질의 모양에 한해
ils_prefix_len > 0으로 켜 줄 때만 동작한다. 임의의 GROUP BY prefix로 일반화하면 CUBRID가 해싱이나 정렬이 아니라 인덱스로GROUP BY안의 중복을 건너뛸 수 있게 된다. 고-카디널리티 선두 컬럼 그룹핑에서 이득이 클 것이다. -
비-list 안쪽에 대한 hash-list-scan. Hash-list-scan은 안쪽이 이미 list-file일 때만 트리거된다. 안쪽이 인덱스 선택을 가진 힙 스캔이라면, 옵티마이저가 sort/hash-build sub-block을 앞에 붙일 수 있겠지만 현재는 그렇게 하지 않는다. nested-loop-with-index 와 머터리얼라이즈드-해시-후-probe 의 비용을 비교하는 모델이 필요하기 때문이다. 이는 planner 쪽 갭이며, scan manager는 단지 그 갭이 외부에 드러나도록 표면화시킬 뿐이다.
소비된 코드 경로:
src/query/scan_manager.h— SCAN_ID, SCAN_TYPE, 서브-타입 struct, 공개 진입점 선언.src/query/scan_manager.c— 모든 디스패치 switch, 종류별scan_open_*/scan_next_*/scan_end_*/scan_close_*, ISS / LISS / MRO / covering-index 헬퍼, hash-list-scan build/probe, single-fetch 래퍼, 스캔 위치 save/jump, OID 버퍼 풀, EXPLAIN 통계 출력.src/query/set_scan.c,src/query/set_scan.h—qproc_next_set_scan.src/query/scan_json_table.cpp,src/query/scan_json_table.hpp—cubscan::json_table::scanner.src/query/show_scan.c,src/query/show_scan.h—SHOW_REQUEST테이블,showstmt_*라이프사이클,showstmt_array_*.src/query/dblink_scan.c,src/query/dblink_scan.h— 외부 데이터를 위한 CCI 브리지.src/query/query_hash_scan.c,src/query/query_hash_scan.h—HASH_LIST_SCAN,qdata_*hscan*,fhs_*extendible-hash 프리미티브.src/query/parallel/px_heap_scan/px_heap_scan.hpp,px_heap_scan.cppparallel_heap_scan::manager템플릿과scan_*_parallel_heap_scanextern-C 진입점들.- 컨텍스트로 참고한 교차 참조:
src/query/query_executor.c(qexec_open_scan,qexec_intprt_fnc),src/storage/heap_file.c(heap_next/heap_scancache_*/heap_scanrange_*패밀리),src/storage/btree.c(btree_keyval_search,btree_range_search),src/query/list_file.c(qfile_open_list_scan,qfile_scan_list_next).
인용된 이론적 레퍼런스:
- Selinger, P. G., Astrahan, M. M., Chamberlin, D. D., Lorie, R. A., & Price, T. G. (1979). Access Path Selection in a Relational Database Management System. Proc. ACM SIGMOD. 옵티마이저/executor 분리와 균일한 액세스 경로 디스패치의 시발점.
- Graefe, G. (1993). Query Evaluation Techniques for Large Databases. ACM Computing Surveys 25(2). iterator 모델과 액세스 메서드 기법의 포괄적 개관. iterator 챕터가 open/next/close 프로토콜의 텍스트북 앵커.
- Graefe, G. (1994). Volcano — An Extensible and Parallel Query
Evaluation System. IEEE TKDE 6(1). Volcano의 정본(canonical)
논문. CUBRID의
scan_next_scan라이프사이클은 그 직계 후예. - Hellerstein, J. M., & Stonebraker, M. (Eds.) (2005). Anatomy of a Database System. In Readings in Database Systems (Red Book), 4판, 4장 Access Methods. 계층화된 DBMS 모델과 액세스 메서드 카탈로그 개념.
- Petrov, A. (2019). Database Internals: A Deep Dive into How Distributed
Data Systems Work. O’Reilly. 1장 Introduction and Overview(파일
조직), 2-3장(covering / skip / loose 스캔을 포함한 B-tree 변형).
로컬 캡처:
knowledge/research/dbms-general/database-internals.md. - Silberschatz, A., Korth, H. F., & Sudarshan, S. (2019). Database
System Concepts, 7판. McGraw-Hill. Query Processing 챕터의 액세스
경로; 다중 테이블 조인 챕터의 hash join build/probe. 로컬 캡처:
knowledge/research/dbms-general/database-system-concepts.md.