콘텐츠로 이동

(KO) PostgreSQL 표현식 평가 — 평탄화 스텝 배열, 디스패치 루프, 그리고 빠른 경로

목차

모든 관계형 엔진에는 **표현식 평가기(expression evaluator)**가 있다. 스칼라 표현식 — 비교 연산, 함수 호출, 상수, 컬럼 참조 — 을 받아 주어진 튜플에서 그 값을 계산하는 컴포넌트다. 평가기는 질의당 수백만 번 호출되며 스캔 필터, 조인 술어, 프로젝션, 집계 전이 함수의 핫 패스에 위치한다. 평가기 성능이 질의 처리량 상한을 직접 결정한다.

Database System Concepts(Silberschatz, 7판, 15장 “Query Processing”)는 표현식 평가를 파이프라인 질의 실행의 맥락에서 다룬다. 튜플이 연산자 트리를 따라 흐를 때 각 연산자는 입력 튜플에 표현식을 적용한다. SELECT는 프로젝션 표현식을, WHERE는 술어 표현식을, 해시 조인은 해시 함수 표현식을 적용한다. 교과서는 이를 “튜플 속성을 인자로 함수를 평가한다”는 단순한 구조로 기술하지만, 실제 구현 과제는 SQL 표현식이 재귀적이고(표현식이 하위 표현식을 포함) 가변 인자를 가지며 nullable하다는 데 있다. SQL 삼치 논리(three-valued logic)는 모든 값이 Datum과 null 플래그를 함께 가져야 함을 의미한다.

Database Internals(Petrov, 7장 “Query Processing”)는 두 가지 평가 전략을 대비한다.

  1. 재귀 트리 순회. 표현식을 노드 트리로 표현하고, 루트에 가상 eval()을 호출해 자식으로 재귀한다. 개념적으로 단순하지만 노드마다, 튜플마다 함수 호출·포인터 디스패치 비용을 치른다.

  2. 컴파일된 평탄 바이트코드. 플랜 시점에 표현식 트리를 선형 명령어 시퀀스로 변환한다. 런타임에는 그 시퀀스를 타이트한 루프로 실행한다. 재귀 없고, 노드별 디스패치 없고, 분기 예측이 더 잘 된다. 컴파일러가 산술 표현식 코드를 생성하는 방식이며, 현대 데이터베이스 엔진이 점차 채택하는 방향이다.

PostgreSQL의 현재 표현식 평가기(PG 10 도입, 구 ExprContext 재귀 모델 교체)는 두 번째 방향을 따른다. 컴파일러 단계(ExecInitExpr, ExecInitExprRec)가 플래너의 Expr 트리를 평탄화된 ExprEvalStep 배열로 변환하고, 런타임 단계 (ExecInterpExpr)가 그 배열을 디스패치 루프로 순회한다. 실행기 README는 설계 동기를 이렇게 요약한다.

“하나의 Expr 노드를 평가하는 데 필요한 작업은 대개 충분히 작아서, 평가 중 트리 순회 오버헤드가 눈에 띄게 크다 … 평탄화 표현은 단일 함수 내에서 비재귀적으로 평가할 수 있어 스택 깊이와 함수 호출 오버헤드를 줄인다 … 이 표현은 빠른 인터프리티드 실행과 네이티브 코드 컴파일 모두에 사용할 수 있다.”

마지막 문장이 핵심이다. 동일한 평탄 스텝 배열이 LLVM JIT 컴파일러 (jit/llvm/)가 동작하는 기반이기도 하다. 인터프리티드 경로와 JIT 경로가 통합된 표현 방식을 공유한다는 점이다.

대부분의 프로덕션 질의 엔진은 컴파일 단계(플랜 시점 또는 실행기 시작)와 실행 단계(튜플당)를 분리한다. 컴파일 단계에서 비용이 드는 작업 — 타입 조회, 함수 결정, null 처리 방식 분석, 인자 슬롯 할당 — 을 한 번 처리하고, 실행 단계는 준비된 상태를 재사용한다. 이 방식으로 플랜 시점 오버헤드를 스캔 내 모든 튜플에 분산시킨다.

실행 단계 내에서는 디스패치 방식에 따라 엔진이 갈린다.

  • 가상 함수(vtable) 디스패치. 각 표현식 노드 타입이 메서드 포인터를 가진다. node->ops->eval(node, ctx) 형태로 디스패치한다. PG 10 이전 PostgreSQL과 많은 내장형 표현식 엔진이 채택했다. 구현이 직관적이지만 노드마다 간접 분기 비용을 치른다.

  • switch 기반 디스패치. 정수 옵코드가 switch 문을 구동한다. 컴파일러가 점프 테이블을 생성하고, 모든 디스패치가 하나의 간접 분기 지점을 거친다. 노드별 vtable 디스패치보다 오버헤드가 낮지만, 분기 지점이 한 곳이어서 분기 예측이 저하된다.

  • computed-goto(직접 스레딩) 디스패치. 각 옵코드 정수를 해당 레이블 주소로 교체한다(GCC 확장). 각 명령어가 다음 명령어 주소로의 computed goto로 끝난다. 디스패치가 여러 분기 지점에서 발생해 CPU 분기 예측기가 각 옵코드마다 더 많은 맥락을 얻는다. JIT 없이 포터블하게 달성할 수 있는 가장 빠른 디스패치 전략이다. PostgreSQL은 HAVE_COMPUTED_GOTO가 정의된 GCC/Clang에서 이 방식을 사용한다.

  • JIT 네이티브 코드 생성. LLVM 등이 스텝 시퀀스를 기계어로 낮춰 인터프리터 루프 자체를 제거한다. PostgreSQL은 PG 11+에서 인트리 LLVM JIT를 지원하며 jit = on으로 활성화한다.

null 처리 계약은 범용적이다. 모든 표현식 결과는 (Datum value, bool isnull) 쌍이다. strict 함수는 인자 중 하나라도 NULL이면 실제 함수를 호출하지 않고 NULL을 반환한다. 평가기는 이를 효율적으로 강제해야 한다. PostgreSQL은 함수 호출 옵코드를 특화해 해결한다. EEOP_FUNCEXPR_STRICT_1EEOP_FUNCEXPR_STRICT_2는 가장 흔한 1인자·2인자 strict 경우를 루프 대신 인라인 null 검사로 처리한다.

별도로 실행 가능한 모든 표현식은 하나의 ExprState 노드로 표현된다. ExprState의 핵심 필드는 다음과 같다.

