본문으로 건너뛰기

5.1 파서·재작성기

PostgreSQL이 SQL 문자열 한 줄을 받아서 실제 데이터를 돌려주기까지는 파싱(분석 포함) → 재작성 → 계획 → 실행 네 단계를 거칩니다. 앞쪽 파싱과 재작성이 어떻게 SQL을 옵티마이저가 다룰 수 있는 형태로 바꾸는지가 이 절의 내용입니다.

전체 흐름​

여기까지가 파싱·분석·재작성입니다. PLAN과 그 비용 모델은 5.2에서 다룹니다.

1. 파서 — 문법만 본다​

PostgreSQL 파서는 flex(렉서) + bison(파서 생성기) 조합입니다. 입력 SQL을 토큰으로 쪼개고, 문법 규칙에 맞게 Raw Parse Tree를 만듭니다.

이 단계에서 일어나는 검사:

  • 문법 오류 (syntax error at or near "...")
  • 키워드 위치
  • 괄호 매칭

아직 일어나지 않는 것:

  • 테이블·컬럼이 실제로 존재하는가
  • 타입이 맞는가
  • 권한이 있는가
SELECT * FROM no_such_table;
-- 문법은 OK — 파서 통과
-- ERROR는 다음 단계(Analyzer)에서 발생

파스 트리는 debug_print_parse 설정이나 외부 라이브러리 pg_query로 볼 수 있습니다. 문법만 검사하려면 PREPARE로 파싱+분석까지 진행해 봅니다.

2. Analyzer — 이름·타입 해석​

Raw Parse Tree를 받아 카탈로그(pg_class, pg_attribute, pg_type 등)와 대조해 Query Tree를 만듭니다.

해결되는 것:

  • 테이블·뷰 이름 → OID
  • 컬럼 이름 → attnum
  • 타입 추론과 캐스팅
  • 함수 시그니처 매칭

오류 예:

ERROR: column "foo" does not exist
ERROR: operator does not exist: integer = text

테이블 권한은 Analyzer가 아니라 Executor 시작 시 검사합니다. permission denied for table 오류도 이때 발생합니다.

이 시점에서 Query Tree의 노드는 RTE(Range Table Entry)·TargetEntry·FuncExpr·OpExpr 등으로 구성됩니다. 옵티마이저가 이해할 수 있는 internal representation입니다.

3. Rewriter — view와 rule을 풀어 펴기​

재작성기는 Query Tree를 받아 view 정의와 rule을 끼워 넣어 확장된 Query Tree를 만듭니다.

view 풀기​

CREATE VIEW recent_orders AS
SELECT id, user_id, total
FROM orders
WHERE created_at > now() - interval '7 days';

SELECT count(*) FROM recent_orders;

재작성기 후:

SELECT count(*) FROM (
SELECT id, user_id, total
FROM orders
WHERE created_at > now() - interval '7 days'
) recent_orders;

view는 항상 underlying 쿼리로 펼쳐집니다. 그래서 view의 인덱스·통계는 원본 테이블 것을 사용합니다. PostgreSQL의 view 자체에 인덱스를 만들 수 없는 이유가 여기에 있습니다(필요하면 materialized view).

rule (CREATE RULE)​

거의 쓰지 않는 옛 기능입니다. 한 쿼리를 다른 쿼리로 변환하는 규칙을 등록할 수 있습니다.

CREATE RULE no_delete AS ON DELETE TO logs DO INSTEAD NOTHING;

복잡한 rule은 디버그가 어려워 운영에서는 trigger를 권장합니다. updatable view 구현 내부에서나 자주 보입니다.

row-level security 변환​

RLS(CREATE POLICY)도 이 단계에서 적용됩니다. 정책이 정의한 USING/WITH CHECK 조건이 WHERE 절에 끼어 들어갑니다. 자세한 내용은 Part IX 9.3.

EXPLAIN과 단계​

EXPLAIN은 PLAN 단계 결과를 보여 줍니다. 파서·분석기·재작성기는 EXPLAIN으로 안 보이지만, 이 단계에도 비용이 누적됩니다.

비용빈도
파싱·분석매 쿼리. prepared statement로 한 번만 가능
재작성매 쿼리
카탈로그 lookup매 쿼리 (system catalog cache로 완화)

매우 짧은 단순 SELECT에서는 이 앞 단계의 오버헤드가 실제 실행보다 큰 경우도 있습니다.

prepared statement로 단계 절약​

PREPARE q1 (int) AS
SELECT * FROM orders WHERE user_id = $1;

EXECUTE q1(42);
EXECUTE q1(43);
EXECUTE q1(44);

PREPARE는 파서·분석기·재작성기를 한 번만 거치고 결과 Query Tree를 캐시합니다. EXECUTE는 그 트리를 가져와 PLAN(나중에는 그것도 캐시 가능)부터 시작합니다.

일반 실행과 prepared statement의 단계 재사용 비교

ORM·드라이버 대부분이 내부적으로 prepared statement를 사용합니다(pgJDBC, psycopg, node-postgres 등). 다만 pgBouncer transaction 모드에서는 명시적인 PREPARE가 동작하지 않으니 주의합니다.

카탈로그 조회 비용​

분석기·재작성기는 매번 카탈로그를 봅니다. 운영에서 카탈로그 자체가 비대해지면 모든 쿼리가 느려집니다.

사례원인대응
pg_class·pg_attribute가 매우 큼수만 개 테이블/파티션파티션 수 조절, autovacuum on catalog
pg_proc 비대자동 생성된 함수 누적정기 정리
pg_statistic 비대매우 큰 테이블 + 많은 컬럼자동 vacuum이 잘 돌도록

relcache·syscache라는 backend 로컬 캐시가 있어, 같은 카탈로그를 반복 조회할 때는 빠릅니다. 단, 새 연결이 만들어질 때마다 캐시를 다시 채우므로 연결 풀링(13.4)이 중요.

view·rule·materialized view 비교​

객체동작
view재작성기에서 항상 풀려 들어감. 자체 스토리지 없음
materialized view실제 테이블처럼 디스크에 저장, REFRESH MATERIALIZED VIEW로 갱신
rule (CREATE RULE)재작성기에서 쿼리 변환. 운영에서는 trigger 권장
trigger실행 시점에 함수 실행. 재작성 단계 무관

정리​

SQL 처리는 파싱 → 분석 → 재작성 → 계획 → 실행 다섯 단계로 이어집니다. 파서는 문법만 보고, 분석기가 카탈로그와 대조해 타입을 검사하며, 재작성기가 view·rule·RLS 정책을 underlying 쿼리에 펼칩니다. view가 자체 인덱스를 못 갖는 것도 이 재작성 때문입니다. view는 언제나 원본 쿼리로 풀려 들어갑니다.

반복 실행에서는 prepared statement로 파서·분석기 비용을 아낄 수 있습니다. 반대로 파티션·함수 같은 객체가 지나치게 많으면 카탈로그 자체가 비대해져 모든 쿼리의 앞단 비용이 커집니다.