콘텐츠로 이동

(KO) PostgreSQL 스키마와 search_path — 이름 해석과 네임스페이스 캐시

관계형 데이터베이스는 거의 모든 문장을 처리할 때마다 단순해 보이는 질문에 답해야 한다. 사용자가 적은 이름이 실제로 어떤 객체를 가리키는가? SQL에서 SELECT * FROM orders를 쓰면 엔진은 어느 orders인지 결정해야 한다. 서로 다른 스키마에 같은 이름의 테이블이 여러 개 있을 수 있고, 임시 orders가 영구 테이블을 가릴 수도 있으며, 아무것도 없을 수도 있다. 이것이 이름 해석(name resolution) 문제다. 이름을 생성하는 파서와 내부 식별자인 OID로 객체를 저장하는 카탈로그 사이에 놓인 경계다.

Codd이 형식화한 고전적 관계형 모델은 릴레이션을 단일 평면 우주의 원소로 다룬다. 그러나 실제 시스템은 단일 전역 평면 네임스페이스가 다중 사용자·다중 애플리케이션 환경에서 확장되지 않는다는 사실을 곧 발견했다. 두 애플리케이션이 모두 config라는 테이블을 원하고, 두 개발자가 모두 temp를 원한다. SQL 표준이 내린 해법은 3단계 이름 계층카탈로그 → 스키마 → 객체 — 이다. 스키마(SQL 용어) 또는 네임스페이스(PostgreSQL 내부 용어)는 객체 이름 공간을 분할하는 이름 있는 컨테이너다. 따라서 sales.ordersarchive.orders는 기본 이름을 공유하더라도 서로 다른 릴레이션이다. Silberschatz 외 Database System Concepts의 “Schemas, Catalogs, and Environments” 절은 이를 “서로 다른 애플리케이션과 사용자가 릴레이션 이름 충돌을 걱정하지 않고 독립적으로 작업할 수 있는” 메커니즘으로 설명하며, 비한정 이름이 여전히 해석되려면 각 SQL 환경이 기본 카탈로그와 스키마를 고정해야 한다고 지적한다.

이름 해석 서브시스템의 설계 공간은 세 가지 속성으로 정의된다.

  1. 한정 수준과 기본값 적용. 완전 한정 이름(db.schema.object)은 모호하지 않지만 장황하다. 생략된 앞 요소를 엔진이 기본값으로 채워야 한다. 흥미로운 질문은 기본값이 무엇이냐, 그리고 얼마나 동적이냐 — 접속 시점에 고정되는지, 세션 중에 사용자가 바꿀 수 있는 가변 파라미터인지 — 이다.

  2. 모호할 때의 탐색 순서. 기본값이 단일 스키마가 아니라 스키마의 순서 있는 목록이면 같은 비한정 이름이 여러 곳에 존재할 수 있다. 해석 규칙은 “목록에서 첫 번째로 일치하는 것이 이긴다”가 되고, 목록의 순서가 의미론적으로 중요해지며 이름 해석은 점 조회가 아닌 순서 스캔이 된다. 객체 유형(테이블 대 함수 대 연산자)마다 규칙이 미묘하게 다를 수 있다. 함수 호출은 인수 타입도 일치해야 하므로 해석기는 단일 답 대신 후보 집합을 반환하고 오버로드 해석이 나머지를 처리하게 한다.

  3. 동시성 아래서의 안정성. 이름 해석은 카탈로그를 읽는다. 카탈로그는 변경 가능하다. 한 백엔드가 orders를 해석하는 동안 다른 백엔드가 그것을 DROP하거나 RENAME하거나, 경로 앞쪽에 새 orders를 만들 수 있다. 올바른 해석기는 찾은 객체를 잠근 뒤 이름이 여전히 동일 객체를 가리키는지 재검증해야 한다. 그렇지 않으면 “이름 조회”와 “OID 사용” 사이에 경쟁 조건(race condition)이 생긴다.

Hellerstein, Stonebraker, Hamilton의 Architecture of a Database System은 이 작업을 관계형 엔진의 쿼리 파싱 및 인가 단계에 위치시킨다. “파서는 먼저 FROM 절의 각 테이블 참조를 고려하고 … 테이블 이름을 완전 한정 이름으로 정규화하며 … 카탈로그 관리자를 호출해 테이블이 등록되어 있는지 확인한다.” 이 책은 인가가 해석과 혼재된다는 점도 강조한다. 사용자가 접근할 수 없는 스키마는 검색에서 보이지 않아야 하기 때문이다. 그렇지 않으면 이름 해석 행위 자체가 객체의 존재를 노출하게 된다.

이 세 속성에 대한 PostgreSQL의 답은 거의 전부 src/backend/catalog/namespace.c 한 파일에 들어 있다. 스키마는 pg_namespace 시스템 카탈로그의 행이다. 기본값은 search_path GUC — 동적으로 설정 가능한 쉼표로 구분된 스키마 이름의 순서 목록이다. 해석 규칙은 “경로에서 일치하는 객체를 담은 첫 번째 스키마가 이긴다”이며, 동시성 안전성은 공유 무효화 메시지 카운터가 구동하는 조회-잠금 재시도 루프로 제공된다.

PostgreSQL 구체적 내용으로 들어가기 전에, 카탈로그 기반 엔진 대부분이 채택하는 엔지니어링 패턴을 정리해 두면 PostgreSQL의 선택이 공통 공간 안에서 이루어진 선택으로 읽힌다.

카탈로그 행으로서의 스키마, (스키마, 이름)으로 키잉되는 객체

섹션 제목: “카탈로그 행으로서의 스키마, (스키마, 이름)으로 키잉되는 객체”

보편적으로 스키마는 고유 식별자를 가진 카탈로그 객체이고, 스키마 범위 객체는 자신을 담은 스키마에 대한 외래 키 유사 참조를 가진다. 이름을 작동시키는 유일성 제약은 UNIQUE (object_name, schema_id) 이지 UNIQUE (object_name)이 아니다. 이 때문에 orders라는 테이블이 두 개 공존할 수 있다. (orders, sales)(orders, archive) 쌍은 서로 다른 인덱스 키다. 따라서 해석기의 내부 루프는 그 복합 인덱스에 대한 점 조회이며, 일치할 때까지 경로의 각 스키마마다 한 번씩 반복된다.

세션 범위의 순서 있는 기본 경로

섹션 제목: “세션 범위의 순서 있는 기본 경로”