// ExprState — src/include/nodes/execnodes.h (condensed)
typedef struct ExprState
{
NodeTag type;
uint8 flags; /* EEO_FLAG_* bitmask */
bool resnull; /* result null flag (scalar expressions) */
Datum resvalue; /* result value (scalar expressions) */
TupleTableSlot *resultslot; /* result slot (projection expressions) */
struct ExprEvalStep *steps; /* flat instruction array */
ExprStateEvalFunc evalfunc; /* dispatch: JIT fn, fast-path, or ExecInterpExpr */
Expr *expr; /* original tree (debug only) */
void *evalfunc_private; /* fast-path or JIT function pointer */
int steps_len;
int steps_alloc;
struct PlanState *parent;
/* ... compilation temporaries ... */
} ExprState;

steps[]의 각 항목은 ExprEvalStep이다. 64바이트 캐시 라인 하나에 맞도록 설계된 고정 크기 구조체다.

// ExprEvalStep — src/include/executor/execExpr.h (condensed)
typedef struct ExprEvalStep
{
intptr_t opcode; /* enum ExprEvalOp, or label address after threading */
Datum *resvalue; /* where to write this step's result Datum */
bool *resnull; /* where to write this step's null flag */
union {
/* EEOP_INNER/OUTER/SCAN_FETCHSOME */
struct { int last_var; bool fixed; TupleDesc known_desc;
const TupleTableSlotOps *kind; } fetch;
/* EEOP_INNER/OUTER/SCAN_VAR */
struct { int attnum; Oid vartype; VarReturningType varreturningtype; } var;
/* EEOP_FUNCEXPR_* */
struct { FmgrInfo *finfo; FunctionCallInfo fcinfo_data;
PGFunction fn_addr; int nargs; bool make_ro; } func;
/* EEOP_BOOL_*_STEP */
struct { bool *anynull; int jumpdone; } boolexpr;
/* EEOP_QUAL / EEOP_JUMP* */
struct { int jumpdone; } qualexpr;
/* EEOP_CONST */
struct { Datum value; bool isnull; } constval;
/* ... ~30 more union arms ... */
} d;
} ExprEvalStep;

opcode 필드는 처음에 ExprEvalOp 열거값으로 저장된다. ExecReadyInterpretedExpr가 direct-threaded 모드로 동작하면 각 옵코드가 해당 EEO_CASE 블록의 GCC 레이블 주소로 교체되고, state->flagsEEO_FLAG_DIRECT_THREADED가 설정된다. 원래 열거값은 디버깅 시 ExecEvalStepOp()로 복원할 수 있다.

ExprEvalOp 열거형은 약 110개의 옵코드를 정의한다. 주요 그룹은 다음과 같다.

그룹대표 옵코드역할
슬롯 프리페치EEOP_INNER/OUTER/SCAN/OLD/NEW_FETCHSOME표현식당 슬롯별 slot_getsomeattrs 한 번 호출
Var 페치EEOP_INNER/OUTER/SCAN_VAR, EEOP_SCAN_SYSVAR프리페치된 슬롯에서 속성 하나 읽기
Var 할당EEOP_ASSIGN_INNER/OUTER/SCAN_VAR, EEOP_ASSIGN_TMPresultslot->tts_values/nulls에 쓰기
함수 호출EEOP_FUNCEXPR, EEOP_FUNCEXPR_STRICT, EEOP_FUNCEXPR_STRICT_1, EEOP_FUNCEXPR_STRICT_2, EEOP_FUNCEXPR_FUSAGE사전 연결된 fcinfo로 fmgr 함수 호출
불리언EEOP_BOOL_AND/OR_STEP_FIRST/LAST, EEOP_BOOL_NOT_STEP, EEOP_QUAL단락 AND/OR/NOT; QUAL은 null을 false로 처리하는 AND
점프EEOP_JUMP, EEOP_JUMP_IF_NULL/NOT_NULL/NOT_TRUE하위 스텝 조건부 건너뛰기
상수EEOP_CONST사전 계산된 Datum 적재
집계EEOP_AGG_PLAIN_TRANS_*, EEOP_AGG_STRICT_*byval/byref × strict/non-strict 조합으로 인라인화된 집계 전이 스텝
종료EEOP_DONE_RETURN, EEOP_DONE_NO_RETURN스텝 루프 종료

컴파일: ExecInitExpr → ExecInitExprRec

섹션 제목: “컴파일: ExecInitExpr → ExecInitExprRec”

ExecInitExprExprState를 할당하고, FETCHSOME 설정 스텝을 삽입하고, ExecInitExprRec를 재귀 호출하고, EEOP_DONE_RETURN을 추가한 뒤 ExecReadyExpr를 호출한다.

// ExecInitExpr — src/backend/executor/execExpr.c (condensed)
ExprState *
ExecInitExpr(Expr *node, PlanState *parent)
{
ExprState *state = makeNode(ExprState);
ExprEvalStep scratch = {0};
state->expr = node;
state->parent = parent;
ExecCreateExprSetupSteps(state, (Node *) node); /* emit FETCHSOME steps */
ExecInitExprRec(node, state, &state->resvalue, &state->resnull);
scratch.opcode = EEOP_DONE_RETURN;
ExprEvalPushStep(state, &scratch);
ExecReadyExpr(state);
return state;
}

ExecInitExprRec는 주 재귀 컴파일러다. Expr 노드 타입에 따라 분기하며 적합한 옵코드를 내보낸다. T_Var의 경우를 보면:

// ExecInitExprRec (T_Var branch) — src/backend/executor/execExpr.c (condensed)
case T_Var:
{
Var *variable = (Var *) node;
scratch.d.var.attnum = variable->varattno - 1; /* 0-based */
scratch.d.var.vartype = variable->vartype;
switch (variable->varno)
{
case INNER_VAR: scratch.opcode = EEOP_INNER_VAR; break;
case OUTER_VAR: scratch.opcode = EEOP_OUTER_VAR; break;
default: scratch.opcode = EEOP_SCAN_VAR; break;
}
ExprEvalPushStep(state, &scratch);
break;
}

T_FuncExpr / T_OpExpr의 경우 ExecInitFunc가 인자 연결과 옵코드 선택을 담당한다.

