콘텐츠로 이동

(KO) CUBRID 질의 평가기 — PRED_EXPR 순회, regu_variable fetch, 그리고 행 단위 필터 엔진

목차

질의 평가기(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의 익히 알려진 진리표로 정의한다.

ABA AND BA OR BNOT A
TTTTF
TFFTF
TUUTF
FFFFT
FUFUT
UUUUU

여기서 드러나는 드라이버 차원의 결과는, 단락 평가 규칙 이 2치 논리와 3치 논리 사이에서 다르다는 점이다. 2치 AND에서는 첫 false가 평가를 종료시키지만, 3치 AND에서는 false만 종료를 일으킨다. unknown은 그렇지 않다는 점이다. 거기에 더해 WHERE 필터는 unknownfalse 로 접어 버린다(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_fncPRED_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는 ExprNodeTag(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.hpp
typedef 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) 술어 평가기가 그대로 읽어 낼 수 있는 모양이다.

형태 균일한(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 하나로 풀린다. 술어 평가기 자체는 피연산자 종류로 분기하지 않는다.

각 계층은 {true, false, unknown, error} 중 하나를 돌려줘야 한다. Postgres는 bool 결과와 별도의 *isNull 출력 파라미터를 써서 unknown을 표현한다. MySQL은 Item::null_valueval_*의 부수 효과로 세팅한다. 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_comp1IS NULL, eval_pred_comp2EXISTS, 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.hSCAN_PRED::pr_eval_fnc 필드는 scan_manager가 스캔별로 캐시해 두는 곳이다. 그래야 행마다 드는 비용이 단지 간접 호출 1회로 끝나고, 루트에서의 반복적 switch로 부풀지 않는다.

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)
불리언 연산자 opcodeBOOL_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 variableregu_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-fetcherfetch_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)

평가기는 네 가지 일을 한다. 그 외의 모든 것은 그 넷 중 하나의 특수 케이스라고 보면 된다. (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은 세 가지 노드 종류를 묶은 태그 유니언이며, 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_OPT_COMP_EVAL_TERM에서 쓰이는 비교 어휘다. 전체 집합은 순서 비교(R_EQ, R_NE, R_LT, R_LE, R_GT, R_GE)와 집합 비교 (R_SUBSET, R_SUBSETEQ, R_SUPERSET, R_SUPERSETEQ), 특수 단항 검사(R_NULLIS NULL, R_EXISTSEXISTS), 한정 비교 (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로 디스패치된다.

eval_predPRED_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_XORB_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은 표현식 피연산자의 고정 크기 디스크립터(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_OIDTYPE_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_dbvalregu_var->type 위의 단일 switch이며, regu variable을 DB_VALUE *로 풀어 준다. 이 함수는 peek 형식이다 — 호출자가 free해선 안 되는 버퍼에 대한 포인터를 돌려 준다. 그 값은 같은 스캔의 다음 연산이 무효화하기 전까지 유효하다. 복사 짝꿍 fetch_copy_dbvalfetch_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"]

경로별 요약은 다이어그램과 짝을 이룬다. 각 경로가 떠맡는 책임은 이렇다.

  1. TYPE_DBVAL — 임베디드 리터럴. &regu_var->value.dbval을 돌려 준다. FETCH_ALL_CONST를 세트한다. 가장 싼 케이스다 — 첫 플래그 세팅 이후엔 일이 0이다.
  2. TYPE_CONSTANT — aptr-result 안 포인터. dbvalptr은 부모의 사전 실행이 머터리얼라이즈된 aptr의 튜플 안을 가리키도록 미리 바인딩해 둔 것이다. 첫 읽기에서는 연결된 부질의를 실행해야 할 수도 있다(EXECUTE_REGU_VARIABLE_XASL). 이후 행은 캐시된 포인터를 그냥 읽는다. XASL_USES_SQ_CACHE가 세트돼 있으면 부질의 캐시인 sq_get이 단축 경로를 만든다.
  3. 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를 세트한다(정의상 행마다 값이 바뀐다).
  4. 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 슬롯을 미리 잡아 둔다.
  5. TYPE_POS_VALUE — 호스트 변수. vd->dbval_ptr[val_pos]다. 질의 동안 상수다. 순수 포인터 산술이다.
  6. TYPE_OID / TYPE_CLASSOID — 현재 OID. obj_oid / class_oid 인자가 regu_var->value.dbval로 들어간다. 엄밀히 상수는 아니지만(행마다 변한다), FETCH_ALL_CONST가 여전히 세트된다. 상수성이 의미를 가질 만한 어떤 피연산자에서도 파생되지 않기 때문이다.
  7. 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를 강제한다. 이들은 행 단위 캐시의 이득을 받지 못한다.
  8. TYPE_FUNC — 함수 호출. FETCH_ALL_CONST가 이미 세트돼 있으면 미리 계산된 funcp->value가 재평가 없이 반환된다. 그렇지 않으면 qdata_evaluate_function (query_opfunc.c)이 funcp->ftype (FUNC_CODE enum) 위에서 디스패치하며, F_-루틴 중 하나로 분기한다 — qdata_convert_dbvals_to_setF_SET / F_MULTISET / F_SEQUENCE용, qdata_insert_substring_functionF_INSERT_SUBSTRING용, qdata_eltF_ELT용, qdata_regexp_function은 정규식 패밀리, JSON 패밀리는 모두 qdata_convert_operands_to_value_and_call로 가며 인자로 db_evaluate_json_* 함수 포인터가 넘어간다. 호출이 끝나면 funcp->operand 체인을 훑어 상수성 플래그를 다시 계산한다.
  9. 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로 재귀적으로 강제됨).
  10. TYPE_REGUVAL_LIST — VALUES 질의(다행 리터럴 테이블). reguval_list->current_value->value로 재귀한다. 리스트는 S_VALUES_SCAN 드라이버가 행마다 current_value를 진행시키며 순회한다.
  11. 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_TYPESER_QSTR_INCOMPATIBLE_COLLATIONS로 드러난다.

TYPE_ATTR_ID 경로는 평가기가 힙(heap)을 만나는 지점이다. 핸드셰 이크는 셋으로 나뉜다. 첫째, scan_open_heap_scanheap_attrinfo_start를 호출해, 술어와 출력 리스트가 참조하는 컬럼들을 위한 HEAP_CACHE_ATTRINFO를 잡는다. 둘째, fetch된 모든 행에서 힙 이터레이터(heap_next / heap_get_visible_version_internal)가 새 RECDESheap_attrinfo_read_dbvalues를 호출해, 참조된 각 컬럼을 캐시 안의 속성별 DB_VALUE로 디코드한다. 셋째, fetch_peek_dbvalTYPE_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.ceval_data_filter가 술어 평가 전에 속성 캐시가 채워졌는지를 보장하는 브리지(bridge)다.

// eval_data_filter — src/query/query_evaluator.c (condensed)
DB_LOGICAL
eval_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_scanSCAN_PRED를 초기화할 때, 행 단위 평가기를 고르기 위해 eval_fnc를 호출한다. eval_fnc는 루트 PRED_EXPR을 들여다보고, 흔한 단일 형태를 재귀 워커를 우회하는 함수 포인터를 돌려 준다.

// eval_fnc — src/query/query_evaluator.c (condensed)
PR_EVAL_FNC
eval_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_fnceval_pred_comp0을 술어가 IS NULL도, EXISTS도, list-file 피연산자도 없는 단일 이진 비교일 때만 돌려 준다. 구조적 복잡성이 어떤 모양이든 끼어들면 더 일반적인 특수 경로가 선택되며, 어떤 특수화도 들어맞지 않을 때만 eval_pred 전체가 폴백으로 떨어진다. 각 특수 경로는 자기가 아는 구조를 인라인한다 — eval_pred_comp0et_type을 다시 분기하지 않는다, 이미 단순 하위 형태의 T_COMP_EVAL_TERM임을 알기 때문이다. eval_pred_comp1rel_op을 검사하지 않는다, 이미 R_NULL임을 알기 때문이다. 호출 한 번당 절약되는 양은 작지만, 모든 스캔의 모든 행에 곱해진다.

ANY/ALL — list-file 워킹을 통한 집합 비교

섹션 제목: “ANY/ALL — list-file 워킹을 통한 집합 비교”

eval_pred_alsm4eval_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_LOGICAL
eval_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_evalnot some(neg-op)로 구현돼 첫 반례에서 V_FALSE를 돌려준다.

eval_value_rel_cmp — 비교 스택의 바닥

섹션 제목: “eval_value_rel_cmp — 비교 스택의 바닥”

양쪽이 DB_VALUE *가 되고 나면, 모든 비교는 결국 eval_value_rel_cmp로 끝난다. 순서는 이렇다.

  1. 상수 강제 변환(coercion, 1회성) — rhs가 FETCH_ALL_CONST regu 이고 도메인이 lhs와 다르면, rhs를 1회 lhs 도메인으로 강제 변환 한다. 의도는 tp_value_compare_with_error 안에서 같은 상수를 행 마다 다시 변환하는 일을 피하려는 것이다. 현재 활성화된 정책은 이렇다 — 수치 ↔ char(char를 double로), 날짜·시간 ↔ char(char를 날짜·시간 도메인으로), 좁은 수치 ↔ 넓은 수치(좁은 쪽을 넓은 쪽으로). lhs 측 미러는 소스에 존재하지만 #if 0으로 비활성화돼 있다 — TODO로 남아 있다.
  2. tp_value_compare_with_error — 타입 인지 비교기. DB_LT / DB_EQ / DB_GT / DB_UNK을 돌려주거나 comparable = false를 세트한다. 일반 REL_OP에서 DB_UNKV_UNKNOWN을 트리거한다. R_NULLSAFE_EQ는 자기만의 NULL-aware 로직이 있어 NULL = NULL → V_TRUE를 인정한다.
  3. REL_OP 매핑 — int 결과가 rel_operator별로 V_TRUE / V_FALSE로 매핑된다. R_EQresult == DB_EQ를 검사한다. R_LEDB_LT 또는 DB_EQ를 받는다. 집합 비교는 DB_SUBSET / DB_SUPERSET / DB_EQ로 매핑된다. R_NULLSAFE_EQ만이 NULL = NULLV_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다. 그 책임은 좁고 열거하기 쉽다.

  1. 속성 캐시가 채워졌는지 확인한다.
  2. 미리 컴파일된 빠른 경로로 술어를 돌린다.
  3. 통과한 경우, 출력 regu 리스트를 채운다(peek는 포인터를 공유한다. 복사가 일어나는 시점은 행이 list 파일에 커밋되거나 페이지-fix 경계를 가로질러 복사될 때뿐이다).

pr_eval_fnc == eval_pred(일반 케이스)일 때 이건 완전 재귀 워커로 펼쳐진다. 특수 경로 중 하나일 때는 본체가 피연산자별 fetch_peek_dbval 한 번씩과 타입별 비교기 하나로 끝난다. 어느 쪽이든 계약은 같다 — 행(RECDES + OID + 캐시된 attrinfo)을 받아 3치 판정을 돌려준다.

scan_manager.c는 스캔마다 FILTER_INFO를 만들고 그것으로 술어 평가를 구동한다. FILTER_INFOSCAN_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_listfetch_val_list로 머터리얼라이즈된다. V_ERROR인 경우, 스캔이 중단되고 에러 코드는 er_set으로 TLS 에 자리한다. V_FALSE / V_UNKNOWN이면 행이 버려지고 스캔이 앞으로 나아간다.

소비자(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_LOGICAL
update_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_EXPRT_PRED / T_EVAL_TERM / T_NOT_TERM.
  • BOOL_OPB_AND / B_OR / B_XOR / B_IS / B_IS_NOT.
  • TYPE_EVAL_TERMT_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::predT_PREDlhs, rhs, bool_op.
  • cubxasl::comp_eval_termT_COMP_EVAL_TERMlhs, rhs, rel_op, type.
  • cubxasl::alsm_eval_termT_ALSM_EVAL_TERMelem, elemset, eq_flag, rel_op, item_type.
  • cubxasl::like_eval_termT_LIKE_*src, pattern, esc_char.
  • cubxasl::rlike_eval_termT_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_cmpset ⨯ 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_evalqfile_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_listrel_operator별 디스패처.

단일 형태 특수 평가기 (src/query/query_evaluator.c)

섹션 제목: “단일 형태 특수 평가기 (src/query/query_evaluator.c)”
  • eval_pred_comp0 — LIST_ID 없는 이진 순서 / 집합 비교.
  • eval_pred_comp1IS NULL (OID not-null 특수 케이스도 처리).
  • eval_pred_comp2 — list 파일 또는 집합 위에서의 EXISTS.
  • eval_pred_comp3 — lhs 또는 rhs가 TYPE_LIST_ID인 비교.
  • eval_pred_alsm4 — set-bound ANY / ALL.
  • eval_pred_alsm5 — list-file-bound ANY / ALL.
  • eval_pred_like6LIKE (db_string_like 호출).
  • eval_pred_rlike7RLIKE (캐시된 compiled_regexdb_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_resultQPROC_QUALIFICATION 정책 적용.

Regu variable 타입과 다형 구조체 (src/query/regu_var.hpp)

섹션 제목: “Regu variable 타입과 다형 구조체 (src/query/regu_var.hpp)”
  • regu_variable_node (별칭 REGU_VARIABLE) — 다형 표현식 피연산자 노드. 태그 유니언 모양.
  • REGU_DATATYPETYPE_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_itemTYPE_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_POSITION regu들을 튜플 위에서 한 번 선형으로 훑으며 읽는다.
  • fetch_peek_min_max_value_of_width_bucket_funcT_WIDTH_BUCKET 전용 fetcher. 술어로 인코딩된 버킷 경계를 미리 훑는다.
  • fetch_init_val_list — 리스트 안의 모든 속성 regu를 attr_descr.cache_dbvalp를 비운다(캐시 무효화 시 호출).
  • fetch_force_not_const_recursiveTYPE_SP나 비슷한 always-not-const 케이스에서 사용. map_regu로 regu 트리를 훑으며 관련된 모든 리프에 FETCH_NOT_CONST를 세트.

함수 호출 디스패처 (src/query/query_opfunc.c)

섹션 제목: “함수 호출 디스패처 (src/query/query_opfunc.c)”
  • qdata_evaluate_functionfuncp->ftype 위의 switch. 함수별 평가기로 라우팅. 하위 케이스:
    • F_SET / F_MULTISET / F_SEQUENCE / F_VIDqdata_convert_dbvals_to_set.
    • F_TABLE_SET / F_TABLE_MULTISET / F_TABLE_SEQUENCEqdata_convert_table_to_set.
    • F_GENERICqdata_evaluate_generic_function.
    • F_CLASS_OFqdata_get_class_of_function.
    • F_INSERT_SUBSTRINGqdata_insert_substring_function.
    • F_ELTqdata_elt.
    • F_BENCHMARKqdata_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_predsrc/query/query_evaluator.c1666
eval_pred_comp0src/query/query_evaluator.c2150
eval_pred_comp1src/query/query_evaluator.c2199
eval_pred_comp2src/query/query_evaluator.c2238
eval_pred_comp3src/query/query_evaluator.c2295
eval_pred_alsm4src/query/query_evaluator.c2353
eval_pred_alsm5src/query/query_evaluator.c2419
eval_pred_like6src/query/query_evaluator.c2477
eval_pred_rlike7src/query/query_evaluator.c2535
eval_fncsrc/query/query_evaluator.c2590
update_logical_resultsrc/query/query_evaluator.c2668
eval_data_filtersrc/query/query_evaluator.c2741
eval_key_filtersrc/query/query_evaluator.c2822
eval_negativesrc/query/query_evaluator.c96
eval_logical_resultsrc/query/query_evaluator.c119
eval_value_rel_cmpsrc/query/query_evaluator.c152
eval_some_evalsrc/query/query_evaluator.c353
eval_all_evalsrc/query/query_evaluator.c417
eval_some_list_evalsrc/query/query_evaluator.c549
eval_all_list_evalsrc/query/query_evaluator.c648
eval_set_list_cmpsrc/query/query_evaluator.c1542
fetch_peek_dbvalsrc/query/fetch.c3901
fetch_peek_arithsrc/query/fetch.c85
fetch_copy_dbvalsrc/query/fetch.c4695
fetch_val_listsrc/query/fetch.c4755
fetch_init_val_listsrc/query/fetch.c4818
fetch_peek_dbval_possrc/query/fetch.c4520
fetch_peek_min_max_value_of_width_bucket_funcsrc/query/fetch.c4581
fetch_force_not_const_recursivesrc/query/fetch.c5069
regu_variable_node::map_regusrc/query/regu_var.cpp41
regu_variable_node::map_regu_and_xaslsrc/query/regu_var.cpp122
regu_variable_node::clear_xasl_localsrc/query/regu_var.cpp142
regu_variable_node::clear_xaslsrc/query/regu_var.cpp210
qdata_evaluate_functionsrc/query/query_opfunc.c6875
TYPE_PRED_EXPR (enum)src/xasl/xasl_predicate.hpp32
BOOL_OP (enum)src/xasl/xasl_predicate.hpp39
TYPE_EVAL_TERM (enum)src/xasl/xasl_predicate.hpp48
REL_OP (enum)src/xasl/xasl_predicate.hpp56
QL_FLAG (enum)src/xasl/xasl_predicate.hpp88
pred_expr (struct)src/xasl/xasl_predicate.hpp150
comp_eval_termsrc/xasl/xasl_predicate.hpp106
alsm_eval_termsrc/xasl/xasl_predicate.hpp114
like_eval_termsrc/xasl/xasl_predicate.hpp123
rlike_eval_termsrc/xasl/xasl_predicate.hpp130
regu_variable_nodesrc/query/regu_var.hpp173
REGU_DATATYPE (enum)src/query/regu_var.hpp44
attr_descr_nodesrc/query/regu_var.hpp77
arith_list_nodesrc/query/regu_var.hpp126
function_nodesrc/query/regu_var.hpp143
regu_variable_list_nodesrc/query/regu_var.hpp224
valptr_list_node (OUTPTR_LIST)src/query/regu_var.hpp117
EXECUTE_REGU_VARIABLE_XASL (macro)src/query/xasl.h527
CHECK_REGU_VARIABLE_XASL_STATUS (macro)src/query/xasl.h579
SCAN_PREDsrc/query/query_evaluator.h94
SCAN_ATTRSsrc/query/query_evaluator.h103
FILTER_INFOsrc/query/query_evaluator.h112
QPROC_QUALIFICATION (enum)src/query/query_evaluator.h62

본 문서는 순수 코드 파생(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_filtereval_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.mdpt_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.mdRECDESeval_data_filter에 닿기 전에 힙 매니저가 적용하는 스냅샷-vs-버전 로직. 평가기 자체는 스냅샷 정보를 보지 않는다. RECDES가 도착할 때쯤이면 MVCC가 이미 보이는 버전으로 필터링해 둔 상태다. 평가기의 일은 순수 SQL 술어이지 가시성이 아니다.

연산자 명명 메모: eval-term 변종은 숫자 접미사(comp0 / comp1 / comp2 / comp3 / alsm4 / alsm5 / like6 / rlike7)로 이름이 붙어 있다. 이 숫자들은 의미를 담지 않는다 — 역사적 자취이며, 아주 오래 전에 이 파일이 코드 생성기에 의해 만들어졌고 그 생성기가 이름을 인덱스 순으로 할당했음을 시사하는 흔적일 가능성이 있다. 지금은 이 매핑이 정본이며, eval_fnc는 이 이름들을 임의로 다룬다.

  1. 술어 평가용 JIT. PostgreSQL은 LLVM 기반 JIT (ExecCompileExpr, v11부터)로 옮겨 갔고, 스캔 위주 워크로드에서 두 자릿수 가속을 보고한다. CUBRID의 인터프리터 설계는 그 방향에 적합하다 — 술어 트리가 이미 작고 워커 친화적이다 — 다만 와이어 직렬화(XASL stream) 때문에 역직렬화 후의 런타임 컴파일이 필요 하다. 추적 경로: eval_pred와 가장 흔한 eval_pred_comp0 경로 를 컴파일하는 LLVM 워커를 프로토타이핑해 TPC-H Q1, Q6, Q14를 벤치마크.
  2. 벡터화 / 배치 평가. fetch 경로는 행 지향이다 — fetch_peek_dbval은 한 번에 DB_VALUE 하나를 돌려준다. 벡터화 대안(Postgres ExecQualBatch, DuckDB의 컬럼 단위)은 eval_pred 가 행 벡터를 받도록 재구조화해야 한다. 가장 큰 장애물은 regu variable 모델이다 — 모든 리프가 현재 결과를 단일 DB_VALUE 슬롯에 저장하지만 이걸 버퍼로 바꿔야 한다.
  3. SIMD 가속 비교. eval_value_rel_cmp는 비교 스택의 바닥이며 대부분의 사이클을 tp_value_compare_with_error에 쓴다. 원시 타입 위의 수치 위주 술어에서, 한 번에 4 / 8 lane을 비교하는 SIMD 경로는 튜플 단위 비용을 극적으로 줄인다. 트레이드오프는 NULL 인지 비교 로직과 도메인 강제 변환의 비용이다 — 둘 다 직선형 벡터 코드와 충돌한다.
  4. 상수 폴딩 정책 경계. FETCH_ALL_CONST / FETCH_NOT_CONST 플래그는 첫 fetch regu를 분류한다. 옵티마이저가 컴파일 시점에 미리 분류해서 후속 행에서 런타임 자기 분류를 건너뛸 수 있을까? 현재의 첫-fetch-시점 플래그 설계는 직렬화 드리프트 (drift)에 견고하지만, REGU_VARIABLE_IS_FLAGED 검사의 행 단위 비용이 누적된다.
  5. eval_value_rel_cmp 안의 비활성화된 lhs 강제 변환 블록. rhs 상수 강제 변환의 lhs 측 미러는 #if 0으로 막혀 있고 do not delete me for future라는 TODO 주석이 달려 있다. 언제 비활성화됐고, 어떤 회귀(regression)가 비활성화를 일으켰는가? 추적 경로: 주변 라인에 대한 git log 고고학.
  6. 부질의 캐시 무효화. XASL_USES_SQ_CACHETYPE_CONSTANTR_EXISTS 경로를 sq_get으로 단축시킨다. 캐시 키는 상관관계 피연산자로 지어진다. 매개변수 재바인딩(prepared statement가 새 호스트 변수로 재실행)에서 무슨 일이 일어나는가? 캐시가 무효화 되는가, 아니면 키가 호스트 변수 값을 포함하는가?
  7. eval_pred_comp0 위의 R_EQ_TORDER. 전순서 등치는 컴파일 시점 고정 술어 형태(IS NULL 없음, EXISTS 없음, LIST_ID 없음)이므로 eval_fnceval_pred_comp0을 돌려 준다. 그런데 eval_pred_comp0db_value_is_null (peek_val1) && et_comp->rel_op != R_NULLSAFE_EQ를 검사해 V_UNKNOWN을 돌려 준다 — R_EQ_TORDER에 NULL 피연산자가 오면 명세상으로는 비교가 명확해야 한다(전순서 아래 NULL = NULL → TRUE). 현재 동작이 의도된 것인가, 아니면 R_EQ_TORDER가 별도 경로를 필요 로 하는가?
  8. 저장 프로시저 결과 캐싱. TYPE_SP는 항상 FETCH_NOT_CONST 를 세트하고 행마다 프로시저를 재평가한다. 어떤 저장 프로시저는 인자에 대한 순수 함수라서 메모이즈(memoise)가 가능하다. 옵트인 장치가 있는가, 아니면 항상 다시 도는가?
  9. T_PREDICATE opcode에 대한 fetch_peek_arith. arith 노드가 중첩 술어(arithptr->pred)를 가질 때, 상수성은 NOT_CONST로 강제된다. 이 경로는 T_CASE나 비슷한 조건부 평가기가 쓰는 경로다. 술어가 행 사이에 캐시되는가, 아니면 다시 훑어지는가? 워커 eval_pred는 새로 호출되며, 깊게 중첩된 CASE 식에서는 비용이 문제될 수 있다.
  10. MVCC 재-fetch 아래에서의 HEAP_CACHE_ATTRINFO::cache_dbvalp 수명. 락 획득 후 행을 MVCC 재-fetch해야 할 때(SELECT ... FOR UPDATEscan_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_POSITIONqfile_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->ftype switch, 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/...)도 참조하지 않았다.

  • 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.hSCAN_PRED, SCAN_ATTRS, FILTER_INFO, QPROC_QUALIFICATION, PR_EVAL_FNC.
  • src/query/fetch.cfetch_peek_dbval, fetch_copy_dbval, fetch_val_list, fetch_peek_arith(약 5 KLOC. 대부분이 opcode별 산술).
  • src/query/fetch.h — fetch 입구.
  • src/query/regu_var.hppregu_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.cppmap_regu, clear_xasl_local, clear_xasl.
  • src/xasl/xasl_predicate.hpppred_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.hEXECUTE_REGU_VARIABLE_XASL, CHECK_REGU_VARIABLE_XASL_STATUS 매크로. XASL 노드 통합.
  • src/query/query_opfunc.cFUNC_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_ID fetch가 읽어 가는 속성 캐시 디코더).
  • 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.mdPRED_EXPRREGU_VARIABLE 트리를 짓는 클라이언트 측 XASL 생성기. 관습적으로 우선형 AND/OR 체인을 만든다.
  • knowledge/code-analysis/cubrid/cubrid-semantic-check.md — XASL 생성 전에 도는 타입 검사와 상수 폴딩 패스. 모든 regu에 도메인을 세트하고 컴파일 시점 상수를 접는다.
  • knowledge/code-analysis/cubrid/cubrid-heap-manager.mdHEAP_CACHE_ATTRINFOheap_attrinfo_read_dbvalues. TYPE_ATTR_ID fetch 경로가 읽는 버퍼.
  • 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판 — 표현식 트리와 트리 워킹 인터프리터.