완전 한정을 강제하는 대신 엔진은 누락된 스키마를 제공하는 세션 설정을 노출한다. Oracle의 CURRENT_SCHEMA, SQL Server의 사용자별 기본 스키마, PostgreSQL의 search_path는 모두 같은 종류의 인스턴스다. 순서 목록 변형(PostgreSQL, 함수용 DB2의 CURRENT PATH)은 단일 스키마 변형보다 표현력이 높다. 개인 스키마를 앞에 놓으면 이름 변경 없이 “내 사본이 공유 사본을 가린다”가 작동한다.

항상 경로에 있는 묵시적·시스템 스키마

섹션 제목: “항상 경로에 있는 묵시적·시스템 스키마”

내장 타입, 함수, 연산자는 사용자가 명시하지 않아도 도달 가능해야 하는 시스템 스키마(pg_catalog, 또는 다른 엔진의 SYS/INFORMATION_SCHEMA 유사체)에 존재한다. 엔진은 시스템 스키마를 유효 경로의 앞이나 뒤에 묵시적으로 삽입한다. PostgreSQL은 명시적으로 나열되지 않으면 pg_catalog를 앞에 추가한다. 그 결과 내장 = 연산자는 항상 찾힌다. 단, 사용자가 pg_catalog를 경로 뒤에 명시적으로 배치하면 자신의 것으로 내장을 의도적으로 가릴 수 있다.

오버로드 가능한 객체를 위한 후보 집합

섹션 제목: “오버로드 가능한 객체를 위한 후보 집합”

테이블은 정확히 하나의 OID로 해석된다. 하지만 함수와 연산자는 오버로드(overloaded) 된다. + 연산자가 여럿이고 substring 함수도 여럿이다. 이름 해석기 혼자서는 일을 끝낼 수 없다. 해석기는 후보 목록(그 이름을 가진 가시적 객체 전체, 경로 위치 태그 포함)을 반환하고, 이후 타입 해석 단계가 인수 타입으로 최적 일치를 고른다. 타입이 동점일 때 경로 위치로 앞쪽 스키마의 후보를 선택할 수 있도록 경로 위치가 유지된다.

동시 DDL 아래서의 정확성이 가장 어려운 패턴이다. 이름을 OID로 해석한 뒤 그 OID를 사용하는 것은 두 단계다. 그 사이에 객체가 사라지거나 대체될 수 있다(고전적인 시간-검사-대-시간-사용, TOCTOU 위험). 견고한 관용구는 다음과 같다. 해석 → OID 잠금 → 잠금을 기다리는 동안 카탈로그 변경 여부 확인 → 변경이 있으면 재해석 후 비교. 재해석이 동일 OID를 반환할 때만 진행이 안전하다. PostgreSQL은 이를 RangeVarGetRelidExtended에 구현한다.

search_path 문자열을 파싱하고, 각 이름을 카탈로그에서 조회하고, ACL 확인을 수행하고, 중복을 제거하는 작업은 모든 문장마다 반복하기에 비용이 크다. 엔진은 파생된 형태(스키마 식별자의 순서 목록)를 캐시하고, 답을 바꿀 수 있는 변화가 발생했을 때만 무효화한다. 경로 문자열 자체, 스키마의 존재·이름, 사용자의 신원($user와 ACL 가시성에 영향), 역할 구성원이 변화의 예시다. 캐시는 백엔드 간에 일관성을 유지해야 하므로 엔진의 카탈로그 무효화 기계와 연결된다.

PostgreSQL은 스키마 이름 해석을 namespace.c에 집중시킨다. 원시 카탈로그 DML을 담당하는 pg_namespace.c 위에 계층화된 구조다. 헤더 주석은 경계를 명확히 그린다. 이 모듈은 “‘네임스페이스 검색 경로’ 정의와 검색 경로 제어 검색 구현에 관련된 루틴을 제공”하고, 형제 파일은 “pg_namespace 시스템 카탈로그를 직접 조작하는 루틴을 담는다.”

스키마는 OID, nspname, nspowner, ACL을 가진 pg_namespace 튜플이다. 부트스트랩 카탈로그는 여기서 중요한 세 가지를 포함한다. pg_catalog(시스템 스키마, OID PG_CATALOG_NAMESPACE), pg_toast(PG_TOAST_NAMESPACE), public이다. 모든 스키마 범위 카탈로그 — pg_class, pg_type, pg_proc, pg_operator 등 — 는 xxxnamespace 컬럼을 가지며, (name, namespace)에 유일 인덱스가 있다. 그 복합 인덱스가 해석 기본 단위다. get_relname_relid, GetSysCacheOid2(TYPENAMENSP, …), PROCNAMEARGSNSP syscache 목록은 모두 단일 (name, schema) 쌍에 대한 그 인덱스 프로브의 얇은 래퍼다.

유효 경로: 파생 상태 변수 세 개

섹션 제목: “유효 경로: 파생 상태 변수 세 개”

사용자가 보는 손잡이는 전역 namespace_search_path에 저장된 원시 문자열인 search_path GUC다. namespace.c는 여기서 백엔드 나머지 부분이 실제로 참조하는 세 개의 active 상태 변수를 파생한다.

// active state — namespace.c
static List *activeSearchPath = NIL; /* ordered list of namespace OIDs */
static Oid activeCreationNamespace = InvalidOid; /* default CREATE target */
static bool activeTempCreationPending = false; /* pg_temp is first but not yet made */

activeSearchPath는 모든 가시성 스캔이 순서대로 탐색하는 OID 목록이다. activeCreationNamespace는 명시적으로 나열된 첫 번째 스키마 — 스키마 한정자 없이 CREATE TABLE foo를 실행할 때 기본 대상이 된다. activeTempCreationPendingpg_temp가 경로 첫 번째에 적혀 있지만 임시 네임스페이스가 아직 물리적으로 생성되지 않은 어색한 경우를 처리한다. 임시 네임스페이스는 첫 번째 임시 테이블 생성 시 지연 생성된다.

파생 과정은 의도적인 2단계 구조를 가진다. preprocessNamespacePath문자열을 ACL 검사를 통과한 OID 목록으로 변환한다(비용이 크고 카탈로그를 건드리는 단계). finalNamespacePath는 이후 중복 제거, 네임스페이스 검색 객체 접근 훅 실행, 묵시적 네임스페이스 선두 삽입pg_catalog(명시적으로 배치되지 않으면 항상)와 임시 네임스페이스(존재할 경우) — 을 수행한다. 이 분리는 첫 번째 비용이 큰 절반을 훅 설치 여부와 무관하게 캐시할 수 있도록 존재한다. 두 번째 절반은 훅이 설치될 때마다 재실행해야 한다.