// ExecInitFunc — src/backend/executor/execExpr.c (condensed)
scratch->d.func.finfo = palloc0(sizeof(FmgrInfo));
scratch->d.func.fcinfo_data = palloc0(SizeForFunctionCallInfo(nargs));
fmgr_info(funcid, scratch->d.func.finfo);
InitFunctionCallInfoData(*fcinfo, flinfo, nargs, inputcollid, NULL, NULL);
scratch->d.func.fn_addr = flinfo->fn_addr; /* hot copy to avoid indirection */
/* emit argument sub-steps directly into fcinfo->args[argno] */
foreach(lc, args) {
ExecInitExprRec(arg, state,
&fcinfo->args[argno].value,
&fcinfo->args[argno].isnull); /* args land in-place */
argno++;
}
/* pick opcode based on strictness × stats tracking × nargs */
if (pgstat_track_functions <= flinfo->fn_stats) {
if (flinfo->fn_strict && nargs > 0) {
if (nargs == 1) scratch->opcode = EEOP_FUNCEXPR_STRICT_1;
else if (nargs == 2) scratch->opcode = EEOP_FUNCEXPR_STRICT_2;
else scratch->opcode = EEOP_FUNCEXPR_STRICT;
} else
scratch->opcode = EEOP_FUNCEXPR;
} else {
scratch->opcode = flinfo->fn_strict ? EEOP_FUNCEXPR_STRICT_FUSAGE
: EEOP_FUNCEXPR_FUSAGE;
}

핵심 설계 포인트가 있다. 인자 하위 스텝이 결과를 fcinfo->args[argno].value / .isnull에 직접 쓴다는 것이다. 임시 변수도, 복사도 없다. 함수 호출 스텝은 실행 시 인자가 이미 올바른 메모리에 있는 상태를 만난다.

ExecInitQual은 WHERE 절 접속사 목록을 위한 특수 ExprState를 만든다. EEOP_BOOL_AND_STEP 대신 EEOP_QUAL을 사용한다. QUAL은 NULL을 false로 처리해 anynull 누산기 없이 단락을 구현한다.

// ExecInitQual — src/backend/executor/execExpr.c (condensed)
state->flags = EEO_FLAG_IS_QUAL;
scratch.opcode = EEOP_QUAL;
foreach_ptr(Expr, node, qual)
{
ExecInitExprRec(node, state, &state->resvalue, &state->resnull);
scratch.d.qualexpr.jumpdone = -1; /* placeholder */
ExprEvalPushStep(state, &scratch);
adjust_jumps = lappend_int(adjust_jumps, state->steps_len - 1);
}
/* back-patch: all QUAL jumpdone targets point past the last step */
foreach_int(jump, adjust_jumps)
state->steps[jump].d.qualexpr.jumpdone = state->steps_len;

점프 타겟이 필요한 곳 어디에서나 동일한 역패치(back-patching) 패턴이 쓰인다. jumpdone = -1로 스텝을 먼저 내보내고, 하위 스텝들을 완성한 뒤 실제 타겟 인덱스를 채운다. 불리언 AND/OR 표현식도 같은 방식이다.

ExecBuildProjectionInfo는 타겟 리스트를 resultslot에 쓰는 ExprState로 컴파일한다. 단순 Var 항목은 EEOP_ASSIGN_*_VAR 하나로 처리한다(소스 슬롯에서 결과 슬롯으로 직접 복사). 복잡한 항목은 두 스텝 패턴을 사용한다. 표현식을 state->resvalue / resnull에 평가한 뒤, EEOP_ASSIGN_TMPresultslot->tts_values[col]에 복사한다.

ExecReadyExpr: JIT 게이트와 fast-path 선택

섹션 제목: “ExecReadyExpr: JIT 게이트와 fast-path 선택”

ExecReadyExpr는 컴파일의 마지막 단계다.

// ExecReadyExpr — src/backend/executor/execExpr.c
static void
ExecReadyExpr(ExprState *state)
{
if (jit_compile_expr(state)) /* returns true if JIT took it */
return;
ExecReadyInterpretedExpr(state);
}

ExecReadyInterpretedExpr는 먼저 단순 패턴을 검사하고 인터프리터 루프를 우회하는 전용 ExecJust* 함수를 설치한다.

패턴 (steps_len)설치되는 evalfunc
2: EEOP_CONSTExecJustConst
3: EEOP_SCAN_FETCHSOME + EEOP_SCAN_VARExecJustScanVar
3: EEOP_INNER_FETCHSOME + EEOP_INNER_VARExecJustInnerVar
3: EEOP_SCAN_FETCHSOME + EEOP_ASSIGN_SCAN_VARExecJustAssignScanVar
3: EEOP_CASE_TESTVAL + EEOP_FUNCEXPR_STRICT*ExecJustApplyFuncToCase
2: EEOP_INNER_VAR (fetch 없음)ExecJustInnerVarVirt

어떤 fast-path도 매칭되지 않으면, 각 옵코드 정수를 GCC 레이블 주소로 교체하고 (EEO_OPCODE 매크로, EEO_USE_COMPUTED_GOTO) evalfunc_private = ExecInterpExpr로 설정한다. 이후 표현식은 state->evalfunc(state, econtext, &isnull)를 호출해 평가한다. 첫 호출은 ExecInterpExprStillValid를 거쳐 스키마 변경 여부를 한 번 확인한 뒤 실제 evalfunc로 넘어간다.

ExecInterpExpr는 인터프리터 본체다. computed goto 모드에서의 구조는 다음과 같다.

// ExecInterpExpr — src/backend/executor/execExprInterp.c (condensed)
static Datum
ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
{
ExprEvalStep *op = state->steps; /* current instruction pointer */
/* ... local slot pointer cache ... */
EEO_DISPATCH(); /* jump to first instruction (computed goto or switch) */
EEO_CASE(EEOP_DONE_RETURN)
{
*isnull = state->resnull;
return state->resvalue;
}
EEO_CASE(EEOP_INNER_FETCHSOME)
{
slot_getsomeattrs(innerslot, op->d.fetch.last_var);
EEO_NEXT(); /* op++; EEO_DISPATCH() */
}
EEO_CASE(EEOP_SCAN_VAR)
{
int attnum = op->d.var.attnum;
*op->resvalue = scanslot->tts_values[attnum]; /* direct array read */
*op->resnull = scanslot->tts_isnull[attnum];
EEO_NEXT();
}
EEO_CASE(EEOP_FUNCEXPR)
{
FunctionCallInfo fcinfo = op->d.func.fcinfo_data;
Datum d;
fcinfo->isnull = false;
d = op->d.func.fn_addr(fcinfo); /* direct call via cached fn_addr */
*op->resvalue = d;
*op->resnull = fcinfo->isnull;
EEO_NEXT();
}
EEO_CASE(EEOP_FUNCEXPR_STRICT_2)
{
FunctionCallInfo fcinfo = op->d.func.fcinfo_data;
NullableDatum *args = fcinfo->args;
if (args[0].isnull || args[1].isnull) /* skip call if either NULL */
*op->resnull = true;
else {
Datum d;
fcinfo->isnull = false;
d = op->d.func.fn_addr(fcinfo);
*op->resvalue = d;
*op->resnull = fcinfo->isnull;
}
EEO_NEXT();
}
EEO_CASE(EEOP_QUAL)
{
if (*op->resnull || !DatumGetBool(*op->resvalue))
EEO_JUMP(op->d.qualexpr.jumpdone); /* false or null → short-circuit */
EEO_NEXT();
}
/* ... ~110 more cases ... */
}

