콘텐츠로 이동

(KO) PostgreSQL 파스 분석 — 원시 트리를 쿼리로 변환하기

목차

SQL 문은 문자열로 엔진에 들어와서 옵티마이저가 추론할 수 있는 구조로 프론트엔드를 빠져나가야 한다. Database System Concepts (Silberschatz, 7e, 15장 “쿼리 처리”, §15.1 “개요”)는 이것을 세 처리 단계 중 첫 번째 단계로 규정한다.

“1. 파싱과 번역. 2. 최적화. 3. 평가.” (DSC §15.1)

그러면서 1단계가 하나의 작업이 아니라, “파서”라는 단어 아래 합쳐진 구문 단계와 의미 단계라는 두 작업임을 명확히 한다.

“따라서 쿼리 처리에서 시스템이 먼저 해야 할 일은 주어진 쿼리를 내부 형식으로 변환하는 것이다. 이 변환 과정은 컴파일러 파서가 수행하는 작업과 유사하다. 내부 형식을 만드는 과정에서 파서는 사용자의 쿼리 구문을 검사하고, 쿼리에 나타나는 릴레이션 이름들이 실제로 데이터베이스에 존재하는 릴레이션 이름인지 검증하는 등의 작업을 수행한다. 시스템은 쿼리의 파스 트리 표현을 구성한 뒤, 이를 관계 대수 식으로 변환한다. 쿼리가 뷰를 참조하는 경우, 변환 단계에서는 뷰의 모든 사용을 해당 뷰를 정의하는 관계 대수 식으로 치환한다.” (DSC §15.1)

그 문장에는 두 가지 활동이 묶여 있다. 이 두 가지의 구분이 모든 프론트엔드 설계의 핵심 축이다.

  1. 구문 분석(파싱). 이 SQL 문은 형식적으로 올바른가? 토큰화하고 문법과 대조하여 파스 트리를 만든다. 이 단계는 입력 문자열만의 순수 함수다 — 문법 밖의 무언가를 참조하지 않는다. 컴파일러 교과서의 LALR 파서가 전형적인 구현이다.
  2. 의미 분석(“변환” / “검증” 절반). 이 형식적으로 올바른 문장에서 이름들이 의미하는 것이 있는가? 테이블 t가 존재하는가? 컬럼 c가 그 테이블에 속하고 타입은 무엇인가? WHERE x는 불리언인가? 이 단계는 카탈로그의 함수다 — 어떤 테이블이 존재하는지 문법은 알 수 없으므로 문법이 할 수 없는 작업이다.

교과서는 이 두 단계의 경계를 암묵적으로 남겨두지만, 이 경계가 하중을 받는 이음매다. 구문 분석은 고정되고 카탈로그 없이 재진입 가능한 루틴일 수 있다. 반면 의미 분석은 시스템 카탈로그를 읽고, 해석하는 릴레이션에 락을 취득하며, 타입이 부여되고 위치가 확정된 참조를 만들어야 한다. 거의 모든 프로덕션 엔진은 여기에 명확한 경계선을 긋고 양쪽에 별개의 중간 표현을 둔다.

  • 원시 파스 트리 — 구문적이고 이름이 미해석된, 사용자가 작성한 내용의 리터럴 형태.
  • 해석된 쿼리 트리 (“내부 형식”, DSC의 관계 대수 식) — 모든 이름이 카탈로그 객체에 결합되고 모든 식에 타입이 부여된 것.

교과서가 부수적으로 언급하는 세 번째 속성은 뷰 확장이다. “변환 단계에서 뷰의 모든 사용을 관계 대수 식으로 치환한다.” 그 치환이 의미 분석 중에 일어나는지 별도의 전용 단계에서 일어나는지는 공학적 선택이다(PostgreSQL은 별도의 리라이터로 미루는데, 다른 문서에서 다룬다). 그러나 교과서는 그것을 “내부 형식으로 변환”이라는 같은 우산 아래 분류하므로, 이 문서가 다루는 설계 공간에 포함된다.

PostgreSQL은 이 분리를 거의 그대로 구현한다. 문법(gram.y, postgres-parser.md가 담당)이 구문 단계를 처리하여 RawStmt를 만들고, 파스 분석 — 이 문서의 주제 — 이 의미 단계를 처리하여 RawStmt를 걸어 내려가며 완전히 해석된 Query를 만든다. 이 문서의 나머지 부분은 REL_18 소스에서 그 과정을 추적한다. 교과서의 “릴레이션 이름이 데이터베이스에 존재하는 릴레이션 이름인지 검증”은 카탈로그 조회와 range-table 구성이 되고, “관계 대수 식으로 변환”은 jointree와 타겟 리스트, 한정자 식을 갖춘 Query 노드가 된다.

교과서는 모델을 제시한다 — 카탈로그에서 이름을 해석하고 타입이 부여된 내부 형식을 만드는 의미 단계를 공급하는 구문 단계. 이 절에서는 프로덕션 의미 분석기에 반복해서 나타나는 공학적 관례, 즉 교과서가 암묵적으로 남겨둔 패턴들을 열거한다. 다음 절 “PostgreSQL의 접근법”에서 나오는 PostgreSQL의 구체적인 선택들은 이 공유된 공간 안에서의 한 가지 설정 집합으로 보면 된다.

”파싱된 것”과 구분되는 “해석된 것”을 위한 별도 IR

섹션 제목: “”파싱된 것”과 구분되는 “해석된 것”을 위한 별도 IR”

원시 파스 트리는 표면 구문을 반영한다. 테이블은 그냥 이름이고, 컬럼 참조는 식별자 목록(a.b.c)이며, SELECT *는 리터럴 별표다. 반면 해석된 트리는 결합된 참조를 담아야 한다. 테이블은 해석된 릴레이션들의 쿼리별 목록 안의 인덱스가 되고, 컬럼 참조는 해당 릴레이션의 n번째 속성을 가리키는 타입 있는 포인터가 되며, 별표는 사라지고 실제 컬럼들로 확장된다. 이 둘을 별개의 노드 타입으로 유지하면(원시 트리를 결과로 그 자리에서 변경하는 대신), 구문 단계는 카탈로그 없는 순수 함수로 남아 프론트엔드 라이브러리에서 실행되거나 캐시되거나 다른 카탈로그 스냅샷을 대상으로 재분석될 수 있다.

Range table: “이 쿼리가 읽는 릴레이션”의 1기반 인덱스 목록

섹션 제목: “Range table: “이 쿼리가 읽는 릴레이션”의 1기반 인덱스 목록”

“이 쿼리가 참조하는 릴레이션들”을 표현하는 반복적인 방식은 평탄한 1기반 인덱스 목록이다. 쿼리가 참조하는 모든 테이블, 서브쿼리, FROM 절 함수, 조인, VALUES 목록이 슬롯 하나씩 배정받는다. 그러면 해석된 컬럼 참조는 테이블 이름을 담는 대신 (range-table 인덱스, 속성 번호) 쌍으로 소스를 인코딩한다. 이 간접 참조 덕분에 나머지 스택이 단순해진다. 옵티마이저는 이름을 재작성하는 대신 range-table 인덱스 참조를 섞어 조인을 재정렬하고, 같은 테이블의 두 컬럼은 range-table 슬롯을 공유하며, 컬럼의 정체성은 문자열이 아닌 정수 쌍이므로 이후의 모든 변환에서 유지된다.

네임스페이스 스택으로서의 스코프

섹션 제목: “네임스페이스 스택으로서의 스코프”