flowchart TD
  GUC["search_path GUC 문자열<br/>namespace_search_path"] --> PRE["preprocessNamespacePath<br/>분할, $user / pg_temp 확장,<br/>get_namespace_oid, ACL_USAGE 확인"]
  PRE --> OIDLIST["oidlist: 명시적 스키마 OID 목록<br/>(ACL 통과, 중복 가능)"]
  OIDLIST --> FIN["finalNamespacePath<br/>중복 제거, 검색 훅,<br/>pg_catalog + pg_temp 선두 삽입"]
  FIN --> ACTIVE["activeSearchPath (OID 목록)<br/>activeCreationNamespace<br/>activeTempCreationPending"]
  ACTIVE --> SCAN["유형별 가시성 스캔<br/>RelnameGetRelid, FuncnameGetCandidates, ..."]
  CACHE["검색 경로 캐시 (simplehash)<br/>(search_path 문자열, roleid) 키"] -. 메모이즈 .-> PRE
  CACHE -. 메모이즈 .-> FIN

묵시적 네임스페이스와 SQL99 특이점

섹션 제목: “묵시적 네임스페이스와 SQL99 특이점”

파일 헤더 주석은 pg_catalog를 뒤에 붙이지 않고 앞에 삽입하는 이유를 명시한다.

// namespace.c header comment
// 2. The system catalog namespace is always searched. If the system
// namespace is present in the explicit path then it will be searched in
// the specified order; otherwise it will be searched after TEMP tables and
// *before* the explicit list. (It might seem that the system namespace
// should be implicitly last, but this behavior appears to be required by
// SQL99. ...)

따라서 묵시적 순서는 임시 네임스페이스 → pg_catalog → 명시적 목록이다. 내장 abs 함수보다 자신의 함수가 먼저 선택되게 하려면 search_pathpg_catalog를 자신의 스키마 뒤에 명시적으로 나열해야 한다. 묵시적 선두 삽입은 다른 경우 내장이 먼저 발견됨을 보장한다.

보안 측면에서 임시 네임스페이스는 특수 처리된다. 릴레이션과 타입을 제외한 모든 객체 유형의 검색에서 무시된다(타입은 임시 테이블이 행 타입을 가지기 때문에 허용해야 한다). 이는 사용자가 임시 스키마에 같은 이름의 객체를 심어 비한정 함수나 연산자 이름을 가로채는 것을 막는다. PostgreSQL이 의도적으로 막은 권한 상승 경로다.

가장 단순한 해석기 RelnameGetRelid는 전체 패턴을 보여준다. 경로가 최신 상태인지 확인한 뒤 순서대로 탐색하고 첫 번째 일치를 반환한다.

// RelnameGetRelid — namespace.c
Oid
RelnameGetRelid(const char *relname)
{
Oid relid;
ListCell *l;
recomputeNamespacePath();
foreach(l, activeSearchPath)
{
Oid namespaceId = lfirst_oid(l);
relid = get_relname_relid(relname, namespaceId);
if (OidIsValid(relid))
return relid; /* first match in path order wins */
}
return InvalidOid; /* not found in path */
}

TypenameGetTypidExtended는 자신의 주석이 “RelnameGetRelid와 본질적으로 같다”고 설명하지만, 비릴레이션 컨텍스트에서 임시 네임스페이스를 건너뛰는 temp_ok 가드를 추가한다. 함수·연산자 해석기(FuncnameGetCandidates, OpernameGetCandidates)도 동일한 골격을 따르되, 후보 목록을 축적하고 각 후보의 pathpos를 기록한다. 오버로드 해석이 나중에 경로 순서로 동점을 깰 수 있도록 한다.

“이 이름이 무엇을 가리키는가?”의 역은 “이 OID가 비한정 이름으로 보이는가?” — pg_table_is_visible()과 psql의 \d가 답하는 질문이다. RelationIsVisibleExt는 미묘함을 보여준다. 경로 안에 있다는 것은 필요 조건이지만 충분 조건이 아니다. 경로 앞쪽 스키마에 같은 이름의 다른 릴레이션이 있으면 현재 것이 가려지기 때문이다. 따라서 빠른 list_member_oid 테스트 뒤, 필요할 경우 같은 이름의 첫 번째 릴레이션에서 멈추는 느린 스캔이 이어진다. 그 첫 번째 결과가 문제의 OID일 때만 true를 반환한다.

이름 조회마다 파생 경로를 재계산하면 성능이 망가진다. PostgreSQL은 협력하는 캐시를 유지한다.

  1. 검색 경로 캐시(search_path 문자열, roleid) 쌍을 키로 하는 simplehash 테이블(nsphash). 이미 검증된 oidlistfinalPath에 매핑된다. 동일한 소수의 search_path 문자열이 문장과 proconfigSET search_path를 가진 함수 전반에 걸쳐 반복되기 때문에, 이 캐시는 비용이 큰 파싱-ACL 검사 작업을 메모이즈한다.

  2. valid 플래그 쌍baseSearchPathValidsearchPathCacheValid. 활성 변수(와 전체 캐시)를 재구축해야 하는지 여부를 통제한다.

위에 activePathGeneration이 있다. 단조 증가 카운터로 유효 경로가 실제로 변경될 때만 증가한다. SearchPathMatcher는 경로 스냅샷과 세대 번호를 캡처하고, 나중에 SearchPathMatchesCurrentEnvironment 호출이 변경이 없으면 단일 정수 비교로 true를 단락 평가한다. 같은 경로 아래서 계획된 것을 재검증해야 하는 캐시 계획의 일반적 경우다.

일관성은 syscache 무효화 콜백으로 유지된다. InitializeSearchPathNAMESPACEOID(스키마 이름 변경 또는 ACL 변경), AUTHOID(역할 이름 변경이 $user에 영향), AUTHMEMROLEMEM(역할 구성원 변경이 ACL에 영향), DATABASEOID(데이터베이스 소유자 변경) 각각에 InvalidationCallback을 등록한다. 이런 이벤트가 발생하면 두 valid 플래그 모두 지워지고 다음 조회 시 재구축된다. GUC에 새 값을 할당하면 assign_search_pathbaseSearchPathValid만 지운다. 캐시는 건드리지 않는다. 따라서 search_path를 이전에 사용했던 값으로 다시 설정하는 비용은 거의 없다.