EEO_NEXTop를 증가시키고 다음 스텝으로 디스패치한다. EEO_JUMPop = &state->steps[target]으로 설정하고 그 위치로 디스패치한다. EEO_DISPATCH는 direct-threaded 모드에서 goto *op->opcode이고 포터블 모드에서는 switch(op->opcode) 폴스루다.

디스패치 매크로: 하나의 소스, 두 가지 전략

섹션 제목: “디스패치 매크로: 하나의 소스, 두 가지 전략”

execExprInterp.c에서 가장 중요한 장치는, 동일한 C 소스가 HAVE_COMPUTED_GOTO 정의 여부에 따라 computed-goto 스레딩 인터프리터 또는 포터블 switch 인터프리터 중 하나로 컴파일된다는 것이다. 작은 매크로 집합이 그 차이를 감춰 모든 옵코드 본체를 한 번만 작성하게 한다.

// EEO_* dispatch macros — src/backend/executor/execExprInterp.c
#if defined(EEO_USE_COMPUTED_GOTO)
#define EEO_SWITCH()
#define EEO_CASE(name) CASE_##name:
#define EEO_DISPATCH() goto *((void *) op->opcode)
#define EEO_OPCODE(opcode) ((intptr_t) dispatch_table[opcode])
#else /* !EEO_USE_COMPUTED_GOTO */
#define EEO_SWITCH() starteval: switch ((ExprEvalOp) op->opcode)
#define EEO_CASE(name) case name:
#define EEO_DISPATCH() goto starteval
#define EEO_OPCODE(opcode) (opcode)
#endif
#define EEO_NEXT() \
do { op++; EEO_DISPATCH(); } while (0)
#define EEO_JUMP(stepno) \
do { op = &state->steps[stepno]; EEO_DISPATCH(); } while (0)

computed-goto 빌드에서 EEO_CASE(EEOP_FOO)는 레이블 CASE_EEOP_FOO:로 전개된다. EEO_SWITCH()는 아무것도 아니고, EEO_DISPATCH()는 이미 스레딩된 op->opcode(레이블 주소를 담고 있다)를 통한 goto *다. 포터블 빌드에서는 동일한 EEO_CASEswitch 안의 case EEOP_FOO:가 되고, EEO_DISPATCH()는 switch 선두로 돌아가는 goto starteval이 된다. EEO_NEXT/EEO_JUMPEEO_DISPATCH()를 직접 내장하므로, 스레딩 모드에서는 옵코드마다 다른 지점에서 다음 디스패치가 발생한다. 바로 이것이 분기 예측기에 옵코드별 이력을 제공하는 메커니즘이다.

computed-goto 테이블은 지연 방식으로 구축된다. ExecInitInterpreterExecInterpExpr(NULL, ...)을 호출하면, 인터프리터는 NULL 센티넬을 감지하고 로컬 dispatch_table[]의 주소를 반환한다. 이 배열은 ExprEvalOp 열거형 순서와 정확히 일치하도록 &&LABEL GCC 레이블 주소 값들을 담고 있다. 컴파일 타임 단언이 그 순서를 보호한다.

// ExecInterpExpr — src/backend/executor/execExprInterp.c (condensed)
#if defined(EEO_USE_COMPUTED_GOTO)
static const void *const dispatch_table[] = {
&&CASE_EEOP_DONE_RETURN,
&&CASE_EEOP_DONE_NO_RETURN,
&&CASE_EEOP_INNER_FETCHSOME,
/* ... one &&label per opcode, in enum order ... */
&&CASE_EEOP_LAST
};
StaticAssertDecl(lengthof(dispatch_table) == EEOP_LAST + 1,
"dispatch_table out of whack with ExprEvalOp");
if (unlikely(state == NULL))
return PointerGetDatum(dispatch_table);
#endif

ExecReadyInterpretedExpr는 이후 EEO_OPCODE 매크로로 각 스텝의 opcode 필드를 열거값에서 dispatch_table[opcode]로 덮어 쓰고 EEO_FLAG_DIRECT_THREADED를 설정한다. 이 재작성 이후 op->opcode에서 원본 열거값을 직접 복원하는 것은 불가능하다. PostgreSQL은 reverse_dispatch_table(ExecInitInterpreter에서 채운다)을 따로 두어 ExecEvalStepOp()가 레이블 주소를 ExprEvalOp로 역매핑할 수 있게 한다. EXPLAIN, JIT, 디버깅이 이 역매핑에 의존한다.

슬롯 설정과 호출별 레지스터 캐시

섹션 제목: “슬롯 설정과 호출별 레지스터 캐시”

ExecInterpExpr가 실제 호출에서 처음 하는 일은 ExprContext의 다섯 입력 슬롯을 로컬 변수에 캐시하는 것이다. 이렇게 하면 옵코드 본체가 스텝마다 econtext를 역참조하지 않고 로컬을 읽는다.