이름 해석에는 여기에 무엇이 보이는지라는 개념이 필요하다. SQL 스코핑은 단순하지 않다. FROM 안의 서브쿼리는 LATERAL 키워드가 있어야만 외부 쿼리 컬럼을 볼 수 있고, 조인의 USING 컬럼은 입력 측 복사본을 가리며, 바깥 쿼리 레벨은 로컬 검색이 실패한 후에야 탐색된다. 관례는 분석기가 FROM 항목 안으로 들어갈 때 확장하고 식별자를 결합된 참조로 변환해야 할 때 참조하는 네임스페이스 구조(계층형 또는 스택형)다. x를 해석하는 것은 현재 네임스페이스에서 x라는 이름의 컬럼을 노출하는 릴레이션을 검색하고 모호성에서는 오류를 발생시키며, 로컬 검색이 실패하면 부모 스코프로 재귀하는 것을 의미한다.

구문 순서가 아닌 데이터 의존성이 주도하는 절 순서

섹션 제목: “구문 순서가 아닌 데이터 의존성이 주도하는 절 순서”

SELECT 절은 SELECT … FROM … WHERE … GROUP BY … HAVING … ORDER BY 순서로 작성되지만, 같은 순서로 분석할 수는 없다. WHERE와 타겟 리스트 모두 컬럼을 참조하므로 FROM(네임스페이스를 채우는)을 먼저 분석해야 한다. GROUP BYDISTINCT는 타겟 리스트 별칭이나 ORDER BY 정렬 항목을 참조할 수 있으므로, 타겟 리스트와 정렬 절을 먼저 만들어야 한다. 관례는 사용자가 작성한 순서가 아닌 의존성 순서로 절을 분석하고, 부분적으로 구성된 조각들(특히 커져가는 타겟 리스트)을 그것들을 확장하는 단계에 꿰는 것이다.

경계에서의 자동 이름 지정과 묵시적 타입 강제 변환

섹션 제목: “경계에서의 자동 이름 지정과 묵시적 타입 강제 변환”

거의 모든 분석기가 공통으로 수행하는 두 가지 관례가 있다. 첫째, 출력 컬럼 이름 지정: 이름 없는 타겟 리스트 식(SELECT a+1)에도 컬럼 레이블이 필요하므로, 분석기가 식의 형태에서 이름을 유도한다(함수 이름, 컬럼 이름 또는 대체 이름). 둘째, 문맥 주도 강제 변환: WHERE, HAVING, CHECK 식은 불리언이어야 하고, LIMIT는 정수여야 하며, UNION의 두 팔은 타입을 공유해야 한다. 분석기는 이 잘 알려진 경계에서 묵시적 캐스트를 적용하고 캐스트가 없으면 오류를 낸다.

다음 절에서 PostgreSQL 심볼 이름을 마주칠 때, 그것이 어떤 종류의 것인지 이미 알고 있어야 한다.

이론 / 관례PostgreSQL 이름
구문 단계 출력 (원시 파스 트리)gram.yRawStmtSelectStmt / InsertStmt / … 래핑
의미 단계 (이 문서)파스 분석 — transformStmttransform* 트리
해석된/내부 형식 (관계 대수)Query 노드 (commandType, jointree, targetList, …)
쿼리별 분석 스크래치 상태ParseState (pstate)
Range table (릴레이션 인덱스 목록)pstate->p_rtableRangeTblEntry 목록; 인덱스가 varno
Range-table 항목 하나addRangeTableEntryForRelation 등이 만드는 RangeTblEntry
결합된 컬럼 참조(varno, varattno, vartype, varcollid) 담은 Var
이름 가시 스코프pstate->p_namespaceParseNamespaceItem (nsitem) 목록
컬럼별 해석 데이터각 nsitem의 p_nscolumns 안의 ParseNamespaceColumn
쿼리 FROM/조인 구조pstate->p_joinlistQuery.jointree (FromExpr)
타겟 리스트 컬럼 슬롯 카운터pstate->p_next_resno → 각 TargetEntry.resno
별표 확장ExpandColumnRefStar / ExpandAllTables
출력 컬럼 자동 이름 지정FigureColname
절 경계에서 불리언/정수 강제 변환coerce_to_boolean, coerce_to_specific_type
테이블 카탈로그 이름 조회transformTableEntryaddRangeTableEntry (릴레이션을 열고 락 취득)

구문 단계 — gram.yRawStmt를 만드는 방법과 카탈로그 없는 이유 — 는 postgres-parser.md가 담당한다. 뷰 확장과 규칙 적용(DSC의 “뷰 사용을 치환”)은 리라이터(postgres-rewriter.md)가 담당하는 별도 단계에서 일어난다. 식 내부 해석 — transformExprA_ExprOpExpr로 변환하고 함수 호출을 해석하며 상수를 폴딩하는 방법 — 은 postgres-parser.md의 식 절이 대부분 담당하며, 이 문서는 transformExpr을 타입이 부여된 Node를 반환하는 블랙박스로 취급한다. 완성된 Query로 플래너가 무엇을 하는지는 postgres-planner-overview.md가 담당한다. 이 문서는 변환 프레임워크를 다룬다: ParseState, range-table과 RTE 구성, FROM/JOIN, 타겟 리스트, GROUP/HAVING/ORDER 해석, 그리고 절들이 Query로 합쳐지는 방식.