flowchart TD
  LOOKUP["이름 조회<br/>RelnameGetRelid / Func... / Type..."] --> RECO["recomputeNamespacePath()"]
  RECO --> VALID{"baseSearchPathValid<br/>&& namespaceUser == roleid?"}
  VALID -->|예| USE["activeSearchPath 그대로 사용"]
  VALID -->|아니오| CACHED["cachedNamespacePath(string, roleid)"]
  CACHED --> SPCACHE{"spcache에 항목 있음<br/>& searchPathCacheValid?"}
  SPCACHE -->|적중| FINAL["oidlist / finalPath 재사용"]
  SPCACHE -->|실패| BUILD["preprocess + final, 항목 삽입"]
  FINAL --> SET["active* 설정, 변경 시 activePathGeneration 증가"]
  BUILD --> SET
  INVAL["syscache 무효화:<br/>pg_namespace / pg_authid /<br/>역할 구성원 / pg_database"] -->|InvalidationCallback| CLEAR["baseSearchPathValid=false<br/>searchPathCacheValid=false"]
  ASSIGN["SET search_path = ..."] -->|assign_search_path| CLEARB["baseSearchPathValid=false<br/>(캐시 유지)"]

이 절은 원시 RangeVar에서 잠긴 릴레이션 OID까지의 호출 흐름, 유형별 해석기, 임시 네임스페이스 생명주기, 캐시·무효화 핵심을 순서대로 따라간다. 심벌이 지속적 앵커이며 줄 번호는 끝의 위치 힌트 표에만 있다.

모든 해석기는 점으로 이어진 이름을 분해하는 것으로 시작한다. DeconstructQualifiedNameString 노드의 List(1~3개 원소)를 받아 스키마와 객체 부분을 반환하며, 3부 이름의 카탈로그 컴포넌트가 현재 데이터베이스와 일치하는지 강제한다. PostgreSQL에는 크로스 데이터베이스 참조가 없다.

// DeconstructQualifiedName — namespace.c
switch (list_length(names))
{
case 1:
objname = strVal(linitial(names));
break;
case 2:
schemaname = strVal(linitial(names));
objname = strVal(lsecond(names));
break;
case 3:
catalogname = strVal(linitial(names));
schemaname = strVal(lsecond(names));
objname = strVal(lthird(names));
if (strcmp(catalogname, get_database_name(MyDatabaseId)) != 0)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cross-database references are not implemented: %s",
NameListToString(names))));
break;
default:
ereport(ERROR, ... "improper qualified name (too many dotted names)" ...);
}
*nspname_p = schemaname; /* NULL means "no explicit schema → search path" */
*objname_p = objname;

NULL 스키마는 검색 경로로 폴백하라는 신호다. 비-NULL 스키마는 LookupExplicitNamespace를 통한 명시적·정확한 조회를 뜻한다.

릴레이션 해석기: RangeVarGetRelidExtended

섹션 제목: “릴레이션 해석기: RangeVarGetRelidExtended”

RangeVarGetRelidExtended는 테이블 이름을 OID로 해석하고 안전하게 잠그는 중심 진입점이다. 파일에서 동시성 인식이 가장 높은 루틴이다. 세 가지 분기가 네임스페이스를 선택한다. RELPERSISTENCE_TEMP RangeVar는 임시 네임스페이스를 강제하고, 명시적 schemanameLookupExplicitNamespace를 호출하며, 그 외에는 RelnameGetRelid로 경로를 검색한다.

// RangeVarGetRelidExtended — namespace.c (namespace selection)
if (relation->relpersistence == RELPERSISTENCE_TEMP)
{
if (!OidIsValid(myTempNamespace))
relId = InvalidOid; /* this probably can't happen? */
else
{
if (relation->schemaname)
{
namespaceId = LookupExplicitNamespace(relation->schemaname, missing_ok);
if (namespaceId != myTempNamespace)
ereport(ERROR, ... "temporary tables cannot specify a schema name" ...);
}
relId = get_relname_relid(relation->relname, myTempNamespace);
}
}
else if (relation->schemaname)
{
namespaceId = LookupExplicitNamespace(relation->schemaname, missing_ok);
if (missing_ok && !OidIsValid(namespaceId))
relId = InvalidOid;
else
relId = get_relname_relid(relation->relname, namespaceId);
}
else
relId = RelnameGetRelid(relation->relname); /* search the path */

그 선택을 감싸는 핵심 구조가 조회-잠금 재시도 루프다. 해석 전에 SharedInvalidMessageCounter를 기록하고, 잠금을 획득한 뒤(잠금 자체가 대기 중인 무효화를 처리한다), 카운터가 증가했는지 확인한다. 증가했으면 이름을 재해석하고, 새 OID가 이미 잠긴 것과 다르면 오래된 잠금을 해제하고 두 번의 연속 해석이 일치할 때까지 루프를 반복한다.

// RangeVarGetRelidExtended — namespace.c (retry core)
inval_count = SharedInvalidMessageCounter;
/* ... resolve relId from name ... */
if (callback)
callback(relation, relId, oldRelId, callback_arg); /* perms before locking */
if (lockmode == NoLock)
break;
if (retry)
{
if (relId == oldRelId)
break; /* answer stable → done */
if (OidIsValid(oldRelId))
UnlockRelationOid(oldRelId, lockmode); /* locked the wrong one; undo */
}
if (!OidIsValid(relId))
AcceptInvalidationMessages(); /* flush negative catcache entries */
else if (!(flags & (RVR_NOWAIT | RVR_SKIP_LOCKED)))
LockRelationOid(relId, lockmode); /* also accepts invalidations */
/* ... RVR_NOWAIT / RVR_SKIP_LOCKED conditional-lock branch ... */
if (inval_count == SharedInvalidMessageCounter)
break; /* nothing changed → done */
retry = true;
oldRelId = relId;

callback은 잠금 전에 실행된다. 주석은 “잠그기 전에 권한을 확인하는 것이 정말 최선”이라고 설명한다. 콜백은 각 재시도마다 새 OID와 함께 재호출되어 권한 검사가 실제로 잠기는 객체를 추적한다. RVROption 플래그(RVR_MISSING_OK, RVR_NOWAIT, RVR_SKIP_LOCKED)는 미발견 및 잠금 경쟁 동작을 조정한다. 친숙한 RangeVarGetRelid(rel, lockmode, missing_ok) 매크로는 콜백 없이 이 함수를 호출하는 것과 동일하다.

DDL의 경우 RangeVarGetCreationNamespaceQualifiedNameGetCreationNamespace생성 위치를 선택한다. 명시적 스키마를 존중하고, pg_temp 별칭을 특수 처리하며(AccessTempTableNamespace로 임시 네임스페이스를 강제 생성), 그 외에는 activeCreationNamespace로 폴백한다. 명시적 경로가 비어 있으면 “생성할 스키마가 선택되지 않았습니다” 오류가 발생한다.