// ExecInterpExpr — src/backend/executor/execExprInterp.c (condensed)
op = state->steps;
resultslot = state->resultslot;
innerslot = econtext->ecxt_innertuple;
outerslot = econtext->ecxt_outertuple;
scanslot = econtext->ecxt_scantuple;
oldslot = econtext->ecxt_oldtuple; /* REL_18: RETURNING OLD */
newslot = econtext->ecxt_newtuple; /* REL_18: RETURNING NEW */
#if defined(EEO_USE_COMPUTED_GOTO)
EEO_DISPATCH(); /* jump straight to first step */
#endif
EEO_SWITCH()
{
EEO_CASE(EEOP_DONE_RETURN) { *isnull = state->resnull; return state->resvalue; }
EEO_CASE(EEOP_DONE_NO_RETURN) { Assert(isnull == NULL); return (Datum) 0; }
/* ... */

oldslot/newslotRETURNING OLD/NEW를 위한 REL-18 추가분으로, EEOP_OLD_VAR / EEOP_NEW_VAR 옵코드에 공급된다. 두 종료 옵코드의 계약도 다르다. EEOP_DONE_RETURN은 Datum을 산출하는 스칼라 표현식에, EEOP_DONE_NO_RETURNEEOP_ASSIGN_*이 이미 resultslot에 출력을 기록한 프로젝션 형 표현식에 쓰인다. 후자는 호출자가 반환 Datum을 요청하지 않았음을 Assert(isnull == NULL)으로 확인한다.

단락 불리언과 런타임 점프 역패치 활용

섹션 제목: “단락 불리언과 런타임 점프 역패치 활용”

컴파일 시점 jumpdone 역패치(앞서 EEOP_QUAL에서 설명)는 불리언 옵코드 런타임에서 효과를 발휘한다. EEOP_BOOL_AND_STEP은 null 플래그를 누산하면서 hard FALSE를 만나는 순간 사전 계산된 AND 종료 타겟으로 점프한다.

// ExecInterpExpr EEOP_BOOL_AND_STEP — src/backend/executor/execExprInterp.c
EEO_CASE(EEOP_BOOL_AND_STEP)
{
if (*op->resnull)
*op->d.boolexpr.anynull = true;
else if (!DatumGetBool(*op->resvalue))
EEO_JUMP(op->d.boolexpr.jumpdone); /* hard FALSE: skip rest */
EEO_NEXT();
}

_FIRST 변형은 첫 번째 접속사 앞에서 *anynull = false를 초기화하고, _LAST 변형은 누산된 NULL을 SQL 삼치 결과로 해소한다(FALSE가 없는데 NULL이 있으면 NULL 반환). EEOP_QUAL은 WHERE 절 전용 특화다. NULL을 즉시 FALSE로 접어 anynull 누산기 자체가 불필요하다.

프로젝션 스텝은 중간 Datum 없이 결과 슬롯의 병렬 배열에 직접 쓴다. 단순 Var 경우는 로드/스토어 쌍 하나로, “임시 공간을 통한 계산” 경우는 EEOP_ASSIGN_TMPstate->resvalue/resnull을 컬럼에 옮긴다.

// ExecInterpExpr EEOP_ASSIGN_TMP — src/backend/executor/execExprInterp.c
EEO_CASE(EEOP_ASSIGN_TMP)
{
int resultnum = op->d.assign_tmp.resultnum;
Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts);
resultslot->tts_values[resultnum] = state->resvalue;
resultslot->tts_isnull[resultnum] = state->resnull;
EEO_NEXT();
}

EEOP_ASSIGN_TMP_MAKE_RO는 expanded-datum(TOAST 확장) 결과를 위한 변형이다. non-null 값을 MakeExpandedObjectReadOnlyInternal로 감싸 저장함으로써, 프로젝션된 튜플이 공유 확장 객체를 실수로 변경하지 못하도록 막는다.

Fast-path 내부: ExecJust*가 루프를 우회하는 방식

섹션 제목: “Fast-path 내부: ExecJust*가 루프를 우회하는 방식”

fast-path 함수들은 단순히 “스텝 하나짜리 루프”가 아니다. FETCHSOME 스텝까지 암묵적으로 처리하도록 작업을 직접 손으로 짠(hand-rolled) 구현이다. ExecJustVarImpl(ExecJustInnerVar/OuterVar/ScanVar 공유 구현)은 스텝 0에서 슬롯 호환성을 확인한 뒤, slot_getattr로 속성을 읽는다. slot_getattr는 요구 시점에 deform하고 경계 검사도 수행한다.

// ExecJustVarImpl — src/backend/executor/execExprInterp.c
static pg_attribute_always_inline Datum
ExecJustVarImpl(ExprState *state, TupleTableSlot *slot, bool *isnull)
{
ExprEvalStep *op = &state->steps[1];
int attnum = op->d.var.attnum + 1; /* back to 1-based for slot_getattr */
CheckOpSlotCompatibility(&state->steps[0], slot);
return slot_getattr(slot, attnum, isnull);
}

*Virt 계열(ExecJustInnerVarVirt 등)은 steps_len=2 경우를 처리한다. 플래너가 슬롯이 항상 가상(virtual) 슬롯임을 증명했을 때 FETCHSOME 스텝이 아예 내보내지지 않으므로, Assert(TTS_IS_VIRTUAL(slot)) 아래 tts_values[attnum]을 직접 읽는다. ExecJustConst는 가장 단순한 경우다. 스텝 0에서 op->d.constval.value를 null 플래그 한 번 저장과 함께 반환한다. ExecJustApplyFuncToCaseEEOP_CASE_TESTVAL 셔플과 strict 함수 호출을 융합해, 일반 EEOP_FUNCEXPR_STRICT 경로가 수행할 인자별 NULL 스캔을 인라인으로 처리한다.

흐름도: ExecInterpExpr 디스패치 루프

섹션 제목: “흐름도: ExecInterpExpr 디스패치 루프”

첫 번째 다이어그램이 컴파일 파이프라인(트리에서 평탄 스텝으로)을 다뤘다면, 두 번째 다이어그램은 런타임 측을 확대한다. ExecInterpExpr가 어떻게 ExprEvalStep들을 이어 가는지, EEO_NEXTEEO_JUMP가 어디서 갈라지는지, 그리고 루프가 어디서 끝나는지를 보여 준다.

flowchart TD
    A[ExecEvalExpr -> state-evalfunc] --> B{first call?}
    B -->|yes| C[ExecInterpExprStillValid<br/>recheck Var vs TupleDesc]
    C --> D[swap evalfunc to<br/>fast-path or ExecInterpExpr]
    B -->|no| D
    D --> E{fast-path<br/>installed?}
    E -->|yes| F[ExecJustConst / ExecJustScanVar<br/>ExecJustApplyFuncToCase ...<br/>no loop]
    F --> Z[return Datum + isnull]
    E -->|no| G[ExecInterpExpr:<br/>op = steps; cache 5 slots]
    G --> H[EEO_DISPATCH<br/>goto label or switch]
    H --> I{opcode}
    I -->|FETCHSOME| J[slot_getsomeattrs] --> K[EEO_NEXT: op++]
    I -->|SCAN_VAR / CONST| L[read into op-resvalue] --> K
    I -->|FUNCEXPR_STRICT_2| M{either arg NULL?}
    M -->|yes| N[resnull = true] --> K
    M -->|no| O[fn_addr fcinfo] --> K
    I -->|QUAL / BOOL_AND_STEP| P{false or null?}
    P -->|yes| Q[EEO_JUMP jumpdone] --> H
    P -->|no| K
    K --> H
    I -->|DONE_RETURN| Y[isnull = resnull] --> Z
    I -->|DONE_NO_RETURN| X[result already in<br/>resultslot tts_values] --> Z

