콘텐츠로 이동

(KO) PostgreSQL PL/pgSQL — 핸들러, PLpgSQL_function AST 컴파일, 트리 순회 실행기

목차

관계형 모델이 만들어낸 지연 문제에 대한 답이 **데이터베이스 내 절차적 언어(procedural language)**다. SQL은 선언적이고 집합 지향적이어서 루프, 지역 변수, IF, 구조적 예외 처리가 없다. “이 행을 읽고, 값을 분기해서, 저 행을 갱신하고, 조건이 만족될 때까지 반복하라”는 로직을 표현하려면 클라이언트/서버 경계를 넘어 데이터를 끌어오거나, 제어 흐름 자체를 데이터 곁으로 밀어 넣어야 한다. 저장 프로시저와 서버 측 함수는 두 번째 답이다. 백엔드 안에서 실행되는 작은 명령형 프로그램이 인프로세스(in-process) 인터페이스로 SQL을 호출하고 최종 결과만 반환한다. Database System Concepts(Silberschatz 7e, ch. 5 “Advanced SQL”)는 이를 표준 SQL/PSM 기능 집합 — DECLARE, 대입, IF/CASE/LOOP/WHILE/FOR, 커서, 조건 핸들러 — 으로 정의하며, PL/pgSQL은 Oracle PL/SQL을 모델로 한 PostgreSQL의 방언이다.

이 구현에서 핵심 질문이 생긴다. 원시 연산이 SQL인 명령형 프로그램을 어떻게 실행하는가? 세 가지 고전적 전략이 있다. 가장 무거운 방식은 프로시저를 네이티브 코드(또는 중간 바이트코드)로 미리 컴파일하는 것이고, 가장 가벼운 방식은 매 구문마다 소스를 다시 파싱하는 순수 소스 레벨 인터프리터다. 그 중간에 — 거의 모든 데이터베이스 PL이 선택하는 지점 — 이 **트리 순회 인터프리터(tree-walking interpreter)**다. 소스를 한 번 구문 노드의 추상 구문 트리(AST, abstract syntax tree)로 파싱하고 캐싱한 다음, 노드 타입에 따라 디스패치하며 재귀적으로 순회한다. AST는 파싱 비용을 이후 호출 전반에 분산시키면서 실행기를 단순하고 디버깅 가능하게 유지한다. Database Internals(Petrov 2019)는 이 파싱-한-번/실행-여러-번 패턴을 쿼리 플랜 캐싱 맥락에서 설명하며, 동일한 논리가 절차 본문에도 적용된다.

PL은 제어 흐름 인터프리터일 뿐 아니라 그 잎 노드가 SQL이다. SQL은 준비(parse, rewrite, plan) 비용이 크기 때문에 두 번째 캐싱 계층이 필수다. 내장된 각 쿼리는 한 번 플래닝하고 그 계획을 재사용하되, 아래 스키마가 바뀌면 무효화해야 한다. 이는 prepared statement를 위해 나머지 백엔드가 사용하는 캐시드 플랜 기계와 같다(postgres-portals-prepared.md 참조). 세 번째 설계 압력은 변수 바인딩이다. PL의 지역 변수가 내장 SQL 안에서 보여야 하므로(SELECT ... WHERE id = my_var), SQL 파서에 알 수 없는 식별자를 PL 데이텀(datum)으로 해석하는 훅이 필요하고, 실행기에는 해당 데이텀의 현재 값을 매개변수로 공급하는 방법이 필요하다. PL 구현의 우아함은 이 세 가지 — AST 캐싱, 플랜 캐싱, 변수 바인딩 — 를 엔진 자체의 쿼리 기계를 복제하지 않고 얼마나 깔끔하게 연결하느냐로 측정된다.

스코핑(scoping) 문제도 AST에 담아야 한다. 명령형 PL은 블록 구조의 변수 스코프(DECLARE ... BEGIN ... END, 중첩 가능, 레이블 가능)를 갖고, 해당 지점에서 내장된 SQL은 그 스코프에서 보이는 변수만 정확히 보아야 한다. 컴파일러는 파싱 중에 *네임스페이스 스택(namespace stack)*을 유지한다. 블록 진입 시 스코프를 푸시하고, 변수 선언 시 항목을 추가하며, 각 내장 쿼리는 그 지점에서 보이는 체인을 스냅샷한다. 결과적으로 실행 시 해석은 결정론적으로 어휘적 스코프를 따른다. 컴파일 타임에 스코핑을 올바르게 처리하면 런타임 인터프리터는 이름을 검색할 필요 없이 평면 배열에 직접 인덱싱한다.

마지막 이론적 복잡성은 롤백 시맨틱이 있는 예외 처리다. SQL/PSM 조건 핸들러와 PL/SQL EXCEPTION 블록은 블록 안의 구문이 오류를 발생시킬 때 핸들러가 SQLSTATE로 잡고 계속 진행할 수 있음을 약속한다. 그러나 실패한 구문의 부분적 효과는 반드시 취소되어야 한다. “블록이 시작된 이후의 구문들을 되돌리는” 깔끔한 방법은 블록을 세이브포인트(savepoint) / 중첩 트랜잭션으로 만드는 것이다. 블록에 진입할 때 세이브포인트를 설정하고, 본문을 실행하고, 오류 발생 시 세이브포인트로 롤백한 다음 핸들러로 디스패치한다. 이는 PL의 구조적 예외 기능을 엔진의 서브트랜잭션 메커니즘(postgres-xact.md)에 직접 연결한다.

서버 측 절차적 언어는 거의 항상 안정적인 호출 인터페이스 뒤에 있는 플러그형 컴포넌트로 구축된다. SQL 실행기에 하드코딩된 부분이 아니다. 반복되는 아키텍처는 네 가지 구성 요소를 갖는다.

  1. 카탈로그에 등록된 언어 핸들러. 엔진은 함수 호출 ABI(“인수 튜플과 결과 슬롯이 여기 있다”)를 정의한다. CREATE LANGUAGE(또는 내장 등록)는 해당 언어로 작성된 함수가 호출될 때 함수 매니저가 호출하는 C 진입점을 명명한다. 핸들러는 브리지다. 제네릭 호출을 받아 프로시저의 소스를 조회하고 실행한다. PostgreSQL이 PL/Python, PL/Perl, PL/Tcl, 서드파티 PL을 일반 확장으로 제공할 수 있는 이유가 이것이다.

  2. 캐시된 AST를 만드는 컴파일 단계. 첫 호출(또는 CREATE FUNCTION 시 유효성 검사 패스)에서 프로시저 본문을 내부 표현으로 파싱하고 함수 OID를 키로 캐싱한다. 파싱 비용은 백엔드당, 함수당 한 번만 지불한다.

  3. 인프로세스 SQL 인터페이스. PL은 SQL의 파싱, 플래닝, 실행을 재구현하지 않는다. 얇은 내부 API를 호출한다. PostgreSQL의 경우 **SPI(Server Programming Interface)**가 일반적인 parse/plan/execute 파이프라인과 캐시드 플랜 계층을 감싼다. PL은 결과 행과 처리된 개수를 돌려받는다. PlannedStmt나 실행기 노드를 직접 다루지 않는다.

  4. 데이텀(datum) / 매개변수 브리지. 지역 변수는 PL이 소유하는 배열에 있다. 내장 SQL에서 보이게 하려면 식별자 참조를 플레이스홀더 매개변수로 변환하는 파서 훅과, 실행기가 플레이스홀더를 평가할 때 라이브 값을 공급하는 매개변수 페치 훅을 설치한다.