// QualifiedNameGetCreationNamespace — namespace.c
if (schemaname)
{
if (strcmp(schemaname, "pg_temp") == 0)
{
AccessTempTableNamespace(false); /* may CommandCounterIncrement */
return myTempNamespace;
}
namespaceId = get_namespace_oid(schemaname, false); /* no USAGE check here */
}
else
{
recomputeNamespacePath();
if (activeTempCreationPending)
{
AccessTempTableNamespace(true); /* pg_temp was first → realize it now */
return myTempNamespace;
}
namespaceId = activeCreationNamespace;
if (!OidIsValid(namespaceId))
ereport(ERROR, ... "no schema has been selected to create in" ...);
}

LookupExplicitNamespace는 명명된 스키마를 해석하고 ACL_USAGE를 적용한다. 권한이 없는 스키마를 들여다볼 수 없도록 막는다. pg_temp 별칭을 세션 임시 네임스페이스로 해석하기도 한다. LookupCreationNamespace는 형제 함수로 ACL_CREATE를 확인하고 pg_temp를 구체화할 의사가 있다. get_namespace_oid는 두 함수가 기반하는 하위 수준의 pg_namespace 프로브다.

// LookupExplicitNamespace — namespace.c
if (strcmp(nspname, "pg_temp") == 0)
{
if (OidIsValid(myTempNamespace))
return myTempNamespace;
/* used only for existing objects; don't init temp ns here — fall through */
}
namespaceId = get_namespace_oid(nspname, missing_ok);
if (missing_ok && !OidIsValid(namespaceId))
return InvalidOid;
aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_USAGE);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, OBJECT_SCHEMA, nspname);
InvokeNamespaceSearchHook(namespaceId, true); /* extension hook */
return namespaceId;

FuncnameGetCandidates는 하나의 OID가 아닌 FuncCandidateList를 반환한다. 명시적 스키마가 있으면 해당 네임스페이스로 필터링하고, 없으면 activeSearchPath를 탐색하면서 후보의 위치(pathpos)를 기록하고 보안상 임시 네임스페이스를 제외한다.

// FuncnameGetCandidates — namespace.c (path filtering)
if (OidIsValid(namespaceId))
{
if (procform->pronamespace != namespaceId) /* explicit schema given */
continue;
}
else
{
ListCell *nsp;
foreach(nsp, activeSearchPath)
{
if (procform->pronamespace == lfirst_oid(nsp) &&
procform->pronamespace != myTempNamespace) /* skip temp ns */
break;
pathpos++;
}
if (nsp == NULL)
continue; /* proc is not in search path */
}

유지된 pathpos는 나중의 오버로드 해석 단계가 인수 타입이 동점일 때 경로 앞쪽 후보를 선호하게 한다. OpernameGetCandidates는 연산자 해석(OPERNAMENSP 검색)에서 구조적으로 동일하며, = 같은 연산자 이름은 후보가 많아 후보별 palloc이 파싱 시간 프로파일에 드러났기 때문에 대량 할당 최적화(SPACE_PER_OP)를 적용한다.

백엔드에는 첫 번째 임시 객체가 생성될 때까지 임시 네임스페이스가 없다. AccessTempTableNamespace는 세션이 임시 네임스페이스에 접근했음을 기록하고(XACT_FLAGS_ACCESSEDTEMPNAMESPACE), 지연 방식으로 InitTempTableNamespace를 호출한다. InitTempTableNamespaceACL_CREATE_TEMP를 확인하고, 복구 중이거나 병렬 워커에서는 거부하며, 스키마 이름을 pg_temp_<MyProcNumber>로 지정하고, 부트스트랩 슈퍼유저 소유로 생성한 뒤 대응하는 pg_toast_temp_<N>도 만들고, OID를 MyProc->tempNamespaceId에 공개하여 다른 백엔드가 사용 중임을 알 수 있게 한다.

// InitTempTableNamespace — namespace.c (creation core)
snprintf(namespaceName, sizeof(namespaceName), "pg_temp_%d", MyProcNumber);
namespaceId = get_namespace_oid(namespaceName, true);
if (!OidIsValid(namespaceId))
{
namespaceId = NamespaceCreate(namespaceName, BOOTSTRAP_SUPERUSERID, true);
CommandCounterIncrement(); /* make it visible */
}
else
RemoveTempRelations(namespaceId); /* prior owner crashed; clean it out */
/* ... create pg_toast_temp_<N> similarly ... */
myTempNamespace = namespaceId;
myTempToastNamespace = toastspaceId;
MyProc->tempNamespaceId = namespaceId; /* advertise "in use" */
myTempNamespaceSubID = GetCurrentSubTransactionId();
baseSearchPathValid = false; /* path now includes temp ns */
searchPathCacheValid = false;

생성은 트랜잭션 방식이다. AtEOXact_Namespace는 커밋 시 before_shmem_exit 정리 콜백을 등록하고, 중단 시에는 myTempNamespace, valid 플래그, MyProc->tempNamespaceId를 리셋하여 다음 시도가 깨끗하게 시작하도록 네임스페이스를 완전히 잊는다. isTempNamespace, isAnyTempNamespace(임의 백엔드의 임시 스키마에 대한 이름 접두사 테스트), checkTempNamespaceStatus(MyProc를 통한 활성/유휴/사용 중 상태)가 임시 술어를 완성한다. SetTempNamespaceState/GetTempNamespaceState는 임시 OID를 병렬 워커에 전달하여 워커가 리더의 경로를 공유할 수 있게 한다.

recomputeNamespacePath는 모든 해석기가 먼저 호출하는 가드다. 현재 역할 기준 경로가 이미 유효하면 단락 평가한다. 그렇지 않으면 (캐시된 것일 수 있는) 항목을 가져와 최종 경로를 TopMemoryContext에 복사하고, 유효 값이 변경된 경우에만 activePathGeneration을 증가시킨다.

// recomputeNamespacePath — namespace.c
Oid roleid = GetUserId();
if (baseSearchPathValid && namespaceUser == roleid)
return; /* fast path: nothing to do */
entry = cachedNamespacePath(namespace_search_path, roleid);
if (baseCreationNamespace == entry->firstNS &&
baseTempCreationPending == entry->temp_missing &&
equal(entry->finalPath, baseSearchPath))
pathChanged = false;
else { /* copy entry->finalPath into TopMemoryContext, swap into baseSearchPath */ }
baseSearchPathValid = true;
namespaceUser = roleid;
activeSearchPath = baseSearchPath; /* publish as active */
activeCreationNamespace = baseCreationNamespace;
activeTempCreationPending = baseTempCreationPending;
if (pathChanged)
activePathGeneration++; /* invalidate SearchPathMatchers */