모든 ExprStateExprContext(CreateExprContext로 할당) 안에서 평가된다. 핵심 필드는 ecxt_per_tuple_memory다. 튜플 처리 후 ResetExprContext가 이 컨텍스트를 리셋한다. 표현식 평가 중 발생하는 모든 Datum 할당은 명시적으로 복사하지 않는 한 이 컨텍스트에 살아 있다. 호출자는 ExecEvalExpr를 부르기 전에 이 컨텍스트로 전환해야 한다.

// ExecEvalExpr — src/include/executor/executor.h (inline, condensed)
static inline Datum
ExecEvalExpr(ExprState *state, ExprContext *econtext, bool *isNull)
{
return state->evalfunc(state, econtext, isNull);
/* caller is already in ecxt_per_tuple_memory */
}

편의 래퍼 ExecEvalExprSwitchContext는 메모리 컨텍스트 전환을 내부에서 처리한다. ExecQual은 이 래퍼를 사용하므로 WHERE 절 평가는 호출자의 메모리 컨텍스트에 무관하게 안전하다.

flowchart TD
    A[플래너의 Expr 트리] -->|ExecInitExpr| B[ExprState\n+ steps 배열]
    B -->|ExecCreateExprSetupSteps| C[FETCHSOME 스텝\n앞에 삽입]
    C -->|ExecInitExprRec| D[노드별 EEOP_* 스텝\n생성]
    D -->|ExecInitFunc| E[fcinfo 인자\n제자리 연결]
    D -->|ExecInitQual| F[EEOP_QUAL 스텝\n+ 점프 역패치]
    B -->|ExecReadyExpr| G{jit_compile_expr?}
    G -->|yes| H[LLVM JIT\n네이티브 코드]
    G -->|no| I[ExecReadyInterpretedExpr]
    I -->|fast-path 매칭| J[ExecJust*\n루프 우회]
    I -->|미매칭 + computed goto| K[옵코드를 레이블 주소로 교체\nEEO_FLAG_DIRECT_THREADED]
    I -->|미매칭 + switch| L[표준 switch\n디스패치]
    K --> M[ExecInterpExpr\n디스패치 루프]
    L --> M
    M -->|EEO_NEXT| M
    M -->|EEOP_DONE_RETURN| N[Datum 결과]

ExecInitExpr — 스칼라 표현식 진입점. ExprState를 할당하고 설정·재귀· 준비 단계를 호출한다. ExecInitExprWithParams는 부모 PlanState 없이 독립 실행되는 표현식에 사용하는 변형이다.

ExecInitQual — WHERE 절 접속사 목록 진입점. EEO_FLAG_IS_QUAL을 설정하고 EEOP_QUAL 옵코드로 null-as-false 의미론을 구현한다.

ExecInitCheck — CHECK 제약 접속사 목록 진입점. 암묵적 AND 목록을 명시적 AND 노드로 변환해 NULL이 TRUE로 처리되게 한다(SQL CHECK 의미론이 WHERE와 다르다).

ExecInitExprListList의 각 항목에 ExecInitExpr를 호출하고 ExprState * 리스트를 반환한다.

ExecBuildProjectionInfotargetList를 결과 TupleTableSlot에 쓰는 ExprState로 컴파일한다. ExprStateProjectionInfo 안에 내장한다(일반 프로젝션에서 추가 palloc 없음).

ExecInitExprRec — 재귀 컴파일러. NodeTag로 분기. 모든 Expr 하위 타입을 처리한다: T_Var, T_Const, T_Param, T_FuncExpr, T_OpExpr, T_BoolExpr, T_CaseExpr, T_CoerceViaIO, T_SubscriptingRef, T_FieldStore, T_ScalarArrayOpExpr, T_SubPlan, T_Aggref, T_WindowFunc 등.

ExecInitFuncFmgrInfo + FunctionCallInfoBaseData를 할당하고 fmgr_info를 호출하며, 인자 하위 표현식을 연결하고 적합한 EEOP_FUNCEXPR_* 옵코드를 선택한다.

ExecCreateExprSetupStepsexpr_setup_walker로 표현식 트리를 순회해 각 튜플 슬롯에서 필요한 최대 속성 번호를 파악하고, 배열 앞에 EEOP_*_FETCHSOME 스텝을 내보낸다.

ExprEvalPushStepExprEvalStep 하나를 state->steps에 추가한다. 공간이 부족하면 배열 전체를 repalloc한다. repalloc이 배열을 이동할 수 있으므로 컴파일 중 state->steps 내부를 직접 가리키는 포인터를 유지하면 안 된다.

ExecReadyExprjit_compile_expr를 호출하고, false를 반환하면 ExecReadyInterpretedExpr를 호출한다.

ExecReadyInterpretedExpr — fast-path 감지 테이블(steps_len 2–5)을 검사한 뒤 direct-threaded 옵코드 패칭을 수행하고 evalfunc_private = ExecInterpExpr를 설정한다.

ExecInterpExpr — 주 디스패치 루프. state == NULL로 호출되면(ExecInitInterpreter 센티넬) dispatch_table 배열 주소를 반환해 옵코드 패칭에 사용되게 한다.

ExecInterpExprStillValid — 첫 번째 호출 래퍼. 컴파일된 Var 접근이 현재 튜플 디스크립터와 여전히 일치하는지 확인하고, 이후 호출은 fast-path 또는 ExecInterpExpr로 직접 전달한다.

ExecJust* 계열 — 가장 흔한 소형 표현식에 특화된 evalfunc들. ExecJustConst, ExecJustInnerVar, ExecJustOuterVar, ExecJustScanVar, ExecJustAssignInnerVar, ExecJustAssignScanVar, ExecJustInnerVarVirt, ExecJustScanVarVirt, 그리고 해시 특화 변형들(ExecJustHashInnerVar, ExecJustHashOuterVar 등).

ExecJustVarImplExecJustInnerVar / ExecJustScanVar 계열이 공유하는 구현. 슬롯 호환성을 확인한 뒤 slot_getattr로 속성을 읽는다(요구 시점 deform). ExecJustVarVirtImpl은 FETCHSOME 스텝이 없는 가상 슬롯 전용 쌍둥이다.

