(KO) CUBRID 질의 평가기 — PRED_EXPR 순회, regu_variable fetch, 그리고 행 단위 필터 엔진
목차
- 학술적 배경
- DBMS 공통 설계 패턴 (Common DBMS Design)
- CUBRID의 구현
- 소스 코드 가이드
- 소스 검증 (2026-05-01 기준)
- CUBRID 너머 — 비교 설계와 연구 동향 (Beyond CUBRID — Comparative Designs & Research Frontiers)
- 출처
학술적 배경
섹션 제목: “학술적 배경”질의 평가기(query evaluator)란 한 행과 술어를 받아 불리언 판정으로,
한 컬럼 참조를 받아 값으로 바꿔 주는 런타임 하위 시스템을 가리킨다.
무엇 을 읽을 것인지는 실행기가 정한다. 그렇게 읽어 온 결과를
보존할 가치가 있는지, 그리고 식 안에서 참조된 모든 표현이 어떤
DB_VALUE로 풀려야 하는지를 정하는 쪽이 평가기다. 잘 만든 평가기는
서로 직교하는 네 가지 관심사를 동시에 다룬다. 술어 트리 순회,
표현식 해석, 3치 논리, 그리고 행마다 값을 언제 복사할지 와 언제
포인터만 공유할지 를 결정하는 머터리얼라이즈(materialise) 규율이
그 넷이다.
Goetz Graefe의 Query Evaluation Techniques for Large Databases (ACM
Computing Surveys 25(2), 1993)가 교과서적 분류를 제시한다. 술어 평가는
표현식 해석의 특수 케이스로 다뤄진다. 즉 표현식이 세 가지 불리언 값
중 하나를 돌려주는 형태이며, 정석적인 구현은 노드 종류마다 메서드
하나씩을 가지는 작은 트리 워킹 인터프리터다. Graefe는 어떤 엔진이든
답해야 하는 세 부속 문제도 짚는다. AND / OR을 어떻게 단락 평가
(short-circuit)해 불필요한 부분 트리를 평가하지 않을 것인가, UNKNOWN
을 매 리프에서 bool 승격(promotion) 비용 없이 어떻게 전파할 것인가,
같은 모양의 술어가 수백만 행에 걸쳐 반복될 때 튜플 단위 해석 비용을
어떻게 분산시킬 것인가. 이 세 가지다.
3치 논리(three-valued logic)가 바닥을 이룬다. ANSI SQL은 WHERE
술어에서의 NULL 전파를 Codd의 익히 알려진 진리표로 정의한다.
A | B | A AND B | A OR B | NOT A |
|---|---|---|---|---|
| T | T | T | T | F |
| T | F | F | T | F |
| T | U | U | T | F |
| F | F | F | F | T |
| F | U | F | U | T |
| U | U | U | U | U |
여기서 드러나는 드라이버 차원의 결과는, 단락 평가 규칙 이 2치 논리와
3치 논리 사이에서 다르다는 점이다. 2치 AND에서는 첫 false가 평가를
종료시키지만, 3치 AND에서는 false만 종료를 일으킨다. unknown은
그렇지 않다는 점이다. 거기에 더해 WHERE 필터는 unknown을 false
로 접어 버린다(Codd의 함의: 술어가 true로 평가될 때만 튜플이 통과된다).
다른 컨텍스트들 — CHECK 제약, 조인 조건, WHEN 절 — 은 세 값을
다르게 처리한다. 따라서 컨텍스트마다 평가기에게 어떤 collapse 규칙을
쓸지 알려 줘야 한다. CUBRID는 그 collapse 정책을
QPROC_QUALIFICATION enum(QPROC_QUALIFIED / QPROC_NOT_QUALIFIED /
QPROC_QUALIFIED_OR_NOT)에 인코딩하고, update_logical_result로 그
값을 흘려 보낸다.
표현식 해석이 두 번째 축이다. Database Internals (Petrov, 12장)는
이를 태그 유니언 워커(tagged-union walker)로 그린다. 한 노드는 opcode
하나와 작은 피연산자 포인터 집합을 들고 있다. 워커는 opcode를 읽고
피연산자로 재귀해 들어가서, 재귀로 계산된 하위 값들에 연산자를
적용한다. 고전적 구현 비용은 행 하나당 opcode 하나당 가상 호출 1회
(또는 switch 점프 1회)다. 따라서 핫 루프(hot loop)는 두 최적화가
서로 경합하는 단위가 된다. 첫째는 연산자 특수화(operator
specialisation) 다. 흔한 형태에 맞춘 전용 루틴을 컴파일해 두고
디스패치를 건너뛴 채 그 루틴을 직접 호출하는 방식이다. 둘째는
JIT 컴파일 이다. 트리 워크 전체를 인라인으로 풀어내는 기계어를
조립해 내는 방식이다. PostgreSQL은 전자(특수화된 fast_qual)에서
후자(LLVM 기반 ExecCompileExpr)로 옮겨 갔다. CUBRID는 전자만
한다. eval_fnc가 PRED_EXPR을 들여다본 뒤, 술어 형태별 평가기
함수 포인터를 돌려준다(이진 등치 비교에는 eval_pred_comp0, LIKE에는
eval_pred_like6 등). 어떤 술어가 너무 합성적이라 특수화로 환원되지
않으면, 완전 재귀 워커인 eval_pred가 폴백(fallback)으로 남아 있다.
세 번째 축은 표현식 피연산자에 대한 레지스터 추상화(register
abstraction) 다. 옵티마이저가 만든 추상 구문 트리에는 리터럴, 속성
참조, 부질의(sub-query) 참조, 산술식이 한꺼번에 섞여 있다. 그래서
런타임이 이들을 한 필드 안에 담아 워커가 굴릴 수 있도록, 모두에게
공통이 되는 포인터 형 하나가 필요하다. Volcano 계열 엔진은 이를
regu variable (regulator variable)이라 부른다. 인스턴스마다 작은
type 태그와 종류별 페이로드(payload)를 담는 union을 들고 있다.
regu variable을 DB_VALUE로 풀어 주는 일은 디스패처 하나(CUBRID의
fetch_peek_dbval)의 몫이다. 이 디스패처가 태그로 분기해 종류별
경로에 위임한다. 술어 워커가 자기 리프의 출신을 몰라도 된다는 점이
이 추상화의 가치다 — lhs가 힙(heap) 거주 컬럼이든, 호스트 파라미터든,
부질의 결과든 같은 비교 루틴이 동일하게 동작한다.
마지막 축은 fetch 시점의 메모리 규율 이다. 행마다 드는 작업이
수백만 행만큼 곱해지므로, 피할 수 있는 모든 복사가 테이블 전체에
걸쳐 분산된다는 점이다. 정석적 최적화는 peek 시맨틱이다 — 페치 측이
새 DB_VALUE로 복사하는 대신, 영속적인(persistent) 버퍼 안의 포인터를
돌려준다. CUBRID는 peek를 기본으로 쓴다(fetch_peek_dbval,
fetch_val_list (..., peek=PEEK)). 복사 짝꿍인 fetch_copy_dbval도
존재한다. 페이지 래치(page latch)가 풀린 뒤에도 값이 살아 있어야
하는 경우(정렬 키, group-by 누산기, 부질의 결과 바인딩 등)에 쓰인다.
Petrov는 이를 모든 데이터베이스 이터레이터(iterator)가 답해야 하는
고전적 소유권 질문으로 짚는다. 누가, 언제 그 값을 비우는가? 가
그것이다.
DBMS 공통 설계 패턴 (Common DBMS Design)
섹션 제목: “DBMS 공통 설계 패턴 (Common DBMS Design)”런타임에 술어와 표현식을 해석하는 모든 엔진 — Postgres, MySQL, SQL Server, CUBRID — 은 같은 네 부속 문제를 세 개의 반복 패턴으로 풀어낸다는 점이다.
디스패치 enum을 동반한 술어 노드 형
섹션 제목: “디스패치 enum을 동반한 술어 노드 형”플랜 트리는 술어를 태그 유니언 노드로 보유하고, 평가기는 그 태그로
분기한다. Postgres는 Expr에 NodeTag(T_OpExpr, T_BoolExpr,
T_NullTest, …)를 둔다. ExecQual이 qual 리스트를 훑고, 각각에
ExecEvalExpr를 호출한 뒤 결과를 AND로 묶는다. MySQL은 가상 메서드
(val_int, val_str, val_real)를 갖는 Item 트리를 만들고,
연산자마다 적절한 val_*을 오버라이드(override)한다. SQL Server의
expression service는 스택 머신(stack machine)을 컴파일한다. 노드마다
작은 프로그램이 되고, 평가기가 행마다 그 프로그램을 해석한다.
CUBRID의 태그 enum은 TYPE_PRED_EXPR이다.
// TYPE_PRED_EXPR — src/xasl/xasl_predicate.hpptypedef enum{ T_PRED = 1, // Boolean operator (AND / OR / XOR / IS / IS NOT) T_EVAL_TERM, // Comparison or membership leaf T_NOT_TERM // Logical negation} TYPE_PRED_EXPR;T_PRED 노드는 BOOL_OP(B_AND / B_OR / B_XOR / B_IS /
B_IS_NOT)를 들고 다닌다. T_EVAL_TERM 노드는 TYPE_EVAL_TERM
판별자(T_COMP_EVAL_TERM, T_ALSM_EVAL_TERM, T_LIKE_EVAL_TERM,
T_RLIKE_EVAL_TERM)를 갖는다. T_NOT_TERM은 자식 pred_expr * 하나를
감싸는 얇은 래퍼(wrapper)다. 이 형태는 교과서가 그리는 재귀 하강
(recursive-descent) 술어 평가기가 그대로 읽어 낼 수 있는 모양이다.
표현식 리프를 위한 regu variable
섹션 제목: “표현식 리프를 위한 regu variable”형태 균일한(shape-uniform) 표현식 노드는 어디에나 있다. Postgres는
공통 헤더를 가진 Expr을 쓴다. 실제 하위 형(Const, Var, OpExpr,
FuncExpr)은 NodeTag 뒤에 가려져 있다. MySQL의 Item은 클래스
계층이며 리프마다 값 접근자를 오버라이드한다. CUBRID는 명시적
REGU_DATATYPE 태그와 페이로드 포인터들의 union을 가진 고정 크기
C 구조체를 택했다 — 평탄한(flat) 표현이다. 이렇게 고른 이유는 XASL
트리가 클라이언트/서버 경계를 가로질러 직렬화되기 때문이다.
평탄한 구조체는 xasl_to_stream / stream_to_xasl로 패킹·언패킹하기
간단하다. CUBRID 식대로는 와이어(wire)를 가로지르는 가상 디스패치를
지원할 수 없으므로 union-tag 형식이 유일한 선택지였다.
regu variable이 같은 워커로 모든 종류의 피연산자를 다룰 수 있게 하는 장치다. 비교 텀, 산술식, 함수 호출, 부질의 참조가 모두 fetcher 하나로 풀린다. 술어 평가기 자체는 피연산자 종류로 분기하지 않는다.
모든 계층을 관통하는 3치 반환
섹션 제목: “모든 계층을 관통하는 3치 반환”각 계층은 {true, false, unknown, error} 중 하나를 돌려줘야 한다.
Postgres는 bool 결과와 별도의 *isNull 출력 파라미터를 써서
unknown을 표현한다. MySQL은 Item::null_value를 val_*의 부수
효과로 세팅한다. CUBRID는 DB_LOGICAL이라는 단일 4치 enum을 쓴다.
V_TRUE, V_FALSE, V_UNKNOWN, V_ERROR를 서로 다른 값으로
인코딩한다. 장점은 모든 계층이 한 반환 경로로 네 결과 모두를 표현할
수 있다는 점이다. 단점은 워커가 모든 재귀 지점에서 unknown-from-NULL
과 error-from-failure를 구분해야 한다는 점이다. 짧은 도우미 두
개 — eval_negative(NOT용)와 eval_logical_result(부분 결과 결합용) —
가 3치 진리표를 코드 형태로 담고 있으며 파일 곳곳에서 재사용된다.
흔한 케이스는 특수화하고, 나머지는 재귀
섹션 제목: “흔한 케이스는 특수화하고, 나머지는 재귀”연산자 특수화 패턴은 이렇다. 술어 트리 분석 시점에, 엔진이 술어
형태를 들여다보고 노드별 디스패치를 건너뛰는 특수 평가기를
선택한다. CUBRID에서 이 일을 하는 함수가 eval_fnc다. PR_EVAL_FNC
함수 포인터를 돌려 준다 — eval_pred_comp0은 NULL이나 LIST_ID
복잡성이 없는 이진 비교용, eval_pred_comp1은 IS NULL,
eval_pred_comp2는 EXISTS, eval_pred_comp3는 한쪽이 list-file
참조인 비교, eval_pred_alsm4 / eval_pred_alsm5는 집합 또는
리스트에 대한 ANY / ALL, eval_pred_like6은 LIKE,
eval_pred_rlike7은 RLIKE다. 술어가 단일 특수 형태로 환원되지 않는
불리언 합성일 때 폴백으로 떨어지는 것이 완전판 eval_pred다.
재귀적 AND/OR/XOR/NOT 워커가 거기에 산다. query_evaluator.h의
SCAN_PRED::pr_eval_fnc 필드는 scan_manager가 스캔별로 캐시해
두는 곳이다. 그래야 행마다 드는 비용이 단지 간접 호출 1회로 끝나고,
루트에서의 반복적 switch로 부풀지 않는다.
CUBRID는 어디에 있는가
섹션 제목: “CUBRID는 어디에 있는가”CUBRID는 해석형(interpreted), JIT가 없는 엔진이다. 워커는 C 함수
eval_pred다. 리프 계산은 함수 포인터 fetch_peek_dbval이다. 특수화
패스(specialisation pass)는 eval_fnc다. 배치(batch) / 벡터화
(vectorised) 경로는 없다 — 리프 하나가 DB_VALUE 하나, 비교 한
번이 행 한 줄이다. LLVM 이후의 Postgres와 비교하면 CUBRID는 JIT
가속을 포기한 셈이다. MySQL의 가상 메서드 오버헤드와 비교하면
CUBRID는 평탄한 구조체가 주는 지역성(locality)을 얻은 셈이다.
공학적 거래는 단순하다. 이해하기 쉽고, 직렬화하기 쉬우며,
스캔 위주 워크로드에서 긴 술어 체인을 만나면 느리다는 점이다.
| 이론 개념 | CUBRID 심볼 |
|---|---|
| 술어 트리 노드 | cubxasl::pred_expr (PRED_EXPR) |
| 불리언 연산자 opcode | BOOL_OP (B_AND / B_OR / B_XOR / B_IS / B_IS_NOT) |
| eval-term 하위 형 | TYPE_EVAL_TERM (T_COMP_EVAL_TERM / T_ALSM_EVAL_TERM / T_LIKE_* / T_RLIKE_*) |
| 3치 결과 | DB_LOGICAL (V_TRUE / V_FALSE / V_UNKNOWN / V_ERROR) |
| 재귀 워커 | eval_pred (query_evaluator.c) |
| 술어 부정 | eval_negative (query_evaluator.c) |
| 3치 결합 | eval_logical_result (query_evaluator.c) |
| 특수화 컴파일러 | PR_EVAL_FNC를 돌려 주는 eval_fnc |
| 이진 등치 특수 경로 | eval_pred_comp0 |
| IS NULL 특수 경로 | eval_pred_comp1 |
| EXISTS 특수 경로 | eval_pred_comp2 |
| list-id 비교 특수 경로 | eval_pred_comp3 |
| ANY/ALL 특수 경로 | eval_pred_alsm4(set), eval_pred_alsm5(list) |
| LIKE / RLIKE 특수 경로 | eval_pred_like6, eval_pred_rlike7 |
| Regu variable | regu_variable_node (REGU_VARIABLE) (regu_var.hpp) |
| Regu 종류 태그 | REGU_DATATYPE (TYPE_DBVAL / TYPE_CONSTANT / TYPE_INARITH / TYPE_ATTR_ID / …) |
| Peek-fetcher 디스패처 | fetch_peek_dbval (fetch.c) |
| Copy-fetcher | fetch_copy_dbval (fetch.c) |
| 행 단위 regu 리스트 머터리얼라이즈 | fetch_val_list (fetch.c) |
| 산술 표현식 평가기 | fetch_peek_arith (fetch.c) |
| 함수 호출 디스패처 | qdata_evaluate_function (query_opfunc.c) |
| scan-manager 측 필터 브리지 | eval_data_filter / eval_key_filter (query_evaluator.c) |
| 3치 자격(qualification) 정책 | QPROC_QUALIFICATION enum (query_evaluator.h) |
| 자격 갱신 | update_logical_result (query_evaluator.c) |
CUBRID의 구현
섹션 제목: “CUBRID의 구현”평가기는 네 가지 일을 한다. 그 외의 모든 것은 그 넷 중 하나의 특수
케이스라고 보면 된다. (1) PRED_EXPR 트리를 3치 논리 아래서 순회하며
V_TRUE / V_FALSE / V_UNKNOWN을 돌려준다. (2) 모든 리프에서 다형
fetcher인 fetch_peek_dbval으로 피연산자 DB_VALUE를 끌어온다.
(3) 술어 형태가 허락하는 곳에서는 재귀를 건너뛰고, 스캔 오픈 시점에
eval_fnc가 골라 둔 단일 형태 특수 평가기를 직접 호출한다. (4)
스캔 매니저와의 인터페이스를 eval_data_filter / eval_key_filter
경계로 가져간다. 이 둘이 실행기의 나머지 부분이 호출하는 입구가
된다. 이 절의 나머지는 한 술어가 실제로 거치는 순서대로 각 부분을
풀어 본다 — eval_pred를 위에서 아래로, 그리고 regu variable을
거쳐 fetch_peek_dbval로 내려가며, 함수 호출 리프에 닿으면
qdata_evaluate_function으로 옆걸음치는 식이다.
PRED_EXPR 트리 형태
섹션 제목: “PRED_EXPR 트리 형태”PRED_EXPR은 세 가지 노드 종류를 묶은 태그 유니언이며, eval-term
변종들의 작은 동물원 위에 얹혀 있다.
flowchart TB
subgraph PE["pred_expr (TYPE_PRED_EXPR tag)"]
direction LR
P_PRED["T_PRED<br/>{ lhs, rhs, bool_op }<br/>BOOL_OP ∈<br/>{B_AND, B_OR, B_XOR,<br/>B_IS, B_IS_NOT}"]
P_EVAL["T_EVAL_TERM<br/>{ et_type, et union }"]
P_NOT["T_NOT_TERM<br/>{ not_term: pred_expr* }"]
end
subgraph ET["eval_term variants (TYPE_EVAL_TERM tag)"]
direction LR
ETC["T_COMP_EVAL_TERM<br/>{ lhs, rhs : regu_variable*<br/> rel_op : REL_OP<br/> type : DB_TYPE }"]
ETA["T_ALSM_EVAL_TERM<br/>{ elem, elemset : regu_variable*<br/> eq_flag : F_ALL or F_SOME<br/> rel_op : REL_OP }"]
ETL["T_LIKE_EVAL_TERM<br/>{ src, pattern, esc_char :<br/> regu_variable* }"]
ETR["T_RLIKE_EVAL_TERM<br/>{ src, pattern, case_sensitive,<br/> compiled_regex }"]
end
P_EVAL --> ETC
P_EVAL --> ETA
P_EVAL --> ETL
P_EVAL --> ETR
P_PRED -. recurse .-> PE
P_NOT -. recurse .-> PE
이 모양이 본 장의 핵심을 떠받치는 다이어그램이다 — eval_pred는
바로 이 그래프 위의 재귀 워커다. T_PRED는 불리언 결합자다.
bool_op이 워커에게 자식을 AND-fold할지 OR-fold할지 알려 준다.
T_NOT_TERM은 하위 결과를 eval_negative로 뒤집는다. T_EVAL_TERM은
리프다. 리프의 et_type이 어떤 비교/매치 함수가 적용될지를 결정한다.
네 가지 eval-term 변종은 SQL이 리프에서 인정하는 네 가지 베이스 술어
형태를 모두 덮는다 — 비교(=, <, …), all/some 한정(> ANY,
= ALL), LIKE 패턴 매치, 정규식 매치(RLIKE / REGEXP)다.
REL_OP은 T_COMP_EVAL_TERM에서 쓰이는 비교 어휘다. 전체 집합은
순서 비교(R_EQ, R_NE, R_LT, R_LE, R_GT, R_GE)와 집합 비교
(R_SUBSET, R_SUBSETEQ, R_SUPERSET, R_SUPERSETEQ), 특수
단항 검사(R_NULL은 IS NULL, R_EXISTS는 EXISTS), 한정 비교
(R_EQ_SOME, R_LT_ALL, …), 전순서 비교(R_EQ_TORDER), null-safe
등치(R_NULLSAFE_EQ)를 모두 인코딩한다. 드라이버는 이 태그로
eval_value_rel_cmp 안의 특수 경로를 고르며, 한정형 *_SOME /
*_ALL 코드는 등가 베이스 op로 번역돼 eval_some_eval /
eval_all_eval로 디스패치된다.
3치 결과와 AND/OR 워커
섹션 제목: “3치 결과와 AND/OR 워커”eval_pred는 PRED_EXPR 트리에 대한 재귀 하강이며, V_TRUE,
V_FALSE, V_UNKNOWN, V_ERROR 중 하나를 돌려준다. 이 워커는 두
가지 구조적 최적화를 한다. (a) 결과가 정해지는 즉시 AND/OR을 단락
평가하고, (b) 우선형(right-linear) 체인을 재귀가 아니라 반복으로
훑는다. 이는 XASL 생성기가 만드는 편향을 그대로 반영한 것이다
(pt_to_pred_expr은 우선형 AND/OR 체인을 내보내므로, 워커가 긴
연언(conjunction)에서 스택을 폭파시키지 않고 rhs로 반복할 수 있다).
flowchart TD
Start["eval_pred (pr)"] --> Type{"pr->type"}
Type -->|T_PRED| Bool{"bool_op"}
Type -->|T_EVAL_TERM| Et{"et_type"}
Type -->|T_NOT_TERM| Not["eval_pred (not_term)<br/>then eval_negative"]
Bool -->|B_AND| AndLoop["result = V_TRUE<br/>walk right-linear<br/>chain on .rhs<br/>break on V_FALSE / V_ERROR"]
Bool -->|B_OR| OrLoop["result = V_FALSE<br/>walk right-linear<br/>chain on .rhs<br/>break on V_TRUE / V_ERROR"]
Bool -->|B_XOR| Xor["eval lhs, eval rhs<br/>UNKNOWN if either UNKNOWN<br/>else lhs != rhs"]
Bool -->|B_IS / B_IS_NOT| Is["eval lhs, eval rhs<br/>compare 3-valued results"]
Et -->|T_COMP_EVAL_TERM| Cmp["fetch lhs, rhs<br/>NULL → V_UNKNOWN<br/>else eval_value_rel_cmp"]
Et -->|T_ALSM_EVAL_TERM| Alsm["fetch elem, elemset<br/>route to eval_some_∗<br/>or eval_all_∗ by F_ALL/F_SOME"]
Et -->|T_LIKE_EVAL_TERM| Like["fetch src, pattern, esc<br/>db_string_like"]
Et -->|T_RLIKE_EVAL_TERM| Rlike["delegate to<br/>eval_pred_rlike7"]
B_AND 가지가 우선형 반복의 전형이다.
// eval_pred — src/query/query_evaluator.c (B_AND, condensed)case B_AND: /* 'pt_to_pred_expr()' will generate right-linear tree */ result = V_TRUE; for (t_pr = pr; result == V_TRUE && t_pr->type == T_PRED && t_pr->pe.m_pred.bool_op == B_AND; t_pr = t_pr->pe.m_pred.rhs) { if (result == V_UNKNOWN) { result = eval_pred (thread_p, t_pr->pe.m_pred.lhs, vd, obj_oid); result = (result == V_TRUE) ? V_UNKNOWN : result; } else { result = eval_pred (thread_p, t_pr->pe.m_pred.lhs, vd, obj_oid); } if (result == V_FALSE || result == V_ERROR) goto exit; } /* tail evaluation for the final right child */ if (result == V_UNKNOWN) { result = eval_pred (thread_p, t_pr, vd, obj_oid); result = (result == V_TRUE) ? V_UNKNOWN : result; } else { result = eval_pred (thread_p, t_pr, vd, obj_oid); } break;이 루프에서 읽어 낼 것은 셋이다. 첫째, for가 .rhs로 진행하지
스택 재귀로 진행하지 않는다는 점이다 — 호출자는 연언 전체를 스택
프레임 한 개로 처리한다. 둘째, result == V_UNKNOWN 가지가 3치
규율을 명시적으로 코드화한다는 점이다 — 일단 UNKNOWN이 끈끈해
지면, 뒤따르는 연언 항이 V_TRUE로 평가돼도 전체 AND를 다시 true로
승격시키지 못한다(결과는 V_UNKNOWN이지 V_TRUE가 아니다). 다만
V_FALSE는 여전히 모든 것을 이긴다. 셋째, V_FALSE에서의
goto exit이 단락 평가다. 엄격한 좌→우 평가기였다면 체인의 나머지를
모두 훑었을 것이다. B_OR 가지는 같은 패턴을 센티넬(sentinel)만
뒤집어 둔 형태다. B_XOR과 B_IS / B_IS_NOT은 결합형 체인이
되지 못하고 의미상 두 자식만 가진다는 점에서 짧고, 우선형이 아니다.
eval_pred 입구의 재귀 깊이 가드(들어갈 때 증가, 나갈 때 감소)는
병적으로 깊은 술어 트리에 대한 보호 장치다.
thread_get_recursion_depth (thread_p) > prm_get_integer_value (PRM_ID_MAX_RECURSION_SQL_DEPTH)가 되면 워커가 ER_MAX_RECURSION_SQL_DEPTH
로 빠져 나간다.
REGU_VARIABLE 다형성
섹션 제목: “REGU_VARIABLE 다형성”regu variable은 표현식 피연산자의 고정 크기 디스크립터(descriptor)
다. 술어 워커가 읽어 와야 하는 종류들을 통합한다. 형태는
REGU_DATATYPE 태그 하나, 플래그 워드 하나, 두 개의 도메인 포인터
(현재와 원본 — XASL 클론(clone)과 관련 있음), 선택적 fetch-into 슬롯
(vfetch_to), 그리고 페이로드의 판별 union 하나로 구성된다.
flowchart TB
subgraph RV["regu_variable_node"]
T["type : REGU_DATATYPE"]
F["flags<br/>(REGU_VARIABLE_HIDDEN_COLUMN,<br/>FETCH_ALL_CONST, FETCH_NOT_CONST,<br/>APPLY_COLLATION, ANALYTIC_WINDOW, …)"]
D["domain : TP_DOMAIN*<br/>original_domain : TP_DOMAIN*"]
VF["vfetch_to : DB_VALUE*<br/>(slot to share-fetch into, when used as a list element)"]
XL["xasl : xasl_node*<br/>(linked sub-query, if any)"]
U["value : union (one of)"]
end
subgraph UN["union value (selected by type)"]
direction TB
DBV["DB_VALUE dbval<br/>(TYPE_DBVAL — embedded literal)"]
DPV["DB_VALUE *dbvalptr<br/>(TYPE_CONSTANT, TYPE_ORDERBY_NUM —<br/>pointer into the sub-query result slot)"]
AR["ARITH_TYPE *arithptr<br/>(TYPE_INARITH, TYPE_OUTARITH —<br/>arithmetic / general expression)"]
AT["ATTR_DESCR attr_descr<br/>(TYPE_ATTR_ID, TYPE_CLASS_ATTR_ID, TYPE_SHARED_ATTR_ID —<br/>attribute fetch through HEAP_CACHE_ATTRINFO)"]
PD["QFILE_TUPLE_VALUE_POSITION pos_descr<br/>(TYPE_POSITION — list-file column read)"]
SR["QFILE_SORTED_LIST_ID *srlist_id<br/>(TYPE_LIST_ID — sub-query list ref)"]
VP["int val_pos<br/>(TYPE_POS_VALUE — host-variable index)"]
FN["function_node *funcp<br/>(TYPE_FUNC — function call)"]
RVL["REGU_VALUE_LIST *reguval_list<br/>(TYPE_REGUVAL_LIST — VALUES query)"]
RVS["REGU_VARIABLE_LIST regu_var_list<br/>(TYPE_REGU_VAR_LIST — CUME_DIST / PERCENT_RANK)"]
SP["sp_node *sp_ptr<br/>(TYPE_SP — stored procedure)"]
end
U --- DBV
U --- DPV
U --- AR
U --- AT
U --- PD
U --- SR
U --- VP
U --- FN
U --- RVL
U --- RVS
U --- SP
이 설계의 이점은, 술어든 표현식이든 모든 피연산자가 — 컬럼이든,
리터럴이든, (SELECT MAX(x) FROM t)의 결과이든, a + b * 3의
결과이든, JSON_EXTRACT(j, '$.foo')의 결과이든 — REGU_VARIABLE
이다. 워커는 이건 어떤 종류지?를 묻지 않는다. 그건
fetcher의 일이고, fetcher가 그 답을 안다.
TYPE_OID와 TYPE_CLASSOID는 페이로드가 없는 경우다 — 디스패처가
현재 행의 obj_oid / class_oid 인자(regu variable과 함께 전달되는)를
읽어 공용 value.dbval 슬롯에 박아 넣으면 끝이라 페이로드가
필요 없다.
플래그는 fetcher 호출 사이를 가로질러 살아남는 정보를 담는다.
특히 REGU_VARIABLE_FETCH_ALL_CONST는 “이 regu의 모든 재귀
피연산자가 첫 fetch에서 상수였으므로, 캐시된 value.dbval이 그대로
유효하다”는 의미다. 그리고 REGU_VARIABLE_FETCH_NOT_CONST는 그
짝꿍이다. 둘 다 fetch_peek_dbval이 첫 호출에서 채워 둔다 — fetcher
가 자기 자신을 분류한다는 점이다 — 이후 호출에서 그 플래그를 읽어
일을 단축한다. 이게 CUBRID의 행 단위 상수 폴딩(constant-folding)이다.
리터럴들로 이루어진 산술식은, 술어 워커가 행마다 다시 보더라도
한 번만 평가된다.
fetch_peek_dbval — regu 디스패처
섹션 제목: “fetch_peek_dbval — regu 디스패처”fetch_peek_dbval은 regu_var->type 위의 단일 switch이며, regu
variable을 DB_VALUE *로 풀어 준다. 이 함수는 peek 형식이다 —
호출자가 free해선 안 되는 버퍼에 대한 포인터를 돌려 준다. 그
값은 같은 스캔의 다음 연산이 무효화하기 전까지 유효하다. 복사
짝꿍 fetch_copy_dbval은 fetch_peek_dbval을 호출해 결과를 받은
뒤 qdata_copy_db_value로 호출자 소유 슬롯에 복제한다.
flowchart LR
In["regu_var<br/>· vd, class_oid, obj_oid, tpl"] --> Disp{"regu_var->type"}
Disp -->|TYPE_DBVAL| RD1["FLAG ALL_CONST<br/>peek = &value.dbval"]
Disp -->|TYPE_CONSTANT| RD2["FLAG NOT_CONST<br/>EXECUTE_REGU_VARIABLE_XASL<br/>(materialise sub-query)<br/>peek = value.dbvalptr"]
Disp -->|TYPE_ATTR_ID<br/>(also CLASS_ATTR_ID, SHARED_ATTR_ID)| RD3["FLAG NOT_CONST<br/>peek = attr_descr.cache_dbvalp<br/>(else heap_attrinfo_access)"]
Disp -->|TYPE_POSITION| RD4["FLAG NOT_CONST<br/>qfile_locate_tuple_value tpl<br/>data_readval into vfetch_to"]
Disp -->|TYPE_POS_VALUE| RD5["FLAG ALL_CONST<br/>peek = vd->dbval_ptr + val_pos<br/>(host variable)"]
Disp -->|TYPE_OID / TYPE_CLASSOID| RD6["FLAG ALL_CONST<br/>db_make_oid into value.dbval"]
Disp -->|TYPE_INARITH<br/>TYPE_OUTARITH| RD7["fetch_peek_arith<br/>(recursive expression eval)"]
Disp -->|TYPE_FUNC| RD8["qdata_evaluate_function<br/>(F_SET / F_JSON_∗ / F_REGEXP_∗ / …)"]
Disp -->|TYPE_SP| RD9["pl::executor.execute<br/>(stored-procedure call)"]
Disp -->|TYPE_REGUVAL_LIST| RDB["recurse on reguval_list<br/>->current_value->value"]
Disp -->|TYPE_ORDERBY_NUM| RDA["FLAG NOT_CONST<br/>peek = value.dbvalptr"]
RD3 --> Cache["cache_dbvalp pointer cached<br/>on first read; subsequent rows<br/>read directly without HEAP lookup"]
RD2 --> SqCache["XASL_USES_SQ_CACHE:<br/>sq_get keyed by aptr correlation"]
경로별 요약은 다이어그램과 짝을 이룬다. 각 경로가 떠맡는 책임은 이렇다.
- TYPE_DBVAL — 임베디드 리터럴.
®u_var->value.dbval을 돌려 준다.FETCH_ALL_CONST를 세트한다. 가장 싼 케이스다 — 첫 플래그 세팅 이후엔 일이 0이다. - TYPE_CONSTANT — aptr-result 안 포인터.
dbvalptr은 부모의 사전 실행이 머터리얼라이즈된 aptr의 튜플 안을 가리키도록 미리 바인딩해 둔 것이다. 첫 읽기에서는 연결된 부질의를 실행해야 할 수도 있다(EXECUTE_REGU_VARIABLE_XASL). 이후 행은 캐시된 포인터를 그냥 읽는다.XASL_USES_SQ_CACHE가 세트돼 있으면 부질의 캐시인sq_get이 단축 경로를 만든다. - TYPE_ATTR_ID (그리고
TYPE_CLASS_ATTR_ID,TYPE_SHARED_ATTR_ID) — 속성 fetch.HEAP_CACHE_ATTRINFO(힙 매니저가heap_next/heap_get_visible_version에서 채워 두는, 클래스별 디코더 캐시) 를 거쳐 읽는다. 첫 호출은regu_var->value.attr_descr.id를 캐시를heap_attrinfo_access로 풀어 낸다. 결과DB_VALUE *는attr_descr.cache_dbvalp에 저장돼 후속 행에서는 조회를 생략한다.FETCH_NOT_CONST를 세트한다(정의상 행마다 값이 바뀐다). - TYPE_POSITION — list-file 튜플 컬럼. 현재 행은
QFILE_TUPLE이며,qfile_locate_tuple_value (tpl, pos_descr.pos_no, ...)가 튜플 안 컬럼 슬롯을 찾고pr_type->data_readval이 그 값을 디코드해regu_var->vfetch_to에 둔다. 디코드된 값은 행 사이에 공유될 수 없으므로 CUBRID는 항상 regu별vfetch_to슬롯을 미리 잡아 둔다. - TYPE_POS_VALUE — 호스트 변수.
vd->dbval_ptr[val_pos]다. 질의 동안 상수다. 순수 포인터 산술이다. - TYPE_OID / TYPE_CLASSOID — 현재 OID.
obj_oid/class_oid인자가regu_var->value.dbval로 들어간다. 엄밀히 상수는 아니지만(행마다 변한다),FETCH_ALL_CONST가 여전히 세트된다. 상수성이 의미를 가질 만한 어떤 피연산자에서도 파생되지 않기 때문이다. - TYPE_INARITH / TYPE_OUTARITH — 산술 / 일반 표현식.
fetch_peek_arith로 위임한다.arithptr->opcode위의 1500줄 짜리 switch이며, 모든 SQL 연산자(T_ADD,T_SUBSTRING,T_CASE,T_DECODE,T_TRIM,T_RAND,T_TO_CHAR, …)를 다룬다. 재귀 구조는 술어 워커와 같다.leftptr,rightptr, 필요하면thirdptr/fourthptr을 fetch한 뒤, opcode별 로직을arithptr->value에 적용한다. 상수성은 상향식으로 계산된다 — 산술 노드가ALL_CONST인 것은 모든 하위 regu가 그러할 때다. 부수 효과를 가진 opcode들(T_RAND,T_SLEEP,T_NEXT_VALUE,T_INCR,T_CASE/T_DECODE/T_IF/T_PREDICATE,T_SYS_GUID,T_ROW_COUNT,T_LAST_INSERT_ID,T_EVALUATE_VARIABLE)은 피연산자 상수성과 무관하게NOT_CONST를 강제한다. 이들은 행 단위 캐시의 이득을 받지 못한다. - TYPE_FUNC — 함수 호출.
FETCH_ALL_CONST가 이미 세트돼 있으면 미리 계산된funcp->value가 재평가 없이 반환된다. 그렇지 않으면qdata_evaluate_function(query_opfunc.c)이funcp->ftype(FUNC_CODEenum) 위에서 디스패치하며, F_-루틴 중 하나로 분기한다 —qdata_convert_dbvals_to_set은F_SET/F_MULTISET/F_SEQUENCE용,qdata_insert_substring_function은F_INSERT_SUBSTRING용,qdata_elt는F_ELT용,qdata_regexp_function은 정규식 패밀리, JSON 패밀리는 모두qdata_convert_operands_to_value_and_call로 가며 인자로db_evaluate_json_*함수 포인터가 넘어간다. 호출이 끝나면funcp->operand체인을 훑어 상수성 플래그를 다시 계산한다. - TYPE_SP — 저장 프로시저(stored procedure) 호출.
cubpl::executor를 짓고,executor.fetch_args_peek으로 인자를 가져온 뒤,executor.execute (*regu_var->value.sp_ptr->value)를 돌린다. 항상NOT_CONST다(저장 프로시저 본체가 호출마다 재평가 되도록fetch_force_not_const_recursive로 재귀적으로 강제됨). - TYPE_REGUVAL_LIST — VALUES 질의(다행 리터럴 테이블).
reguval_list->current_value->value로 재귀한다. 리스트는S_VALUES_SCAN드라이버가 행마다current_value를 진행시키며 순회한다. - TYPE_ORDERBY_NUM —
ORDER BY n참조. fetch 관점에서는TYPE_CONSTANT처럼 동작하지만, 출력 행마다 숫자 바인딩이 바뀌므로NOT_CONST다.
공통 후처리부는 REGU_VARIABLE_APPLY_COLLATION이 세트돼 있으면
콜레이션을 다시 적용하고, 도메인 정련(domain refinement)을 돌려
DB_TYPE_VARIABLE을 fetch된 값의 실제 타입을 결정한다.
TYPE_REGUVAL_LIST에 대해서는 현재 행의 도메인이 헤드 행과
일치하는지 교차 검사한다(UNION이 강제하는 동질성 규칙과 동일).
이 단계의 오류는 ER_QPROC_INCOMPATIBLE_TYPES나
ER_QSTR_INCOMPATIBLE_COLLATIONS로 드러난다.
속성 fetch 처음부터 끝까지
섹션 제목: “속성 fetch 처음부터 끝까지”TYPE_ATTR_ID 경로는 평가기가 힙(heap)을 만나는 지점이다. 핸드셰
이크는 셋으로 나뉜다. 첫째, scan_open_heap_scan이
heap_attrinfo_start를 호출해, 술어와 출력 리스트가 참조하는
컬럼들을 위한 HEAP_CACHE_ATTRINFO를 잡는다. 둘째, fetch된 모든
행에서 힙 이터레이터(heap_next /
heap_get_visible_version_internal)가 새 RECDES를
heap_attrinfo_read_dbvalues를 호출해, 참조된 각 컬럼을 캐시 안의
속성별 DB_VALUE로 디코드한다. 셋째, fetch_peek_dbval이
TYPE_ATTR_ID regu에 닿으면 heap_attrinfo_access (regu_var->value.attr_descr.id, attr_cache)를 호출하고 그 결과
포인터를 attr_descr.cache_dbvalp에 캐시한다. 그래서 같은 행 안의
다음 peek는 직접 역참조 한 번이 된다. cache_dbvalp는 행 사이에
지워지지 않는다 — 그 아래의 버퍼는 재사용되며, 그 안의 값은
heap_attrinfo_read_dbvalues가 덮어쓴다. fetcher는 힙 매니저의
행 단위 리셋(reset)을 신뢰한다.
query_evaluator.c의 eval_data_filter가 술어 평가 전에 속성
캐시가 채워졌는지를 보장하는 브리지(bridge)다.
// eval_data_filter — src/query/query_evaluator.c (condensed)DB_LOGICALeval_data_filter (THREAD_ENTRY * thread_p, OID * oid, RECDES * recdesp, HEAP_SCANCACHE * scan_cache, FILTER_INFO * filterp){ SCAN_PRED *scan_predp = filterp->scan_pred; SCAN_ATTRS *scan_attrsp = filterp->scan_attrs; DB_LOGICAL ev_res;
if (scan_attrsp != NULL && scan_attrsp->attr_cache != NULL && scan_predp->regu_list != NULL) { /* read the predicate values from the heap into the attribute cache */ if (heap_attrinfo_read_dbvalues (thread_p, oid, recdesp, scan_attrsp->attr_cache) != NO_ERROR) return V_ERROR; /* ... class-attribute scan special case ... */ }
/* evaluate the predicates of the data filter via the compiled fast path */ ev_res = V_TRUE; if (scan_predp->pr_eval_fnc && scan_predp->pred_expr) ev_res = (*scan_predp->pr_eval_fnc) (thread_p, scan_predp->pred_expr, filterp->val_descr, oid);
if (ev_res == V_TRUE && scan_predp->regu_list && filterp->val_list) { /* row passed: fetch the output regu list into val_list */ if (fetch_val_list (thread_p, scan_predp->regu_list, filterp->val_descr, filterp->class_oid, oid, NULL, PEEK) != NO_ERROR) return V_ERROR; } return ev_res;}여기서 셋을 읽어 낼 수 있다. 첫째, heap_attrinfo_read_dbvalues가
술어 전에 호출된다. 그래서 술어 안의 모든 TYPE_ATTR_ID regu가
이미 채워진 캐시 값을 만난다. 둘째, 술어가 scan_predp->pr_eval_fnc를
거친다. 그 포인터는 스캔 오픈 시점에 eval_fnc가 세팅한 것이므로,
이건 일반 eval_pred가 아니라 특수화된 경로다. 셋째, 출력
리스트(fetch_val_list (..., PEEK))는 술어가 V_TRUE를 돌려줄
때만 머터리얼라이즈된다. 탈락한 행은 사영(projection) 비용을 내지
않는다.
eval_key_filter는 인덱스 스캔용 짝꿍이다. RECDES에서 읽는 대신,
DB_MIDXKEY(다중 컬럼 B-tree 키)에서 키 컬럼들을 디코드해 속성
캐시에 자리잡게 한 뒤 같은 pr_eval_fnc를 돌린다 — 다만 obj_oid = NULL로 호출한다. 키 필터가 키에 없는 컬럼은 참조할 수 없기
때문이다.
eval_fnc — 스캔 오픈 시점의 연산자 특수화
섹션 제목: “eval_fnc — 스캔 오픈 시점의 연산자 특수화”scan_open_heap_scan이 SCAN_PRED를 초기화할 때, 행 단위 평가기를
고르기 위해 eval_fnc를 호출한다. eval_fnc는 루트 PRED_EXPR을
들여다보고, 흔한 단일 형태를 재귀 워커를 우회하는 함수
포인터를 돌려 준다.
// eval_fnc — src/query/query_evaluator.c (condensed)PR_EVAL_FNCeval_fnc (THREAD_ENTRY * thread_p, const PRED_EXPR * pr, DB_TYPE * single_node_type){ *single_node_type = DB_TYPE_NULL; if (pr == NULL) return NULL;
if (pr->type == T_EVAL_TERM) { switch (pr->pe.m_eval_term.et_type) { case T_COMP_EVAL_TERM: { const COMP_EVAL_TERM *et_comp = &pr->pe.m_eval_term.et.et_comp; *single_node_type = et_comp->type;
if (et_comp->rel_op == R_NULL) return (PR_EVAL_FNC) eval_pred_comp1; /* IS NULL */ if (et_comp->rel_op == R_EXISTS) return (PR_EVAL_FNC) eval_pred_comp2; /* EXISTS */ if (et_comp->lhs->type == TYPE_LIST_ID || et_comp->rhs->type == TYPE_LIST_ID) return (PR_EVAL_FNC) eval_pred_comp3; /* list-id cmp */ return (PR_EVAL_FNC) eval_pred_comp0; /* binary cmp */ } case T_ALSM_EVAL_TERM: { const ALSM_EVAL_TERM *et_alsm = &pr->pe.m_eval_term.et.et_alsm; *single_node_type = et_alsm->item_type; return (et_alsm->elemset->type != TYPE_LIST_ID) ? (PR_EVAL_FNC) eval_pred_alsm4 /* set-bound ALL/SOME */ : (PR_EVAL_FNC) eval_pred_alsm5; /* list-bound ALL/SOME */ } case T_LIKE_EVAL_TERM: return (PR_EVAL_FNC) eval_pred_like6; case T_RLIKE_EVAL_TERM: return (PR_EVAL_FNC) eval_pred_rlike7; } } /* general case: predicate is a Boolean composition; need full walker */ return (PR_EVAL_FNC) eval_pred;}판단은 순수 구조적이다 — 값 흐름 분석도 없고, 비용 모델도 없다.
eval_fnc는 eval_pred_comp0을 술어가 IS NULL도, EXISTS도,
list-file 피연산자도 없는 단일 이진 비교일 때만 돌려 준다. 구조적
복잡성이 어떤 모양이든 끼어들면 더 일반적인 특수 경로가 선택되며,
어떤 특수화도 들어맞지 않을 때만 eval_pred 전체가 폴백으로
떨어진다. 각 특수 경로는 자기가 아는 구조를 인라인한다 —
eval_pred_comp0은 et_type을 다시 분기하지 않는다, 이미 단순
하위 형태의 T_COMP_EVAL_TERM임을 알기 때문이다.
eval_pred_comp1은 rel_op을 검사하지 않는다, 이미 R_NULL임을
알기 때문이다. 호출 한 번당 절약되는 양은 작지만, 모든 스캔의 모든
행에 곱해진다.
ANY/ALL — list-file 워킹을 통한 집합 비교
섹션 제목: “ANY/ALL — list-file 워킹을 통한 집합 비교”eval_pred_alsm4와 eval_pred_alsm5는 한정 비교의 특수 경로다.
_alsm4는 rhs를 DB_SET으로 fetch해 eval_some_eval /
eval_all_eval(set_get_element을 통한 집합 워킹)로 디스패치한다.
_alsm5는 rhs를 EXECUTE_REGU_VARIABLE_XASL로 list-file로
머터리얼라이즈한 뒤 eval_some_list_eval / eval_all_list_eval
(qfile_scan_list_next를 통한 list-file 스캔)로 디스패치한다.
// eval_pred_alsm5 — src/query/query_evaluator.c (condensed; list-bound ANY/ALL)DB_LOGICALeval_pred_alsm5 (THREAD_ENTRY * thread_p, const PRED_EXPR * pr, val_descr * vd, OID * obj_oid){ const ALSM_EVAL_TERM *et_alsm = &pr->pe.m_eval_term.et.et_alsm;
/* materialise the inner sub-query */ EXECUTE_REGU_VARIABLE_XASL (thread_p, et_alsm->elemset, vd); if (CHECK_REGU_VARIABLE_XASL_STATUS (et_alsm->elemset) != XASL_SUCCESS) return V_ERROR;
QFILE_SORTED_LIST_ID *srlist_id = et_alsm->elemset->value.srlist_id; if (srlist_id->list_id->tuple_cnt == 0) return (et_alsm->eq_flag == F_ALL) ? V_TRUE : V_FALSE; /* empty-set ANSI rule */
DB_VALUE *peek_val1 = NULL; if (fetch_peek_dbval (thread_p, et_alsm->elem, vd, NULL, obj_oid, NULL, &peek_val1) != NO_ERROR) return V_ERROR; if (db_value_is_null (peek_val1)) return V_UNKNOWN;
return (et_alsm->eq_flag == F_ALL) ? eval_all_list_eval (thread_p, peek_val1, srlist_id->list_id, et_alsm->rel_op) : eval_some_list_eval (thread_p, peek_val1, srlist_id->list_id, et_alsm->rel_op);}빈 집합 규칙(F_ALL이 빈 집합이면 V_TRUE, F_SOME이 빈 집합이면
V_FALSE)은 ANSI 정석이며, CUBRID가 우변을 스캔하지 않고 단락 평가
하는 유일한 자리다. eval_all_list_eval / eval_some_list_eval
도우미는 qfile_scan_list_next로 list 파일을 훑는다. 각 튜플의
0번 컬럼을 DB_VALUE로 디코드해 lhs와 비교하도록
eval_value_rel_cmp에 먹인다. 워크는 결정적 답을 보면 단락 평가
한다 — some_list_eval은 첫 매치에서 V_TRUE를 돌려주고,
all_list_eval은 not some(neg-op)로 구현돼 첫 반례에서
V_FALSE를 돌려준다.
eval_value_rel_cmp — 비교 스택의 바닥
섹션 제목: “eval_value_rel_cmp — 비교 스택의 바닥”양쪽이 DB_VALUE *가 되고 나면, 모든 비교는 결국
eval_value_rel_cmp로 끝난다. 순서는 이렇다.
- 상수 강제 변환(coercion, 1회성) — rhs가
FETCH_ALL_CONSTregu 이고 도메인이 lhs와 다르면, rhs를 1회 lhs 도메인으로 강제 변환 한다. 의도는tp_value_compare_with_error안에서 같은 상수를 행 마다 다시 변환하는 일을 피하려는 것이다. 현재 활성화된 정책은 이렇다 — 수치 ↔ char(char를 double로), 날짜·시간 ↔ char(char를 날짜·시간 도메인으로), 좁은 수치 ↔ 넓은 수치(좁은 쪽을 넓은 쪽으로). lhs 측 미러는 소스에 존재하지만#if 0으로 비활성화돼 있다 — TODO로 남아 있다. tp_value_compare_with_error— 타입 인지 비교기.DB_LT/DB_EQ/DB_GT/DB_UNK을 돌려주거나comparable = false를 세트한다. 일반REL_OP에서DB_UNK은V_UNKNOWN을 트리거한다.R_NULLSAFE_EQ는 자기만의 NULL-aware 로직이 있어NULL = NULL → V_TRUE를 인정한다.- REL_OP 매핑 —
int결과가rel_operator별로V_TRUE/V_FALSE로 매핑된다.R_EQ는result == DB_EQ를 검사한다.R_LE는DB_LT또는DB_EQ를 받는다. 집합 비교는DB_SUBSET/DB_SUPERSET/DB_EQ로 매핑된다.R_NULLSAFE_EQ만이NULL = NULL을V_TRUE로 처리한다.
R_EQ_TORDER는 정렬과 group-by에서 NULL을 명확한 값(보통 가장
작은 값)으로 다룰 때 쓰는 전순서 비교의 특수 케이스다. 전순서
플래그를 세팅한 채 tp_value_compare_with_error를 호출하면
NULL = NULL에 대해서도 DB_EQ가 나온다.
처음부터 끝까지: 스캔이 행을 내놓고 → 술어가 판정하고 → 출력 사영
섹션 제목: “처음부터 끝까지: 스캔이 행을 내놓고 → 술어가 판정하고 → 출력 사영”flowchart TD S1["scan_next_heap_scan or scan_next_index_scan"] --> S2["heap_get_visible_version_internal:<br/>fetch RECDES + heap_attrinfo_read_dbvalues"] S2 --> S3["eval_data_filter (or eval_key_filter):<br/>scan_pred->pr_eval_fnc()"] S3 -->|V_TRUE| S4["fetch_val_list PEEK on output regu_list:<br/>each regu resolved via fetch_peek_dbval"] S3 -->|V_FALSE / V_UNKNOWN| S5["skip row, advance scan"] S3 -->|V_ERROR| S6["bubble error up to qexec_intprt_fnc"] S4 --> S7["row passed to consumer:<br/>list-file write or upstream operator"] S5 --> S1 S7 --> S1
변곡점은 eval_data_filter다. 그 책임은 좁고 열거하기 쉽다.
- 속성 캐시가 채워졌는지 확인한다.
- 미리 컴파일된 빠른 경로로 술어를 돌린다.
- 통과한 경우, 출력 regu 리스트를 채운다(peek는 포인터를 공유한다. 복사가 일어나는 시점은 행이 list 파일에 커밋되거나 페이지-fix 경계를 가로질러 복사될 때뿐이다).
pr_eval_fnc == eval_pred(일반 케이스)일 때 이건 완전 재귀
워커로 펼쳐진다. 특수 경로 중 하나일 때는 본체가 피연산자별
fetch_peek_dbval 한 번씩과 타입별 비교기 하나로 끝난다. 어느
쪽이든 계약은 같다 — 행(RECDES + OID + 캐시된 attrinfo)을 받아
3치 판정을 돌려준다.
scan-manager와의 통합
섹션 제목: “scan-manager와의 통합”scan_manager.c는 스캔마다 FILTER_INFO를 만들고 그것으로 술어
평가를 구동한다. FILTER_INFO는 SCAN_PRED(regu 리스트, PRED_EXPR,
캐시된 pr_eval_fnc), SCAN_ATTRS(속성 ID들과
HEAP_CACHE_ATTRINFO), VAL_DESCR(호스트 변수, sys datetime),
클래스 OID, 그리고 — 인덱스 측 필터의 경우 — B-tree 속성 ID와
함수형 인덱스 컬럼을 들고 다닌다. 인덱스 스캔에는 세 가지 다른
FILTER_INFO가 공존한다. range 필터(B-tree 워크의 경계용),
key 필터(워크 도중 키마다 적용; 힙 fetch 없음), data
필터(힙 fetch 후 적용)다. 각각은 scan_open_index_scan 시점에
한 번 만들어지고 행마다 재사용된다. 스캔 측 전체 서사는
cubrid-scan-manager.md를 참고한다.
scan_manager가 평가기와 맺는 핵심 계약은 이렇다.
- 입력: (
RECDES,OID, 채워진 속성 캐시) 형태의 행. - 출력:
DB_LOGICAL∈ {V_TRUE,V_FALSE,V_UNKNOWN,V_ERROR}. - 부수 효과:
V_TRUE인 경우, 출력 regu 리스트가 XASL의val_list로fetch_val_list로 머터리얼라이즈된다.V_ERROR인 경우, 스캔이 중단되고 에러 코드는er_set으로 TLS 에 자리한다.V_FALSE/V_UNKNOWN이면 행이 버려지고 스캔이 앞으로 나아간다.
update_logical_result — 자격 정책
섹션 제목: “update_logical_result — 자격 정책”소비자(consumer)마다 V_UNKNOWN에 대한 collapse 규칙이 다르다는
점이다. WHERE 절은 unknown → false를 원한다. WHERE NOT 절은
unknown → unknown을 원한다(부정도 unknown이다). 외부 조인
(outer-join)의 자격 술어는 세 번째 자리에 앉는다 — qualified와
not-qualified 양쪽 행이 모두 흥미로운 자리다. CUBRID는 이 정책을
QPROC_QUALIFICATION enum에 인코딩하고, 결과를
update_logical_result로 흘려 보낸다.
// update_logical_result — src/query/query_evaluator.c (condensed)DB_LOGICALupdate_logical_result (THREAD_ENTRY * thread_p, DB_LOGICAL ev_res, int *qualification){ if (ev_res == V_ERROR) return ev_res;
if (qualification != NULL) { switch (*qualification) { case QPROC_QUALIFIED: if (ev_res != V_TRUE) return V_FALSE; /* unknown → false */ break; case QPROC_NOT_QUALIFIED: if (ev_res != V_FALSE) return V_FALSE; /* unknown → false */ break; case QPROC_QUALIFIED_OR_NOT: /* both qualified and not-qualified are interesting; bookkeep */ if (ev_res == V_TRUE) *qualification = QPROC_QUALIFIED; else if (ev_res == V_FALSE) *qualification = QPROC_NOT_QUALIFIED; /* V_UNKNOWN: leave qualification unchanged */ break; } } return (ev_res == V_TRUE) ? V_TRUE : V_FALSE;}이게 scan_manager가 술어 평가 후 호출해, 3치 결과를 스캔별 자격
정책으로 접어 넣는 함수다. QPROC_QUALIFIED_OR_NOT 케이스만이
unknown을 바꾸지 않고 통과시킨다 — 안티 조인이나 외부 조인 필터에서
쓰인다. 행의 자격 상태가 강제 대상이 아니라 관찰 대상인 자리다.
루프 닫기 — eval_pred 재귀 vs 행 단위 경로
섹션 제목: “루프 닫기 — eval_pred 재귀 vs 행 단위 경로”그림을 정박해 두자면, 전형적인 WHERE x > 0 AND y < 10 술어는
두 COMP_EVAL_TERM 리프의 불리언 합성이라 eval_pred를 통과한다 —
eval_fnc가 스캔 오픈 시점에 eval_pred를 돌려줬다는 뜻이다. 행마다
eval_pred가 우선형 AND 체인을 훑으며 자기를 각 연언 항을
호출하고, 거기서 다시 T_EVAL_TERM으로 들어가 fetch_peek_dbval을
두 번(lhs, rhs) 돌리고 eval_value_rel_cmp를 한 번 돌린다.
WHERE x = ?(호스트 파라미터) 같은 술어는 eval_pred_comp0을
직접 통과한다 — eval_fnc가 단일 이진 비교를 알아채고 재귀를
건너뛴 셈이다.
따름 정리는 이렇다. 술어 복잡성 비용은 대부분 트리 워크
오버헤드인 반면, 표현식 복잡성 비용은 대부분 fetch_peek_arith
재귀와 qdata_evaluate_function 작업이다. 두 관심사는 직교한다 —
복잡한 rhs(예를 들어 함수 다섯 개를 포함한 10층 깊이의 산술식)를
가진 단일 비교는 워커 오버헤드가 0인 대신 fetch 오버헤드가
거대하다. 한쪽만 최적화해서는 혼합 케이스에서 가속이 없다 —
그래서 두 경로 모두 살아 있다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”심볼명을 anchor로 삼는다. 라인번호는 본 문서의
updated:시점에 한정된다. 심볼은 리팩토링을 가로질러 안정적이다.
술어 타입과 디스패치 enum (src/xasl/xasl_predicate.hpp)
섹션 제목: “술어 타입과 디스패치 enum (src/xasl/xasl_predicate.hpp)”cubxasl::pred_expr— 술어 노드. 세 종류의 태그 유니언.TYPE_PRED_EXPR—T_PRED/T_EVAL_TERM/T_NOT_TERM.BOOL_OP—B_AND/B_OR/B_XOR/B_IS/B_IS_NOT.TYPE_EVAL_TERM—T_COMP_EVAL_TERM/T_ALSM_EVAL_TERM/T_LIKE_EVAL_TERM/T_RLIKE_EVAL_TERM.REL_OP— 관계 opcode 전체 집합. 순서, 집합, 한정, 전순서, null-safe까지 포함.QL_FLAG— ANY / ALL 한정용F_ALL/F_SOME.cubxasl::pred—T_PRED용lhs,rhs,bool_op.cubxasl::comp_eval_term—T_COMP_EVAL_TERM용lhs,rhs,rel_op,type.cubxasl::alsm_eval_term—T_ALSM_EVAL_TERM용elem,elemset,eq_flag,rel_op,item_type.cubxasl::like_eval_term—T_LIKE_*용src,pattern,esc_char.cubxasl::rlike_eval_term—T_RLIKE_*용src,pattern,case_sensitive,compiled_regex.
드라이버와 3치 논리 (src/query/query_evaluator.c)
섹션 제목: “드라이버와 3치 논리 (src/query/query_evaluator.c)”eval_pred— 재귀 워커. 일반 케이스의 입구.eval_negative— 3치 NOT (V_TRUE ↔ V_FALSE,V_UNKNOWN/V_ERROR은 그대로 통과).eval_logical_result— 두 부분 결과의 3치 AND (V_ERROR우선,V_FALSE는 끈끈, 그 외에는V_UNKNOWN).eval_value_rel_cmp— 타입 인지 비교기. rhs 상수 1회 강제 변환과REL_OP별 결과 매핑을 동반.eval_set_list_cmp—set ⨯ set/set ⨯ list/list ⨯ list비교. 필요하면 list 파일을 정렬하고 multi-set / sort-list 비교기로 디스패치.
Set / list 도우미 (src/query/query_evaluator.c)
섹션 제목: “Set / list 도우미 (src/query/query_evaluator.c)”eval_some_eval/eval_all_eval— element-vs-set ANY / ALL.eval_some_list_eval/eval_all_list_eval—qfile_scan_list_next를 통한 element-vs-list-file ANY / ALL.eval_item_card_set/eval_item_card_sort_list— 매칭 멤버의 cardinality (IN 비교를 위한 multi-set 시맨틱).eval_sub_multi_set_to_sort_list/eval_sub_sort_list_to_multi_set/eval_sub_sort_list_to_sort_list— subset 관계.eval_eq_multi_set_to_sort_list,eval_ne_*,eval_le_*,eval_lt_*의 multi-set / sort-list 조합 —eval_set_list_cmp의 primitive.eval_multi_set_to_sort_list/eval_sort_list_to_multi_set/eval_sort_list_to_sort_list—rel_operator별 디스패처.
단일 형태 특수 평가기 (src/query/query_evaluator.c)
섹션 제목: “단일 형태 특수 평가기 (src/query/query_evaluator.c)”eval_pred_comp0— LIST_ID 없는 이진 순서 / 집합 비교.eval_pred_comp1—IS NULL(OIDnot-null 특수 케이스도 처리).eval_pred_comp2— list 파일 또는 집합 위에서의EXISTS.eval_pred_comp3— lhs 또는 rhs가TYPE_LIST_ID인 비교.eval_pred_alsm4— set-boundANY/ALL.eval_pred_alsm5— list-file-boundANY/ALL.eval_pred_like6—LIKE(db_string_like호출).eval_pred_rlike7—RLIKE(캐시된compiled_regex로db_string_rlike호출).eval_fnc— 스캔 오픈 시점에 위 중 하나(또는 폴백eval_pred) 를 고르는 디스패처.
필터 브리지 (src/query/query_evaluator.c)
섹션 제목: “필터 브리지 (src/query/query_evaluator.c)”eval_data_filter— 힙 측 술어를 위한scan_manager브리지. 속성 캐시를 채우고,pr_eval_fnc를 호출하고, 조건부로 출력 regu 리스트를 머터리얼라이즈한다.eval_key_filter— 인덱스 키 술어용 짝꿍.DB_MIDXKEY에서 키 컬럼들을 디코드해 속성 캐시에 자리잡게 한 뒤pr_eval_fnc를 호출.update_logical_result—QPROC_QUALIFICATION정책 적용.
Regu variable 타입과 다형 구조체 (src/query/regu_var.hpp)
섹션 제목: “Regu variable 타입과 다형 구조체 (src/query/regu_var.hpp)”regu_variable_node(별칭REGU_VARIABLE) — 다형 표현식 피연산자 노드. 태그 유니언 모양.REGU_DATATYPE—TYPE_DBVAL/TYPE_CONSTANT/TYPE_INARITH/TYPE_OUTARITH/TYPE_ATTR_ID/TYPE_CLASS_ATTR_ID/TYPE_SHARED_ATTR_ID/TYPE_POSITION/TYPE_LIST_ID/TYPE_POS_VALUE/TYPE_OID/TYPE_CLASSOID/TYPE_FUNC/TYPE_REGUVAL_LIST/TYPE_REGU_VAR_LIST/TYPE_ORDERBY_NUM/TYPE_SP.regu_variable_node::map_regu— regu 트리에 대한 재귀 워커 (clear_xasl,fetch_force_not_const_recursive, 그리고 모든 리프를 건드려야 하는 직렬화 후 패치 작업이 사용).regu_variable_node::clear_xasl/clear_xasl_local— 노드별 정리. rand-seed, 정규식 객체, 임베디드DB_VALUE를 free한다.attr_descr_node(ATTR_DESCR) —TYPE_ATTR_ID페이로드:id,type,cache_attrinfo,cache_dbvalp.arith_list_node(ARITH_TYPE) —TYPE_INARITH/TYPE_OUTARITH페이로드:domain,value,leftptr,rightptr,thirdptr,opcode,pred,rand_seed.function_node(FUNCTION_TYPE) —TYPE_FUNC페이로드:value,operand,ftype,tmp_obj(F_REGEXP_*용 컴파일된 정규식 캐시).regu_value_list/regu_value_item—TYPE_REGUVAL_LIST페이로드 (다행 VALUES 리터럴).valptr_list_node(별칭VALPTR_LIST,OUTPTR_LIST) — 출력 사영용 regu variable 리스트 (xasl->outptr_list).regu_variable_list_node(REGU_VARIABLE_LIST) — regu variable 의 단일 연결 리스트. 술어 피연산자 리스트, 출력 리스트, 함수 인자 의 빌딩 블록.REGU_VARIABLE_HIDDEN_COLUMN/..._FETCH_ALL_CONST/..._FETCH_NOT_CONST/..._APPLY_COLLATION/..._ANALYTIC_WINDOW/..._UPD_INS_LIST/..._STRICT_TYPE_CAST/..._CORRELATED— fetch와 사영 동작을 조절하는 플래그들.REGU_VARIABLE_IS_FLAGED/_SET_FLAG/_CLEAR_FLAG/_GET_TYPE— 인라인 접근자.
Regu 워커 구현 (src/query/regu_var.cpp)
섹션 제목: “Regu 워커 구현 (src/query/regu_var.cpp)”regu_variable_node::map_regu— 일반 비지터(visitor).arithptr->leftptr/rightptr,funcp->operand체인,sp_ptr->args체인,reguval_list->regu_list체인,regu_var_list체인을 거쳐 재귀한다. 워커는 의도적으로 제한적이다 —TYPE_LIST_ID,TYPE_DBVAL,TYPE_ATTR_ID(그리고 비슷한 리프들)는 자식이 없으므로 건너뛴다.regu_variable_node::map_regu_and_xasl— 같은 워커. 다만regu.xasl이 세트돼 있으면 그것도 방문한다.regu_variable_node::clear_xasl_local/regu_variable_node::clear_xasl— 더 깊은 XASL 정리 경로에 묶이는 리프별 정리.
Fetch 디스패처와 도우미 (src/query/fetch.c, src/query/fetch.h)
섹션 제목: “Fetch 디스패처와 도우미 (src/query/fetch.c, src/query/fetch.h)”fetch_peek_dbval— regu별 타입 switch. 비소유(non-owning) 포인터 반환.fetch_copy_dbval— peek 후 복사하는 편의 함수.fetch_val_list— 리스트 안의 모든 regu를 대응하는vfetch_to로 fetch한다.peek=PEEK은 포인터를 공유하고,peek=COPY는 복사한다. 빠른 경로가 있다 — 리스트 머리(head)가TYPE_POSITION이면fetch_peek_dbval_pos를 호출해 튜플을 한 번에 훑으면서 다시 디코드하지 않는다.fetch_peek_arith— 재귀 산술 / 일반 표현식 평가기.arithptr->opcode위의 약 3.7 KLOC switch.fetch_peek_dbval_pos— 빠른 튜플 위치 fetcher.TYPE_POSITIONregu들을 튜플 위에서 한 번 선형으로 훑으며 읽는다.fetch_peek_min_max_value_of_width_bucket_func—T_WIDTH_BUCKET전용 fetcher. 술어로 인코딩된 버킷 경계를 미리 훑는다.fetch_init_val_list— 리스트 안의 모든 속성 regu를attr_descr.cache_dbvalp를 비운다(캐시 무효화 시 호출).fetch_force_not_const_recursive—TYPE_SP나 비슷한 always-not-const 케이스에서 사용.map_regu로 regu 트리를 훑으며 관련된 모든 리프에FETCH_NOT_CONST를 세트.
함수 호출 디스패처 (src/query/query_opfunc.c)
섹션 제목: “함수 호출 디스패처 (src/query/query_opfunc.c)”qdata_evaluate_function—funcp->ftype위의 switch. 함수별 평가기로 라우팅. 하위 케이스:F_SET/F_MULTISET/F_SEQUENCE/F_VID→qdata_convert_dbvals_to_set.F_TABLE_SET/F_TABLE_MULTISET/F_TABLE_SEQUENCE→qdata_convert_table_to_set.F_GENERIC→qdata_evaluate_generic_function.F_CLASS_OF→qdata_get_class_of_function.F_INSERT_SUBSTRING→qdata_insert_substring_function.F_ELT→qdata_elt.F_BENCHMARK→qdata_benchmark.- JSON 패밀리 (
F_JSON_*) →qdata_convert_operands_to_value_and_call (..., db_evaluate_json_*). - 정규식 패밀리 (
F_REGEXP_COUNT/_INSTR/_LIKE/_REPLACE/_SUBSTR) →qdata_regexp_function.
EXECUTE_REGU_VARIABLE_XASL/CHECK_REGU_VARIABLE_XASL_STATUS(src/query/xasl.h의 매크로) —regu_var->xasl이 세트돼 있을 때 부질의 머터리얼라이즈를 구동하는 도우미.
이 개정 시점의 위치 힌트 (2026-05-01)
섹션 제목: “이 개정 시점의 위치 힌트 (2026-05-01)”| 심볼 | 파일 | 라인 |
|---|---|---|
eval_pred | src/query/query_evaluator.c | 1666 |
eval_pred_comp0 | src/query/query_evaluator.c | 2150 |
eval_pred_comp1 | src/query/query_evaluator.c | 2199 |
eval_pred_comp2 | src/query/query_evaluator.c | 2238 |
eval_pred_comp3 | src/query/query_evaluator.c | 2295 |
eval_pred_alsm4 | src/query/query_evaluator.c | 2353 |
eval_pred_alsm5 | src/query/query_evaluator.c | 2419 |
eval_pred_like6 | src/query/query_evaluator.c | 2477 |
eval_pred_rlike7 | src/query/query_evaluator.c | 2535 |
eval_fnc | src/query/query_evaluator.c | 2590 |
update_logical_result | src/query/query_evaluator.c | 2668 |
eval_data_filter | src/query/query_evaluator.c | 2741 |
eval_key_filter | src/query/query_evaluator.c | 2822 |
eval_negative | src/query/query_evaluator.c | 96 |
eval_logical_result | src/query/query_evaluator.c | 119 |
eval_value_rel_cmp | src/query/query_evaluator.c | 152 |
eval_some_eval | src/query/query_evaluator.c | 353 |
eval_all_eval | src/query/query_evaluator.c | 417 |
eval_some_list_eval | src/query/query_evaluator.c | 549 |
eval_all_list_eval | src/query/query_evaluator.c | 648 |
eval_set_list_cmp | src/query/query_evaluator.c | 1542 |
fetch_peek_dbval | src/query/fetch.c | 3901 |
fetch_peek_arith | src/query/fetch.c | 85 |
fetch_copy_dbval | src/query/fetch.c | 4695 |
fetch_val_list | src/query/fetch.c | 4755 |
fetch_init_val_list | src/query/fetch.c | 4818 |
fetch_peek_dbval_pos | src/query/fetch.c | 4520 |
fetch_peek_min_max_value_of_width_bucket_func | src/query/fetch.c | 4581 |
fetch_force_not_const_recursive | src/query/fetch.c | 5069 |
regu_variable_node::map_regu | src/query/regu_var.cpp | 41 |
regu_variable_node::map_regu_and_xasl | src/query/regu_var.cpp | 122 |
regu_variable_node::clear_xasl_local | src/query/regu_var.cpp | 142 |
regu_variable_node::clear_xasl | src/query/regu_var.cpp | 210 |
qdata_evaluate_function | src/query/query_opfunc.c | 6875 |
TYPE_PRED_EXPR (enum) | src/xasl/xasl_predicate.hpp | 32 |
BOOL_OP (enum) | src/xasl/xasl_predicate.hpp | 39 |
TYPE_EVAL_TERM (enum) | src/xasl/xasl_predicate.hpp | 48 |
REL_OP (enum) | src/xasl/xasl_predicate.hpp | 56 |
QL_FLAG (enum) | src/xasl/xasl_predicate.hpp | 88 |
pred_expr (struct) | src/xasl/xasl_predicate.hpp | 150 |
comp_eval_term | src/xasl/xasl_predicate.hpp | 106 |
alsm_eval_term | src/xasl/xasl_predicate.hpp | 114 |
like_eval_term | src/xasl/xasl_predicate.hpp | 123 |
rlike_eval_term | src/xasl/xasl_predicate.hpp | 130 |
regu_variable_node | src/query/regu_var.hpp | 173 |
REGU_DATATYPE (enum) | src/query/regu_var.hpp | 44 |
attr_descr_node | src/query/regu_var.hpp | 77 |
arith_list_node | src/query/regu_var.hpp | 126 |
function_node | src/query/regu_var.hpp | 143 |
regu_variable_list_node | src/query/regu_var.hpp | 224 |
valptr_list_node (OUTPTR_LIST) | src/query/regu_var.hpp | 117 |
EXECUTE_REGU_VARIABLE_XASL (macro) | src/query/xasl.h | 527 |
CHECK_REGU_VARIABLE_XASL_STATUS (macro) | src/query/xasl.h | 579 |
SCAN_PRED | src/query/query_evaluator.h | 94 |
SCAN_ATTRS | src/query/query_evaluator.h | 103 |
FILTER_INFO | src/query/query_evaluator.h | 112 |
QPROC_QUALIFICATION (enum) | src/query/query_evaluator.h | 62 |
소스 검증 (2026-05-01 기준)
섹션 제목: “소스 검증 (2026-05-01 기준)”본 문서는 순수 코드 파생(sources: [])이라, 짝지을 원본 분석 자료가
없다. 따라서 교차 검증은 이 프로젝트의 code-analysis/cubrid/ 폴더
안 자매 문서들 — 평가기 주변 모듈을 다루는 — 를 이뤄진다.
각 자매 주장은 현재 소스 트리를 다시 검증한다.
cubrid-scan-manager.md— 행 단위 드라이버(scan_next_*,scan_handle_single_scan)와 평가기 술어를 감싸는INDX_SCAN_ID::range_pred/key_pred/scan_pred트리플을 서술한다. 그 문서는eval_data_filter와eval_key_filter를 평가기로 가는 브리지로 나열한다. 본 문서는 이를 외향 입구로 확정하고,pr_eval_fnc이 스캔 오픈 시점에eval_fnc로 미리 컴파일된다는 사실을 더한다. 스캔 매니저는scan_init_scan_pred도중eval_fnc를 직접 호출해SCAN_PRED::pr_eval_fnc슬롯을 채운다. 특수화가 일어나는 자리는 여기다.cubrid-query-executor.md— 연산자 수준 드라이버 (qexec_execute_mainblock_internal,qexec_intprt_fnc,qexec_open_scan), XASL 트리 모양(aptr_list/dptr_list/scan_ptr), 후처리 단계를 서술한다. 평가기에 대한 실행기의 역할은 둘이다. (a) 사영된 출력을 머터리얼라이즈하기 위해qexec_intprt_fnc안에서fetch_val_list를 호출한다. (b) 평가기가 부질의 참조를 머터리얼라이즈하려고 호출하는EXECUTE_REGU_VARIABLE_XASL매크로는xasl.h에 구현돼 있고, 실행기로 다시 들어가는 재귀 호출이다. 교차 문서 간 모순은 없다.cubrid-xasl-generator.md—pt_to_pred_expr이 어떻게PRED_EXPR트리를 짓는지를 서술한다. 특히 우선형(right-linear) AND/OR 체인을 내보낸다는 점이다. 평가기의 반복 전략(for (t_pr = pr; ...; t_pr = t_pr->pe.m_pred.rhs))은 이 구조 불변식에 의존한다 — 좌선형(left-linear) 트리였다면 연언 항마다 재귀 호출을 강제하고 재귀 깊이 검사 오버헤드를 치렀을 것이다. XASL 생성기의 트리 모양 가정과 평가기의 반복 계약은 함께 설계된 셈이다.cubrid-semantic-check.md— 타입 검사와 상수 폴딩을 포함한 4 단계 의미 검사를 서술한다. 평가기는 그 두 단계의 런타임 짝꿍 이다 — 타입 검사된 트리(모든regu_variable->domain이 세트됨)와 상수 폴딩된 형태(가능한 곳마다 리터럴 부분 트리가TYPE_DBVAL로 접혀 있어 행 단위 작업이 최소화)에 의존한다. 평가기의REGU_VARIABLE_FETCH_ALL_CONST플래그는 같은 발상의 스캔 단위 버전이다 — 의미 검사 패스가 컴파일 시점 상수를 접고, 평가기는 스캔의 나머지를 위해 첫 행 상수를 캐시한다.cubrid-mvcc.md—RECDES가eval_data_filter에 닿기 전에 힙 매니저가 적용하는 스냅샷-vs-버전 로직. 평가기 자체는 스냅샷 정보를 보지 않는다.RECDES가 도착할 때쯤이면 MVCC가 이미 보이는 버전으로 필터링해 둔 상태다. 평가기의 일은 순수 SQL 술어이지 가시성이 아니다.
연산자 명명 메모: eval-term 변종은 숫자 접미사(comp0 / comp1 /
comp2 / comp3 / alsm4 / alsm5 / like6 / rlike7)로 이름이
붙어 있다. 이 숫자들은 의미를 담지 않는다 — 역사적 자취이며, 아주
오래 전에 이 파일이 코드 생성기에 의해 만들어졌고 그 생성기가
이름을 인덱스 순으로 할당했음을 시사하는 흔적일 가능성이 있다.
지금은 이 매핑이 정본이며, eval_fnc는 이 이름들을 임의로 다룬다.
미해결 질문
섹션 제목: “미해결 질문”- 술어 평가용 JIT. PostgreSQL은 LLVM 기반 JIT
(
ExecCompileExpr, v11부터)로 옮겨 갔고, 스캔 위주 워크로드에서 두 자릿수 가속을 보고한다. CUBRID의 인터프리터 설계는 그 방향에 적합하다 — 술어 트리가 이미 작고 워커 친화적이다 — 다만 와이어 직렬화(XASL stream) 때문에 역직렬화 후의 런타임 컴파일이 필요 하다. 추적 경로:eval_pred와 가장 흔한eval_pred_comp0경로 를 컴파일하는 LLVM 워커를 프로토타이핑해 TPC-H Q1, Q6, Q14를 벤치마크. - 벡터화 / 배치 평가. fetch 경로는 행 지향이다 —
fetch_peek_dbval은 한 번에DB_VALUE하나를 돌려준다. 벡터화 대안(PostgresExecQualBatch, DuckDB의 컬럼 단위)은eval_pred가 행 벡터를 받도록 재구조화해야 한다. 가장 큰 장애물은 regu variable 모델이다 — 모든 리프가 현재 결과를 단일DB_VALUE슬롯에 저장하지만 이걸 버퍼로 바꿔야 한다. - SIMD 가속 비교.
eval_value_rel_cmp는 비교 스택의 바닥이며 대부분의 사이클을tp_value_compare_with_error에 쓴다. 원시 타입 위의 수치 위주 술어에서, 한 번에 4 / 8 lane을 비교하는 SIMD 경로는 튜플 단위 비용을 극적으로 줄인다. 트레이드오프는 NULL 인지 비교 로직과 도메인 강제 변환의 비용이다 — 둘 다 직선형 벡터 코드와 충돌한다. - 상수 폴딩 정책 경계.
FETCH_ALL_CONST/FETCH_NOT_CONST플래그는 첫 fetch 후 regu를 분류한다. 옵티마이저가 컴파일 시점에 미리 분류해서 후속 행에서 런타임 자기 분류를 건너뛸 수 있을까? 현재의 첫-fetch-시점 플래그 설계는 직렬화 드리프트 (drift)에 견고하지만,REGU_VARIABLE_IS_FLAGED검사의 행 단위 비용이 누적된다. eval_value_rel_cmp안의 비활성화된 lhs 강제 변환 블록. rhs 상수 강제 변환의 lhs 측 미러는#if 0으로 막혀 있고 do not delete me for future라는 TODO 주석이 달려 있다. 언제 비활성화됐고, 어떤 회귀(regression)가 비활성화를 일으켰는가? 추적 경로: 주변 라인에 대한 git log 고고학.- 부질의 캐시 무효화.
XASL_USES_SQ_CACHE는TYPE_CONSTANT와R_EXISTS경로를sq_get으로 단축시킨다. 캐시 키는 상관관계 피연산자로 지어진다. 매개변수 재바인딩(prepared statement가 새 호스트 변수로 재실행)에서 무슨 일이 일어나는가? 캐시가 무효화 되는가, 아니면 키가 호스트 변수 값을 포함하는가? eval_pred_comp0위의R_EQ_TORDER. 전순서 등치는 컴파일 시점 고정 술어 형태(IS NULL없음,EXISTS없음,LIST_ID없음)이므로eval_fnc가eval_pred_comp0을 돌려 준다. 그런데eval_pred_comp0은db_value_is_null (peek_val1) && et_comp->rel_op != R_NULLSAFE_EQ를 검사해V_UNKNOWN을 돌려 준다 —R_EQ_TORDER에 NULL 피연산자가 오면 명세상으로는 비교가 명확해야 한다(전순서 아래NULL = NULL → TRUE). 현재 동작이 의도된 것인가, 아니면R_EQ_TORDER가 별도 경로를 필요 로 하는가?- 저장 프로시저 결과 캐싱.
TYPE_SP는 항상FETCH_NOT_CONST를 세트하고 행마다 프로시저를 재평가한다. 어떤 저장 프로시저는 인자에 대한 순수 함수라서 메모이즈(memoise)가 가능하다. 옵트인 장치가 있는가, 아니면 항상 다시 도는가? T_PREDICATEopcode에 대한fetch_peek_arith. arith 노드가 중첩 술어(arithptr->pred)를 가질 때, 상수성은NOT_CONST로 강제된다. 이 경로는T_CASE나 비슷한 조건부 평가기가 쓰는 경로다. 술어가 행 사이에 캐시되는가, 아니면 다시 훑어지는가? 워커eval_pred는 새로 호출되며, 깊게 중첩된 CASE 식에서는 비용이 문제될 수 있다.- MVCC 재-fetch 아래에서의
HEAP_CACHE_ATTRINFO::cache_dbvalp수명. 락 획득 후 행을 MVCC 재-fetch해야 할 때(SELECT ... FOR UPDATE의scan_next_index_scan안), 그 아래 페이지가 바뀌어 있을 수 있다.attr_descr.cache_dbvalp가 올바르게 무효화되는가? 현재fetch_peek_dbval은 재검증을 하지 않는 듯 보인다. 계약은 호출자가fetch_init_val_list를 적시에 호출한다는 가정에 의존한다.
CUBRID 너머 — 비교 설계와 연구 동향 (Beyond CUBRID — Comparative Designs & Research Frontiers)
섹션 제목: “CUBRID 너머 — 비교 설계와 연구 동향 (Beyond CUBRID — Comparative Designs & Research Frontiers)”분석이 아닌 포인터(pointers). 각 항목은 후속 문서의 시작점이며, 깊이는 의도적으로 얕다.
- PostgreSQL의 LLVM JIT.
ExecCompileExpr(v11)이Expr트리를 머신 코드로 컴파일한다. 측정은 워크로드에 따라 갈린다 — 스캔이 지배적인 OLAP 질의에서 수십 퍼센트, OLTP 트랜잭션 지향 질의에서는 거의 0이다. CUBRID의 등가 작업은 컴파일 결과를 클라이언트/서버 경계에 어떻게 위치시킬지 정해야 한다 — 와이어를 가로지르는 것은 XASL이 아니라 컴파일된 코드인가, 아니면 서버에서 디시리얼라이즈 후 컴파일하는 것인가? - 컬럼 지향 / 벡터화 평가 — DuckDB, Apache Arrow / DataFusion,
Postgres의
ExecQualBatch. 이 엔진들은 한 번에 한 컬럼의 1024 값을 처리해 한 행 한 비교의 인터프리터 오버헤드를 분산시킨다. CUBRID의 행 지향 fetcher는 이 패턴에 부적합하지만TYPE_POSITION과qfile_locate_tuple_value는 이미 list-file 위에서 한 번 훑기 를 한다 — 작은 디딤돌이다. MonetDB/X100(Boncz et al., CIDR 2005)이 그 설계 공간의 정초 논문이다. - MySQL의 Item 트리 — 가상 디스패치를 통한 다형성. 평탄한 union -tag 모양과 비교하면, 코드가 더 OO 풍이고 새 연산자를 추가하기 쉽다는 장점이 있지만, 행마다 가상 호출 비용을 진다. CUBRID의 와이어 직렬화 요구사항이 이 선택지를 닫아 버렸지만, 직렬화 형식 이 변하는 미래에는 다시 열릴 수 있는 질문이다.
- 3치 논리의 의미론적 변형 —
SQL:2003표준은IS DISTINCT FROM을 도입했고, MySQL은<=>(null-safe equality)를 가진다. 이런 연산자는 NULL을 일급 값으로 다뤄 collapse 규칙의 부담을 평가기 쪽에서 표면 SQL 쪽으로 옮긴다. CUBRID에는R_NULLSAFE_EQ로 대응 되는 짝꿍이 있다. 추적할 가치가 있는 항목은 outer-join 의미론과IS NOT DISTINCT FROM도입이update_logical_result의 정책 모듈 을 어떻게 단순화할 수 있는가다. - 함수 호출 디스패치 비용 — Postgres의
FmgrInfo캐시, CUBRID의funcp->ftypeswitch, MySQL의 가상 메서드. 셋이 모두 같은 일을 한다. 반복되는 호출을 함수 식별자에서 함수 포인터로 가는 비용을 분산시키는 일이다. 함수 호출 메커니즘 자체 보다 어떤 함수가 핫 패스에 있느냐 가 더 큰 변수다 — 정규식, JSON, 문자열 함수가 평가기의 시간을 가장 많이 가져간다. - 상수 폴딩의 컴파일 시점 vs 런타임 — Postgres의
eval_const_expressions, CUBRID의 의미 검사 패스, MySQL의Item단순화. 셋 모두 컴파일 시점 폴딩을 한다. 거기에 더해 CUBRID는FETCH_ALL_CONST로 행 단위 폴딩을 한다 — 런타임 첫 행에서 상수성을 발견한다는 점이다. 이 두 폴딩 패스 사이의 빈자리는 없는가, 아니면 의도적으로 양쪽에서 잡고 있는가? - Predicate pushdown의 한계. 옵티마이저가 술어를 가능한 한 깊게 밀어 넣는다 — 인덱스 키, 함수형 인덱스, 외부 스토리지(FDW). CUBRID의 평가기에는 같은 술어 평가기가 인덱스 측에서도 돌고 힙 측에서도 돈다. 그래서 푸시다운된 술어가 옮겨진 자리에서도 그대로 같은 코드를 돌린다. 이건 단순함의 승리이지만, 인덱스 측 비교를 더 빠르게 만드는 마이크로옵티마이즈(예: 정수 키에 대한 SIMD)는 공유 평가기를 분기시켜야 한다.
이 절의 의도는 다음 문서들의 씨앗을 뿌리는 것이지, 여기서 분석하는 것이 아니다. 각 항목은 차례가 오면 자체 큐레이트 노트가 되어야 한다.
본 문서는 sources: []이다 — /data/hgryoo/references/cubrid/의
CUBRID 소스 트리에서만 도출된다. 어떤 원본 분석 파일
(raw/code-analysis/cubrid/...)도 참조하지 않았다.
CUBRID 소스
섹션 제목: “CUBRID 소스”src/query/query_evaluator.c— 술어 평가기(약 3 KLOC). 드라이버:eval_pred. 특수 경로:eval_pred_comp{0,1,2,3}/eval_pred_alsm{4,5}/eval_pred_like6/eval_pred_rlike7. 필터 브리지:eval_data_filter/eval_key_filter. 특수화기:eval_fnc.src/query/query_evaluator.h—SCAN_PRED,SCAN_ATTRS,FILTER_INFO,QPROC_QUALIFICATION,PR_EVAL_FNC.src/query/fetch.c—fetch_peek_dbval,fetch_copy_dbval,fetch_val_list,fetch_peek_arith(약 5 KLOC. 대부분이 opcode별 산술).src/query/fetch.h— fetch 입구.src/query/regu_var.hpp—regu_variable_node,REGU_DATATYPE,attr_descr_node,arith_list_node,function_node,regu_variable_list_node,valptr_list_node,REGU_VARIABLE_*플래그 집합.src/query/regu_var.cpp—map_regu,clear_xasl_local,clear_xasl.src/xasl/xasl_predicate.hpp—pred_expr,pred,comp_eval_term,alsm_eval_term,like_eval_term,rlike_eval_term,TYPE_PRED_EXPR,BOOL_OP,TYPE_EVAL_TERM,REL_OP,QL_FLAG.src/query/xasl.h—EXECUTE_REGU_VARIABLE_XASL,CHECK_REGU_VARIABLE_XASL_STATUS매크로. XASL 노드 통합.src/query/query_opfunc.c—FUNC_CODE위의qdata_evaluate_function디스패치. 함수별 평가기 (qdata_convert_dbvals_to_set,qdata_insert_substring_function,qdata_elt,qdata_regexp_function,qdata_convert_operands_to_value_and_call+db_evaluate_json_*).- 컨텍스트용 교차 참조:
src/query/scan_manager.{c,h}(eval_data_filter/eval_key_filter의 호출자 측),src/query/query_executor.c(출력 머터리얼라이즈를 위해fetch_val_list를 호출하는 행 단위 드라이버),src/storage/heap_attrinfo.h(TYPE_ATTR_IDfetch가 읽어 가는 속성 캐시 디코더).
자매 문서
섹션 제목: “자매 문서”knowledge/code-analysis/cubrid/cubrid-query-executor.md— 연산자 수준 드라이버. 술어 평가가 행 단위 풀(pull) 루프 안의eval_data_filter에서 호출된다.knowledge/code-analysis/cubrid/cubrid-scan-manager.md— 튜플 단위 스캔 디스패처.SCAN_PRED::pr_eval_fnc가 스캔 오픈 시점에eval_fnc로 세팅된다.knowledge/code-analysis/cubrid/cubrid-xasl-generator.md—PRED_EXPR과REGU_VARIABLE트리를 짓는 클라이언트 측 XASL 생성기. 관습적으로 우선형 AND/OR 체인을 만든다.knowledge/code-analysis/cubrid/cubrid-semantic-check.md— XASL 생성 전에 도는 타입 검사와 상수 폴딩 패스. 모든 regu에 도메인을 세트하고 컴파일 시점 상수를 접는다.knowledge/code-analysis/cubrid/cubrid-heap-manager.md—HEAP_CACHE_ATTRINFO와heap_attrinfo_read_dbvalues.TYPE_ATTR_IDfetch 경로가 읽는 버퍼.knowledge/code-analysis/cubrid/cubrid-mvcc.md— 술어 평가 전에 도는 가시성 검사. 평가기는 보이지 않는 행을 절대 보지 않는다.
교재 챕터 / 논문
섹션 제목: “교재 챕터 / 논문”- Graefe, Goetz. Query Evaluation Techniques for Large Databases, ACM Computing Surveys 25(2), 1993 — 술어 평가, 함수 디스패치, 표현식 해석. 술어 워커 / regu variable 프레이밍의 정본.
- Hellerstein, Joseph; Stonebraker, Michael (eds.). Readings in Database Systems, 4판 — iterator-vs-vector 트레이드오프와 3치 논리를 다루는 연산자 구현 챕터들.
- Petrov, Alex. Database Internals, O’Reilly 2019, 12장 “Query Execution” — 스캔 + 필터 파이프라인. peek-vs-copy 소유권.
- Codd, E. F. The Relational Model for Database Management: Version 2, Addison-Wesley 1990 — SQL 술어에서의 3치 논리.
- Aho, Alfred; Lam, Monica; Sethi, Ravi; Ullman, Jeffrey. Compilers: Principles, Techniques, and Tools (Dragon Book), 2판 — 표현식 트리와 트리 워킹 인터프리터.