cachedNamespacePath는 simplehash 프런트엔드다. spcache_init/spcache_insert(searchPath, roleid) 항목을 찾거나 생성하면, preprocessNamespacePathoidlist(문자열 → ACL 검사된 OID, $userpg_temp 확장 포함)를 채우고 finalNamespacePathfinalPath(중복 제거, 검색 훅, 묵시적 네임스페이스 선두 삽입)를 채운다. LastSearchPathCacheEntry 포인터는 직전 키를 메모이즈하여 동일 문자열의 반복 조회가 해시 프로브조차 생략하도록 한다.

// preprocessNamespacePath — namespace.c ($user / pg_temp / normal)
if (strcmp(curname, "$user") == 0)
{
/* substitute the schema named like the current role, if USAGE-accessible */
tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
/* ... get_namespace_oid(rolename) + ACL_USAGE check → maybe lappend ... */
}
else if (strcmp(curname, "pg_temp") == 0)
{
if (OidIsValid(myTempNamespace))
oidlist = lappend_oid(oidlist, myTempNamespace);
else if (oidlist == NIL)
*temp_missing = true; /* pg_temp first but not yet created */
}
else
{
namespaceId = get_namespace_oid(curname, true);
if (OidIsValid(namespaceId) &&
object_aclcheck(NamespaceRelationId, namespaceId, roleid, ACL_USAGE) == ACLCHECK_OK)
oidlist = lappend_oid(oidlist, namespaceId); /* silently drop inaccessible */
}

해석되지 않거나 접근 권한이 없는 이름은 조용히 삭제된다. GUC 값은 이미 수락된 것이므로 여기서 오류는 허용되지 않는다. finalNamespacePath는 묵시적 스키마를 목록 앞쪽에 삽입한다.

// finalNamespacePath — namespace.c (implicit prepend)
if (!list_member_oid(finalPath, PG_CATALOG_NAMESPACE))
finalPath = lcons_oid(PG_CATALOG_NAMESPACE, finalPath); /* pg_catalog first */
if (OidIsValid(myTempNamespace) &&
!list_member_oid(finalPath, myTempNamespace))
finalPath = lcons_oid(myTempNamespace, finalPath); /* temp even earlier */

check_search_path는 목록의 구문만 검증한다(스키마 존재 여부는 아니다. 유효한 설정 다수가 아직 생성되지 않은 스키마를 이름으로 지정한다). 캐시를 이용해 반복적인 SplitIdentifierString을 건너뛴다. assign_search_pathbaseSearchPathValid만 지우고(다음 사용 시 지연 재계산) 의도적으로 캐시를 지우지 않는다. syscache 콜백이 일관성의 핵심이다.

// InvalidationCallback — namespace.c
static void
InvalidationCallback(Datum arg, int cacheid, uint32 hashvalue)
{
/* schema/role/ACL change may alter resolution → rebuild everything */
baseSearchPathValid = false;
searchPathCacheValid = false;
}

이 콜백은 InitializeSearchPath에서 NAMESPACEOID, AUTHOID, AUTHMEMROLEMEM, DATABASEOID에 등록된다. GetSearchPathMatcher/SearchPathMatchesCurrentEnvironmentactivePathGeneration을 이용해 O(1) “이 캐시 계획이 올바른 경로 아래서 계획된 것인가?” 검사를 수행하며, 세대 번호가 다를 때만 원소별 비교로 폴백한다. fetch_search_pathfetch_search_path_arraycurrent_schemas() 같은 SQL 함수에 활성 경로를 노출한다.

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

섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”
심벌종류파일
activeSearchPath / activeCreationNamespace / activeTempCreationPending정적 상태src/backend/catalog/namespace.c136 / 139 / 142
activePathGeneration정적 카운터src/backend/catalog/namespace.c145
baseSearchPathValid정적 플래그src/backend/catalog/namespace.c158
searchPathCacheValid / SearchPathCacheContext정적 캐시 상태src/backend/catalog/namespace.c164 / 165
SearchPathCacheKey / SearchPathCacheEntry구조체src/backend/catalog/namespace.c167 / 173
myTempNamespace / myTempToastNamespace / myTempNamespaceSubID정적 상태src/backend/catalog/namespace.c201 / 203 / 205
namespace_search_pathGUC 문자열src/backend/catalog/namespace.c211
spcachekey_hash / spcachekey_equal정적 함수src/backend/catalog/namespace.c253 / 273
SPCACHE_RESET_THRESHOLD매크로src/backend/catalog/namespace.c297
spcache_init / spcache_lookup / spcache_insert정적 함수src/backend/catalog/namespace.c306 / 344 / 374
RangeVarGetRelidExtended함수src/backend/catalog/namespace.c441
RangeVarGetCreationNamespace함수src/backend/catalog/namespace.c654
RelnameGetRelid함수src/backend/catalog/namespace.c885
RelationIsVisibleExt정적 함수src/backend/catalog/namespace.c925
TypenameGetTypidExtended함수src/backend/catalog/namespace.c1008
FuncnameGetCandidates함수src/backend/catalog/namespace.c1192
OpernameGetCandidates함수src/backend/catalog/namespace.c1888
DeconstructQualifiedName함수src/backend/catalog/namespace.c3304
LookupNamespaceNoError함수src/backend/catalog/namespace.c3358
LookupExplicitNamespace함수src/backend/catalog/namespace.c3388
LookupCreationNamespace함수src/backend/catalog/namespace.c3431
CheckSetNamespace함수src/backend/catalog/namespace.c3462
QualifiedNameGetCreationNamespace함수src/backend/catalog/namespace.c3490
get_namespace_oid함수src/backend/catalog/namespace.c3538
isTempNamespace / isAnyTempNamespace / isOtherTempNamespace함수src/backend/catalog/namespace.c3652 / 3690 / 3713
checkTempNamespaceStatus함수src/backend/catalog/namespace.c3732
GetTempNamespaceState / SetTempNamespaceState함수src/backend/catalog/namespace.c3808 / 3824
GetSearchPathMatcher / SearchPathMatchesCurrentEnvironment함수src/backend/catalog/namespace.c3855 / 3914
preprocessNamespacePath / finalNamespacePath정적 함수src/backend/catalog/namespace.c4110 / 4201
cachedNamespacePath정적 함수src/backend/catalog/namespace.c4247
recomputeNamespacePath정적 함수src/backend/catalog/namespace.c4302
AccessTempTableNamespace / InitTempTableNamespace정적 함수src/backend/catalog/namespace.c4365 / 4393
AtEOXact_Namespace함수src/backend/catalog/namespace.c4515
check_search_path / assign_search_pathGUC 훅src/backend/catalog/namespace.c4660 / 4716
InvalidationCallback정적 함수src/backend/catalog/namespace.c4799
fetch_search_path / fetch_search_path_array함수src/backend/catalog/namespace.c4822 / 4862
SearchPathMatcher / RVROption구조체 / 열거형src/include/catalog/namespace.h59 / 70