좋은 구현과 단순한 구현을 구분하는 설계 긴장은 다음과 같다.

  • 재파싱 대 캐싱. 단순한 PL은 호출마다 본문을 다시 파싱한다. 좋은 구현은 AST와 쿼리별 플랜을 캐싱하고 플랜 무효화를 처리한다.
  • 항상 실행기를 통하는 경로 대 빠른 경로 표현식. 모든 스칼라 표현식(x := a + 1)을 전체 SQL 실행기로 라우팅하면 정확하지만 느리다. 성숙한 PL은 테이블 없는 단순 표현식을 특수 처리해 SPI/실행기 오버헤드를 건너뛰고 표현식 인터프리터를 직접 실행한다.
  • 평면 오류 대 구조적 핸들러 + 롤백. EXCEPTION WHEN ... THEN을 지원하려면 핸들러가 실행되기 전에 부분 작업이 롤백되도록 서브트랜잭션/세이브포인트 기계에 연결해야 한다.

PostgreSQL의 PL/pgSQL은 네 구성 요소 모두의 교과서적 예시이며, 위의 모든 긴장에서 “좋은 구현” 선택을 한다.

PL/pgSQL은 핸들러 계약에 맞는 세 C 함수를 등록하는 로더블 모듈(plpgsql.so)이다. 호출 핸들러(plpgsql_call_handler), 익명 DO 블록용 인라인 핸들러(plpgsql_inline_handler), 그리고 CREATE FUNCTION이 본문의 구문을 검사하는 유효성 검사기(plpgsql_validator)다. 본문 텍스트 자체는 다른 함수의 소스처럼 pg_proc.prosrc에 저장된다.

// plpgsql_call_handler — src/pl/plpgsql/src/pl_handler.c
SPI_connect_ext(nonatomic ? SPI_OPT_NONATOMIC : 0);
/* Find or compile the function */
func = plpgsql_compile(fcinfo, false);
...
func->cfunc.use_count++;
PG_TRY();
{
if (CALLED_AS_TRIGGER(fcinfo))
retval = PointerGetDatum(plpgsql_exec_trigger(func, ...));
else if (CALLED_AS_EVENT_TRIGGER(fcinfo))
plpgsql_exec_event_trigger(func, ...);
else
retval = plpgsql_exec_function(func, fcinfo,
NULL, NULL,
procedure_resowner, !nonatomic);
}
PG_FINALLY();
{ func->cfunc.use_count--; func->cur_estate = save_cur_estate; ... }
PG_END_TRY();
SPI_finish();

구조가 전체 이야기를 담는다. SPI에 연결하고, 함수를 컴파일하거나 가져오고, use-count를 증가시켜 아래에서 해제되지 않도록 보호하고, PG_TRY 안에서 트리거/이벤트-트리거/일반-함수 실행기로 디스패치하고, SPI 연결을 해제한다. nonatomic 플래그는 CALL로 프로시저를 호출할 때 트랜잭션 제어를 허용하는 컨텍스트에서 설정되며, SPI_connect_ext로 흘러 들어가 프로시저 안의 COMMIT/ROLLBACK을 허용한다. use_count 규율은 재귀 및 재진입 호출에서 캐시된 컴파일 함수를 안전하게 재사용하기 위한 것이다.

유효성 검사기CREATE FUNCTION 시점 경로다. 의사 타입(pseudotype) 인수/반환값을 거부하고(합법적인 TRIGGER, RECORD, VOID, 다형적 케이스 제외), check_function_bodies가 켜진 경우 가짜 fcinfo를 설정하고 본문을 테스트 컴파일해서 구문 오류가 첫 호출이 아닌 정의 시점에 나타나도록 한다.

// plpgsql_validator — src/pl/plpgsql/src/pl_handler.c
if (check_function_bodies)
{
...
flinfo.fn_oid = funcoid;
if (is_dml_trigger) { ... fake_fcinfo->context = (Node *) &trigdata; }
else if (is_event_trigger) { ... fake_fcinfo->context = (Node *) &etrigdata; }
/* Test-compile the function */
plpgsql_compile(fake_fcinfo, true); /* forValidator = true */
...
}

인라인 핸들러DO 블록을 실행한다. 카탈로그 항목이 없으므로 plpgsql_compile_inline으로 소스 텍스트를 직접 컴파일한다. DO 블록이 COMMIT/ROLLBACK을 할 수 있으므로 트랜잭션 경계를 넘어 유지되는 전용 EStateResourceOwner를 만들고, 이후 임시 컴파일된 함수를 해제한다.

// plpgsql_inline_handler — src/pl/plpgsql/src/pl_handler.c
func = plpgsql_compile_inline(codeblock->source_text);
func->cfunc.use_count++;
...
simple_eval_estate = CreateExecutorState();
simple_eval_resowner = ResourceOwnerCreate(NULL, "PL/pgSQL DO block simple expressions");
PG_TRY();
{
retval = plpgsql_exec_function(func, fake_fcinfo,
simple_eval_estate,
simple_eval_resowner,
simple_eval_resowner, /* also the proc resowner */
codeblock->atomic);
}
PG_CATCH();
{ ... plpgsql_free_function_memory(func); PG_RE_THROW(); }
PG_END_TRY();
plpgsql_free_function_memory(func);

컴파일: 소스 텍스트에서 PLpgSQL_function으로

섹션 제목: “컴파일: 소스 텍스트에서 PLpgSQL_function으로”

plpgsql_compile은 얇은 캐시 래퍼다. funccache.ccached_function_compile에 위임하는데, 이 함수는 함수 OID(다형성의 경우 인수 타입 포함)를 키로 사용하며 캐시 미스 시에만 비용이 큰 plpgsql_compile_callback을 호출한다. 결과 포인터는 fcinfo->flinfo->fn_extra에 저장되어 같은 쿼리 안에서 반복 호출 시 해시 조회도 건너뛴다.

// plpgsql_compile — src/pl/plpgsql/src/pl_comp.c
function = (PLpgSQL_function *)
cached_function_compile(fcinfo,
fcinfo->flinfo->fn_extra,
plpgsql_compile_callback,
plpgsql_delete_callback,
sizeof(PLpgSQL_function),
false,
forValidator);
fcinfo->flinfo->fn_extra = function; /* avoid search next time */
return function;