ExecJustAssignVarImplExecJustAssign{Inner,Outer,Scan}Var 계열이 공유하는 구현. slot_getattr로 소스 속성 하나를 resultslot->tts_values[resultnum]에 직접 복사한다. 단일 컬럼 프로젝션 fast-path다.

ExecJustApplyFuncToCase — steps_len=3 패턴 EEOP_CASE_TESTVAL + strict EEOP_FUNCEXPR_STRICT{,_1,_2}를 위한 융합 fast-path. CaseTestExpr 값을 제자리에 놓고, 인자 NULL 스캔을 인라인으로 수행한 뒤 fn_addr를 직접 호출한다.

ExecJustHashInnerVar / ExecJustHashOuterVar / *Strict / *WithIV — 해시 조인 fast-path들. ExecReadyInterpretedExpr의 steps_len 4·5 케이스에서 매칭된다(EEOP_HASHDATUM_* 옵코드). *WithIV는 초기값 씨드를 포함한다 (EEOP_HASHDATUM_SET_INITVAL).

CheckOpSlotCompatibility — cassert 빌드에서, 런타임에 제공된 슬롯이 FETCHSOME 스텝이 컴파일 시 기대한 TupleTableSlotOps와 일치하는지 단언한다. “런타임 CheckVarSlotCompatibility 생략” 최적화 뒤에 있는 저비용 가드다.

ExecInitInterpreter — 일회성 설정. ExecInterpExpr(NULL, ...)을 호출해 dispatch_table을 캡처하고, ExecEvalStepOp를 위해 reverse_dispatch_table을 채운다. EEO_FLAG_INTERPRETER_INITIALIZED로 중복 실행을 막는다.

EEO_CASE / EEO_DISPATCH / EEO_NEXT / EEO_JUMP / EEO_OPCODE — 하나의 옵코드 본체를 computed-goto 레이블(HAVE_COMPUTED_GOTO)이나 switch 케이스 중 하나로 컴파일되게 만드는 매크로 집합. EEO_NEXTop++; EEO_DISPATCH()이고, EEO_JUMP(n)op = &steps[n]; EEO_DISPATCH()다.

표현식 엔진은 모든 실행기 노드 아래에 위치한다. 주요 호출 지점은 다음과 같다.

  • ExecScan / ExecScanExtended(execScan.c) — 스캔 튜플마다 ExecQual(WHERE) 후 ExecProject(타겟 리스트) 호출.
  • ExecProject(executor.h, 인라인) — 프로젝션 ExprStateExecEvalExprNoReturnSwitchContext 호출. EEOP_ASSIGN_* 스텝이 resultslot->tts_values[]를 채운다.
  • ExecAgg(nodeAgg.c) — execExpr.cExecBuildAggTransCall로 컴파일된 EEOP_AGG_PLAIN_TRANS_* 스텝 사용.
  • ExecHashJoin / ExecHash(nodeHashjoin.c, nodeHash.c) — 해시 표현식을 ExecInitExprList로 컴파일한 ExprState 배열 사용.

위치 힌트 (2026-06-05 기준, 커밋 273fe94)

섹션 제목: “위치 힌트 (2026-06-05 기준, 커밋 273fe94)”
심볼파일대략적 라인
ExprState 구조체src/include/nodes/execnodes.h86
ExprEvalOp 열거형src/include/executor/execExpr.h66
ExprEvalStep 구조체src/include/executor/execExpr.h300
EEO_FLAG_DIRECT_THREADEDsrc/include/executor/execExpr.h31
ExecInitExprsrc/backend/executor/execExpr.c143
ExecInitQualsrc/backend/executor/execExpr.c229
ExecInitChecksrc/backend/executor/execExpr.c315
ExecBuildProjectionInfosrc/backend/executor/execExpr.c370
ExecInitExprRecsrc/backend/executor/execExpr.c919
ExecInitFuncsrc/backend/executor/execExpr.c2704
ExprEvalPushStepsrc/backend/executor/execExpr.c2678
ExecCreateExprSetupStepssrc/backend/executor/execExpr.c2883
ExecReadyExprsrc/backend/executor/execExpr.c902
ExecReadyInterpretedExprsrc/backend/executor/execExprInterp.c250
ExecInterpExprsrc/backend/executor/execExprInterp.c468
ExecInterpExprStillValidsrc/backend/executor/execExprInterp.c2295
ExecJustVarImplsrc/backend/executor/execExprInterp.c2554
ExecJustInnerVarsrc/backend/executor/execExprInterp.c2571
ExecJustScanVarsrc/backend/executor/execExprInterp.c2585
ExecJustAssignVarImplsrc/backend/executor/execExprInterp.c2592
ExecJustApplyFuncToCasesrc/backend/executor/execExprInterp.c2639
ExecJustConstsrc/backend/executor/execExprInterp.c2677
ExecJustHashInnerVarsrc/backend/executor/execExprInterp.c2840
CheckOpSlotCompatibilitysrc/backend/executor/execExprInterp.c2441
ExecInitInterpretersrc/backend/executor/execExprInterp.c2936
EEO_* 디스패치 매크로src/backend/executor/execExprInterp.c102
dispatch_table StaticAssertDeclsrc/backend/executor/execExprInterp.c606
EEOP_BOOL_AND_STEP 케이스src/backend/executor/execExprInterp.c1055
EEOP_ASSIGN_TMP 케이스src/backend/executor/execExprInterp.c879
ExecEvalExpr 인라인src/include/executor/executor.h389
ExecProject 인라인src/include/executor/executor.h479
ExecQual 인라인src/include/executor/executor.h515