위 내용은 /data/hgryoo/references/postgres, 브랜치 REL_18_STABLE, 커밋 273fe94852b(2026-06-05) 작업 트리를 기준으로 검증했다.

  • 활성 상태 삼중. activeSearchPath, activeCreationNamespace, activeTempCreationPending은 136142번 줄 근처에 파일 범위 정적으로 정의되어 있으며 인용 그대로다. 헤더 블록(65131번 줄)이 묵시적 네임스페이스 순서와 activeTempCreationPending 우회 방법을 그대로 문서화한다.

  • 조회-잠금 루프. RangeVarGetRelidExtendedfor (;;) 재시도(483번 줄부터)는 inval_countSharedInvalidMessageCounter와 비교하고, OID 변경 시 오래된 oldRelId 잠금을 해제하며, 잠금 전에 사용자 콜백을 호출한다. 줄별로 확인했다. RVROption 열거형과 RangeVarGetRelid 편의 매크로는 namespace.h(70~82번 줄)에 있다.

  • 묵시적 선두 삽입 순서. finalNamespacePathPG_CATALOG_NAMESPACE를 먼저 lcons_oid하고 그 다음 myTempNamespace를 처리한다. 따라서 실제 목록 순서는 [temp, pg_catalog, …explicit]이며 헤더 주석의 SQL99 근거와 일치한다. 임시 네임스페이스 보안 제외는 FuncnameGetCandidates(!= myTempNamespace 가드)와 TypenameGetTypidExtendedtemp_ok 파라미터로 적용된다.

  • 임시 생명주기. InitTempTableNamespace는 스키마를 pg_temp_%d(MyProcNumber)로 명명하고, BOOTSTRAP_SUPERUSERID 소유로 생성하고, MyProc->tempNamespaceId를 설정하며, 두 valid 플래그를 모두 지운다. 복구/병렬 워커 거부와 ACL_CREATE_TEMP 검사가 생성 전에 있다. AtEOXact_Namespace는 커밋 시 before_shmem_exitRemoveTempRelationsCallback을 등록한다.

  • 두 캐시 일관성. nsphash simplehash는 lib/simplehash.h에서 SH_PREFIX nsphash로 인스턴스화된다. SPCACHE_RESET_THRESHOLD는 256이다. recomputeNamespacePath는 실제 변경 시에만 activePathGeneration을 증가시킨다. InvalidationCallback은 두 플래그를 모두 지우며 NAMESPACEOID, AUTHOID, AUTHMEMROLEMEM, DATABASEOID에 초기화 경로(4769번 줄 근처 CacheRegisterSyscacheCallback 블록)에서 등록된다. assign_search_path는 주석 “This does not invalidate the search path cache.”에서 확인했듯이 baseSearchPathValid만 지운다.

  • 범위 경계. 원시 pg_namespace 카탈로그 DML(NamespaceCreate, 스키마 이름 변경/삭제)은 헤더의 역할 분리에 맞게 이 파일이 아닌 pg_namespace.c에 있다. 릴레이션 디스크립터 캐싱은 relcache에, ACL 평가 세부 사항은 acl.c에 있으며 상호 참조만 한다.

PostgreSQL 너머 — 비교 설계와 연구 과제

섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 과제”

경로는 System R 카탈로그 관리자의 계보다

섹션 제목: “경로는 System R 카탈로그 관리자의 계보다”

PostgreSQL의 이름 해석 구조는 System R 카탈로그 관리자의 직계 후손이다. System R 아키텍처 논문(Astrahan 외, 1976; 로컬에 research/dbms-papers/systemr.md로 있음)은 RDS(Relational Data System) 계층이 릴레이션에 관한 릴레이션인 자기 기술 카탈로그를 유지하고, SQL 컴파일러가 문장 준비 중에 이름을 내부 식별자에 바인딩하기 위해 카탈로그를 참조한다고 설명한다. PostgreSQL은 이 자기 기술 카탈로그(pg_namespace, pg_class, pg_proc은 일반 힙 릴레이션)와 파싱 시점 바인딩 규율을 모두 물려받았다. System R이 정적 접근 모듈에 바인딩한 반면, PostgreSQL은 파싱마다 재바인딩하되 파생 경로를 적극적으로 캐시하여 정적 바인딩이 제공했을 성능을 회복한다. Selinger 접근 경로 선택 논문(research/dbms-papers/systemr-optimizer.md)은 여기서 파생된다. 옵티마이저는 이름을 전혀 보지 않고 OID만 처리한다. 해석이 계획 시작 전에 이름 공간을 이미 축소했기 때문이다. PostgreSQL이 보존하는 깔끔한 관심사 분리다.

후보 집합 문제로서의 오버로드 해석

섹션 제목: “후보 집합 문제로서의 오버로드 해석”

FuncCandidateList를 반환하는 결정은 PostgreSQL을 타입 주도 오버로드 해석의 계보에 놓는다. SQL:1999는 함수 경로(CURRENT_PATH in DB2)를 통한 스키마 한정 루틴 해석을 표준화했고, PostgreSQL의 pathpos 태깅은 두 후보가 인수 타입에서 동등하게 좋을 때 표준의 “최적” 규칙이 필요로 하는 동점 처리다. 핵심 통찰 — 이름 가시성(어떤 후보가 범위 안에 있는가)을 최적 선택(어느 후보가 이기는가)에서 분리 — 은 namespace.c가 타입 시스템을 모르게 한다. 가시적인 모든 것을 수집하고 넘겨준다. Architecture of a Database System은 이 2단계 바인딩을 확장 가능한 엔진의 표준으로 설명한다. 함수 집합이 열려 있어 해석기에 하드코딩할 수 없는 엔진에서 그렇다.