실제 작업은 plpgsql_compile_callback에 있다. 전용 함수별 메모리 컨텍스트(func_cxt, “PL/pgSQL function”) 안에서 완전히 실행되어, 컴파일 중 모든 palloc이 함수의 수명을 갖고 컨텍스트 하나를 삭제하면 전체 트리가 회수된다. prosrc 위에 스캐너를 초기화하고, 최외곽 네임스페이스 레벨(함수 이름으로 명명, 매개변수와 FOUND 같은 특수 변수 포함)을 푸시하고, 선언된 각 인수마다 PLpgSQL_var/PLpgSQL_rec 데이텀을 만든 다음, function->action(루트 PLpgSQL_stmt_block)을 채우는 Bison 문법(plpgsql_yyparse)을 실행한다.

// plpgsql_compile_callback — src/pl/plpgsql/src/pl_comp.c
func_cxt = AllocSetContextCreate(CurrentMemoryContext,
"PL/pgSQL function", ALLOCSET_DEFAULT_SIZES);
plpgsql_compile_tmp_cxt = MemoryContextSwitchTo(func_cxt);
function->fn_signature = format_procedure(fcinfo->flinfo->fn_oid);
function->fn_cxt = func_cxt;
function->resolve_option = plpgsql_variable_conflict;
...
plpgsql_ns_init();
plpgsql_ns_push(NameStr(procStruct->proname), PLPGSQL_LABEL_BLOCK);
plpgsql_start_datums();
/* ... build a PLpgSQL_var/rec per argument, add to ns + datum list ... */

컴파일러의 두 가지 영속 출력은 구문 트리(function->action)와 데이텀 배열(function->datums[])이다. 함수가 아는 모든 변수, 행, 레코드, 레코드 필드는 정수 인덱스 dno로 참조되는 PLpgSQL_datum이다. 문법은 SQL을 플래닝하지 않는다. 각 내장 쿼리는 원시 텍스트를 담은 query 필드와 그 지점에서 보이는 네임스페이스 체인을 스냅샷한 ns 필드를 가진 PLpgSQL_expr로 캡처된다. 플래닝은 첫 실행까지 미뤄진다.

flowchart TD
  A["CREATE FUNCTION ... LANGUAGE plpgsql<br/>body stored in pg_proc.prosrc"] --> B["first call:<br/>plpgsql_call_handler"]
  B --> C["plpgsql_compile<br/>(funccache: hit? reuse)"]
  C -->|miss| D["plpgsql_compile_callback<br/>per-function MemoryContext"]
  D --> E["scanner + Bison grammar<br/>plpgsql_yyparse"]
  E --> F["PLpgSQL_function:<br/>action = stmt tree<br/>datums[] = variables"]
  C -->|hit| F
  F --> G["cached on fn_extra<br/>use_count guards reuse"]
  G --> H["plpgsql_exec_function<br/>tree-walking executor"]

PLpgSQL_datum은 공통 수퍼타입이다. 네 가지 구체적 종류가 중요하다. PLpgSQL_var(스칼라, 또는 DTYPE_PROMISE 지연 특수 변수), PLpgSQL_row(다중 컬럼 INTO와 다중 OUT 매개변수 반환에 사용되는 고정 변수 목록), PLpgSQL_rec(전체 복합/RECORD 값, *확장 레코드(expanded record)*로 저장), PLpgSQL_recfield(레코드의 한 필드, 레코드의 현재 튜플 디스크립터 기준으로 지연 해석)다.

// PLpgSQL_var — src/pl/plpgsql/src/plpgsql.h
typedef struct PLpgSQL_var
{
PLpgSQL_datum_type dtype; /* PLPGSQL_DTYPE_VAR or _PROMISE */
int dno; /* index into datums[] */
char *refname;
...
PLpgSQL_type *datatype;
/* Fields below here can change at runtime */
Datum value;
bool isnull;
bool freeval;
PLpgSQL_promise_type promise; /* PLPGSQL_PROMISE_NONE if normal var */
} PLpgSQL_var;

프로미스(promise) 메커니즘은 작지만 특징적인 최적화다. TG_NAME, TG_OP, NEW, OLD 같은 트리거 컨텍스트 변수는 프로미스 데이텀으로 선언되어 함수가 처음 읽을 때만 구체화된다. TG_TABLE_NAME을 건드리지 않는 트리거는 그것을 계산하는 비용을 치르지 않는다. 레코드는 항상 확장 형태(ExpandedRecordHeader)로 유지되어, 필드 접근과 필드 대입이 평면 튜플을 매번 분해하고 재조립하는 대신 제자리에서 변경할 수 있다.

plpgsql_exec_functionPLpgSQL_execstate를 설정하고, 함수의 데이텀을 실행별 저장소로 복사하고(재귀와 재진입이 각자의 변수 값을 갖도록), 깔끔한 CONTEXT: 역추적을 위한 에러 컨텍스트 콜백을 설치하고, 실제 인수 값을 인수 데이텀에 저장한 다음 트리를 순회한다.

// plpgsql_exec_function — src/pl/plpgsql/src/pl_exec.c
plpgsql_estate_setup(&estate, func, (ReturnSetInfo *) fcinfo->resultinfo,
simple_eval_estate, simple_eval_resowner);
estate.atomic = atomic;
...
copy_plpgsql_datums(&estate, func);
/* Store the actual call argument values into the appropriate variables */
for (i = 0; i < func->fn_nargs; i++)
{
int n = func->fn_argvarnos[i];
switch (estate.datums[n]->dtype)
{
case PLPGSQL_DTYPE_VAR:
assign_simple_var(&estate, (PLpgSQL_var *) estate.datums[n],
fcinfo->args[i].value, fcinfo->args[i].isnull, false);
... /* commandeer R/W expanded objects; force arrays to expanded form */
break;
case PLPGSQL_DTYPE_REC:
... exec_move_row_from_datum(...);
break;
}
}

인터프리터 핵심은 두 함수다. exec_stmt_block은 블록의 지역 선언 변수를 초기화하고 본문을 실행한다. exec_stmts는 구문 목록을 순회하며 stmt->cmd_type으로 switch해서 일치하는 exec_stmt_* 핸들러를 호출한다. 각 핸들러는 네 가지 리턴 코드 중 하나를 반환한다. PLPGSQL_RC_OK, PLPGSQL_RC_EXIT, PLPGSQL_RC_RETURN, PLPGSQL_RC_CONTINUE이며, 이 코드들이 재귀를 타고 올라가 EXIT/CONTINUE/RETURN과 루프 레이블 매칭을 구현한다.