커밋 273fe94, 브랜치 REL_18_STABLE 기준으로 검증했다.

  • ExprEvalOp 열거형은 EEOP_LAST로 끝난다. ExecInterpExprdispatch_table에는 lengthof(dispatch_table) == EEOP_LAST + 1을 확인하는 StaticAssertDecl이 있다. 약 618라인에서 확인됨.
  • ExecReadyInterpretedExpr의 fast-path 패턴은 steps_len 2, 3, 4, 5를 커버한다. steps_len=5 케이스는 ExecJustHashInnerVarWithIV 패턴이다 (초기값을 포함한 해시 조인 이너 Var).
  • ExecJustHashOuterVarStrict가 존재한다(steps_len=4, EEOP_HASHDATUM_FIRST_STRICT 패턴).
  • EEOP_RETURNINGEXPR 옵코드가 REL_18에 존재한다. RETURNING 절의 OLD/NEW 지원으로 추가됐다.
  • EEO_FLAG_HAS_OLD / EEO_FLAG_HAS_NEW 플래그와 EEOP_OLD_VAR / EEOP_NEW_VAR 옵코드가 존재한다. RETURNING OLD/NEW를 위한 REL_18 기능이다.
  • EEOP_MERGE_SUPPORT_FUNC가 존재한다. MERGE 지원 함수 평가를 위해 추가됐다.
  • jit_compile_exprUSE_LLVM 없이 컴파일하면 false를 반환하는 스텁이다. LLVM이 있을 때는 jit/llvm/llvmjit_expr.c에 구현된다.
  • EEO_USE_COMPUTED_GOTOHAVE_COMPUTED_GOTO가 있을 때마다 정의된다 (execExprInterp.c 상단 약 88라인). dispatch_table[] / reverse_dispatch_table[] 쌍과 goto *op->opcode EEO_DISPATCH 본체가 확인된다. 포터블 빌드는 goto starteval; switch(...)를 사용한다.
  • ExecInterpExpr(NULL, ...)ExecInitInterpreter 부트스트랩으로 PointerGetDatum(dispatch_table)을 반환한다. 센티넬 검사는 if (unlikely(state == NULL))이다.
  • ecxt_oldtuple / ecxt_newtupleExecInterpExpr에서 oldslot/newslot 로컬로 캐시된다. REL-18에서 추가됐으며 EEOP_OLD_VAR/EEOP_NEW_VAR에 공급된다.
  • XLOG2 rmgr이나 B_DATACHECKSUMSWORKER_* BackendType 없음. REL_18 확인.

미해결:

  • ExecInterpExprStillValid가 스키마 불일치를 발견하는 정확한 경계 조건 (플랜 무효화가 먼저 잡는 경우와 비교해)은 완전히 추적하지 못했다.

PostgreSQL 너머 — 비교 설계와 연구 프론티어

섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프론티어”

MonetDB(와 DuckDB 같은 후계 엔진)는 벡터화(vectorised) 모델로 표현식을 평가한다. 튜플 하나씩 평가기를 호출하는 대신, N개의 값으로 이루어진 열 지향 배치에 한 번에 작동한다. 내부 루프는 배열에 대한 산술 루프여서 SIMD 자동 벡터화가 가능하다. PostgreSQL의 평탄 스텝 모델은 행 단위로 동작한다. 병렬 실행(nodeGather / 워커 프로세스)은 도입했지만 표현식 평가기 내부 벡터화는 없다. 각 튜플이 ExecInterpExpr를 독립적으로 통과한다.

HyPer(Neumann 2011, “Efficiently Compiling Efficient Query Plans for Modern Hardware”)는 produce/consume 코드 생성 모델을 도입했다. 플랜 트리를 인터프리팅하는 대신 전체 질의를 LLVM IR로 컴파일하고, 연산자 간 구체화를 제거한 타이트한 루프를 만든다. 인터프리터 오버헤드가 전혀 없다.

PostgreSQL의 LLVM JIT(jit/llvm/)는 이 아이디어의 부분적 적용이다. jit_above_cost, jit_inline_above_cost, jit_optimize_above_cost 임계치를 넘을 때 ExprState 스텝 배열을 네이티브 코드로 JIT 컴파일한다. 플랜 노드 간 융합은 아직 없다(각 노드의 표현식이 독립적으로 컴파일됨). 그러나 복잡한 표현식에서는 스텝별 디스패치 오버헤드가 제거된다. Neon 등 클라우드 네이티브 PostgreSQL 포크들이 질의별 코드 생성 방향을 실험하고 있다.

CUBRID는 QPROC_DB_VALUE_LIST 노드 트리에 대한 가상 디스패치 eval_with_args로 표현식을 재귀 평가한다. PostgreSQL이 PG 10 이전에 사용하던 패턴과 동일하다. 노드마다 함수 호출 오버헤드를 치르지만 구현이 단순하고 확장이 쉽다. PG 10 재설계(Andres Freund 주도)는 기존 재귀 평가기가 OLTP 질의 지연 시간의 유의미한 비율을 차지한다는 프로파일링 결과에서 출발했다.

SQL의 삼치 논리(TRUE / FALSE / NULL)는 모든 표현식 스텝이 null 플래그를 가져야 함을 요구한다. PostgreSQL의 (Datum, bool isnull) 쌍은 최소한의 표현이다. 일부 시스템은 배치 단위 null 비트맵(열 지향 null 표현)을 쓰거나 Datum 도메인 안에 NULL을 센티넬 값으로 인코딩한다(임의 타입에는 위험하다). PostgreSQL의 스텝별 resvalue / resnull 쌍은 최대한 범용적이다. 임의 불투명 Datum 타입에서 동작하며 타입별 null 인코딩이 불필요하다. 특화된 EEOP_FUNCEXPR_STRICT_1/2 옵코드는 균일한 표현 아래서도 가장 흔한 null 검사 패턴을 특수 처리하면 측정 가능한 성능 이득을 얻을 수 있음을 보여 준다.

연구 프론티어: 적응형 표현식 컴파일

섹션 제목: “연구 프론티어: 적응형 표현식 컴파일”

최근 학술 연구(예: “Adaptive Execution of Compiled Queries”, ICDE 2018)는 인터프리티드 모드로 실행을 시작하다가 충분히 자주 평가되는 표현식을 JIT 컴파일 코드로 투명하게 전환하는 방식을 탐구한다. PostgreSQL은 현재 플랜 시작 시 예상 비용을 기반으로 JIT 결정을 한 번만 내린다. 실행 중 표현식별 적응형 전환은 PostgreSQL에서 열린 연구 과제로 남아 있다.

  • src/backend/executor/execExpr.c — 표현식 컴파일러
  • src/backend/executor/execExprInterp.c — 인터프리터와 fast-path
  • src/include/executor/execExpr.hExprEvalOp, ExprEvalStep, 플래그 비트
  • src/include/nodes/execnodes.hExprState, ExprContext
  • src/include/executor/executor.hExecEvalExpr, ExecProject, ExecQual 인라인
  • src/backend/executor/README — “Expression Trees”, “Expression Initialization”, “Expression Evaluation” 절
  • knowledge/research/dbms-general/database-system-concepts.md — 15장 파이프라이닝 프레임
  • knowledge/research/dbms-general/database-internals.md — 7장 질의 처리, 표현식 평가 전략
  • knowledge/code-analysis/postgres/postgres-executor.md — Volcano 반복자 모델, TupleTableSlot, ExprContext 수명
  • knowledge/code-analysis/postgres/postgres-fmgr.mdFmgrInfo, FunctionCallInfo, fmgr_info 결정 (선참조)