PostgreSQL의 접근법 {#postgresqls-approach}

섹션 제목: “PostgreSQL의 접근법 {#postgresqls-approach}”

PostgreSQL 파스 분석은 원시 트리 위에서 상호 재귀하는 transform* 함수 패밀리로 구현된 의미 단계다. 그 형태를 결정하는 네 가지 설계 결정이 있다.

  1. 하나의 가변 ParseState가 패스 전체를 꿰고 있다. 누산기를 호출 스택 위로 반환하는 대신, 분석기가 모든 transform* 함수가 읽고 변경하는 pstate를 가지고 다닌다. range-table이 p_rtable에서 자라고, 가시 스코프는 p_namespace에서, 조인 구조는 p_joinlist에서, 다음 타겟 리스트 슬롯은 p_next_resno에서 자란다.
  2. 원시 …Stmt 노드와 해석된 Query 노드는 별개의 타입이다. transformSelectStmtSelectStmt(구문적)를 소비하고 Query(해석된)를 만든다. 원시 트리는 결과로 변경되지 않으며, 두 트리는 노드 태그가 다르다.
  3. 절들은 구문 순서가 아닌 의존성 순서로 변환된다. FROM이 가장 먼저(네임스페이스를 채우므로), 타겟 리스트와 ORDER BYGROUP BY / DISTINCT 전에(이들이 재사용하므로), 콜레이션 배정과 집계 합법성 검사가 마지막으로(전체 트리가 필요하므로).
  4. 노드 태그에 대한 단일 디스패치. transformStmt가 원시 문의 노드 태그로 스위치하여 transformSelectStmt, transformInsertStmt 등을 선택한다. “최적화 가능”하지 않은 것은 CMD_UTILITY Query로 감싸져 변환 없이 통과된다.

파스 분석은 parse_analyze_fixedparams(그리고 다른 파라미터 처리 방식의 _varparams / _withcb 변형)로 진입한다. 각각 새 최상위 ParseState를 만들고, 소스 텍스트를 기록하고(오류 커서 위치 목적), transformTopLevelStmt를 호출한다.

// parse_analyze_fixedparams — src/backend/parser/analyze.c (condensed)
ParseState *pstate = make_parsestate(NULL);
Query *query;
pstate->p_sourcetext = sourceText;
if (numParams > 0)
setup_parse_fixed_parameters(pstate, paramTypes, numParams);
pstate->p_queryEnv = queryEnv;
query = transformTopLevelStmt(pstate, parseTree);
// ... JumbleQuery, post_parse_analyze_hook ...
free_parsestate(pstate);
return query;

transformTopLevelStmt는 문 위치 데이터를 복사하고 최상위 전용 SELECT … INTOCREATE TABLE AS 재작성을 허용하기 위해서만 존재한다. 그런 다음 디스패처인 transformStmt로 넘어간다.

// transformStmt — src/backend/parser/analyze.c (condensed)
switch (nodeTag(parseTree))
{
case T_InsertStmt:
result = transformInsertStmt(pstate, (InsertStmt *) parseTree);
break;
case T_DeleteStmt:
result = transformDeleteStmt(pstate, (DeleteStmt *) parseTree);
break;
case T_UpdateStmt:
result = transformUpdateStmt(pstate, (UpdateStmt *) parseTree);
break;
case T_SelectStmt:
{
SelectStmt *n = (SelectStmt *) parseTree;
if (n->valuesLists)
result = transformValuesClause(pstate, n);
else if (n->op == SETOP_NONE)
result = transformSelectStmt(pstate, n);
else
result = transformSetOperationStmt(pstate, n);
}
break;
// ... DeclareCursor, Explain, CreateTableAs, Call ...
default:
/* not optimizable: wrap a CMD_UTILITY Query around it */
result = makeNode(Query);
result->commandType = CMD_UTILITY;
result->utilityStmt = (Node *) parseTree;
break;
}

default 분기가 교과서의 “등”이다. CREATE TABLE, GRANT, 대부분의 DDL은 여기서 의미 분석이 필요 없다(분석이 있다면 parse_utilcmd.c에 따라 실행 시점에 수행된다). 서브쿼리 재귀는 형제 진입점인 parse_sub_analyze를 사용하는데, 이것은 parentParseState로 부모에 연결된 자식 ParseState를 만든다. 이 연결이 내부 쿼리가 외부 컬럼을 해석할 수 있게 한다(상관 참조).

flowchart TB
  RAW["RawStmt<br/>(from gram.y — syntactic only)"]
  PA["parse_analyze_fixedparams<br/>make_parsestate(NULL)"]
  TTLS["transformTopLevelStmt<br/>(SELECT INTO rewrite, location)"]
  TS["transformStmt<br/>(switch on nodeTag)"]
  RAW --> PA --> TTLS --> TS
  TS -->|T_SelectStmt| TSEL["transformSelectStmt"]
  TS -->|T_InsertStmt| TINS["transformInsertStmt"]
  TS -->|T_UpdateStmt| TUPD["transformUpdateStmt"]
  TS -->|T_DeleteStmt| TDEL["transformDeleteStmt"]
  TS -->|other| UTIL["CMD_UTILITY Query<br/>(passed through)"]
  TSEL --> Q["Query<br/>(resolved internal form)"]
  TINS --> Q
  TUPD --> Q
  TDEL --> Q

그림 1 — 디스패치 깔때기. 문법에서 나온 카탈로그 없는 RawStmtparse_analyze_fixedparams에 진입하면, ParseState를 할당하고 transformStmt에서 노드 태그로 디스패치한다. 최적화 가능한 문은 실제 의미 변환을 받고, 나머지는 CMD_UTILITY Query로 감싸져 통과된다. 모든 분기가 해석된 Query를 만든다. (출처: analyze.c.)

ParseState — 패스 전체를 꿰는 스크래치패드

섹션 제목: “ParseState — 패스 전체를 꿰는 스크래치패드”

모든 transform* 함수는 첫 번째 인수로 ParseState *pstate를 받는다. 이것이 분석기의 작업 메모리다. 누적되는 range-table, 현재 이름 가시 스코프, 구성 중인 조인 구조, 어떤 구성이 나타났는지 기록하는 플래그들, 상관 참조를 위해 부모 쿼리 레벨로의 링크가 여기에 있다.

// struct ParseState — src/include/parser/parse_node.h (condensed)
struct ParseState
{
ParseState *parentParseState; /* stack link to outer query */
const char *p_sourcetext; /* for error cursor positions */
List *p_rtable; /* range table so far (RangeTblEntry list) */
List *p_rteperminfos; /* permission info, one per RTE_RELATION */
List *p_joinlist; /* join items -> Query.jointree fromlist */
List *p_namespace; /* currently-referenceable RTEs (nsitems) */
bool p_lateral_active; /* are LATERAL-only names visible now? */
List *p_ctenamespace; /* visible WITH items */
Relation p_target_relation; /* INSERT/UPDATE/DELETE/MERGE target */
ParseExprKind p_expr_kind; /* what kind of expression we're in */
int p_next_resno; /* next TargetEntry.resno, from 1 */
/* flags recording what the query contains: */
bool p_hasAggs;
bool p_hasWindowFuncs;
bool p_hasTargetSRFs;
bool p_hasSubLinks;
/* ... hooks for parameter/columnref resolution ... */
};

핵심 작업을 담당하는 세 필드는 먼저 알아두어야 한다.

  • p_rtablerange table이다. 쿼리가 읽는 모든 릴레이션의 인덱스 목록이다. 1기반 위치가 모든 해석된 Var가 담는 varno다.
  • p_namespace가시 스코프다. 어떤 RTE가 한정 이름(p_rel_visible)으로 그리고/또는 비한정 이름(p_cols_visible)으로 현재 참조 가능한지를 기술하는 ParseNamespaceItem(nsitem) 목록이다. p_rtable같지 않다. RTE는 추가되는 순간 range-table에 존재하지만, nsitem이 네임스페이스에 push될 때만 가시가 된다. 나중의 FROM 항목은 LATERAL을 통하지 않으면 앞선 형제를 볼 수 없다.
  • p_joinlist 는 최상위 FROM 항목들(RangeTblRefJoinExpr)을 누적하여, 마지막에 Query.jointree의 fromlist가 된다.

transformSelectStmt가 표준 워크이며, 그 본문은 의존성 순서로 된 절 체크리스트로 읽힌다. 소스의 순서 주석 자체가 설계 문서다.

// transformSelectStmt — src/backend/parser/analyze.c (condensed)
Query *qry = makeNode(Query);
qry->commandType = CMD_SELECT;
/* process the WITH clause independently of all else */
if (stmt->withClause) { ... qry->cteList = transformWithClause(pstate, ...); }
/* make FOR UPDATE/SHARE and WINDOW info available downstream */
pstate->p_locking_clause = stmt->lockingClause;
pstate->p_windowdefs = stmt->windowClause;
/* process the FROM clause --- fills p_rtable, p_namespace, p_joinlist */
transformFromClause(pstate, stmt->fromClause);
/* transform targetlist (expands '*', auto-names columns) */
qry->targetList = transformTargetList(pstate, stmt->targetList,
EXPR_KIND_SELECT_TARGET);
markTargetListOrigins(pstate, qry->targetList);
/* transform WHERE and HAVING (each coerced to boolean) */
qual = transformWhereClause(pstate, stmt->whereClause,
EXPR_KIND_WHERE, "WHERE");
qry->havingQual = transformWhereClause(pstate, stmt->havingClause,
EXPR_KIND_HAVING, "HAVING");
/* ORDER BY first because GROUP BY and DISTINCT both need its results */
qry->sortClause = transformSortClause(pstate, stmt->sortClause,
&qry->targetList, EXPR_KIND_ORDER_BY, false);
qry->groupClause = transformGroupClause(pstate, stmt->groupClause,
&qry->groupingSets, &qry->targetList,
qry->sortClause, EXPR_KIND_GROUP_BY, false);
/* ... DISTINCT / DISTINCT ON, LIMIT/OFFSET, window clauses ... */
qry->rtable = pstate->p_rtable;
qry->rteperminfos = pstate->p_rteperminfos;
qry->jointree = makeFromExpr(pstate->p_joinlist, qual);
qry->hasSubLinks = pstate->p_hasSubLinks;
qry->hasAggs = pstate->p_hasAggs;
assign_query_collations(pstate, qry);
if (pstate->p_hasAggs || qry->groupClause || qry->groupingSets || qry->havingQual)
parseCheckAggregates(pstate, qry);
return qry;

두 가지 순서 결정은 직관적이지 않으므로 짚고 넘어갈 필요가 있다.

  • 타겟 리스트와 WHERE 전에 FROM. 두 절 모두 컬럼을 참조하는데, 컬럼 참조는 소스 RTE가 p_namespace에 있어야 해석된다. transformFromClause가 가장 먼저 실행되어 어떤 식도 변환되기 전에 네임스페이스를 완전히 채운다.
  • GROUP BY와 DISTINCT 전에 ORDER BY. 소스 주석이 직접 말한다. “transformGroupClause와 transformDistinctClause 모두 결과가 필요하므로 ORDER BY를 먼저 한다.” 세 절 모두 타겟 리스트에 resjunk(숨겨진) 항목을 추가할 수 있으며, 공유된 참조 &qry->targetList로 협조한다. 정렬 절이 먼저 구성되어야 그룹/구별 단계가 그 결과와 매칭할 수 있다.

마지막 세 줄이 전체 트리 패스다. jointree가 누적된 p_joinlist와 WHERE 한정자에서 조립되고, assign_query_collations가 모든 식을 걸어 콜레이션을 배정하며, parseCheckAggregates가 비집계 타겟 리스트 컬럼이 GROUP BY에 나타나야 한다는 SQL 규칙을 강제한다.

flowchart TB
  WITH["WITH (CTEs)"]
  FROM["transformFromClause<br/>builds RTEs + namespace + joinlist"]
  TL["transformTargetList<br/>'*' expansion, FigureColname"]
  WH["transformWhereClause<br/>coerce_to_boolean"]
  HAV["HAVING<br/>coerce_to_boolean"]
  OB["transformSortClause<br/>(ORDER BY first)"]
  GB["transformGroupClause<br/>DISTINCT"]
  LIM["LIMIT / OFFSET / windows"]
  COLL["assign_query_collations"]
  AGG["parseCheckAggregates"]
  QRY["assemble Query<br/>rtable, jointree, targetList"]
  WITH --> FROM --> TL --> WH --> HAV --> OB --> GB --> LIM --> QRY --> COLL --> AGG

그림 2 — transformSelectStmt 내부의 절 변환 순서. SQL 키워드 순서가 아닌 데이터 의존성이 순서를 결정한다. FROM이 아무것도 컬럼을 참조하기 전에 네임스페이스를 채우고, ORDER BY가 GROUP BY/DISTINCT 전에 오는 이유는 후자가 정렬 타겟 리스트를 재사용하기 때문이다. 콜레이션 배정과 집계 합법성 검사가 조립된 트리 위에서 마지막으로 실행된다. (출처: analyze.c transformSelectStmt.)

transformFromClause는 원시 FROM 목록을 왼쪽에서 오른쪽으로 순서대로 처리하여(LATERAL 가시성에 순서가 중요하다), 각 항목을 조인 노드로 변환하고 이름을 네임스페이스에 병합한다.

// transformFromClause — src/backend/parser/parse_clause.c (condensed)
foreach(fl, frmList)
{
Node *n = lfirst(fl);
ParseNamespaceItem *nsitem;
List *namespace;
n = transformFromClauseItem(pstate, n, &nsitem, &namespace);
checkNameSpaceConflicts(pstate, pstate->p_namespace, namespace);
/* new items are LATERAL-only until the whole FROM list is done */
setNamespaceLateralState(namespace, true, true);
pstate->p_joinlist = lappend(pstate->p_joinlist, n);
pstate->p_namespace = list_concat(pstate->p_namespace, namespace);
}
/* once FROM is fully parsed, make everything unconditionally visible */
setNamespaceLateralState(pstate->p_namespace, false, true);

transformFromClauseItem은 FROM 항목 종류에 대한 타입 스위치다. 평범한 테이블(RangeVar), 서브 SELECT(RangeSubselect), 집합 반환 함수(RangeFunction), JSON_TABLE, TABLESAMPLE, 명시적 JoinExpr 각각이 전용 변환으로 라우팅되어 하나 이상의 RTE를 추가하고 노출하는 nsitem을 반환한다.

// transformFromClauseItem — src/backend/parser/parse_clause.c (condensed)
if (IsA(n, RangeVar))
{
/* Plain relation reference, or perhaps a CTE reference */
nsitem = getNSItemForSpecialRelationTypes(pstate, rv);
if (!nsitem)
nsitem = transformTableEntry(pstate, rv); /* opens + locks the table */
*top_nsitem = nsitem;
*namespace = list_make1(nsitem);
rtr = makeNode(RangeTblRef);
rtr->rtindex = nsitem->p_rtindex; /* points at the new RTE */
return (Node *) rtr;
}
else if (IsA(n, RangeSubselect)) { ... transformRangeSubselect ... }
else if (IsA(n, JoinExpr)) { ... recurse into larg / rarg ... }

평범한 테이블은 결국 transformTableEntryaddRangeTableEntry에 도달한다. 교과서의 “쿼리에 나타나는 릴레이션 이름들이 데이터베이스에 존재하는 릴레이션 이름인지 검증”이 실제로 일어나는 곳이다. 카탈로그에서 릴레이션을 열고(적절한 락을 취득하며), TupleDesc를 바탕으로 RangeTblEntry를 구성한다.

// addRangeTableEntryForRelation — src/backend/parser/parse_relation.c (condensed)
RangeTblEntry *rte = makeNode(RangeTblEntry);
char *refname = alias ? alias->aliasname : RelationGetRelationName(rel);
rte->rtekind = RTE_RELATION;
rte->relid = RelationGetRelid(rel); /* the catalog OID */
rte->relkind = rel->rd_rel->relkind;
rte->rellockmode = lockmode;
rte->eref = makeAlias(refname, NIL);
buildRelationAliases(rel->rd_att, alias, rte->eref); /* column names */
perminfo = addRTEPermissionInfo(&pstate->p_rteperminfos, rte);
perminfo->requiredPerms = ACL_SELECT;
/* append to the range table; its 1-based position is the varno */
pstate->p_rtable = lappend(pstate->p_rtable, rte);
return buildNSItemFromTupleDesc(rte, list_length(pstate->p_rtable),
perminfo, rel->rd_att);

반환된 nsitem이 이 릴레이션 컬럼들의 해석 테이블이다. buildNSItemFromTupleDesc는 릴레이션의 TupleDesc를 순회하여 컬럼별로 나중에 Var를 합성하는 데 필요한 정확한 데이터를 기록한다.

// buildNSItemFromTupleDesc — src/backend/parser/parse_relation.c (condensed)
for (varattno = 0; varattno < maxattrs; varattno++)
{
Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno);
if (attr->attisdropped)
continue; /* dropped column: leave entry all-zero */
nscolumns[varattno].p_varno = rtindex; /* range-table index */
nscolumns[varattno].p_varattno = varattno + 1; /* 1-based attno */
nscolumns[varattno].p_vartype = attr->atttypid;
nscolumns[varattno].p_vartypmod = attr->atttypmod;
nscolumns[varattno].p_varcollid = attr->attcollation;
nscolumns[varattno].p_varnosyn = rtindex;
nscolumns[varattno].p_varattnosyn = varattno + 1;
}
nsitem->p_rte = rte;
nsitem->p_rtindex = rtindex;
nsitem->p_nscolumns = nscolumns;
nsitem->p_rel_visible = true; /* qualified refs OK */
nsitem->p_cols_visible = true; /* unqualified refs OK */

관계는 이렇다. RTE는 p_rtable에 들어가(varno를 얻음), nsitem은 p_namespace에 들어가(참조 가능하게 됨), 각 ParseNamespaceColumn은 그 컬럼에 대한 컬럼 참조가 될 Var를 위한 조리법이다. 나중에 transformExprt.c를 해석할 때, t의 nsitem을 찾아 컬럼 c를 찾고, (p_varno, p_varattno, p_vartype, p_varcollid)를 새 Var에 그대로 복사한다.

조인, LATERAL, 그리고 가시성 댄스

섹션 제목: “조인, LATERAL, 그리고 가시성 댄스”

명시적 JoinExpr에서 transformFromClauseItem은 왼쪽 팔을 재귀 처리하고, 일시적으로 왼쪽 팔의 이름을 노출하여 오른쪽 팔이 LATERAL 참조할 수 있게 하고, 오른쪽 팔을 처리한 뒤 다시 제거한다.

// transformFromClauseItem (JoinExpr arm) — parse_clause.c (condensed)
j->larg = transformFromClauseItem(pstate, j->larg, &l_nsitem, &l_namespace);
/* expose left side to the right side for LATERAL, then process RHS */
lateral_ok = (j->jointype == JOIN_INNER || j->jointype == JOIN_LEFT);
setNamespaceLateralState(l_namespace, true, lateral_ok);
sv_namespace_length = list_length(pstate->p_namespace);
pstate->p_namespace = list_concat(pstate->p_namespace, l_namespace);
j->rarg = transformFromClauseItem(pstate, j->rarg, &r_nsitem, &r_namespace);
/* remove the left-side RTEs from the namespace again */
pstate->p_namespace = list_truncate(pstate->p_namespace, sv_namespace_length);
checkNameSpaceConflicts(pstate, l_namespace, r_namespace);

NATURAL 조인과 USING에서는 두 쪽의 컬럼 이름 목록을 순회하여 병합된 조인 컬럼을 계산한다. 조인 자체는 병합된 컬럼 집합을 노출하는 nsitem을 가진 RTE_JOIN 항목이 된다. ParseNamespaceItem 주석이 인코딩하는 미묘한 가시성 규칙 — 별칭 없는 조인은 한정 참조에 대해서는 멤버 테이블을 숨기지 않지만 비한정 참조에 대해서는 컬럼을 숨긴다 — 이 바로 p_rel_visiblep_cols_visible이 하나가 아닌 두 개의 별개 불리언인 이유다.

타겟 리스트 — 별표 확장과 자동 이름 지정

섹션 제목: “타겟 리스트 — 별표 확장과 자동 이름 지정”

transformTargetList는 원시 ResTarget 노드 목록을 TargetEntry 노드 목록으로 변환한다. 두 가지 특별 작업이 있다. something.* 확장과 레이블 없는 컬럼 자동 이름 지정이다.

// transformTargetList — src/backend/parser/parse_target.c (condensed)
expand_star = (exprKind != EXPR_KIND_UPDATE_SOURCE);
foreach(o_target, targetlist)
{
ResTarget *res = (ResTarget *) lfirst(o_target);
if (expand_star && IsA(res->val, ColumnRef) &&
IsA(llast(((ColumnRef *) res->val)->fields), A_Star))
{
/* "tbl.*" or "*": expand into one target per column */
p_target = list_concat(p_target,
ExpandColumnRefStar(pstate,
(ColumnRef *) res->val, true));
continue;
}
/* ordinary expression */
p_target = lappend(p_target,
transformTargetEntry(pstate, res->val, NULL,
exprKind, res->name, false));
}

transformTargetEntry는 식을 변환하고(transformExpr 경유), 핵심적으로 사용자가 AS …를 작성하지 않았을 때 컬럼 이름을 부여한다.

// transformTargetEntry — src/backend/parser/parse_target.c (condensed)
if (expr == NULL)
expr = transformExpr(pstate, node, exprKind);
if (colname == NULL && !resjunk)
colname = FigureColname(node); /* derive a label from the expr shape */
return makeTargetEntry((Expr *) expr,
(AttrNumber) pstate->p_next_resno++, /* assign resno */
colname, resjunk);

pstate->p_next_resno++가 타겟 리스트 컬럼 카운터다. 각 TargetEntry가 1부터 시작하는 다음 resno를 받는다. FigureColname은 식을 내려가며 레이블을 선택한다. ColumnRef의 마지막 필드 이름, FuncCall의 함수 이름 등을 사용하며, 더 나은 것이 없으면 리터럴 "?column?"으로 대체한다.

// FigureColname — src/backend/parser/parse_target.c (condensed)
char *FigureColname(Node *node)
{
char *name = NULL;
(void) FigureColnameInternal(node, &name);
if (name != NULL)
return name;
return "?column?"; /* the famous default */
}

목록이 구성된 후 markTargetListOrigins는 각 TargetEntry에 실제 릴레이션의 단순 Var인 경우 기반 테이블/컬럼(resorigtbl / resorigcol)을 주석으로 단다. psql \d 스타일 도구와 information_schema가 결과 컬럼의 소스 테이블을 보고할 수 있는 이유다. 서브쿼리와 CTE는 파고들지만 조인이나 뷰는 의도적으로 건드리지 않는다.

타겟 리스트에 대한 GROUP BY / ORDER BY 해석

섹션 제목: “타겟 리스트에 대한 GROUP BY / ORDER BY 해석”

ORDER BYGROUP BY 항목은 세 가지 방식으로 작성할 수 있다. 완전한 식, 타겟 리스트 컬럼 별칭, 또는 출력 컬럼 번호(ORDER BY 1). findTargetlistEntrySQL92가 SQL92 별칭/번호 규칙을 구현한다. 맨 식별자는 기존 타겟 리스트 이름과 매칭하고(동등하지 않은 중복에서는 오류), 정수 상수는 n번째 출력 컬럼을 선택한다.

// findTargetlistEntrySQL92 — src/backend/parser/parse_clause.c (condensed)
if (IsA(node, ColumnRef) && list_length(cref->fields) == 1 &&
IsA(linitial(cref->fields), String))
{
char *name = strVal(linitial(cref->fields));
if (exprKind == EXPR_KIND_GROUP_BY)
{
/* GROUP BY prefers a FROM-column match over a tlist alias */
if (colNameToVar(pstate, name, true, location) != NULL)
name = NULL;
}
if (name != NULL)
{
foreach(tl, *tlist) /* match against existing tlist names */
{
TargetEntry *tle = (TargetEntry *) lfirst(tl);
if (!tle->resjunk && strcmp(tle->resname, name) == 0)
{
if (target_result != NULL &&
!equal(target_result->expr, tle->expr))
ereport(ERROR, (errcode(ERRCODE_AMBIGUOUS_COLUMN), ...));
target_result = tle;
}
}
if (target_result != NULL) return target_result;
}
}
if (IsA(node, A_Const)) { /* "ORDER BY 3" -> the 3rd output column */ }

EXPR_KIND_GROUP_BY 분기는 실제 SQL 미묘함을 인코딩한다. SQL92에 따르면 GROUP BY의 식별자는 타겟 리스트 별칭이 아닌 FROM 컬럼을 가리키므로, 코드가 먼저 이름이 FROM 컬럼으로 해석되는지 확인(colNameToVar)하고 그렇지 않으면 tlist 별칭 규칙으로 넘어간다. transformSortClausetransformGroupClause 모두 이 해석기를 거친 다음 addTargetToSortList를 호출하는데, 정렬/그룹 식이 이미 출력 컬럼이 아니면 resjunk 타겟 리스트 항목을 추가한다. b가 선택되지 않더라도 SELECT a FROM t ORDER BY b가 작동하는 이유가 이것이다.

모든 절 변환의 잎은 transformExpr(postgres-parser.md의 식 절이 담당)이며, 문맥을 알려주는 ParseExprKind 태그와 함께 호출된다. transformWhereClause가 대표적이다 — 변환한 뒤 강제 변환한다.

// transformWhereClause — src/backend/parser/parse_clause.c
Node *qual = transformExpr(pstate, clause, exprKind);
qual = coerce_to_boolean(pstate, qual, constructName);
return qual;

ParseExprKind 열거형(EXPR_KIND_WHERE, EXPR_KIND_HAVING, EXPR_KIND_GROUP_BY, EXPR_KIND_LIMIT, …)은 하나의 transformExpr이 문맥별 오류 메시지를 만드는 방법이다. 예를 들어 “aggregate functions are not allowed in WHERE” 같은 메시지를 각 절이 자체 식 워커를 가질 필요 없이 만든다. 패스 전체에서 가장 많이 재사용되는 문맥 조각이다.

아래 심볼들은 파스 분석 패스의 안정적인 앵커다. 줄 번호는 변하므로 심볼 이름으로 grep해서 찾는다. 위치 표는 문서화된 리비전 기준 줄 번호를 기록한다.

  • parse_analyze_fixedparams — 최상위 진입점. 루트 ParseState를 만들고 소스 텍스트와 고정 $n 파라미터 타입을 설정하고 transformTopLevelStmt를 호출하며 JumbleQuerypost_parse_analyze_hook을 실행하고 pstate를 해제한다. 형제: parse_analyze_varparams(추론된 파라미터 타입)와 parse_analyze_withcb(호출자 제공 파라미터 훅).
  • parse_sub_analyze — 하위 문 재귀 진입점. parentParseState가 설정된 자식 ParseState를 만들어 서브쿼리가 외부 쿼리의 상관 참조를 해석할 수 있게 한다.
  • transformTopLevelStmtstmt_location / stmt_len을 결과에 복사하고 transformOptionalSelectInto로 최상위 전용 SELECT … INTO 재작성을 허용한다.
  • transformStmt — 노드 태그 디스패처. T_SelectStmt/T_InsertStmt/T_UpdateStmt/T_DeleteStmt/T_MergeStmt를 해당 변환으로 라우팅하고 나머지는 CMD_UTILITY Query로 감싼다. 결과에 QSRC_ORIGINAL, canSetTag = true를 표시한다.
  • stmt_requires_parse_analysis — 동반 술어(transformStmt가 비자명 작업을 수행하는 문 타입에서만 true). 호출자가 재분석이 필요한지 결정하는 데 사용한다.
  • transformSelectStmt — 골격 함수. WITH, FROM, 타겟 리스트, WHERE/HAVING, ORDER BY → GROUP BY → DISTINCT, LIMIT, 윈도우, 콜레이션, 집계 검사. Query를 조립한다.
  • transformValuesClause — SELECT로서의 독립적인 VALUES (…). 컬럼 중심 중간 형식을 만든 뒤 RTE_VALUES를 만든다.
  • transformInsertStmt / transformUpdateStmt / transformDeleteStmt — DML 변환들. 각각 setTargetTable을 호출하여 타겟 릴레이션을 추가하고 락을 취득한 뒤 해당 절을 변환한다(RETURNINGtransformReturningList를 경유).

ParseState와 네임스페이스 (parse_node.h, parse_node.c)

섹션 제목: “ParseState와 네임스페이스 (parse_node.h, parse_node.c)”
  • struct ParseState — 꿰어진 스크래치 상태. p_rtable, p_namespace, p_joinlist, p_next_resno가 핵심 필드다.
  • make_parsestate / free_parsestate — pstate 할당/해제. make_parsestate는 서브쿼리를 위해 부모를 상속한다.
  • struct ParseNamespaceItem — 가시적인 릴레이션 하나. RTE, range-table 인덱스, 컬럼별 데이터, p_rel_visible / p_cols_visible / p_lateral_only 가시성 플래그를 담는다.
  • struct ParseNamespaceColumn — 컬럼별 Var 구성 조리법(p_varno, p_varattno, p_vartype, p_varcollid, 구문적 변형).
  • ParseExprKind (열거형) — transformExpr에 꿰어지는 문맥별 태그.

FROM / range-table (parse_clause.c, parse_relation.c)

섹션 제목: “FROM / range-table (parse_clause.c, parse_relation.c)”
  • transformFromClause — FROM 항목을 왼쪽에서 오른쪽으로 순회. 네임스페이스를 병합하고 p_joinlist를 구성한다.
  • transformFromClauseItemRangeVar / RangeSubselect / RangeFunction / RangeTableFunc / JsonTable / RangeTableSample / JoinExpr 종류별 타입 스위치. 조인은 LATERAL 가시성 댄스를 수반하며 재귀한다.
  • transformTableEntryaddRangeTableEntry / addRangeTableEntryForRelation — 릴레이션을 열고 락을 취득하며, RTE_RELATION을 만들고 p_rtable에 추가한다.
  • addRangeTableEntryForSubquery — 분석된 서브 Query에서 RTE_SUBQUERY를 만든다.
  • buildNSItemFromTupleDesc — 릴레이션의 TupleDesc에서 ParseNamespaceColumn[]을 채운다.
  • addNSItemToQuery — 올바른 가시성 플래그로 nsitem을 joinlist 및/또는 네임스페이스에 추가한다.
  • setTargetTable — INSERT/UPDATE/DELETE/MERGE 타겟을 추가하고 락을 취득하며 p_target_relation / p_target_nsitem을 설정한다.
  • scanRTEForColumn / colNameToVar — (비한정) 컬럼 이름을 속성 / Var로 해석하고 모호성에서 오류를 낸다.
  • GetRTEByRangeTablePosn(varno, levelsup) 위치의 RTE를 가져오며 부모 pstate를 걸어 올라간다.
  • addRTEPermissionInfoRTE_RELATION에 대한 RTEPermissionInfo를 기록한다(권한은 나중에 실행 시점에 확인된다).
  • transformTargetListResTarget 목록 → TargetEntry 목록. something.* 확장 포함.
  • transformTargetEntry — 식 하나를 변환하고 p_next_resno++resno를 배정하며 FigureColname으로 자동 이름을 지정한다.
  • transformExpressionList — 타겟 리스트와 같지만 맨 식(ROW(), VALUES)을 위한 것.
  • ExpandColumnRefStar / ExpandAllTablestbl.* / 맨 *를 컬럼별 타겟으로 확장한다.
  • FigureColname / FigureColnameInternal — 식 형태에서 출력 컬럼 이름을 유도한다. "?column?" 대체.
  • markTargetListOrigins / markTargetListOrigin — 단순 Var 타겟의 resorigtbl / resorigcol을 설정한다.
  • resolveTargetListUnknowns — 여전히 unknown인 출력 컬럼을 text로 강제 변환한다.

정렬 / 그룹화 / 절 강제 변환 (parse_clause.c)

섹션 제목: “정렬 / 그룹화 / 절 강제 변환 (parse_clause.c)”
  • transformSortClause — ORDER BY → SortGroupClause 목록(GROUP BY 전에 실행).
  • transformGroupClause / transformGroupingSet — GROUP BY. CUBE/ROLLUP/GROUPING SETS 평탄화 포함.
  • transformDistinctClause / transformDistinctOnClause — DISTINCT와 DISTINCT ON.
  • findTargetlistEntrySQL92 / findTargetlistEntrySQL99 — 정렬/그룹 항목을 tlist 별칭, 출력 컬럼 번호, 또는 완전한 식으로 해석한다.
  • transformWhereClause — 변환 + coerce_to_boolean.
  • transformLimitClause — 변환 + coerce_to_specific_type(INT8) + 변수 없음 검사.
  • transformWindowDefinitionsWindowDefWindowClause.
  • makeFromExpr (makefuncs.c) — p_joinlist + WHERE 한정자를 Query.jointree로 감싼다.
  • assign_query_collations (parse_collate.c) — 모든 식에 콜레이션을 배정한다.
  • parseCheckAggregates (parse_agg.c) — GROUP-BY/집계 합법성 규칙을 강제한다.
  • transformWithClause (parse_cte.c) — WITH 항목을 cteList로 분석한다.
  • transformExpr (parse_expr.c) — 모든 절이 귀결되는 식별 해석기(여기서는 블랙박스. postgres-parser.md가 담당).

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

섹션 제목: “위치 힌트 (2026-06-05, REL_18 273fe94 기준)”
심볼파일
parse_analyze_fixedparamsparser/analyze.c105
parse_sub_analyzeparser/analyze.c222
transformTopLevelStmtparser/analyze.c249
transformOptionalSelectIntoparser/analyze.c273
transformStmtparser/analyze.c312
stmt_requires_parse_analysisparser/analyze.c447
transformDeleteStmtparser/analyze.c553
transformInsertStmtparser/analyze.c633
transformSelectStmtparser/analyze.c1389
transformValuesClauseparser/analyze.c1532
transformUpdateStmtparser/analyze.c2472
struct ParseStateinclude/parser/parse_node.h192
struct ParseNamespaceIteminclude/parser/parse_node.h292
struct ParseNamespaceColumninclude/parser/parse_node.h328
ParseExprKind (열거형)include/parser/parse_node.h38
make_parsestateparser/parse_node.c39
transformFromClauseparser/parse_clause.c112
transformTableEntryparser/parse_clause.c395
transformFromClauseItemparser/parse_clause.c1054
transformWhereClauseparser/parse_clause.c1854
transformLimitClauseparser/parse_clause.c1881
findTargetlistEntrySQL92parser/parse_clause.c2006
findTargetlistEntrySQL99parser/parse_clause.c2172
transformGroupClauseparser/parse_clause.c2632
transformSortClauseparser/parse_clause.c2732
transformWindowDefinitionsparser/parse_clause.c2765
GetRTEByRangeTablePosnparser/parse_relation.c545
scanRTEForColumnparser/parse_relation.c815
colNameToVarparser/parse_relation.c898
buildNSItemFromTupleDescparser/parse_relation.c1309
addRangeTableEntryparser/parse_relation.c1487
addRangeTableEntryForRelationparser/parse_relation.c1584
addRangeTableEntryForSubqueryparser/parse_relation.c1655
addNSItemToQueryparser/parse_relation.c2710
addRTEPermissionInfoparser/parse_relation.c3980
transformTargetEntryparser/parse_target.c75
transformTargetListparser/parse_target.c121
transformExpressionListparser/parse_target.c220
resolveTargetListUnknownsparser/parse_target.c288
markTargetListOriginsparser/parse_target.c318
ExpandColumnRefStarparser/parse_target.c1123
ExpandAllTablesparser/parse_target.c1296
FigureColnameparser/parse_target.c1713
assign_query_collationsparser/parse_collate.c101
parseCheckAggregatesparser/parse_agg.c1138
transformWithClauseparser/parse_cte.c110
transformExprparser/parse_expr.c119
makeFromExprnodes/makefuncs.c336
  • 문법 출력(RawStmt)은 카탈로그 객체를 전혀 참조하지 않는다. 카탈로그 조회가 처음 일어나는 곳이 파스 분석이다. parse_clause.c / parse_relation.c에서 transformTableEntryaddRangeTableEntryaddRangeTableEntryForRelation을 2026-06-05에 읽어 검증했다. 릴레이션이 파스 분석 안에서 카탈로그에서 열리며(RelationGetRelid, 락 취득), 문법에서 열리지 않는다. DSC §15.1의 “릴레이션 이름이 데이터베이스에 존재하는 릴레이션 이름인지 검증”이 구체적으로 실현된 부분이다.
  • transformStmt는 최적화 불가능한 모든 문을 CMD_UTILITY Query로 감싸고 추가 분석을 수행하지 않는다. 2026-06-05에 analyze.ctransformStmt default: 분기에서 검증했다. DDL 의미 분석(parse_utilcmd.c)은 실행 시점으로 미뤄진다. parser/README 줄 “parse_utilcmd.c parse analysis for utility commands (done at execution time)“으로 확인했다.
  • 절 변환 순서는 의존성 주도이며 SQL 키워드 순서가 아니다. analyze.ctransformSelectStmt를 읽어 검증했다. transformFromClause가 타겟 리스트와 WHERE 전에 실행되고, “Do ORDER BY first because both transformGroupClause and transformDistinctClause need the results”라는 소스 주석이 transformSortClause 앞에 있으며, transformGroupClause와 DISTINCT 변환보다 먼저 호출된다.
  • 컬럼의 해석된 정체성은 (varno, varattno)에 타입과 콜레이션이다. nsitem의 ParseNamespaceColumn에서 복사된다. parse_relation.cbuildNSItemFromTupleDesc에서 검증했다. 각 ParseNamespaceColumnp_varno, p_varattno, p_vartype, p_vartypmod, p_varcollid를 기록한다. Var에 필요한 정확한 필드들이다.
  • 이름 지정 불가능한 식의 기본 출력 컬럼 이름이 리터럴 문자열 "?column?"이다. 2026-06-05에 parse_target.cFigureColname에서 검증했다. 파라미터가 아닌 하드코딩된 대체값이다.
  • p_rel_visiblep_cols_visible은 두 개의 독립적인 불리언이다. 별칭 없는 JOIN이 멤버 테이블을 한정 이름으로는 가시적으로 유지하면서 비한정 참조에서는 컬럼을 숨길 수 있도록. 2026-06-05에 struct ParseNamespaceItem 주석과 transformFromClauseItem의 조인 분기에서 검증했다.
  • LATERAL 가시성은 오른쪽 팔 처리 전에 왼쪽 팔의 네임스페이스를 일시적으로 연결하고, 이후 잘라내는 방식으로 구현된다. 2026-06-05에 transformFromClauseItemJoinExpr 분기에서 list_concatlist_truncate를 오른쪽 재귀 주위로 확인했다.
  • WHERE/HAVING은 불리언으로 강제 변환되고 LIMIT는 int8로 강제 변환된다. 절 변환 내부에서. 검증: transformWhereClausecoerce_to_boolean을 호출하고, transformLimitClausecoerce_to_specific_type(..., INT8OID, ...)checkExprIsVarFree를 2026-06-05에 호출한다.
  1. 자기 참조 DML의 락 순서 보장. setTargetTable의 주석은 “타겟이 소스 릴레이션으로도 언급되는 경우에 대비해” 타겟에 대한 쓰기 락을 읽기 락보다 먼저 취득해야 한다고 주장한다. 나중의 코드 경로(예를 들어 타겟을 재참조하는 CTE나 규칙 확장)가 이 의도된 순서를 위반하는지 불분명하다. 조사 경로: transformInsertStmt / transformUpdateStmt / transformMergeStmtsetTargetTable 호출자를 추적하고 transformFromClause 락 취득 순서와 비교한다.
  2. 플랜 무효화 시 재분석 비용. stmt_requires_parse_analysis가 false를 반환하면 호출자가 전체 analyze/rewrite/plan 파이프라인을 건너뛸 수 있으므로, 카탈로그 의존성이 변경된 캐시된 플랜에서 파스 분석이 정확히 언제 재실행되는지 불명확하다. 조사 경로: plancache.cRevalidateCachedQueryparse_analyze_*로의 재진입을 따라간다. 미래의 postgres-plan-cache.md가 담당할 영역이다.
  3. markTargetListOrigins와 보안 장벽 뷰. 이 함수는 의도적으로 뷰를 파고들지 않는다(뷰를 소유자로 보고한다). 이것이 information_schema 소비자와의 관계에서 행 수준 보안이나 보안 장벽 뷰 누수 방지와 관찰 가능한 방식으로 상호작용하는지 불분명하다. 조사 경로: 리라이터의 뷰 확장 처리와 대조한다(postgres-rewriter.md, 아직 작성되지 않음).

PostgreSQL을 넘어서 — 비교 설계와 연구 최전선

섹션 제목: “PostgreSQL을 넘어서 — 비교 설계와 연구 최전선”
  • 재사용 지점으로서의 원시 트리 / 쿼리 트리 분리. PostgreSQL의 카탈로그 없는 RawStmt와 해석된 Query 사이의 명확한 경계가 libpg_query(pganalyze)가 SQL 린터와 ORM을 위한 독립 라이브러리로 문법을 제공할 수 있게 한 바로 그 이음매다. 실행 서버 없이 1단계(파싱)를 얻는다. SQLite가 파스 + 해석 + 코드생성을 한 패스로 합치는 방식(별도의 해석된 IR이 전혀 없음)과 비교하면 PostgreSQL이 두 트리의 비용을 왜 감수하는지가 명확해진다.
  • 조인 재정렬 기반으로서의 Range table. PostgreSQL은 모든 컬럼 참조를 (varno, varattno)로 인코딩하여 옵티마이저가 range-table 인덱스를 교환함으로써 조인을 재정렬할 수 있게 한다. 이것이 고전적인 System R / Selinger(SIGMOD 1979) 표현이다. 다음에 작성할 문서는 postgres-planner-overview.mdRelOptInfo가 range-table을 어떻게 키로 사용하는지에 대한 설명이다. 이 문서의 RTE 구성이 그 입력이다.
  • 컴파일러 문헌의 이름 해석과 바인딩. ParseState 네임스페이스 스택은 컴파일러의 심볼 테이블 / 스코프 체인의 데이터베이스 아날로그다(Aho, Lam, Sethi & Ullman, Compilers, 2장 & 6장). LATERAL “노출 후 잘라내기” 댄스가 본질적으로 블록 구조 언어의 어휘 스코핑 규칙과 같은 중첩 스코프 관리다. 이 연결을 짚는 메모는 컴파일러 교육을 받은 독자들의 방향을 잡아줄 것이다.
  • 뷰 확장: 분석 시점 vs. 재작성 시점. DSC §15.1은 뷰 치환을 “변환” 아래 분류하지만, PostgreSQL은 별도의 규칙 기반 리라이터(Stonebraker의 POSTGRES 규칙 시스템, The Design of POSTGRES, SIGMOD 1986)로 미룬다. 깔끔한 analyze/rewrite 경계와 두 번째 트리 순회 비용이라는 트레이드오프가 아직 작성되지 않은 postgres-rewriter.md의 주제다. 이 문서의 Query가 정확히 그 리라이터의 입력이다.
  • 전체론적 의미 검사 vs. 스트리밍. PostgreSQL은 마지막에 여러 전체 트리 패스를 수행한다(assign_query_collations, parseCheckAggregates). 첫 오류까지의 지연 시간을 낮추려는 엔진이라면 이것들을 단일 하강에 인터리브할 수 있다. DuckDB의 바인더가 패스를 구조화하는 방식과 비교하면 PostgreSQL이 단순한 2단계 형태로 무엇을 교환하는지 드러날 것이다.

소비된 원시 분석 자료: 없음 — 이 문서는 REL_18 소스 트리와 아래 교과서 참고 문헌에서 직접 합성되었다(sources: []).

교과서 장:

  • Silberschatz, Korth & Sudarshan, Database System Concepts, 7판, 15장 “쿼리 처리”, §15.1 “개요” — 파싱/변환/최적화/평가 프레임워크와 의미 분석·뷰 확장에 대한 “릴레이션 이름 검증”/“관계 대수 식으로 변환” 설명. knowledge/research/dbms-general/database-system-concepts.md에 캡처됨.
  • Hellerstein, Stonebraker & Hamilton, Architecture of a Database System (FnT 2007), §4 “관계형 쿼리 처리기” — 파서 → 리라이터 → 옵티마이저 → 실행기 파이프라인으로서의 쿼리 처리기. dbms-papers/fntdb07-architecture.md 아래 캡처됨.

소스 코드 (REL_18, 273fe94, 2026-06-05 기준):

  • src/backend/parser/README — 디렉터리 맵. 구문/의미 분리와 유틸리티 명령의 “실행 시점 처리” 메모.
  • src/backend/parser/analyze.c — 진입점, transformStmt 디스패치, transformSelectStmt와 DML 변환들.
  • src/backend/parser/parse_clause.c — FROM/JOIN, WHERE/HAVING/LIMIT, ORDER BY / GROUP BY 해석.
  • src/backend/parser/parse_relation.c — range-table과 RTE 구성, 네임스페이스/nsitem 구성, 컬럼 해석.
  • src/backend/parser/parse_target.c — 타겟 리스트 변환, 별표 확장, FigureColname, 오리진 마킹.
  • src/backend/parser/parse_node.c, src/include/parser/parse_node.hParseState, ParseNamespaceItem, ParseNamespaceColumn, ParseExprKind.
  • src/backend/parser/parse_collate.c, parse_agg.c, parse_cte.c, parse_expr.c, src/backend/nodes/makefuncs.c — 마무리 패스들과 식 해석 잎.

이 트리의 관련 문서:

  • postgres-parser.md — 구문 단계(gram.y, 스캐너)와 식 내부 해석(transformExpr 내부).
  • postgres-planner-overview.md — 완성된 Query로 플래너가 하는 작업.
  • postgres-executor.md — 결과 플랜의 수요 풀 실행.
  • postgres-overview-query-processing.md — 이 문서가 속한 쿼리 처리 서브시스템 맵.