(KO) PostgreSQL 룰 시스템과 쿼리 리라이터 — 뷰, DO INSTEAD, RLS
목차
이론적 배경
섹션 제목: “이론적 배경”SQL 문장은 파싱과 의미 분석을 거쳐도 바로 최적화 단계로 넘어갈 수 없다. 뷰와 접근 룰이라는 두 핵심 추상은 다른 쿼리로 정의되며, 뷰를 참조하는 쿼리는 그 뷰가 표현하는 쿼리의 약칭일 뿐이다. 이 약칭을 풀어내는 작업이 쿼리 재작성(query rewriting, 또는 query modification으로도 불림)이다. 쿼리 재작성은 비용 기반 최적화 이전에 쿼리 표현 위에서 실행되는 소스-대-소스 변환으로, 파생 릴레이션과 룰이 적용된 릴레이션에 대한 참조를 그것들을 정의하는 쿼리로 대체한다.
이 기법을 처음 명명하고 구현한 것은 INGRES다. Stonebraker, Wong, Kreps & Held의 “The Design and Implementation of INGRES”(TODS 1976)는 query modification을 뷰와 접근 제어 모두의 메커니즘으로 소개한다. 뷰는 그 정의 쿼리 자체로 저장되고, 뷰를 대상으로 하는 쿼리는 그 뷰 이름 대신 정의 쿼리를 대입하는 방식으로 수정된다. 보호(접근 제어)와 무결성 제약도 같은 방식으로 처리한다. 시스템이 사용자 쿼리에 자격 조건을 덧붙여 룰이 허용하는 행에만 접근하게 만드는 것이다. 핵심 통찰은 뷰, 권한, 무결성이 모두 동일한 연산이라는 점이다. 쿼리를 좀 더 제한된 형태로 재작성하는 것이 세 경우 모두 같은 작업이므로, 최적화기 앞에서 하나의 엔진이 세 경우를 모두 처리할 수 있다.
Database System Concepts(Silberschatz, Korth & Sudarshan, 7판)의 §4.2 “Views”는 같은 아이디어를 다룬다. 뷰는 “논리 모델의 일부는 아니지만 사용자에게 가상 릴레이션으로 노출되는 릴레이션”이고, “뷰 정의는 뷰를 사용하는 쿼리에 대입된다”(§4.2.3 “Materialized Views”는 물리적 결과 저장과의 대비를 다룬다). 교과서에서 설명하는 뷰 확장—“다른 뷰로 정의된 뷰의 의미를 정의하는 방법”으로, “뷰 릴레이션을 그 정의로 치환하고 뷰 릴레이션이 더 이상 없을 때까지 반복”(§4.2.4)—은 리라이터가 수행하는 재귀적 대입과 정확히 일치한다. 교과서는 뷰 정의가 순환적이면 확장이 루프를 돌 수 있으므로 엔진이 이를 감지하고 차단해야 한다는 점도 지적한다.
POSTGRES, INGRES의 직접 후속이자 PostgreSQL의 직계 조상은 query modification을 일급 확장 가능한 룰 시스템으로 일반화했다. Stonebraker, Rowe & Hirohama의 “The Implementation of POSTGRES”(IEEE TKDE 1990)와 그 이전의 “The Design of POSTGRES”(SIGMOD 1986)는 ON event TO object WHERE qual DO [INSTEAD] action 형식의 룰을 카탈로그에 저장하고 적용하는 프로덕션 룰 서브시스템(PRS, 이후 PRS2)을 설명한다. POSTGRES는 뷰를 룰로 구현했다. 뷰는 그 정의 쿼리를 액션으로 갖는 ON SELECT … DO INSTEAD 룰 하나를 담고 있는 릴레이션일 뿐이며, 뷰 기계와 일반 룰 기계는 동일하다. 룰 적용 전략은 두 가지를 제공했다. 실행 전 쿼리를 수정하는 쿼리 재작성(INGRES 방식)과, 실행 중 튜플별로 룰을 발동하는 튜플 수준 구현이다. 현대 PostgreSQL은 룰 시스템에 쿼리 재작성 전략을 유지하고, 튜플 수준 반응은 트리거로 분리했다.
룰 기반 재작성이 가진 세 가지 성질은 이를 채택하는 모든 엔진을 형성하며, 소스를 읽기 전에 명확히 짚고 갈 필요가 있다.
- 쿼리 하나가 쿼리 목록이 된다. INSTEAD 룰은 원본을 대체하고, ALSO 룰은 원본과 함께 추가 작업을 하며, DO NOTHING 룰은 원본을 삭제한다. 따라서 리라이터의 출력은 빈 목록일 수도, 둘 이상의 쿼리 목록일 수도 있다.
- 대입은 재귀적이며 종료를 보장해야 한다. 뷰 위에 뷰를 쌓으면 레이어별로 확장되고, 룰 액션이 룰 대상 릴레이션을 참조하면 루프가 생긴다. 엔진에는 C 스택 오버플로에 의존하지 않는 명시적 순환 감지가 필요하다.
- 재작성은 플래닝 이전에 논리적
Query에서 실행된다. 출력도 여전히Query트리—플래너의 입력—이므로, 리라이터는 파서의 언어로 동작한다. range-table 항목, 타겟 리스트, 자격 조건을 조작하며 플랜 노드나 튜플을 건드리지 않는다.
PostgreSQL은 POSTGRES 룰 시스템의 충실한, 다만 더 정교해진 구현이다. 파스 분석과 플래닝 사이에서 저장된 룰 액션을 Query 트리에 대입하며, 뷰는 ON SELECT 룰, INSERT/UPDATE/DELETE 룰은 일반 경우, 행 수준 보안 정책은 같은 재귀 위에 덧붙인 마지막 구간으로 처리한다. 이 문서의 나머지 부분은 REL_18 소스 안에서 이 구성요소들을 추적한다.
DBMS 공통 설계
섹션 제목: “DBMS 공통 설계”이론은 모델을 제공한다. 파생 릴레이션에 대한 참조를 정의 쿼리로 대체하되, 최적화기 이전에 처리한다. 이 절은 교과서가 암묵적으로 다루는, 프로덕션 리라이터가 그 모델을 올바르고 안전하게 만들기 위해 채택하는 공학적 관례를 정리한다. ## PostgreSQL의 접근법에서 다루는 PostgreSQL의 구체적 선택은 이 공유된 선택지 공간 안의 하나다.
텍스트가 아닌 논리 쿼리 표현에서 재작성한다. 성숙한 엔진은 SQL 문자열을 재작성하지 않는다. 이름이 이미 오브젝트 ID로 해석되고, 타입이 확정되고, range table에 범위 내 모든 릴레이션이 열거된 분석된 쿼리 트리를 재작성한다. 뷰를 대입한다는 것은 뷰의 이미 분석된 서브트리를 외부 트리에 이어 붙이고 변수 참조를 재조정하는 것이지, 텍스트를 다시 파싱하는 것이 아니다. 이 덕분에 재작성은 결정적이고 재파싱 모호성에 영향을 받지 않는다.
뷰는 룰의 특수 사례다. 카탈로그에서 뷰를 별도 오브젝트 종류로 분리해 각자의 확장 경로를 두는 대신, INGRES/POSTGRES 계열은 뷰를 단 하나의 무조건적 ON SELECT DO INSTEAD 룰을 소유하는 릴레이션으로 모델링한다. 뷰 확장이 곧 룰 적용이 되며, 코드 경로가 둘이 아니라 하나다. 이 방식의 비용은 “뷰 갱신”이 공짜가 아니라는 점이다. 엔진은 방법을 알려주는 INSTEAD 룰 또는 INSTEAD OF 트리거를 찾거나, 뷰의 기반 릴레이션을 갱신 쿼리로 끌어올리는 자동 갱신을 유도해야 한다.
읽기 룰과 쓰기 룰을 구분한다. 읽기 측 확장(ON SELECT)과 쓰기 측 룰(ON INSERT/UPDATE/DELETE)은 반대 형태를 갖는다. 읽기 룰은 항상 발동하고, 항상 대체(INSTEAD)하며, 무조건적이고, 릴레이션 참조 대신 서브셀렉트를 대입한다. 쓰기 룰은 조건부일 수 있고, INSTEAD가 아닌 ALSO(추가)일 수 있으며, RETURNING 리스트를 가질 수 있고, 서브트리를 이어 붙이는 대신 완전한 추가 쿼리들을 생성한다. 두 형태를 혼동하면 어색한 특수 처리가 생긴다. PostgreSQL이 fireRIRrules(읽기)와 fireRules(쓰기)로 분리한 이유가 여기 있다.
조건부 INSTEAD에는 보완 기본 쿼리가 필요하다. 룰이 “WHERE qual인 경우에만 이 UPDATE 대신 그것을 하라”고 말하면, qual이 거짓인 행에는 여전히 원래 액션이 필요하다. 표준 기법은 룰 액션(+qual)과 함께, 원본 쿼리에 AND NOT qual(정확히는 3값 논리를 맞추기 위해 AND qual IS NOT TRUE)을 붙인 복사본을 함께 생성하는 것이다. 이 두 쿼리가 입력 행을 분할한다.
명시적 작업 스택으로 재귀를 제한한다. 룰 액션이 룰 대상 릴레이션을 참조할 수 있고 뷰가 전이적으로 자기 자신을 참조할 수 있으므로, 엔진은 현재 재작성 중인 (릴레이션, 이벤트) 쌍을 명시적으로 관리하며 재진입 시 오류를 발생시킨다. C 스택 오버플로에 의존하지 않는다. 뷰 확장에는 유사한 “활성 조회 룰” OID 집합을 쓴다.
락 획득이 리라이터의 숨겨진 임무다. 저장된 룰 액션은 원본 파스가 락하지 않은 릴레이션을 참조한다. 그 서브트리를 플래닝하고 실행하기 전에 리라이터가 액션이 언급하는 모든 릴레이션에 올바른 락 모드를 잡아야 한다. 그렇지 않으면 재작성과 실행 사이에 스키마가 바뀔 수 있다. “대입하면서 락 획득”이라는 책임은 눈에 잘 띄지 않아 미묘한 버그의 잦은 원인이 된다.
행 수준 보안은 이그제큐터 검사가 아닌 재작성이다. 행별 접근 정책을 강제하기에 가장 깔끔한 지점은 뷰가 확장되는 곳, 즉 리라이터다. 리라이터가 정책의 자격 조건을 릴레이션의 range-table 항목에 보안 배리어 자격 조건으로 붙이면, 플래너와 이그제큐터는 이를 평범하지만 최적화가 막혀 있는 WHERE 절로 처리한다. INGRES의 쿼리 수정 아이디어를 30년 후에 그대로 재사용하는 것이다.
flowchart LR
P["파서 +<br/>파스 분석"] --> QA["Query 트리<br/>QSRC_ORIGINAL"]
QA --> RW
subgraph RW["쿼리 리라이터 (QueryRewrite)"]
direction TB
S1["1단계: RewriteQuery<br/>쓰기 룰 실행 + 자동 갱신 뷰"]
S2["2단계: fireRIRrules<br/>뷰 확장 + SEARCH/CYCLE + RLS"]
S3["3단계: canSetTag 지정<br/>command result tag"]
S1 --> S2 --> S3
end
RW --> QL["Query 트리 목록<br/>0개, 1개, 또는 여러 개"]
QL --> PL["플래너<br/>각 쿼리 → PlannedStmt"]
PL --> EX["이그제큐터"]
그림 1 — 재작성이 위치하는 곳. 리라이터는 파스 분석과 플래닝 사이 단계다. Query 하나를 받아 Query 트리 목록을 내놓으며, 플래너가 각각을 PlannedStmt로 변환한다. 교차 참조: postgres-analyze-transform.md가 입력을 생성하는 파스 분석 단계를 다루고, postgres-planner-overview.md가 출력을 소비하는 플래너를 다룬다.
PostgreSQL의 접근법
섹션 제목: “PostgreSQL의 접근법”PostgreSQL의 리라이터는 src/backend/rewrite/ 아래에 있다. 공개 진입점은 rewriteHandler.c의 QueryRewrite로, 최상위 분석 쿼리 하나마다 tcop/postgres.c의 pg_rewrite_query에서 호출된다. 룰은 RewriteRule 구조체(pg_rewrite 카탈로그 행에서 빌드하며, relcache에 rel->rd_rules로 캐시됨)이고, 뷰는 rd_rules에 _RETURN이라는 이름의 CMD_SELECT 룰 하나를 가진 릴레이션이다.
세 단계 진입점
섹션 제목: “세 단계 진입점”QueryRewrite는 의도적으로 얇다. 입력이 수정되지 않은 원본 쿼리임을 단언한 뒤 세 단계를 실행한다.
// QueryRewrite — src/backend/rewrite/rewriteHandler.c (condensed)List *QueryRewrite(Query *parsetree){ int64 input_query_id = parsetree->queryId; List *querylist; List *results;
Assert(parsetree->querySource == QSRC_ORIGINAL); Assert(parsetree->canSetTag);
/* Step 1: apply all non-SELECT rules, possibly getting 0 or many queries */ querylist = RewriteQuery(parsetree, NIL, 0, 0);
/* Step 2: apply all the RIR (retrieve-instead-retrieve) rules on each query */ results = NIL; foreach(l, querylist) { Query *query = (Query *) lfirst(l); query = fireRIRrules(query, NIL); query->queryId = input_query_id; results = lappend(results, query); }
/* Step 3: decide which result query sets the command tag */ /* ... if the original survives it sets the tag; else the last INSTEAD ... */ return results;}명명 방식은 POSTGRES 역사를 반영한다. RIR = “retrieve instead retrieve”, 즉 뷰를 정의하는 무조건적 ON SELECT DO INSTEAD 룰이다. 1단계가 쓰기 룰을 처리해 실행할 쿼리 집합을 만들고, 2단계가 각 쿼리 안에서 뷰(및 RLS, SEARCH/CYCLE)를 확장하며, 3단계는 어느 쿼리가 행 수를 보고할지를 결정하는 정리 작업이다.
flowchart TB QR["QueryRewrite(parsetree)"] --> RQ["RewriteQuery<br/>(1단계: 쓰기 룰)"] RQ -->|"product query 목록"| LOOP["각 결과 쿼리에 대해"] LOOP --> RIR["fireRIRrules<br/>(2단계)"] RIR --> VE["뷰 RTE 확장<br/>ApplyRetrieveRule"] RIR --> SC["SEARCH / CYCLE 확장<br/>rewriteSearchAndCycle"] RIR --> RLS["RLS 정책 적용<br/>get_row_security_policies"] VE -.->|"뷰 본문으로 재귀"| RIR RIR --> TAG["3단계: canSetTag 지정"] TAG --> OUT["플래닝 가능한 Query 트리 목록"]
그림 2 — QueryRewrite 제어 흐름. 1단계(RewriteQuery)가 쓰기 룰을 목록으로 평탄화하고, 2단계(fireRIRrules)가 각 결과 안에서 뷰, 재귀 CTE SEARCH/CYCLE 절, 행 수준 보안을 확장하며, 3단계가 command tag를 지정한다.
뷰는 ON SELECT DO INSTEAD 룰이다
섹션 제목: “뷰는 ON SELECT DO INSTEAD 룰이다”뷰에는 저장소가 없다. CREATE VIEW v AS SELECT …는 relkind가 RELKIND_VIEW인 빈 릴레이션을 만든 다음 DefineQueryRewrite로 ON SELECT 룰을 설치한다. 이 함수는 뷰만 ON SELECT 룰을 가질 수 있다는 규칙, 액션이 단일 무조건적 SELECT여야 한다는 규칙, 타겟 리스트가 뷰의 컬럼과 정확히 일치해야 한다는 규칙을 강제한다.
// DefineQueryRewrite — src/backend/rewrite/rewriteDefine.c (condensed)if (event_type == CMD_SELECT){ /* Rules ON SELECT are restricted to view definitions, so this had * better be a view ... */ if (event_relation->rd_rel->relkind != RELKIND_VIEW && event_relation->rd_rel->relkind != RELKIND_MATVIEW) ereport(ERROR, /* "relation cannot have ON SELECT rules" */ ...);
/* ... the one action must be an unconditional INSTEAD SELECT ... */ if (length(action) != 1 || query->commandType != CMD_SELECT) ereport(ERROR, /* "rules on SELECT must have action INSTEAD SELECT" */ ...); if (event_qual != NULL) ereport(ERROR, /* "event qualifications are not implemented for rules on SELECT" */ ...);
/* ... the SELECT's targetlist must exactly match the view's columns ... */ checkRuleResultList(query->targetList, RelationGetDescr(event_relation), true, /* requireColumnNameMatch = */ event_relation->rd_rel->relkind != RELKIND_MATVIEW);
/* ... and the rule must be named _RETURN (ViewSelectRuleName) */}재작성 시 뷰의 정의 쿼리를 읽으려면 get_view_query가 relcache에서 그 _RETURN 룰의 단일 액션을 찾아 반환한다. 반환 포인터는 읽기 전용이므로 수정 전에 반드시 복사해야 한다.
// get_view_query — src/backend/rewrite/rewriteHandler.c (condensed)Query *get_view_query(Relation view){ Assert(view->rd_rel->relkind == RELKIND_VIEW); for (int i = 0; i < view->rd_rules->numLocks; i++) { RewriteRule *rule = view->rd_rules->rules[i]; if (rule->event == CMD_SELECT) { if (list_length(rule->actions) != 1) elog(ERROR, "invalid _RETURN rule action specification"); return (Query *) linitial(rule->actions); /* read-only! */ } } elog(ERROR, "failed to find _RETURN rule for view");}“빈 릴레이션 + 룰”이라는 이중 인코딩 덕분에 information_schema.views, pg_get_viewdef, \d+ v가 모두 저장된 Query에서 뷰 정의를 재구성한다. Query 자체가 뷰의 정의다.
DefineQueryRewrite에는 릴레이션을 뷰로 변환하는 경로도 있다. 데이터가 없고 허용되지 않는 기능이 없는 평범한 테이블에 ON SELECT 룰을 설치하면, 이 함수가 릴레이션의 relkind를 RELKIND_VIEW로 바꾸고 저장소를 분리한다. pg_dump가 한때 “테이블 + 룰” 방식으로 뷰를 재생성할 때 의존하던 메커니즘이다. 같은 함수가 이벤트 릴레이션에 AccessExclusiveLock을 잡는 이유는, ON SELECT 룰을 설치하면 이미 진행 중인 SELECT가 곧 가상 릴레이션이 될 대상을 스캔하는 것을 막아야 하기 때문이다.
// DefineQueryRewrite — src/backend/rewrite/rewriteDefine.c (condensed)/* If we are installing an ON SELECT rule, we had better grab * AccessExclusiveLock to ensure no SELECTs are currently running on the * event relation. ... */event_relation = table_open(event_relid, AccessExclusiveLock);
if (event_relation->rd_rel->relkind != RELKIND_RELATION && event_relation->rd_rel->relkind != RELKIND_MATVIEW && event_relation->rd_rel->relkind != RELKIND_VIEW && event_relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) ereport(ERROR, /* "relation cannot have rules" */ ...);락 수준은 DefineRule에서도 (RangeVarGetRelid(stmt->relation, AccessExclusiveLock, false)로) 일치하게 잡는다.
fireRIRrules — 뷰 확장, SELECT 측 주역
섹션 제목: “fireRIRrules — 뷰 확장, SELECT 측 주역”QueryRewrite의 2단계는 fireRIRrules다. 쿼리의 range table을 순회하며 서브쿼리, CTE, SubLink로 재귀한다. ON SELECT 룰을 가진 릴레이션 RTE마다 ApplyRetrieveRule을 적용해 릴레이션 RTE를 뷰 본문을 서브쿼리로 갖는 서브쿼리 RTE로 변환한다.
// ApplyRetrieveRule — src/backend/rewrite/rewriteHandler.c (condensed)static Query *ApplyRetrieveRule(Query *parsetree, RewriteRule *rule, int rt_index, Relation relation, List *activeRIRs){ Query *rule_action; RangeTblEntry *rte;
if (list_length(rule->actions) != 1) elog(ERROR, "expected just one rule action"); if (rule->qual != NULL) elog(ERROR, "cannot handle qualified ON SELECT rule");
/* ... (UPDATE/DELETE-on-view 경로는 타겟 릴레이션 복사본 + wholerow Var를 추가함) ... */
/* Make a modifiable copy of the view query and lock the rels it mentions */ rule_action = copyObject(linitial(rule->actions)); AcquireRewriteLocks(rule_action, true, (rc != NULL));
/* Recursively expand any view references inside the view */ rule_action = fireRIRrules(rule_action, activeRIRs);
/* Plug the view query in as a subselect: relation RTE -> subquery RTE */ rte = rt_fetch(rt_index, parsetree->rtable); rte->rtekind = RTE_SUBQUERY; rte->subquery = rule_action; rte->security_barrier = RelationIsSecurityView(relation); rte->tablesample = NULL; rte->inh = false; return parsetree;}rte->rtekind = RTE_SUBQUERY; rte->subquery = rule_action; 변환이 핵심이다. 플래너는 이후 뷰를 인라인 서브쿼리와 완전히 동일하게 처리하며, 보통은 서브쿼리 풀-업으로 다시 평탄화한다. 두 가지 세부 사항이 중요하다. 첫째, security_barrier는 뷰 릴레이션에서 복사하는데, WITH (security_barrier) 뷰는 서브쿼리가 qual 재배치를 막아 RLS가 이 위에 쌓이는 기반이 된다. 둘째, 재귀 fireRIRrules(rule_action, activeRIRs) 호출이 뷰 안에 중첩된 뷰를 확장하며, 재귀는 호출 직전에 검사하는 activeRIRs OID 목록으로 제한한다.
// fireRIRrules — src/backend/rewrite/rewriteHandler.c (condensed)if (list_member_oid(activeRIRs, RelationGetRelid(rel))) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), errmsg("infinite recursion detected in rules for relation \"%s\"", RelationGetRelationName(rel))));activeRIRs = lappend_oid(activeRIRs, RelationGetRelid(rel));foreach(l, locks) parsetree = ApplyRetrieveRule(parsetree, lfirst(l), rt_index, rel, activeRIRs);activeRIRs = list_delete_last(activeRIRs);구체화 뷰(RELKIND_MATVIEW)는 참조될 때 fireRIRrules가 확장하지 않는다. 실제 저장소를 갖기 때문에 루프에서 건너뛴다. 그 ON SELECT 룰은 MV 자신의 정의 쿼리를 실행하는 REFRESH 시에만 사용한다.
RewriteQuery와 fireRules — 쓰기 룰 경로
섹션 제목: “RewriteQuery와 fireRules — 쓰기 룰 경로”1단계 RewriteQuery는 CMD_INSERT / CMD_UPDATE / CMD_DELETE / CMD_MERGE를 처리한다. 각 커맨드마다 먼저 타겟 리스트를 정규화하고(rewriteTargetListIU — 컬럼 기본값 채우기, DEFAULT 마커 확장, 물리적 컬럼 순서로 재배치), 그 다음 matchLocks로 해당 룰을 수집해 fireRules로 실행한다.
// RewriteQuery — src/backend/rewrite/rewriteHandler.c (condensed)locks = matchLocks(event, rt_entry_relation, result_relation, parsetree, &hasUpdate);
product_orig_rt_length = list_length(parsetree->rtable);product_queries = fireRules(parsetree, result_relation, event, locks, &instead, &returning, &qual_product);matchLocks는 이벤트가 일치하고 (쓰기 룰의 경우) 타겟이 결과 릴레이션인 룰만 반환하며, 세션 복제 역할도 고려하고 MERGE 타겟에 대한 룰은 금지한다. fireRules는 매칭된 룰을 순회하며 각각을 QuerySource로 분류한다.
// fireRules — src/backend/rewrite/rewriteHandler.c (condensed)foreach(l, locks){ RewriteRule *rule_lock = (RewriteRule *) lfirst(l); Node *event_qual = rule_lock->qual; QuerySource qsrc;
if (rule_lock->isInstead) { if (event_qual != NULL) qsrc = QSRC_QUAL_INSTEAD_RULE; /* conditional INSTEAD */ else { qsrc = QSRC_INSTEAD_RULE; /* unconditional INSTEAD */ *instead_flag = true; } } else qsrc = QSRC_NON_INSTEAD_RULE; /* ALSO rule */
if (qsrc == QSRC_QUAL_INSTEAD_RULE && !*instead_flag) { /* build the complementary "default" query: original AND NOT qual */ if (*qual_product == NULL) *qual_product = copyObject(parsetree); *qual_product = CopyAndAddInvertedQual(*qual_product, event_qual, rt_index, event); }
foreach(r, rule_lock->actions) /* rewrite & emit each rule action */ { Query *rule_action = rewriteRuleAction(parsetree, lfirst(r), event_qual, rt_index, event, returning_flag); rule_action->querySource = qsrc; rule_action->canSetTag = false; results = lappend(results, rule_action); }}세 QuerySource 값이 룰 분류를 인코딩한다. QSRC_INSTEAD_RULE(무조건적 INSTEAD — 원본 쿼리가 제거됨), QSRC_QUAL_INSTEAD_RULE(조건부 INSTEAD — 원본은 살아남지만 CopyAndAddInvertedQual이 rule_qual IS NOT TRUE를 붙여 3값 논리를 처리함), QSRC_NON_INSTEAD_RULE(DO ALSO — 추가, 원본도 실행됨). CMD_NOTHING 액션은 아무것도 생성하지 않으므로 DO INSTEAD NOTHING은 단순히 쿼리를 삭제한다. rewriteRuleAction이 실제 작업을 한다. 저장된 액션을 깊게 복사하고, 락을 잡으며(AcquireRewriteLocks), 액션의 range table을 파스 트리에 병합하고, 변수 번호를 조정하며, OLD/NEW 참조(PRS2_OLD_VARNO / PRS2_NEW_VARNO)가 원본 쿼리의 릴레이션과 타겟 리스트를 가리키도록 고친다.
자동 갱신 가능 뷰 — 기반 릴레이션 끌어올리기
섹션 제목: “자동 갱신 가능 뷰 — 기반 릴레이션 끌어올리기”INSERT/UPDATE/DELETE가 뷰를 대상으로 하는데 무조건적 INSTEAD 룰도 INSTEAD OF 트리거도 발동하지 않은 경우, RewriteQuery는 rewriteTargetView로 뷰의 기반 릴레이션을 직접 갱신하는 쿼리로 재작성하려 시도한다.
// RewriteQuery — src/backend/rewrite/rewriteHandler.c (condensed)if (!instead && rt_entry_relation->rd_rel->relkind == RELKIND_VIEW && !view_has_instead_trigger(rt_entry_relation, event, parsetree->mergeActionList)){ if (qual_product != NULL) /* conditional INSTEAD blocks auto-update */ error_view_not_updatable(rt_entry_relation, parsetree->commandType, parsetree->mergeActionList, gettext_noop("Views with conditional DO INSTEAD rules are not automatically updatable."));
parsetree = rewriteTargetView(parsetree, rt_entry_relation);
if (parsetree->commandType == CMD_INSERT) product_queries = lcons(parsetree, product_queries); else product_queries = lappend(product_queries, parsetree); instead = true; /* prevent the original from being emitted again */ returning = true; updatableview = true;}rewriteTargetView는 뷰가 자동 갱신 가능한지 검사한다(view_query_is_auto_updatable — 단일 기반 릴레이션, 집계/DISTINCT/GROUP BY 없음 등). 기반 릴레이션을 RowExclusiveLock으로 열고, 쿼리의 range table에 새 RTE를 추가하며, ChangeVarNodes로 뷰의 타겟 리스트 Var들이 기반 테이블을 가리키도록 재조정한다. 결과가 또 뷰일 수 있으므로 재작성된 쿼리는 다시 RewriteQuery를 재귀 통과한다. WITH CHECK OPTION은 여기서 withCheckOptions를 누적하는 방식으로 강제하며, RLS가 쓰는 것과 같은 목록이다.
쓰기 룰 재귀 제한
섹션 제목: “쓰기 룰 재귀 제한”뷰 확장이 activeRIRs를 쓰듯, 쓰기 룰 재귀는 (릴레이션, 이벤트) 쌍의 명시적 rewrite_events 목록을 쓴다. product queries로 재귀하기 전에 RewriteQuery는 해당 쌍이 이미 진행 중인지 확인한다.
// RewriteQuery — src/backend/rewrite/rewriteHandler.c (condensed)foreach(n, rewrite_events){ rewrite_event *rev = (rewrite_event *) lfirst(n); if (rev->relation == RelationGetRelid(rt_entry_relation) && rev->event == event) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), errmsg("infinite recursion detected in rules for relation \"%s\"", RelationGetRelationName(rt_entry_relation))));}rev = palloc(sizeof(rewrite_event));rev->relation = RelationGetRelid(rt_entry_relation);rev->event = event;rewrite_events = lappend(rewrite_events, rev);/* ... 각 product query로 재귀 ... */rewrite_events = list_delete_last(rewrite_events);행 수준 보안 접합부
섹션 제목: “행 수준 보안 접합부”행 수준 보안(RLS)은 fireRIRrules의 마지막 구간에서 처리한다. 뷰 확장, CTE 재귀, SubLink 처리 이후다. 이는 의도적 설계다. RLS qual이 SubLink를 포함할 수 있어 앞서 처리했다면 두 번 순회될 수 있기 때문이다. 평범한 릴레이션 또는 파티션드 테이블 RTE마다 리라이터가 RLS 서브시스템에 정책 자격 조건을 요청하고 그것을 RTE의 securityQuals 앞에 붙인다.
// fireRIRrules — src/backend/rewrite/rewriteHandler.c (condensed)get_row_security_policies(parsetree, rte, rt_index, &securityQuals, &withCheckOptions, &hasRowSecurity, &hasSubLinks);
if (securityQuals != NIL || withCheckOptions != NIL){ /* if RLS quals have SubLinks, lock their rels and fire RIR rules on them, guarding against infinite recursion via activeRIRs */ ... /* prepend RLS quals so they sort *before* any security-barrier-view quals */ rte->securityQuals = list_concat(securityQuals, rte->securityQuals); parsetree->withCheckOptions = list_concat(withCheckOptions, parsetree->withCheckOptions);}if (hasRowSecurity) parsetree->hasRowSecurity = true;get_row_security_policies(rowsecurity.c 소재)는 securityQuals(읽기 측 — SELECT/UPDATE/DELETE 가시성)와 withCheckOptions(쓰기 측 — 새 행/갱신된 행이 정책을 만족하지 않으면 문장이 오류를 냄)를 반환한다. qual을 평범한 WHERE 대신 securityQuals에 두는 것이 그것들을 보안 배리어 qual로 만드는 방법이다. 플래너가 잠재적으로 정보를 유출하는 사용자 함수를 그 아래로 밀어내지 못하게 한다. 정책 선택, USING 대 WITH CHECK, 허용적/제한적 조합, FORCE ROW LEVEL SECURITY의 더 깊은 메커니즘은 교차 참조 postgres-row-level-security.md에서 다루며, 이 문서는 리라이터가 그것을 호출하는 접합부만 다룬다.
SEARCH / CYCLE 절 확장
섹션 제목: “SEARCH / CYCLE 절 확장”fireRIRrules 안에는 재귀 CTE의 SEARCH와 CYCLE 절 확장도 포함된다. 이 구간은 이미 모든 Query를 순회하므로 편리하게 처리할 수 있다. rewriteSearchAndCycle은 WITH RECURSIVE … SEARCH BREADTH/DEPTH FIRST 또는 … CYCLE CTE를 추가 컬럼을 가진 평범한 재귀 CTE로 재작성한다. SEARCH는 깊이 추적 ROW(...) / 배열, CYCLE은 방문한 행 배열과 사이클 표시 CASE 식을 추가한다.
// fireRIRrules — src/backend/rewrite/rewriteHandler.c (condensed)foreach(lc, parsetree->cteList){ CommonTableExpr *cte = lfirst_node(CommonTableExpr, lc); if (cte->search_clause || cte->cycle_clause) { cte = rewriteSearchAndCycle(cte); lfirst(lc) = cte; }}이 덕분에 플래너와 이그제큐터는 SEARCH/CYCLE을 전혀 알지 못한다. CTE를 받을 때는 이미 장부 컬럼이 SQL로 구체화된 평범한 재귀 유니온이다.
소스 탐방
섹션 제목: “소스 탐방”심볼 이름에 닻을 내리고, 줄 번호에 의존하지 말 것. PostgreSQL 소스는 릴리스 사이에 움직이지만 함수나 구조체 이름은 안정적이다.
git grep -n '<symbol>' src/backend/rewrite/로 현재 위치를 찾는다. 아래 위치 힌트 표의 줄 번호는 커밋273fe94(REL_18_STABLE) 기준으로 관찰한 것이며 빠른 참고용이다.
진입점 및 조율 (rewriteHandler.c)
섹션 제목: “진입점 및 조율 (rewriteHandler.c)”QueryRewrite— 단일 공개 진입점. 세 단계를 실행한다(RewriteQuery→ 쿼리별fireRIRrules→ command-tag 지정).tcop/postgres.c의pg_rewrite_query에서 호출된다.RewriteQuery— 1단계. INSERT/UPDATE/DELETE/MERGE 쓰기 룰을 실행하고, 타겟 리스트를 정규화하며, 자동 갱신 가능 뷰를 처리하고, product queries와 데이터 수정 CTE로 재귀한다.rewrite_events재귀 스택을 유지한다.fireRIRrules— 2단계. range table을 순회하며ApplyRetrieveRule로 뷰를 확장하고, 서브쿼리/CTE/SubLink로 재귀하며, SEARCH/CYCLE을 확장하고, 최종 구간으로 RLS를 적용한다.activeRIRsOID 목록을 유지한다.
쓰기 룰 기계 (rewriteHandler.c)
섹션 제목: “쓰기 룰 기계 (rewriteHandler.c)”matchLocks—relation->rd_rules에서 이벤트가 일치하고 (쓰기 룰의 경우) 타겟이 결과 릴레이션인RewriteRule들을 선택한다.SessionReplicationRole을 존중하고 MERGE 타겟에 대한 룰을 차단한다.fireRules— 매칭된 룰을 순회하며 INSTEAD / 조건부 INSTEAD / ALSO를QuerySource로 분류하고,qual_product기본 복사본을 만들며, 각 액션을rewriteRuleAction으로 생성한다.rewriteRuleAction— 저장된 룰 액션을 깊게 복사하고, 락을 잡으며, range table을 파스 트리에 병합하고, varno를 조정하며,OLD/NEW(PRS2_OLD_VARNO/PRS2_NEW_VARNO) 참조를 수정한다.CopyAndAddInvertedQual/AddInvertedQual— 조건부 INSTEAD 룰 처리 시 살아남는 원본 쿼리에rule_qual IS NOT TRUE를 붙인다.rewriteTargetListIU— 컬럼 기본값을 채우고,SetToDefault마커를 확장하며, GENERATED / identity 컬럼을 처리하고, 물리적 순서로 재배치한다.rewriteValuesRTE/rewriteValuesRTEToNulls— 다중 행INSERT … VALUES의 기본값 처리.
뷰 확장 및 갱신 가능 뷰 (rewriteHandler.c)
섹션 제목: “뷰 확장 및 갱신 가능 뷰 (rewriteHandler.c)”ApplyRetrieveRule— 뷰의 릴레이션 RTE를 뷰 본문을 담은 서브쿼리 RTE로 변환한다.security_barrier를 설정하고fireRIRrules로 재귀한다.get_view_query— relcache에서 뷰의_RETURN룰 액션을 읽어온다(읽기 전용 포인터).rewriteTargetView— 뷰의 단일 기반 릴레이션을 갱신 쿼리에 끌어올려 뷰를 자동 갱신 가능하게 만든다.WITH CHECK OPTION을 강제한다.view_has_instead_trigger— 해당 이벤트에 뷰의 INSTEAD OF 트리거가 있는지 확인한다(있으면 자동 갱신을 시도하지 않음).error_view_not_updatable— “cannot insert/update/delete view” 오류 및 힌트 패밀리.markQueryForLocking—FOR [KEY] UPDATE/SHARE를 확장된 뷰 안의 포함된 릴레이션들로 밀어 넣는다.
락 획득 및 조작 (rewriteHandler.c, rewriteManip.c)
섹션 제목: “락 획득 및 조작 (rewriteHandler.c, rewriteManip.c)”AcquireRewriteLocks— (저장된) 쿼리가 언급하는 모든 릴레이션에 올바른 락 모드를 잡는다. 삭제된 컬럼 JOIN alias var를 수정하고 서브쿼리, CTE, SubLink로 재귀한다.acquireLocksOnSubLinks— SubLink 서브쿼리에 도달하는 워커.ChangeVarNodes/OffsetVarNodes/ReplaceVarsFromTargetList(rewriteManip.c소재) — 리라이터가 트리를 이어 붙일 때 쓰는 변수 재번호 매기기와 대입 프리미티브.
룰 정의 (rewriteDefine.c)
섹션 제목: “룰 정의 (rewriteDefine.c)”DefineRule—CREATE RULE실행. 액션/qual을 파스 분석한 뒤DefineQueryRewrite를 호출한다.DefineQueryRewrite— 핵심: 이벤트/relkind 조합을 검증하고, ON SELECT = 뷰 불변식을 강제하며, 선택적으로 릴레이션을 뷰로 변환하고,InsertRule로pg_rewrite에 삽입한다.checkRuleResultList— SELECT/RETURNING 타겟 리스트가 릴레이션과 타입 호환(뷰의 경우 이름도 호환)되는지 검증한다.InsertRule—pg_rewrite카탈로그 행을 쓴다.SetRelationRuleStatus가pg_class.relhasrules를 뒤집고 SI 무효화를 브로드캐스트한다.
행 수준 보안 접합부 (rowsecurity.c)
섹션 제목: “행 수준 보안 접합부 (rowsecurity.c)”get_row_security_policies— 각 RTE의securityQuals(읽기 측)와withCheckOptions(쓰기 측),hasRowSecurity/hasSubLinks플래그를 반환한다. 리라이터가 RLS 서브시스템에 진입하는 유일한 지점이다.
재귀 CTE 절 확장 (rewriteSearchCycle.c)
섹션 제목: “재귀 CTE 절 확장 (rewriteSearchCycle.c)”rewriteSearchAndCycle—SEARCH/CYCLE재귀 CTE를 추가 장부 컬럼이 있는 평범한 재귀 유니온으로 재작성한다.make_path_rowexpr— search/cycle 컬럼에 쓸ROW(...)식을 만든다.
핵심 자료 구조
섹션 제목: “핵심 자료 구조”RewriteRule(rewrite/prs2lock.h) —ruleId,event(CmdType),qual,actions,enabled,isInstead. 릴레이션의 룰은rel->rd_rules(RuleLock, 역시prs2lock.h소재;rel->rd_rules는utils/rel.h에 선언됨)에 달려 있다.QuerySource열거형(nodes/parsenodes.h) —QSRC_ORIGINAL,QSRC_PARSER,QSRC_INSTEAD_RULE,QSRC_QUAL_INSTEAD_RULE,QSRC_NON_INSTEAD_RULE. 각Query에 출처를 태그한다.RangeTblEntry(nodes/parsenodes.h) — 리라이터가 뷰의rtekind를RTE_RELATION에서RTE_SUBQUERY로 뒤집고, RLS 및 배리어 뷰 처리에 쓰이는securityQuals/security_barrier를 기록한다.PRS2_OLD_VARNO/PRS2_NEW_VARNO(nodes/primnodes.h) — 파서가 룰 액션 안의OLD/NEW에 할당하는 예약된 range-table 인덱스(1과 2).
위치 힌트 (2026-06-05 기준, REL_18 273fe94)
섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”| 심볼 | 파일 | 줄 |
|---|---|---|
QueryRewrite | rewriteHandler.c | 4635 |
RewriteQuery | rewriteHandler.c | 3947 |
fireRIRrules | rewriteHandler.c | 2026 |
fireRules | rewriteHandler.c | 2458 |
matchLocks | rewriteHandler.c | 1671 |
rewriteRuleAction | rewriteHandler.c | 350 |
CopyAndAddInvertedQual | rewriteHandler.c | 2355 |
rewriteTargetListIU | rewriteHandler.c | 808 |
ApplyRetrieveRule | rewriteHandler.c | 1746 |
get_view_query | rewriteHandler.c | 2549 |
rewriteTargetView | rewriteHandler.c | 3281 |
view_has_instead_trigger | rewriteHandler.c | 2588 |
error_view_not_updatable | rewriteHandler.c | 3186 |
markQueryForLocking | rewriteHandler.c | 1926 |
AcquireRewriteLocks | rewriteHandler.c | 147 |
DefineRule | rewriteDefine.c | 190 |
DefineQueryRewrite | rewriteDefine.c | 224 |
checkRuleResultList | rewriteDefine.c | 506 |
InsertRule | rewriteDefine.c | 52 |
get_row_security_policies | rowsecurity.c | 98 |
rewriteSearchAndCycle | rewriteSearchCycle.c | 203 |
make_path_rowexpr | rewriteSearchCycle.c | 117 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”이 문서의 주장은 커밋 273fe94(REL_18_STABLE) 작업 트리에서 직접 확인했다. 핵심 항목은 다음과 같다.
-
QueryRewrite가 단일 진입점이며 정확히 세 단계를 실행한다. 전체를 읽어 확인했다. 1단계RewriteQuery(parsetree, NIL, 0, 0), 2단계querylist에 대한foreach에서fireRIRrules(query, NIL)호출, 3단계canSetTag지정 루프.parsetree->querySource == QSRC_ORIGINAL및parsetree->canSetTag단언이 최상위 쿼리에만 적용됨을 확인한다.QueryRewrite읽기로 검증. -
뷰는 하나의 ON SELECT DO INSTEAD
_RETURN룰을 가진 릴레이션이다.DefineQueryRewrite의event_type == CMD_SELECT분기가relkindVIEW 또는 MATVIEW, 단일 무조건적CMD_SELECT액션,event_qual == NULL, 이름ViewSelectRuleName(_RETURN)을 요구한다.get_view_query는rd_rules에서CMD_SELECT이벤트로 그 룰을 찾아 단일 액션을 반환한다. 두 함수 읽기로 검증. -
뷰 확장이 RTE를 제자리에서 변환한다:
RTE_RELATION→RTE_SUBQUERY.ApplyRetrieveRule이rte->rtekind = RTE_SUBQUERY; rte->subquery = rule_action; rte->security_barrier = RelationIsSecurityView(relation);로 종결한다.ApplyRetrieveRule읽기로 검증. -
뷰 확장 재귀는
activeRIRs로, 쓰기 룰 재귀는rewrite_events로 제한된다.fireRIRrules가 OID를 푸시하기 전에list_member_oid(activeRIRs, …)이면errmsg("infinite recursion detected in rules for relation …")를 발생시키고,RewriteQuery가 같은 오류를rewrite_events(릴레이션, 이벤트) 스캔에서 발생시킨다. 두 재귀 가드 읽기로 검증. -
조건부 INSTEAD 룰은 반전 qual이 붙은
qual_product기본 복사본을 생성한다.fireRules가QSRC_QUAL_INSTEAD_RULE및!*instead_flag아래에서CopyAndAddInvertedQual을 호출하며, 이 함수는 (헤더 주석에 따라)'AND rule_qual IS NOT TRUE'를 붙인다.NOT x가 아닌 이 형식으로 3값 논리를 올바르게 처리한다.fireRules와CopyAndAddInvertedQual읽기로 검증. -
자동 갱신 가능 뷰는 기반 릴레이션을 끌어올린다. 조건부 INSTEAD 룰은 이를 막는다.
RewriteQuery의!instead && relkind == VIEW && !view_has_instead_trigger(...)분기가qual_product != NULL이면error_view_not_updatable을, 그렇지 않으면rewriteTargetView를 호출한다.rewriteTargetView는 기반 릴레이션을RowExclusiveLock으로 열고, 새 RTE를 추가하며,ChangeVarNodes로 Var를 재조정한다. 해당 분기와rewriteTargetView읽기로 검증. -
RLS는
fireRIRrules의 마지막 구간이며securityQuals앞에 붙인다. RLS 루프는 뷰 확장, CTE, SubLink 구간 이후에 실행하며get_row_security_policies를 호출하고rte->securityQuals = list_concat(securityQuals, rte->securityQuals),parsetree->withCheckOptions에 추가한다. 헤더 주석이 마지막에 처리하는 이유(query_tree_walker가 새 qual을 두 번 재귀하는 것을 방지)를 설명한다.fireRIRrules의 RLS 블록과rowsecurity.c의get_row_security_policies시그니처 읽기로 검증. -
AcquireRewriteLocks가 저장된 룰 액션이 참조하는 릴레이션에 락을 잡는다.rewriteRuleAction과ApplyRetrieveRule모두 저장된 액션을copyObject한 직후AcquireRewriteLocks(rule_action, true, …)를 호출한다. 원본 파스가 해당 릴레이션을 락하지 않았기 때문이다. 함수 헤더가 스키마 안정성 목적과forExecute락 모드 논리를 설명한다.AcquireRewriteLocks및 두 호출 위치 읽기로 검증. -
SEARCH/CYCLE 확장이
fireRIRrules안에서 일어난다.cte->search_clause || cte->cycle_clause루프가rewriteSearchAndCycle을 호출하며,rewriteSearchCycle.c파일 헤더가 정확한 목표 재작성(추가ROW(...)/ 배열 컬럼)을 설명한다. 루프와rewriteSearchAndCycle서문 읽기로 검증.
미해결 질문
섹션 제목: “미해결 질문”-
rewriteTargetView의WITH CHECK OPTION누적과 RLSwithCheckOptions의 정확한 상호작용. 둘 다parsetree->withCheckOptions에 추가하지만, 순서와 이그제큐터가 실행 시 뷰 CHECK OPTION과 RLS WITH CHECK를 어떻게 구분하는지는 여기서 완전히 추적하지 않았다. 조사 경로:executor/execMain.c의ExecWithCheckOptions와parsenodes.h의WithCheckOption. -
pg_rewrite룰 저장과 relcacherd_rules가 DDL 전반에 걸쳐 동기화를 유지하는 방법.SetRelationRuleStatus가 SI 무효화를 브로드캐스트하지만,RuleLock에 대한 정확한 relcache 재빌드 경로는 주석을 신뢰하는 선에서 멈췄다. 조사 경로:utils/cache/relcache.c의RelationBuildRuleLock. -
서브쿼리 풀-업이 항상 뷰 확장을 되돌리는지, 아니면 뷰 서브셀렉트가 플랜에 남는 경우가 있는지.
ApplyRetrieveRule이RTE_SUBQUERY를 남기면 플래너가 보통 평탄화하지만, 보안 배리어 뷰와 특정 구성이 풀-업을 막는다. 정확한 조건은postgres-planner-overview.md에 속한다. 조사 경로:optimizer/prep/prepjointree.c의pull_up_subqueries.
PostgreSQL 너머 — 비교 설계와 연구 프런티어
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프런티어”포인터만 제시한다. 각 항목은 후속 문서의 출발점이며 깊이는 의도적으로 얕다.
-
INGRES query modification — 직접 조상. Stonebraker, Wong, Kreps & Held, “The Design and Implementation of INGRES”(ACM TODS 1976)가 뷰, 보호, 무결성의 통합 메커니즘으로 query modification을 소개했다. PostgreSQL의 리라이터는 POSTGRES 룰 시스템이 일반화한 같은 아이디어의 인식 가능한 구현이다(
dbms-papers/systemr.md가 System R 대응물이며, 참고 문헌의 POSTGRES 설계 논문들이 더 가까운 계보다). INGRES 튜플 대입 의미론과 PostgreSQL의Query트리 이어붙이기를 나란히 비교하면 30년의 공학이 무엇을 가져왔는지 보인다. -
PostgreSQL이 폐기한 일반 룰 시스템. POSTGRES(Stonebraker, Rowe & Hirohama 1990)는 임의 릴레이션에 대한 ON SELECT 룰과 튜플 수준 룰 발동을 포함한 훨씬 야심찬 프로덕션 룰 시스템을 지원했다. 현대 PostgreSQL은 이를 의도적으로 좁혔다. ON SELECT 룰은 뷰로 제한되고, 튜플 수준 반응은 트리거로 이동했다. 일반 룰 시스템이 축소된 이유 — 최적화기 상호작용, 예상치 못한 의미론, 트리거의 부상 — 는 가치 있는 설계 회고다.
-
트리거 대 룰, 경쟁하는 재작성/반응 메커니즘. 룰은 컴파일 타임(재작성 시) 쿼리 변환이고, 트리거는 실행 시 행별 콜백이다. 둘 다 겹치는 의도를 표현할 수 있다(예: INSTEAD 룰 대 INSTEAD OF 트리거로 갱신 가능 뷰 구현). 성능과 의미론은 매우 다르다. PostgreSQL 문서의 “Rules versus Triggers” 챕터가 표준 비교이며, 어떤 문제에 어떤 도구가 맞는지 매핑하는 문서가 이것을 보완할 것이다. 교차 참조:
postgres-ddl-execution.md(트리거 정의). -
구체화 뷰와 증분 뷰 유지. PostgreSQL 구체화 뷰는 저장소 지원이 있고 정의 쿼리를 재실행(
REFRESH MATERIALIZED VIEW)해 갱신하므로fireRIRrules가 건너뛴다. 연구 프런티어는 증분 유지 — 전체 재계산 없이 기반 테이블 델타를 MV로 전파하는 것이다(Gupta & Mumick의 고전 IVM 서베이, DBToaster와pg_ivm확장의 최근 작업). 델타 전파 재작성이 이 파이프라인에 어떻게 맞아들어갈지는 열린 설계 질문이다. -
보안 배리어 뷰와 RLS 정보 유출 차단.
ApplyRetrieveRule에서 설정하는security_barrier플래그와 RLS 구간의securityQuals배치 모두, 플래너가 저렴하지만 정보를 유출하는 사용자 정의 조건을 보안 qual 아래로 밀어내지 못하게 막기 위해 존재한다(“leaky view” 계열의 정보 노출 버그). 보장이 실제로 강제되는 곳은 플래너 측이다 —qual_is_pushdown_safe,LEAKPROOF함수 표시. 교차 참조:postgres-row-level-security.md와postgres-planner-overview.md. -
재귀 쿼리 SEARCH/CYCLE — 재작성 대 런타임 기능. PostgreSQL은 SQL:2016
SEARCH/CYCLE을 순수 재작성으로 구현한다(rewriteSearchAndCycle). 장부 컬럼이 있는 평범한 재귀 유니온으로 변환하므로 이그제큐터는 이 기능의 존재를 알지 못한다. 다른 엔진은 사이클 감지를 런타임 재귀 CTE 오퍼레이터로 밀어 넣는다. 재작성 단순성 대 배열 추가 사이클 추적의 런타임 효율성이라는 트레이드오프는 구체적인 비교 지점이다.
교과서 장 (knowledge/research/dbms-general/ 아래)
섹션 제목: “교과서 장 (knowledge/research/dbms-general/ 아래)”- Database System Concepts(Silberschatz, Korth & Sudarshan, 7판) — §4.2 “Views”: 저장된 쿼리로서의 뷰 정의, 재귀적 대입으로서의 뷰 확장(“뷰 릴레이션을 그 정의로 치환”, “뷰 릴레이션이 더 이상 없을 때까지 반복”), §4.2.3의 구체화 뷰(물리적 저장 대 대입).
논문 (계보; .omc/plans/postgres-paper-bibliography.md 참고)
섹션 제목: “논문 (계보; .omc/plans/postgres-paper-bibliography.md 참고)”- Stonebraker, Wong, Kreps & Held, “The Design and Implementation of INGRES”(ACM TODS 1976) — 뷰, 보호, 무결성의 통합 메커니즘으로서의 query modification. 리라이터의 직접적 지적 조상.
- Stonebraker & Rowe, “The Design of POSTGRES”(SIGMOD 1986)와 Stonebraker, Rowe & Hirohama, “The Implementation of POSTGRES”(IEEE TKDE 1990) — 프로덕션 룰 시스템(PRS/PRS2), 룰로서의 뷰, 쿼리 재작성 대 튜플 수준 룰 전략.
rewriteHandler.c의 설계 기원. - (비교 참고, 미확보) Gupta & Mumick, “Maintenance of Materialized Views”(IEEE Data Eng. Bull. 1995) — 증분 뷰 유지. 구체화 뷰 후속 문서용.
PostgreSQL 소스 (/data/hgryoo/references/postgres/ 아래, REL_18 273fe94)
섹션 제목: “PostgreSQL 소스 (/data/hgryoo/references/postgres/ 아래, REL_18 273fe94)”src/backend/rewrite/rewriteHandler.c—QueryRewrite,RewriteQuery,fireRIRrules,fireRules,ApplyRetrieveRule,rewriteRuleAction,rewriteTargetView,matchLocks,AcquireRewriteLocks,CopyAndAddInvertedQual,get_view_query,view_has_instead_trigger,error_view_not_updatable,rewriteTargetListIU.src/backend/rewrite/rewriteDefine.c—DefineRule,DefineQueryRewrite,checkRuleResultList,InsertRule. ON SELECT = 뷰 불변식과 릴레이션-뷰 변환.src/backend/rewrite/rewriteSearchCycle.c—rewriteSearchAndCycle,make_path_rowexpr. SEARCH/CYCLE 절 확장.src/backend/rewrite/rowsecurity.c—get_row_security_policies. 리라이터가 호출하는 RLS 진입점(메커니즘은 교차 참조 문서 소유).src/backend/rewrite/rewriteManip.c—ChangeVarNodes,OffsetVarNodes,ReplaceVarsFromTargetList,AddInvertedQual. 변수 조작 프리미티브.src/backend/tcop/postgres.c—pg_rewrite_query.QueryRewrite의 호출자.
교차 참조 (인접 메커니즘을 소유하는 형제 문서)
섹션 제목: “교차 참조 (인접 메커니즘을 소유하는 형제 문서)”postgres-analyze-transform.md— 리라이터가 소비하는Query트리를 생성하는 파스 분석. 룰 액션에 대한OLD/NEWPRS2_*_VARNO할당.postgres-row-level-security.md—get_row_security_policies뒤의 RLS 서브시스템: 정책 선택, USING 대 WITH CHECK, 허용적/제한적 조합, FORCE ROW LEVEL SECURITY.postgres-planner-overview.md— 리라이터의Query트리 목록 출력을 소비. 보통 뷰 확장을 되돌리는 서브쿼리 풀-업. 보안 배리어 qual 풀다운 안전성.postgres-ddl-execution.md—pg_rewrite와pg_trigger를 채우는CREATE VIEW/CREATE RULE/ 트리거 정의.postgres-executor.md— 플래닝된, 재작성 후 쿼리를 실행. 런타임에WITH CHECK OPTION과 RLS 쓰기 검사를 강제.