// exec_stmts — src/pl/plpgsql/src/pl_exec.c
foreach(s, stmts)
{
PLpgSQL_stmt *stmt = (PLpgSQL_stmt *) lfirst(s);
estate->err_stmt = stmt;
if (*plpgsql_plugin_ptr && (*plpgsql_plugin_ptr)->stmt_beg)
((*plpgsql_plugin_ptr)->stmt_beg) (estate, stmt);
CHECK_FOR_INTERRUPTS();
switch (stmt->cmd_type)
{
case PLPGSQL_STMT_BLOCK: rc = exec_stmt_block(estate, ...); break;
case PLPGSQL_STMT_ASSIGN: rc = exec_stmt_assign(estate, ...); break;
case PLPGSQL_STMT_IF: rc = exec_stmt_if(estate, ...); break;
case PLPGSQL_STMT_FORI: rc = exec_stmt_fori(estate, ...); break;
case PLPGSQL_STMT_EXECSQL: rc = exec_stmt_execsql(estate, ...); break;
case PLPGSQL_STMT_RETURN: rc = exec_stmt_return(estate, ...); break;
... /* one case per PLpgSQL_stmt_type */
}
if (rc != PLPGSQL_RC_OK) return rc;
}

각 구문 앞의 *plpgsql_plugin_ptr 랑데부 훅은 pldebugger/프로파일러 확장이 연결하는 연결점이다(_PG_init에서 find_rendezvous_variable로 설정). 구문마다 CHECK_FOR_INTERRUPTS()를 호출하기 때문에 무한 루프도 취소할 수 있다.

flowchart TD
  S["exec_stmts(list)"] --> L{"for each stmt"}
  L --> P["plugin stmt_beg hook<br/>CHECK_FOR_INTERRUPTS"]
  P --> D{"switch cmd_type"}
  D -->|ASSIGN| A["exec_stmt_assign<br/>exec_assign_expr -> exec_assign_value"]
  D -->|IF/CASE/LOOP/FOR| C["제어 흐름 핸들러<br/>하위 블록으로 재귀"]
  D -->|EXECSQL| Q["exec_stmt_execsql<br/>SPI plan + execute"]
  D -->|BLOCK + EXCEPTION| X["exec_stmt_block<br/>subtransaction + PG_TRY"]
  D -->|RETURN| R["exec_stmt_return<br/>set estate.retval"]
  A --> RC{"rc"}
  C --> RC
  Q --> RC
  X --> RC
  R --> RC
  RC -->|RC_OK| L
  RC -->|EXIT/RETURN/CONTINUE| U["재귀를 타고 위로 전파"]

PL/pgSQL 안의 SQL: SPI와 단순 표현식 빠른 경로

섹션 제목: “PL/pgSQL 안의 SQL: SPI와 단순 표현식 빠른 경로”

PL/pgSQL은 플래너나 실행기를 직접 호출하지 않는다. 모든 내장 구문은 첫 실행 시 파서 설정 훅을 가진 SPI로 준비되는 PLpgSQL_expr이며, 이 훅이 SQL 파서로 하여금 PL/pgSQL 변수 참조를 해석하게 한다.

// exec_prepare_plan — src/pl/plpgsql/src/pl_exec.c
options.parserSetup = (ParserSetupHook) plpgsql_parser_setup;
options.parserSetupArg = expr;
options.parseMode = expr->parseMode;
options.cursorOptions = cursorOptions;
plan = SPI_prepare_extended(expr->query, &options);
SPI_keepplan(plan);
expr->plan = plan;
/* Check to see if it's a simple expression */
exec_simple_check_plan(estate, expr);

전체 SQL 구문(INSERT, 다중 테이블 SELECT 등)은 일반 경로를 통한다. exec_stmt_execsql은 플랜을 준비하고, 캐시된 CachedPlanSource의 커맨드 태그를 확인해 데이터 변경 명령인지 감지하고, 현재 데이텀 값에서 ParamListInfo를 만들어 SPI_execute_plan_with_paramlist로 실행한다. INTO STRICT가 있을 때 “너무 많은 행” 감지를 위해 최대 두 행을 요청한다.

// exec_stmt_execsql — src/pl/plpgsql/src/pl_exec.c
if (expr->plan == NULL)
exec_prepare_plan(estate, expr, CURSOR_OPT_PARALLEL_OK);
...
paramLI = setup_param_list(estate, expr);
if (stmt->into) tcount = (stmt->strict || stmt->mod_stmt || too_many_rows_level) ? 2 : 1;
else tcount = 0;
rc = SPI_execute_plan_with_paramlist(expr->plan, paramLI,
estate->readonly_func, tcount);

변수 바인딩은 우아하며, 두 절반으로 구성된다. 컴파일 타임 식별자 해석기와 런타임 값 공급자다. 준비 시점에 plpgsql_parser_setupplpgsql_param_ref/plpgsql_post_column_ref를 설치한다. 이 함수들은 PLpgSQL_expr에 스냅샷된 네임스페이스 체인(expr->ns)을 참조해서 PL/pgSQL 데이텀과 일치하는 참조를 PARAM_EXTERN Param으로 재작성한다. 핵심 불변식은 Paramparamid가 정확히 데이텀의 dno + 1이라는 점이다. 런타임에 별도의 심볼 테이블을 참조할 필요가 없다. 파라미터 ID 자체가 배열 인덱스다.

// plpgsql_param_ref — src/pl/plpgsql/src/pl_comp.c
snprintf(pname, sizeof(pname), "$%d", pref->number);
nse = plpgsql_ns_lookup(expr->ns, false, pname, NULL, NULL, NULL);
if (nse == NULL)
return NULL; /* name not known to plpgsql */
return make_datum_param(expr, nse->itemno, pref->location);

런타임에 플랜이 실행되기 전 setup_param_list는 쿼리에 매개변수가 필요한지 판단한다. 실행 상태당 공유 ParamListInfo 하나를 재사용하고(ParamExternData 배열을 구체화하지 않는다 — 값은 지연 페치된다), parserSetupArg가 활성 expr을 다시 가리키게 한다. 쿼리가 아무것도 참조하지 않으면 NULL을 반환해 plancache.c가 커스텀 플랜이 무의미하다는 것을 알도록 한다.

// setup_param_list — src/pl/plpgsql/src/pl_exec.c
if (!bms_is_empty(expr->paramnos))
{
paramLI = estate->paramLI; /* the common ParamListInfo */
paramLI->parserSetupArg = expr; /* where hook functions find the expr */
}
else
paramLI = NULL; /* no params -> no custom plan */
return paramLI;

실행기가 최종적으로 Param을 평가할 때 plpgsql_param_eval_var로 PL/pgSQL에 콜백한다. 데이텀 배열에서 라이브 값을 직접 읽는다. 복사도 중간 구조도 없다.

// plpgsql_param_eval_var — src/pl/plpgsql/src/pl_exec.c
int dno = op->d.cparam.paramid - 1;
estate = (PLpgSQL_execstate *) econtext->ecxt_param_list_info->paramFetchArg;
var = (PLpgSQL_var *) estate->datums[dno];
/* inlined version of exec_eval_datum() */
*op->resvalue = var->value;
*op->resnull = var->isnull;