동적으로 변경 가능한 검색 경로는 매우 강력하며, PostgreSQL 보안 권고의 잘 알려진 유형의 원천이기도 하다. 비한정 함수 호출은 호출자의 경로를 기준으로 해석되기 때문에, 자신의 search_path를 고정하지 않은 SECURITY DEFINER 함수는 공격자가 경로 앞쪽의 쓰기 가능 스키마에 심어 둔 함수를 호출하도록 속을 수 있다. FuncnameGetCandidates/OpernameGetCandidates의 임시 네임스페이스 제외는 한 가지 경로를 막는다(pg_temp를 통한 함수 가로채기 불가). proconfigSET search_path 메커니즘 — 이를 위해 simplehash 캐시가 구체적으로 만들어졌다 — 은 함수가 자신의 해석 환경을 고정하게 한다. 이는 살아 있는 설계 긴장이다. 최대 유연성(세션별, 함수별 경로) 대 이름 해석이 예측 가능하고 악용 불가능해야 한다는 원칙. “항상 스키마 한정하거나 SECURITY DEFINER에서 search_pathSET하라”는 현대 지침은 동적 해석기를 선택한 시스템의 운영 잔여물이다.

  • Oracle은 순서 있는 목록 대신 단일 CURRENT_SCHEMA에 공개 동의어를 사용한다. 현재 스키마, 개인 동의어, 공개 동의어, SYS 딕셔너리 순으로 해석한다. 동의어 간접 계층이 PostgreSQL이 경로 순서에 넣는 작업을 수행한다. Oracle에는 세션별로 재정렬 가능한 목록이 없어 유연성 대신 단순하고 악용하기 어려운 해석 규칙을 선택했다.
  • SQL Server는 데이터베이스 사용자별로 기본 스키마를 바인딩하고 비한정 이름을 user_default_schema → dbo 순서로 해석한다. 두 단계 폴백은 사용자 제어 순서가 없는 퇴화된 두 원소 경로다. SQL 표준의 “기본 스키마”에 PostgreSQL 목록보다 가깝다.
  • DB2함수 해석에서 순서 있는 CURRENT PATH로 PostgreSQL과 가장 유사하지만, 테이블 해석에는 단일 CURRENT SCHEMA를 유지한다. 테이블, 타입, 함수, 연산자를 하나의 순서 있는 경로 아래 통합한 점은 PostgreSQL의 특이점이다.
  • BigTable, Dremel의 카탈로그 논의(research/dbms-papers/bigtable.md, dremel.md)에서 분산/컬럼형 시스템은 일반적으로 네임스페이스를 별도 메타데이터 서비스로 평탄화하거나 외부화한다. 세션별 변경 가능 경로는 상태 비저장의 대규모 병렬 쿼리 계층에서 잘 살아남지 못한다. PostgreSQL의 SetTempNamespaceState/병렬 워커 경로 전파는 그 세계에 대한 작은 양보다. 세션 로컬 경로를 파생할 수 없는 워커에게 리더의 경로를 넘겨줘야 하기 때문이다.

흥미로운 미해결 문제는 해석 의미론(System R 이후 정착됨)보다 규모에서의 캐싱과 무효화에 더 관련된다. PostgreSQL의 백엔드별 simplehash와 세대 카운터 설계는 소수의 구별되는 경로를 가진 프로세스-당-연결 서버에 최적이다. 두 경계선이 그것을 늘린다. (1) 수천 개의 테넌트 각각이 고유한 search_path를 가진 경우(공유 PostgreSQL의 멀티 테넌트 SaaS)는 SPCACHE_RESET_THRESHOLD 리셋 로직을 압박하며, 카탈로그 메타데이터에 확장 가능한 잠금 관리자(research/dbms-papers/scalable-lock-manager.md)의 무효화 규율을 적용해야 하는 공유 크로스 백엔드 경로 캐시 연구를 동기화한다. (2) 연결 풀링은 백엔드별 가정을 깨뜨린다. 풀된 연결의 경로는 클라이언트 간에 리셋되어 “소수의 구별되는 문자열” 가정이 “많은 일시적 문자열”로 바뀐다. 이것이 임계값 리셋이 경계를 두기 위해 만들어진 바로 그 꾸준한 성장 경우다. 두 경우 모두 PostgreSQL의 우아한 단일 파일 해석기가 원래 맞추어지지 않은 배포 현실과 만나는 엔지니어링 압박 지점이다.

  • 주요 소스 코드 (PostgreSQL REL_18_STABLE, 커밋 273fe94852b, 2026-06-05, /data/hgryoo/references/postgres):

    • src/backend/catalog/namespace.c — 여기서 분석한 해석, 가시성, 임시 네임스페이스, 캐싱, 무효화 서브시스템 전체.
    • src/include/catalog/namespace.hSearchPathMatcher, RVROption, RangeVarGetRelid 매크로, 공개 프로토타입.
    • src/include/catalog/pg_namespace.hpg_namespace 카탈로그 폼, PG_CATALOG_NAMESPACE/PG_TOAST_NAMESPACE OID 매크로.
    • src/backend/catalog/pg_namespace.c — 원시 카탈로그 DML(NamespaceCreate). 범위 경계를 위해 참조만 함.
    • lib/simplehash.h — 검색 경로 캐시를 뒷받침하는 nsphash 템플릿.
  • 교과서 앵커 (knowledge/research/dbms-general/):

    • Database System Concepts (Silberschatz, Korth, Sudarshan) — 3단계 이름 계층과 기본 스키마 의미론을 위한 “Schemas, Catalogs, and Environments.”
    • Architecture of a Database System (Hellerstein, Stonebraker, Hamilton), fntdb07-architecture.md — 쿼리 파싱/인가 단계, 카탈로그 관리자, 2단계 이름 바인딩.
  • 논문 앵커 (knowledge/research/dbms-papers/):

    • System R (Astrahan 외, 1976), systemr.md — PostgreSQL 해석기가 내려받은 자기 기술 카탈로그와 컴파일 시점 바인딩.
    • Selinger 외 (1979), systemr-optimizer.md — 옵티마이저는 해석 후 OID 기반으로 동작. 이름/계획 분리.
    • scalable-lock-manager.md, bigtable.md, dremel.md — 규모에서의 카탈로그 메타데이터 캐싱과 무효화 비교 프레이밍.
  • 상호 참조 (이 폴더의 형제 문서):

    • postgres-system-catalogs.md — 카탈로그 중 pg_namespace와 부트스트랩 스키마 레이아웃.
    • postgres-relcache.md — 이 해석기가 생성하는 OID를 소비하는 릴레이션 디스크립터 캐싱.
    • postgres-catcache-syscache.md — 유형별 스캔이 프로브하는 RELOID/TYPENAMENSP/PROCNAMEARGSNSP syscache.
    • postgres-cache-invalidation.mdInvalidationCallback을 구동하는 SharedInvalidMessageCounterCacheRegisterSyscacheCallback 기계.
    • postgres-lock-manager.md — 조회-잠금 루프가 사용하는 LockRelationOid/ConditionalLockRelationOid.
    • postgres-parser.md — 이 모듈이 해석하는 RangeVar/List 이름을 생성한다.
    • postgres-guc-parameters.mdcheck_search_path/assign_search_path가 연결하는 check_hook/assign_hook GUC 기계.