스칼라 표현식의 경우 — x := a + 1, IF 조건, 루프 경계 — SPI 실행기로 라우팅하면 낭비다. PL/pgSQL은 표현식이 집계 없는, 서브링크 없는, FROM 절 없는 **테이블 없는 단일 컬럼 SELECT**로 컴파일될 때 “단순”으로 표시한다.

// exec_is_simple_query — src/pl/plpgsql/src/pl_exec.c
if (query->commandType != CMD_SELECT) return false;
if (query->rtable != NIL) return false;
if (query->hasAggs || query->hasWindowFuncs || query->hasTargetSRFs ||
query->hasSubLinks || query->cteList || query->jointree->fromlist ||
query->jointree->quals || query->groupClause || ...) return false;
if (list_length(query->targetList) != 1) return false;
return true; /* treat it as a simple plan */

단순 표현식은 exec_eval_simple_expr이 평가한다. 추출된 ExprState를 SPI 튜플 기계를 완전히 우회하고 함수의 eval ExprContext에 직접 실행한다. 미묘한 점은 캐시드 플랜 재검증이다. 단순 표현식은 로컬 트랜잭션당 CachedPlan 참조 카운트를 캐싱하고, 사용할 때마다 CachedPlanIsSimplyValid를 확인한다. DDL 무효화가 발생하면 다시 플래닝하고, 단순성을 재검사하고, 표현식이 더 이상 단순하지 않으면 일반 경로로 폴백한다. 이것이 PL/pgSQL의 가장 큰 성능 레버다. 산술 연산과 비교는 실행기 본체에 진입하지 않는다.

EXECUTE '<동적 문자열>'은 정반대다. exec_stmt_dynexecute는 문자열 표현식을 평가한 다음 플랜을 저장하지 않고 SPI_execute_extended로 실행한다. 텍스트가 호출마다 다를 수 있기 때문이다. CALL(exec_stmt_call)은 allow_nonatomic = trueSPI_execute_plan_extended를 사용하므로 호출된 프로시저가 커밋/롤백을 할 수 있고, MyProc->vxid.lxid를 전후로 비교해 트랜잭션 경계를 감지한다.

서브트랜잭션으로 구현된 예외 블록

섹션 제목: “서브트랜잭션으로 구현된 예외 블록”

BEGIN ... EXCEPTION WHEN ... END 블록은 인터프리터가 트랜잭션 매니저를 건드리는 지점이다. block->exceptions가 NULL이 아닐 때, exec_stmt_block은 내부 서브트랜잭션을 열고, 새로운 eval econtext 아래 PG_TRY에서 본문을 실행하고, 성공 시 서브트랜잭션을 해제한다. 오류 발생 시 서브트랜잭션을 롤백해 본문의 부분적 효과를 취소하고, ErrorData를 복사하고, SQLSTATE 조건 일치를 위해 핸들러 목록을 스캔한다.

// exec_stmt_block (exception path) — src/pl/plpgsql/src/pl_exec.c
BeginInternalSubTransaction(NULL);
MemoryContextSwitchTo(oldcontext);
PG_TRY();
{
plpgsql_create_econtext(estate); /* econtext tied to the subxact */
rc = exec_stmts(estate, block->body);
...
ReleaseCurrentSubTransaction(); /* commit inner subxact */
estate->eval_econtext = old_eval_econtext;
}
PG_CATCH();
{
edata = CopyErrorData();
FlushErrorState();
RollbackAndReleaseCurrentSubTransaction(); /* undo the body */
...
foreach(e, block->exceptions->exc_list)
{
PLpgSQL_exception *exception = (PLpgSQL_exception *) lfirst(e);
if (exception_matches_conditions(edata, exception->conditions))
{
assign_text_var(estate, state_var, unpack_sql_state(edata->sqlerrcode));
assign_text_var(estate, errm_var, edata->message);
estate->cur_error = edata;
rc = exec_stmts(estate, exception->action); /* run handler */
break;
}
}
if (e == NULL) ReThrowError(edata); /* no handler matched: re-raise */
}
PG_END_TRY();

이 비용은 실재한다. EXCEPTION 블록을 포함한 함수는 그 블록 진입마다 서브트랜잭션 설정/해제 비용을 지불한다. PL/pgSQL 문서가 예외 블록으로 타이트한 루프를 감싸지 말라고 경고하는 이유다. 매직 변수 SQLSTATESQLERRM은 핸들러가 실행되기 전에 캡처된 ErrorData로 채워지는 일반 데이텀(sqlstate_varno, sqlerrm_varno)이다.

flowchart TD
  E["BEGIN...EXCEPTION 블록 진입"] --> B["BeginInternalSubTransaction<br/>plpgsql_create_econtext"]
  B --> T["PG_TRY: exec_stmts(body)"]
  T -->|오류 없음| OK["ReleaseCurrentSubTransaction<br/>(내부 서브트랜잭션 커밋)"]
  T -->|오류 발생| Cat["PG_CATCH:<br/>CopyErrorData + FlushErrorState"]
  Cat --> RB["RollbackAndReleaseCurrentSubTransaction<br/>(본문 효과 취소)"]
  RB --> M{"exc_list에서<br/>SQLSTATE 일치?"}
  M -->|일치| H["SQLSTATE/SQLERRM 설정<br/>exec_stmts(handler.action)"]
  M -->|불일치| RT["ReThrowError(edata)<br/>외부로 전파"]
  OK --> Z["블록 리턴 코드 처리"]
  H --> Z

이 서브트랜잭션-per-블록 모델은 실제 세이브포인트/XID 메커니즘을 트랜잭션 매니저(postgres-xact.md)에 위임한다. PL/pgSQL은 Begin/Release/Rollback 호출과 핸들러 디스패치만 조율한다.

반환값: 스칼라, 복합, 집합 반환

섹션 제목: “반환값: 스칼라, 복합, 집합 반환”

exec_stmt_returnestate->retval/retisnull/rettype을 설정하고 PLPGSQL_RC_RETURN을 신호해서 전체 재귀를 plpgsql_exec_function까지 되감는다. 거기서 값이 선언된 반환 타입으로 강제 변환된다. 의도적 빠른 경로가 있다. RETURN 표현식이 단순 변수 참조인 경우(stmt->retvarno >= 0OUT 매개변수가 있는 함수에서 항상 참), 실행기는 exec_eval_expr을 거치지 않고 데이텀의 값을 retval에 직접 복사한다. 읽기/쓰기 확장 객체(대형 배열 등)를 평탄화된 읽기 전용 복사본 대신 저렴하게 호출자의 컨텍스트로 이전할 수 있다.

// exec_stmt_return — src/pl/plpgsql/src/pl_exec.c
if (estate->retisset)
return PLPGSQL_RC_RETURN; /* SRF: final RETURN ends tuple production */
...
if (stmt->retvarno >= 0)
{
PLpgSQL_datum *retvar = estate->datums[stmt->retvarno];
switch (retvar->dtype)
{
case PLPGSQL_DTYPE_PROMISE:
plpgsql_fulfill_promise(estate, (PLpgSQL_var *) retvar);
/* FALL THRU */
case PLPGSQL_DTYPE_VAR:
{
PLpgSQL_var *var = (PLpgSQL_var *) retvar;
estate->retval = var->value; /* R/W expanded value transfers cheaply */
estate->retisnull = var->isnull;
estate->rettype = var->datatype->typoid;
}
...
}
}

집합 반환(set-returning) PL/pgSQL 함수(RETURNS SETOF ...)는 다르게 동작한다. RETURN NEXT/RETURN QUERY는 함수를 끝내는 대신 행을 Tuplestorestate(estate->tuple_store)에 추가하고, 최종 평범한 RETURN은 완료 신호만 보낸다(위의 retisset 조기 반환). 구체화된 튜플스토어는 함수 매니저가 전달한 ReturnSetInfo로 반환된다. 집합 반환 함수가 사용하는 동일한 SFRM_Materialize 프로토콜이다. PL/pgSQL SRF가 호출자에게 행이 도달하기 전에 전체 결과를 버퍼링하는 이유가 이것이다.

.c 파일은 책임별로 명확히 나뉜다. **pl_handler.c**는 함수 매니저 경계(핸들러, 유효성 검사기, GUC, xact 콜백)다. **pl_comp.c**는 소스 텍스트를 PLpgSQL_function AST로 변환하고 네임스페이스와 파서 훅을 소유한다. **pl_exec.c**는 트리 순회 인터프리터와 SPI/단순 표현식 브리지다. **pl_funcs.c**는 네임스페이스 스택 프리미티브, AST 트리 워커, 메모리 해제, dump_* AST 프리티 프린터를 담는다. 공유 타입 어휘는 **plpgsql.h**에 있다.

  • _PG_initplpgsql.* GUC(variable_conflict, check_asserts, extra_warnings/extra_errors), xact/subxact 콜백(plpgsql_xact_cb, plpgsql_subxact_cb), PLpgSQL_plugin 랑데부 포인터를 등록한다.
  • plpgsql_call_handler — 함수 매니저가 호출하는 PG_FUNCTION_INFO_V1 진입점. SPI를 연결하고, 함수를 컴파일/페치하고, plpgsql_exec_function/plpgsql_exec_trigger/plpgsql_exec_event_trigger로 디스패치한다.
  • plpgsql_inline_handlerDO 블록을 실행한다. 인라인 컴파일, 전용 EState/ResourceOwner 생성, 실행 후 함수 해제.
  • plpgsql_validatorCREATE FUNCTION 시점 검사와 check_function_bodies 하에서의 선택적 테스트 컴파일.

컴파일과 네임스페이스 (pl_comp.c)

섹션 제목: “컴파일과 네임스페이스 (pl_comp.c)”
  • plpgsql_compile/plpgsql_compile_callback — 캐시 래퍼와 함수별 컨텍스트 AST 빌더.
  • plpgsql_compile_inlineDO 블록 컴파일 경로.
  • plpgsql_parser_setup, plpgsql_pre_column_ref, plpgsql_post_column_ref, resolve_column_ref, plpgsql_param_ref, make_datum_param — SQL 식별자를 PL/pgSQL 데이텀으로 해석하고 PARAM_EXTERN 파라미터를 내보내는 파서 훅.
  • plpgsql_build_variable, plpgsql_build_datatype, add_parameter_name, add_dummy_return — 데이텀 생성.

네임스페이스, 트리 순회, 덤프, 해제 (pl_funcs.c)

섹션 제목: “네임스페이스, 트리 순회, 덤프, 해제 (pl_funcs.c)”
  • plpgsql_ns_init, plpgsql_ns_push, plpgsql_ns_pop, plpgsql_ns_additem, plpgsql_ns_lookup, plpgsql_ns_lookup_label, plpgsql_ns_find_nearest_loop — 컴파일 타임 네임스페이스 스택 (블록 스코프 변수 해석).
  • plpgsql_stmt_typename, plpgsql_getdiag_kindname — 사람이 읽을 수 있는 이름.
  • plpgsql_statement_tree_walker_impl, plpgsql_mark_local_assignment_targets — 분석에 사용하는 범용 AST 순회.
  • plpgsql_free_function_memory, plpgsql_delete_callback — 컨텍스트 정리.
  • plpgsql_dumptreedump_* 패밀리 — DEBUG AST 프린터.
  • plpgsql_exec_function, plpgsql_exec_trigger, plpgsql_exec_event_trigger — 최상위 진입점. copy_plpgsql_datums, plpgsql_estate_setup.
  • exec_toplevel_block, exec_stmt_block, exec_stmts — 재귀 핵심.
  • exec_stmt_assign, exec_stmt_if, exec_stmt_case, exec_stmt_loop, exec_stmt_while, exec_stmt_fori, exec_stmt_fors, exec_stmt_forc, exec_stmt_foreach_a, exec_stmt_exit, exec_stmt_return, exec_stmt_return_next, exec_stmt_return_query, exec_stmt_raise, exec_stmt_assert, exec_stmt_getdiag — 구문별 핸들러.
  • exec_stmt_execsql, exec_stmt_dynexecute, exec_stmt_dynfors, exec_stmt_open, exec_stmt_fetch, exec_stmt_close, exec_stmt_perform, exec_stmt_call — SQL과 커서 구문.
  • exec_prepare_plan, setup_param_list, exec_assign_expr, exec_assign_value, exec_eval_expr, exec_run_select, exec_move_row — 표현식/대입/행 기계.
  • exec_eval_simple_expr, exec_simple_check_plan, exec_is_simple_query, exec_save_simple_expr, plpgsql_param_eval_var, plpgsql_param_compile — 단순 표현식 빠른 경로.
  • plpgsql_create_econtext, plpgsql_destroy_econtext, assign_simple_var, assign_text_var, exception_matches_conditions — 런타임 지원.

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

섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”
심볼파일
plpgsql_call_handlersrc/pl/plpgsql/src/pl_handler.c224
plpgsql_inline_handlersrc/pl/plpgsql/src/pl_handler.c316
plpgsql_validatorsrc/pl/plpgsql/src/pl_handler.c442
_PG_initsrc/pl/plpgsql/src/pl_handler.c148
plpgsql_compilesrc/pl/plpgsql/src/pl_comp.c106
plpgsql_compile_callbacksrc/pl/plpgsql/src/pl_comp.c167
plpgsql_compile_inlinesrc/pl/plpgsql/src/pl_comp.c739
plpgsql_param_refsrc/pl/plpgsql/src/pl_comp.c1056
resolve_column_refsrc/pl/plpgsql/src/pl_comp.c1083
plpgsql_ns_lookupsrc/pl/plpgsql/src/pl_funcs.c130
plpgsql_ns_find_nearest_loopsrc/pl/plpgsql/src/pl_funcs.c214
plpgsql_stmt_typenamesrc/pl/plpgsql/src/pl_funcs.c232
plpgsql_free_function_memorysrc/pl/plpgsql/src/pl_funcs.c716
plpgsql_exec_functionsrc/pl/plpgsql/src/pl_exec.c493
exec_stmt_blocksrc/pl/plpgsql/src/pl_exec.c1663
exec_stmtssrc/pl/plpgsql/src/pl_exec.c1996
exec_stmt_callsrc/pl/plpgsql/src/pl_exec.c2197
exec_stmt_forisrc/pl/plpgsql/src/pl_exec.c2696
exec_stmt_returnsrc/pl/plpgsql/src/pl_exec.c3197
exec_stmt_raisesrc/pl/plpgsql/src/pl_exec.c3725
exec_prepare_plansrc/pl/plpgsql/src/pl_exec.c4173
exec_stmt_execsqlsrc/pl/plpgsql/src/pl_exec.c4208
exec_stmt_dynexecutesrc/pl/plpgsql/src/pl_exec.c4440
exec_assign_valuesrc/pl/plpgsql/src/pl_exec.c5061
exec_eval_exprsrc/pl/plpgsql/src/pl_exec.c5665
exec_run_selectsrc/pl/plpgsql/src/pl_exec.c5753
exec_eval_simple_exprsrc/pl/plpgsql/src/pl_exec.c6019
setup_param_listsrc/pl/plpgsql/src/pl_exec.c6250
plpgsql_param_eval_varsrc/pl/plpgsql/src/pl_exec.c6672
exec_is_simple_querysrc/pl/plpgsql/src/pl_exec.c8205
plpgsql_create_econtextsrc/pl/plpgsql/src/pl_exec.c8610
PLpgSQL_datum_type (열거형)src/pl/plpgsql/src/plpgsql.h62
PLpgSQL_stmt_type (열거형)src/pl/plpgsql/src/plpgsql.h103
PLpgSQL_expr (구조체)src/pl/plpgsql/src/plpgsql.h230
PLpgSQL_var (구조체)src/pl/plpgsql/src/plpgsql.h332
PLpgSQL_rec (구조체)src/pl/plpgsql/src/plpgsql.h412
PLpgSQL_stmt_block (구조체)src/pl/plpgsql/src/plpgsql.h525
PLpgSQL_function (구조체)src/pl/plpgsql/src/plpgsql.h958
PLpgSQL_execstate (구조체)src/pl/plpgsql/src/plpgsql.h1012

/data/hgryoo/references/postgresREL_18_STABLE 커밋 273fe94 기준으로 검증했다.

  • 핸들러 트리오와 유효성 검사기. plpgsql_call_handler, plpgsql_inline_handler, plpgsql_validatorpl_handler.cPG_FUNCTION_INFO_V1 매크로와 함께 존재한다. CALLED_AS_TRIGGER/CALLED_AS_EVENT_TRIGGER 디스패치, use_count 가드, SPI_connect_ext(nonatomic ? SPI_OPT_NONATOMIC : 0) 호출이 그대로 인용되었다. PG_MODULE_MAGIC_EXT(.name = "plpgsql", .version = PG_VERSION)이 PG18 모듈 매직 형식을 확인한다.
  • 컴파일 캐시. plpgsql_compilecached_function_compile(funccache.c)에 위임한다 — PL별 캐싱의 PG17+ 통합 — plpgsql_compile_callback/plpgsql_delete_callback을 전달한다. 결과가 fcinfo->flinfo->fn_extra에 저장됨을 확인했다.
  • AST/데이텀 모델. PLpgSQL_datum_type 열거형은 정확히 VAR, ROW, REC, RECFIELD, PROMISE를 나열한다. PLpgSQL_stmt_typeexec_stmts 스위치에서 사용하는 26개 PLPGSQL_STMT_* 상수를 나열한다. PLpgSQL_var는 런타임 value/isnull/promise를 갖는다. PLpgSQL_recExpandedRecordHeader *erh를 저장한다(레코드는 항상 확장됨). 네 구조체 형태 모두 인용된 발췌와 일치한다.
  • 단순 표현식 경로. exec_is_simple_query의 거부 목록(hasAggs, hasWindowFuncs, hasTargetSRFs, hasSubLinks, cteList, 비어 있지 않은 rtable/fromlist/quals 등)과 단일 타겟 목록 요구사항이 직접 인용되었다. exec_eval_simple_expr의 per-LXID CachedPlanIsSimplyValid 재검증과 재플래닝-또는-폴백 로직이 확인되었다. plpgsql_param_eval_var는 인라인으로 estate->datums[dno]를 읽는다.
  • 예외 서브트랜잭션. exec_stmt_blockPG_TRY 하에서 BeginInternalSubTransaction(NULL)을 호출하고, PG_CATCHCopyErrorData + FlushErrorState + RollbackAndReleaseCurrentSubTransaction을 수행한 다음 block->exceptions->exc_list를 순회하며 exception_matches_conditions를 호출하고, 핸들러가 일치하지 않으면 ReThrowError(edata)를 호출한다. 그대로다.
  • PG19 전용 주장 없음. 이 문서는 REL_18 사실만 주장한다. 어떤 post-18 rmgr, BackendType, 실행기 변경도 참조하지 않는다. funccache 기반 컴파일 캐시와 PG_MODULE_MAGIC_EXT는 이 트리에 있는 PG17/PG18 기능이다.

두 가지 단순화는 의도적이다. 커서 구문(OPEN/FETCH/CLOSE, exec_stmt_forc)과 트리거/이벤트-트리거 실행기는 가이드에서 명명되었지만 발췌되지 않았다. 이들의 SPI 커서 메커니즘은 postgres-spi.md에 속하기 때문이다. 읽기/쓰기 확장 객체 대입 최적화(expr_rwopt, PLPGSQL_RWOPT_*)는 “배열이 확장 형태로 강제되는 이유” 수준에서 설명하고 모든 exec_assign_value 분기를 추적하지는 않는다.

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

섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프론티어”
  • PL/pgSQL 대 Oracle PL/SQL. PL/pgSQL의 표면 구문은 의도적으로 PL/SQL과 유사하다(블록 구조, %TYPE/%ROWTYPE, EXCEPTION WHEN). 그러나 실행 모델은 크게 다르다. Oracle은 PL/SQL을 바이트코드(또는 네이티브 컴파일의 경우 C/기계어)로 컴파일하고 전용 PL/SQL 가상 머신으로 실행하며, PL/SQL은 SQL 엔진과 긴밀히 통합된 런타임을 공유한다. PL/pgSQL은 arm’s-length SPI 경계만으로 SQL 엔진에 접근하는 순수 트리 순회 인터프리터다. 구문별 디스패치 비용(PL/pgSQL의 switch + 핸들러 호출 대 바이트코드 goto)을 측정하면 PostgreSQL이 구현 단순성과 확장으로의 PL 제공 능력을 위해 무엇을 교환하는지 정량화할 수 있다.

  • SPI 경계는 확장성 이음새다. 모든 PL이 SPI와 핸들러 ABI로 엔진과 소통하기 때문에, PostgreSQL은 코어 변경 없이 PL/Python, PL/Perl, PL/Tcl, PL/v8, 임의의 서드파티 PL을 지원한다. 이는 Stonebraker 시대의 “확장 가능한 타입 시스템” 철학(POSTGRES 설계 논문, dbms-papers/ 참조)이 언어로 확장된 것이다. 비용은 어떤 PL도 엔진이 볼 수 없는 구문 간 최적화를 할 수 없다는 것이다. 이득은 깔끔한 플러그인 모델이다.

  • 단순 표현식 빠른 경로 대 JIT. PL/pgSQL의 가장 큰 성능 이득은 스칼라 표현식을 SPI 실행기 밖에 유지하는 것이다(exec_eval_simple_expr). 자연스러운 연구 프론티어는 백엔드의 LLVM JIT(postgres-expression-eval.md, jit/)가 개별 ExprState뿐 아니라 전체 PL/pgSQL 함수 본문을 네이티브 코드로 컴파일할 수 있는지다. Oracle 네이티브 컴파일과 SQL Server의 네이티브 컴파일 저장 프로시저(Hekaton)가 하는 방식이다. PostgreSQL은 의도적으로 그렇게 하지 않는다. 대부분의 워크로드에서 인터프리터 오버헤드는 SQL 실행 비용에 비해 허용 가능하다고 판단한다. 그러나 연산 집약적인 PL/pgSQL에서는 이 가정이 깨진다.

  • 예외 블록과 서브트랜잭션 비용. EXCEPTION을 내부 서브트랜잭션에 연결하는 것은 정확하고 단순하지만, 블록당 세이브포인트 오버헤드를 부과하고 서브트랜잭션 XID를 소비한다(pg_subtrans SLRU, postgres-multixact.md/postgres-slru.md). undo-log MVCC(Oracle이나 MySQL/InnoDB 등)가 있는 엔진은 이 세금 없이 예외 처리를 제공할 수 있다. 타이트한 루프에서 BeginInternalSubTransaction 비용을 측정하고 undo 기반 엔진의 동등한 기능과 비교하면 그 교환을 정량화할 수 있다.

  • CUBRID 저장 프로시저. CUBRID는 역사적으로 외부 JVM에서 실행되고 엔진에 브리지된 Java 저장 프로시저에 의존했다. 인프로세스 트리 순회 인터프리터와는 설계 공간의 매우 다른 지점이다. 호출/마샬링 경로(CUBRID의 JVM 브리지 라운드 트립 대 PL/pgSQL의 인프로세스 SPI 호출)를 나란히 비교하면 인프로세스 실행이 지연 시간에서 무엇을 얻는지 부각된다(cubrid 트리의 CUBRID 분석 참조).

인트리 소스 파일 (REL_18_STABLE, 커밋 273fe94)

섹션 제목: “인트리 소스 파일 (REL_18_STABLE, 커밋 273fe94)”
  • src/pl/plpgsql/src/pl_handler.c_PG_init, GUC/xact 콜백 등록, 함수 매니저 핸들러 ABI를 구현하는 plpgsql_call_handler/plpgsql_inline_handler/plpgsql_validator 트리오.
  • src/pl/plpgsql/src/pl_comp.cplpgsql_compile, plpgsql_compile_callback, plpgsql_compile_inline, 파서 훅(plpgsql_parser_setup, plpgsql_param_ref, resolve_column_ref, make_datum_param), 데이텀/데이터타입 생성.
  • src/pl/plpgsql/src/pl_exec.cplpgsql_exec_function, exec_stmt_block, exec_stmts, 전체 exec_stmt_* 패밀리, SPI 브리지(exec_prepare_plan, exec_stmt_execsql, setup_param_list), 단순 표현식 빠른 경로(exec_eval_simple_expr, exec_is_simple_query, plpgsql_param_eval_var).
  • src/pl/plpgsql/src/pl_funcs.c — 컴파일 타임 네임스페이스 스택, AST 트리 워커, plpgsql_free_function_memory, dump_* AST 프린터.
  • src/pl/plpgsql/src/plpgsql.hPLpgSQL_datum_type/PLpgSQL_stmt_type 열거형, PLpgSQL_expr, PLpgSQL_var/row/rec/recfield, PLpgSQL_stmt_block, PLpgSQL_function, PLpgSQL_execstate 구조체.
  • Database System Concepts (Silberschatz, Korth, Sudarshan, 7e), ch. 5 “Advanced SQL” — SQL/PSM 함수와 프로시저, 절차적 구문, 조건 핸들러(knowledge/research/dbms-general/database-system-concepts.md).
  • Database Internals (Petrov 2019) — 파싱-한-번/실행-여러-번 플랜 캐싱과 AST + 플랜 캐싱을 동기화하는 비용 모델(knowledge/research/dbms-general/database-internals.md).
  • Stonebraker & Rowe (1986), “The Design of POSTGRES” 및 POSTGRES 데이터 모델/구현 논문 — 절차적 언어의 핸들러/SPI 플러그인 모델이 계승하는 확장 가능한 타입 시스템 계보(.omc/plans/postgres-paper-bibliography.md 인용).

형제 문서 (교차 참조 — 메커니즘은 해당 문서 소유, 여기서 중복하지 않음)

섹션 제목: “형제 문서 (교차 참조 — 메커니즘은 해당 문서 소유, 여기서 중복하지 않음)”
  • postgres-spi.md — PL/pgSQL의 모든 내장 쿼리가 라우팅하는 SPI(SPI_connect, SPI_prepare_extended, SPI_execute_plan_with_paramlist, SPI 커서).
  • postgres-fmgr.mdplpgsql_call_handler가 연결하는 V1 함수 호출 ABI(PG_FUNCTION_ARGS, fcinfo, FmgrInfo.fn_extra)와 언어 핸들러 등록.
  • postgres-portals-prepared.md — PL/pgSQL의 PLpgSQL_expr별 플랜과 단순 표현식 재검증이 의존하는 캐시드 플랜/CachedPlanSource 기계.
  • postgres-expression-eval.mdexec_eval_simple_expr이 직접 실행하는 ExprState/ExprContext 인터프리터.
  • postgres-xact.mdEXCEPTION 블록을 위해 BeginInternalSubTransaction/RollbackAndReleaseCurrentSubTransaction이 구동하는 (서브)트랜잭션 상태 기계.
  • postgres-triggers.md/postgres-event-triggers.mdplpgsql_exec_trigger/plpgsql_exec_event_trigger가 서비스하는 트리거와 이벤트-트리거 실행 경로.
  • postgres-architecture-overview.md — 축(확장성): 핸들러 ABI 뒤에 로더블 확장으로서의 플러그형 절차적 언어.