(KO) PostgreSQL 의존성 추적 — pg_depend, CASCADE, 삭제 순서 결정
목차
이론적 배경
섹션 제목: “이론적 배경”관계형 데이터베이스는 테이블의 평면 집합이 아니라 스키마 오브젝트의 그래프다. 테이블은 타입을 참조하고, 뷰는 테이블을 참조하며, 제약은 컬럼을 참조하고, 인덱스는 연산자 클래스를 참조한다. 함수는 인자 타입을 참조하고, 시퀀스는 컬럼에 소유되며, 익스텐션은 자신이 설치한 모든 것을 소유한다. CREATE로 한 오브젝트 위에 다른 오브젝트를 쌓는 순간, 엔진은 의무를 떠안는다. 사용자가 DROP을 요청하면 두 가지 질문에 올바르게 답해야 한다.
-
이 삭제가 안전한가? 다른 오브젝트가 대상을 여전히 참조한다면, 삭제하면 카탈로그에 끊어진 참조가 남는다. 엔진은 삭제를 거부(RESTRICT)하거나 의존자를 함께 제거(CASCADE)해야 한다.
-
오브젝트를 어떤 순서로 제거해야 하는가? 삭제 대상 집합이 확정된 뒤에도 임의 순서로 삭제할 수 없다. 참조하는 쪽이 참조받는 쪽보다 먼저 제거되어야 한다. 이는 의존성 그래프에 대한 위상 정렬 문제다.
교과서의 고전 프레임은 무결성 제약과 참조 무결성 문헌에서 나온다. Database System Concepts(Silberschatz, Korth, Sudarshan)는 ON DELETE CASCADE, ON DELETE RESTRICT, ON DELETE SET NULL 같은 참조 동작을 행 수준(외래 키) 에서 소개한다. 스키마 수준 의존성 추적은 같은 아이디어를 행에서 카탈로그 오브젝트로 한 단계 끌어올린 것이다. DDL 엔진도 정의 수준에서 CASCADE/RESTRICT 의미론이 필요하다. 카탈로그 자체가 “단지 테이블”이므로 의존성 정보는 자연스럽게 테이블에 저장된다. 행이 오브젝트 그래프의 에지인 자기 기술적(self-describing) 관계다.
삭제 순서 결정의 절반은 그래프 이론이다. 의존성 에지는 유향 그래프를 형성한다. “X로부터 도달 가능한 모든 것을 삭제하라”는 도달 가능성/폐쇄 계산이고, “안전한 순서로 삭제하라”는 유도 부분 그래프의 위상 정렬이다. 까다로운 점은 그래프가 깔끔한 DAG가 아니라는 것이다. PostgreSQL은 의존성 사이클을 의도적으로 허용한다. 테이블과 그 행 타입(rowtype)은 서로를 참조하고, 복합 타입과 그 배열 타입은 루프를 형성한다. 단순 위상 정렬은 사이클에서 교착 상태에 빠진다. 그래서 엔진은 에지 종류를 구분한다. 사이클은 지정된 “약한” 에지, 즉 두 오브젝트가 논리적으로 하나임을 나타내는 내부(internal) 의존성에서 끊어진다. Database Internals(Petrov)는 나머지 엔진이 참조하는 메타데이터 계층으로 카탈로그/데이터 사전을 다루며, 의존성 추적은 DDL 전반에 걸쳐 메타데이터를 자기 일관적으로 유지하는 무결성 규율이다.
두 번째 축은 범위다. 대부분의 의존성은 하나의 데이터베이스 내부에 국한된다. 뷰는 같은 데이터베이스의 테이블에 의존한다. 그러나 일부 참조 대상 오브젝트는 클러스터 전체에서 공유된다. 롤(role)과 테이블스페이스(tablespace)가 그렇다. 이들은 데이터베이스별 복사본 없이 공유 카탈로그에 존재한다. 롤은 여러 데이터베이스에 걸쳐 오브젝트를 소유한다. “이 롤을 삭제할 수 있는가?”를 추적하려면 모든 백엔드가 어느 데이터베이스에 연결되어 있든 기여하고 참조하는 별도의 클러스터 전역 의존성 카탈로그가 필요하다. 이것이 pg_depend / pg_shdepend 분리의 이유다.
DBMS 공통 설계
섹션 제목: “DBMS 공통 설계”DROP ... CASCADE를 지원하는 SQL 엔진은 모두 동일한 핵심 문제를 푼다. 아래 패턴은 공유 설계 공간이다. 다음 절의 PostgreSQL 구체적 선택은 이 공간 안의 한 지점으로 읽힌다.
시스템 카탈로그로서의 의존성 정보
섹션 제목: “시스템 카탈로그로서의 의존성 정보”“오브젝트 A가 오브젝트 B에 의존한다”의 자연스러운 저장 위치는 행이 그래프 에지인 양방향 카탈로그 테이블이다. (depender_class, depender_oid, depender_subid) → (referenced_class, referenced_oid, referenced_subid). 모든 스키마 오브젝트는 (카탈로그 OID, 행 OID, 선택적 컬럼 번호)로 주소를 지정할 수 있다. 이 오브젝트 주소를 쓰면 오브젝트 종류별 컬럼 없이 단일 에지 테이블로 모든 타입 간 의존성을 기술한다. 카탈로그는 양방향으로 인덱싱된다. “depender” 인덱스는 “A가 무엇에 의존하는가?”를 답하고, “reference” 인덱스는 “B에 의존하는 것은 무엇인가?”를 답한다.
의존성 종류 / 강도
섹션 제목: “의존성 종류 / 강도”단일 에지 타입으로는 부족하다. 엔진은 에지를 강도로 분류한다.
- 하드/노멀 에지는 “B는 A보다 오래 살아야 한다. B를 삭제하려면 A를 통해 명시적으로 캐스케이드해야 한다”를 의미한다. RESTRICT로 보호되는 에지다.
- 소프트/자동 에지는 “A는 B를 위해서만 존재한다. B가 사라지면 A를 조용히 삭제한다”를 의미한다. 컬럼 기본값,
serial시퀀스가 예다. - 내부/구현 에지는 “A는 B의 일부다. A를 직접 삭제할 수 없다. B를 삭제하면 A도 함께 사라진다”를 의미한다. 이 에지가 의존성 사이클을 끊고, 제약을 구현하는 인덱스를 직접 삭제하지 못하게 막는다.
2단계 순회: 위로 재지정한 뒤 아래로 수집
섹션 제목: “2단계 순회: 위로 재지정한 뒤 아래로 수집”CASCADE 엔진은 단순 순방향 폐쇄로 작동할 수 없다. 사용자가 DROP을 내부 서브컴포넌트(제약을 구현하는 자동 생성 인덱스)에 겨냥하면, 요청을 먼저 소유 오브젝트로 위로 재지정해야 전체 단위가 올바른 순서로 삭제된다. 견고한 순회는 노드별로 두 단계를 거친다. 먼저 노드 자신의 의존성을 스캔해 위임해야 할 소유자를 찾고(상향 재지정/사이클 처리), 그다음 역방향 에지를 스캔해 노드에 의존하는 모든 것을 찾아 재귀한다(하향 수집). 노드를 모든 의존자 뒤에 킬 리스트에 추가하면 위상 삭제 순서가 자연스럽게 도출된다.
RESTRICT vs. CASCADE 게이트
섹션 제목: “RESTRICT vs. CASCADE 게이트”전체 삭제 대상 집합이 확정된 뒤, 보고 패스가 합법성을 결정한다. 소프트/내부/자동 에지를 통해서만 도달한 오브젝트는 항상 삭제할 수 있다. 어차피 사라질 것들이기 때문이다. 하드 에지를 통해 도달한 오브젝트가 RESTRICT의 장벽이다. 그런 오브젝트가 집합에 있고 사용자가 CASCADE를 지정하지 않았다면, 전체 DROP이 “다른 오브젝트가 의존하고 있다”는 오류와 함께 중단된다.
로컬 vs. 공유(클러스터 전역) 의존성
섹션 제목: “로컬 vs. 공유(클러스터 전역) 의존성”여러 데이터베이스에서 참조되는 오브젝트, 즉 롤과 테이블스페이스는 데이터베이스별 카탈로그로 추적할 수 없다. 데이터베이스 X에 연결된 백엔드는 데이터베이스 Y에 저장된 카탈로그 행을 볼 수 없기 때문이다. 표준 해법은 depender의 데이터베이스 OID를 스탬프로 찍는 두 번째 공유 의존성 카탈로그다. DROP ROLE은 하나의 전역 테이블을 스캔해서 각 데이터베이스에 연결하지 않고도 “롤이 다른 3개 데이터베이스에서 오브젝트를 소유하고 있다”고 보고할 수 있다.
PostgreSQL의 접근
섹션 제목: “PostgreSQL의 접근”PostgreSQL은 이 설계를 두 카탈로그와 하나의 재귀 순회 엔진으로 구현한다. 로컬 카탈로그는 pg_depend, 클러스터 공유 카탈로그는 **pg_shdepend**다. 에지 엔드포인트는 ObjectAddress 트리플 (classId, objectId, objectSubId)다. classId는 오브젝트가 속한 카탈로그의 OID(예: pg_class의 경우 RelationRelationId), objectId는 오브젝트 자신의 OID, objectSubId는 컬럼 번호(전체 오브젝트면 0)다.
의존성 종류
섹션 제목: “의존성 종류”사용자 가시 강도는 pg_depend.deptype에 저장되는 char 코드 열거형이다.
// DependencyType — src/include/catalog/dependency.htypedef enum DependencyType{ DEPENDENCY_NORMAL = 'n', /* hard: RESTRICT-guarded */ DEPENDENCY_AUTO = 'a', /* soft: auto-drop with referenced */ DEPENDENCY_INTERNAL = 'i', /* A is part of B; drop B, not A */ DEPENDENCY_PARTITION_PRI = 'P', /* partition: primary owner */ DEPENDENCY_PARTITION_SEC = 'S', /* partition: secondary owner */ DEPENDENCY_EXTENSION = 'e', /* A is a member of extension B */ DEPENDENCY_AUTO_EXTENSION = 'x', /* auto, but also extension-owned */} DependencyType;각 종류의 의미:
- normal (
n) — 뷰가 선택하는 테이블에 대한 의존성, 테이블 컬럼이 자신의 데이터 타입에 대한 의존성. 참조된 오브젝트를 삭제하려면 CASCADE가 필요하다. - auto (
a) —DEFAULT표현식의pg_attrdef행이 해당 컬럼에 소유된다. 컬럼이 사라지면 기본값도 조용히 사라진다. - internal (
i) — UNIQUE/PK 제약을 구현하는 인덱스가 그 제약에 내부적으로 의존한다.DROP INDEX로 삭제할 수 없다.DROP CONSTRAINT를 써야 하며, 잘못 지정된DROP INDEX는 순회 과정에서 제약으로 위로 재지정된다. - extension (
e) —CREATE EXTENSION스크립트가 빌드한 모든 오브젝트는pg_extension행에 대한 extension 의존성을 얻는다.DROP EXTENSION이 그 모든 것에 캐스케이드된다. 상세 내용은postgres-extensions.md에서 다룬다. - partition (
P/S) — 파티션 자식이 부모 및 파티션 테이블 루트에 갖는 의존성. 쌍으로 모델링해 파티션 링크 삭제를 강제하고 파티션이 고아가 되는 것을 막는다.
pg_shdepend.deptype은 다른 열거형이다. 공유 의존성은 구현 구조가 아니라 소유권과 ACL에 관한 것이다.
// SharedDependencyType — src/include/catalog/dependency.htypedef enum SharedDependencyType{ SHARED_DEPENDENCY_OWNER = 'o', /* referenced role owns object */ SHARED_DEPENDENCY_ACL = 'a', /* role appears in object's ACL */ SHARED_DEPENDENCY_INITACL = 'i', /* role in pg_init_privs ACL */ SHARED_DEPENDENCY_POLICY = 'r', /* role referenced in RLS policy */ SHARED_DEPENDENCY_TABLESPACE = 't', /* object lives in tablespace */ SHARED_DEPENDENCY_INVALID = 0,} SharedDependencyType;에지 기록
섹션 제목: “에지 기록”기본 연산은 recordDependencyOn이다. depender를 먼저, 참조 대상을 나중에 받는다. 이 함수는 배치 처리형 recordMultipleDependencies에 위임한다. 이 함수는 두 가지 특징적인 최적화를 담는다. 첫째, 핀된(pinned) 오브젝트(부트스트랩 시 시스템이 고정한 오브젝트로, 삭제 불가)를 완전히 건너뛴다. 이들에 대한 에지를 기록하면 엄청난 공간 낭비이기 때문이다. 둘째, 슬롯 배치 단위로 다중 삽입한다.
// recordMultipleDependencies — src/backend/catalog/pg_depend.cfor (i = 0; i < nreferenced; i++, referenced++){ /* If the referenced object is pinned by the system, there's no real * need to record dependencies on it. This saves lots of space... */ if (isObjectPinned(referenced)) continue;
/* DROP routines should lock the object exclusively before they check * dependencies. */ dependencyLockAndCheckObject(referenced->classId, referenced->objectId); /* ... fill a TupleTableSlot with the 7 pg_depend columns ... */ slot[slot_stored_count]->tts_values[Anum_pg_depend_deptype - 1] = CharGetDatum((char) behavior); /* ... batched CatalogTuplesMultiInsertWithInfo when slots fill ... */}isObjectPinned는 공간 절약 장치다. 단순히 IsPinnedObject를 호출해 initdb 시 빌드된 하드코딩 핀 목록을 참조한다.
// isObjectPinned — src/backend/catalog/pg_depend.cstatic boolisObjectPinned(const ObjectAddress *object){ return IsPinnedObject(object->classId, object->objectId);}대부분의 호출자는 에지를 직접 열거하지 않는다. 대신 표현식 트리(뷰 쿼리, CHECK 제약, 기본값)를 recordDependencyOnExpr에 넘긴다. 이 함수는 노드 트리를 순회해 표현식이 언급하는 모든 오브젝트를 수집하고, 중복을 제거한 뒤 요청된 강도로 한꺼번에 기록한다.
// recordDependencyOnExpr — src/backend/catalog/dependency.ccontext.addrs = new_object_addresses();context.rtables = list_make1(rtable);find_expr_references_walker(expr, &context); /* collect refs */eliminate_duplicate_dependencies(context.addrs);recordMultipleDependencies(depender, context.addrs->refs, context.addrs->numrefs, behavior);DROP 엔진 개요
섹션 제목: “DROP 엔진 개요”performDeletion은 의존성 인식 DROP 전체의 단일 진입점이다. pg_depend를 한 번 열고, 대상을 잠근 뒤, findDependentObjects로 삭제 집합을 빌드하고, reportDependentObjects에서 RESTRICT/CASCADE 게이트를 통과시킨 다음, 목록을 순서대로 삭제한다.
flowchart TD A["performDeletion(object, behavior, flags)"] --> B["table_open(pg_depend)<br/>AcquireDeletionLock(target)"] B --> C["findDependentObjects()<br/>recursive: build targetObjects in safe deletion order"] C --> D["reportDependentObjects()<br/>RESTRICT gate + NOTICE 'drop cascades to ...'"] D --> E["deleteObjectsInList()<br/>iterate targetObjects front-to-back"] E --> F["deleteOneObject() per entry<br/>doDeletion + purge pg_depend + pg_shdepend rows"] F --> G["table_close(pg_depend)"]
findDependentObjects 헤더 주석의 핵심 불변식은 오브젝트의 의존자가 오브젝트 자신보다 먼저 targetObjects에 추가된다는 것이다. 완성된 배열을 앞에서 뒤로 순회하면 그 자체가 유효한 삭제 순서가 된다. 재귀는 각 노드를 의존하는 모든 것을 재귀 처리한 후에만 추가한다.
2단계 순회와 사이클 끊기
섹션 제목: “2단계 순회와 사이클 끊기”findDependentObjects는 노드별로 위로 재지정/아래로 수집 단계를 수행한다. 1단계는 노드 자신의 pg_depend 행(depender 인덱스)을 스캔해 위임해야 할 내부/extension 소유자를 찾는다. 최외곽 수준에서 소유자가 발견되면 삭제는 불법이다. 소유자로부터 재귀하는 도중이라면 허용된다.
flowchart TD
N["visit object"] --> S{"already on stack<br/>or in targetObjects?"}
S -->|yes| R["merge flags, return<br/>(breaks cycles)"]
S -->|no| P1["Phase 1: scan OWN deps<br/>(depender index)"]
P1 --> I{"internal/extension<br/>owner found?"}
I -->|"outer level"| ERR["redirect: recurse into owner<br/>or ERROR if not pending"]
I -->|"recursing from owner"| OK["allowed, continue"]
P1 --> P2["Phase 2: scan INVERSE deps<br/>(reference index)"]
P2 --> SORT["collect dependents,<br/>qsort by OID desc"]
SORT --> REC["recurse into each dependent"]
REC --> ADD["append THIS object to targetObjects<br/>(after all dependents)"]
사이클 끊기 요령은 DEPENDENCY_INTERNAL 케이스에 있다. 순회가 “소유된” 오브젝트에서 의존성 루프에 진입하면, internal 에지가 강제로 “소유하는” 오브젝트로 전환하고 거기서 다시 시작한다. 루프는 항상 internal 에지에서 끊어지며, 임의의 지점에서 끊어지지 않는다. 헤더 주석은 명시적이다. “the check for internal dependency below guarantees that we will not break a loop at an internal dependency.”
소스 분석
섹션 제목: “소스 분석”코드는 세 파일에 걸쳐 있다. dependency.c(순회/삭제 엔진 및 표현식 워커), pg_depend.c(로컬 카탈로그 CRUD 및 특수 조회), pg_shdepend.c(클러스터 공유 카탈로그). 호출 흐름 순으로 살펴본다.
기록: pg_depend.c
섹션 제목: “기록: pg_depend.c”recordDependencyOn은 배치형 recordMultipleDependencies의 단일 에지 래퍼다. 배치형 함수가 실제 작업자다. RowExclusiveLock으로 pg_depend를 열고, 최대 MAX_CATALOG_MULTI_INSERT_BYTES / sizeof(FormData_pg_depend) 개의 튜플 슬롯을 할당한다. 참조 대상 오브젝트마다 핀을 건너뛰고, 참조 대상을 잠근 뒤 재확인하고, 7개 컬럼을 채우고, 배치 단위로 다중 삽입한다. 7개 컬럼은 두 ObjectAddress 트리플과 deptype이다.
// recordMultipleDependencies — src/backend/catalog/pg_depend.cslot[n]->tts_values[Anum_pg_depend_refclassid - 1] = ObjectIdGetDatum(referenced->classId);slot[n]->tts_values[Anum_pg_depend_refobjid - 1] = ObjectIdGetDatum(referenced->objectId);slot[n]->tts_values[Anum_pg_depend_refobjsubid - 1] = Int32GetDatum(referenced->objectSubId);slot[n]->tts_values[Anum_pg_depend_deptype - 1] = CharGetDatum((char) behavior);slot[n]->tts_values[Anum_pg_depend_classid - 1] = ObjectIdGetDatum(depender->classId);slot[n]->tts_values[Anum_pg_depend_objid - 1] = ObjectIdGetDatum(depender->objectId);slot[n]->tts_values[Anum_pg_depend_objsubid - 1] = Int32GetDatum(depender->objectSubId);dependencyLockAndCheckObject는 동시성 안전망이다. 오브젝트 B를 참조하는 행이 커밋되기 전에 B가 사라지면 안 된다. B에 AccessShareLock을 잡고(DROP의 AccessExclusiveLock과 충돌), B가 아직 존재하는지 재확인한다. 먼저 syscache를 시도하고, 같은 커맨드 앞부분에서 생성된 오브젝트가 보이도록 SnapshotSelf 스캔으로 폴백한다.
// dependencyLockAndCheckObject — src/backend/catalog/pg_depend.cif (LockHeldByMe(&tag, AccessShareLock, true)) return;LockDatabaseObject(classId, objectId, 0, AccessShareLock);cache = get_object_catcache_oid(classId);if (cache != -1 && SearchSysCacheExists1(cache, ObjectIdGetDatum(objectId))) return;/* else SnapshotSelf scan; ERROR "referenced %s was concurrently dropped" */recordDependencyOnCurrentExtension은 e 에지가 심어지는 방법이다. CREATE EXTENSION이 진행 중(creating_extension)이면, 새로 생성되는 extension 가능 오브젝트는 CurrentExtensionObject에 대한 DEPENDENCY_EXTENSION 에지를 기록한다. pg_depend.c의 변경 함수인 deleteDependencyRecordsFor, changeDependencyFor, changeDependenciesOf는 오브젝트 재정의(CREATE OR REPLACE)와 이름 변경/재할당 시 depender 또는 reference 인덱스를 통해 에지를 삭제하거나 재지정한다.
조회: pg_depend.c 특수 쿼리
섹션 제목: “조회: pg_depend.c 특수 쿼리”읽기 전용 헬퍼 패밀리가 두 인덱스를 재사용해 특정 질문에 답한다. getOwnedSequences는 컬럼이 소유한 시퀀스(serial/identity)를 찾고, getExtensionOfObject는 e 에지를 스캔해 오브젝트를 소유한 익스텐션을 찾으며, get_index_constraint는 인덱스가 내부적으로 구현하는 제약을 찾는다.
// get_index_constraint — src/backend/catalog/pg_depend.c/* scan pg_depend for the index as depender; the INTERNAL edge to a * pg_constraint row is the owning constraint */if (deprec->refclassid == ConstraintRelationId && deprec->refobjsubid == 0 && deprec->deptype == DEPENDENCY_INTERNAL){ constraintId = deprec->refobjid; break;}순회: findDependentObjects
섹션 제목: “순회: findDependentObjects”이 재귀 함수가 CASCADE의 핵심이다. 방문할 오브젝트, objflags(방문 이유), 공유 flags, 재귀 stack, 누적 targetObjects, 선택적 pendingObjects 목록(다중 삭제용), 열린 pg_depend를 받는다.
재귀에 두 가지 가드를 먼저 적용한다. 스택 검사(이미 외곽 수준에서 방문 중 → 플래그 병합 후 반환, 사이클 끊기)와 targetObjects 검사(이미 완전히 처리됨):
// findDependentObjects — src/backend/catalog/dependency.cif (stack_address_present_add_flags(object, objflags, stack)) return;check_stack_depth();if (object_address_present_add_flags(object, objflags, targetObjects)) return;if (IsPinnedObject(object->classId, object->objectId)) ereport(ERROR, ... "cannot drop %s because it is required by the database system" ...);1단계 — 위로 재지정. depender 인덱스로 pg_depend를 스캔해(오브젝트가 depender인 행) 소유자를 찾는다. INTERNAL/EXTENSION 에지에서 세 가지 케이스를 하나의 switch 분기로 처리한다. 최외곽 수준(stack == NULL)에서는 삭제가 불법이다. 소유자가 이미 pendingObjects에 있으면 bail out하고 pending 항목이 처리하게 한다.
// findDependentObjects (Phase 1, INTERNAL case) — dependency.cif (stack == NULL){ if (pendingObjects && object_address_present(&otherObject, pendingObjects)) { systable_endscan(scan); ReleaseDeletionLock(object); return; } /* remember owner; complain after the loop (prefer EXTENSION) */ if (!OidIsValid(owningObject.classId) || foundDep->deptype == DEPENDENCY_EXTENSION) owningObject = otherObject; break;}다른 곳에서 재귀 중일 때, 케이스 3은 삭제를 소유자로 재지정한다. 현재 오브젝트의 잠금을 해제하고, 소유자를 잠근 뒤, pg_depend 튜플이 아직 살아있는지 재확인하고, DEPFLAG_REVERSE로 소유자에 재귀한다. 이것이 사이클 끊기다. 루프는 항상 소유하는 쪽 끝에서 다시 진입한다.
// findDependentObjects (Phase 1, case 3) — dependency.cReleaseDeletionLock(object);AcquireDeletionLock(&otherObject, 0);if (!systable_recheck_tuple(scan, tup)) { /* owner gone: bail */ }systable_endscan(scan);findDependentObjects(&otherObject, DEPFLAG_REVERSE, flags, stack, targetObjects, pendingObjects, depRel);2단계 — 아래로 수집. 자신의 의존성 스캔 후, reference 인덱스로 재스캔해(오브젝트가 참조된 쪽인 행) 직접 의존자를 찾는다. 각각을 잠그고, 생존 여부를 재확인하고, subflags 비트로 분류한 뒤 dependentObjects에 버퍼링한다. 버퍼는 qsort 후 재귀된다. 현재 오브젝트는 그 후에 추가된다.
// findDependentObjects (Phase 2) — dependency.cswitch (foundDep->deptype){ case DEPENDENCY_NORMAL: subflags = DEPFLAG_NORMAL; break; case DEPENDENCY_AUTO: case DEPENDENCY_AUTO_EXTENSION: subflags = DEPFLAG_AUTO; break; case DEPENDENCY_INTERNAL: subflags = DEPFLAG_INTERNAL; break; case DEPENDENCY_PARTITION_PRI: case DEPENDENCY_PARTITION_SEC: subflags = DEPFLAG_PARTITION; break; case DEPENDENCY_EXTENSION: subflags = DEPFLAG_EXTENSION; break;}/* ... buffer dependentObjects[], then: */if (numDependentObjects > 1) qsort(dependentObjects, numDependentObjects, sizeof(ObjectAddressAndFlags), object_address_comparator);/* recurse into each, THEN append self */add_exact_object_address_extra(object, &extra, targetObjects);qsort는 정확성이 아닌 결정론적 출력을 위한 것이다. DROP ... CASCADE 알림이 실행마다 안정적으로 나오도록 한다(회귀 테스트에 중요하다). 비교자는 OID 내림차순(최신 먼저, 보통 올바른 삭제 순서), 그다음 카탈로그 ID, 그다음 subId 순으로 정렬하되 0(전체 오브젝트)이 먼저 온다.
// object_address_comparator — src/backend/catalog/dependency.cif (obja->objectId > objb->objectId) return -1; /* OID descending */if (obja->objectId < objb->objectId) return 1;if (obja->classId < objb->classId) return -1;/* subId compared as unsigned so 0 (whole object) sorts first */게이트: reportDependentObjects
섹션 제목: “게이트: reportDependentObjects”targetObjects가 빌드된 뒤, 이 패스가 합법성을 결정하고 “drop cascades to N other objects” 메시지를 내보낸다. 목록을 뒤에서 앞으로 순회한다(의존성 순서, 가독성이 좋다). AUTO | INTERNAL | PARTITION | EXTENSION을 통해 도달한 오브젝트는 항상 삭제할 수 있다. 오직 순수한 NORMAL 에지를 통해서만 도달한 오브젝트가 RESTRICT를 발동한다.
// reportDependentObjects — src/backend/catalog/dependency.cif (extra->flags & (DEPFLAG_AUTO | DEPFLAG_INTERNAL | DEPFLAG_PARTITION | DEPFLAG_EXTENSION)) ereport(DEBUG2, (errmsg_internal("drop auto-cascades to %s", objDesc)));else if (behavior == DROP_RESTRICT){ appendStringInfo(&clientdetail, _("%s depends on %s"), objDesc, otherDesc); ok = false;}else appendStringInfo(&clientdetail, _("drop cascades to %s"), objDesc);ok가 false면 ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST(“cannot drop %s because other objects depend on it” + “Use DROP … CASCADE” 힌트)를 발생시킨다. 파티션 무결성도 강제한다. DEPFLAG_IS_PART로 플래그가 설정되었지만 파티션 에지를 통해 도달하지 않은 오브젝트는 파티션을 고아로 만들려는 시도이므로 거부한다.
삭제: deleteOneObject와 doDeletion
섹션 제목: “삭제: deleteOneObject와 doDeletion”deleteObjectsInList는 targetObjects를 앞에서 뒤로(안전한 순서) 순회하며 각각에 deleteOneObject를 호출한다. deleteOneObject는 오브젝트 드롭 훅을 발동시키고, doDeletion으로 타입별 삭제를 수행한다. 그다음 이 오브젝트가 depender인 모든 pg_depend 행, 매칭되는 pg_shdepend 행, 주석/보안 레이블/초기 권한을 제거한다. 마지막으로 CommandCounterIncrement를 호출해 다음 단계가 변경을 볼 수 있게 한다.
// deleteOneObject — src/backend/catalog/dependency.cInvokeObjectDropHookArg(object->classId, object->objectId, object->objectSubId, flags);doDeletion(object, flags); /* type-specific drop *//* purge outgoing pg_depend rows (incoming ones are already gone) */while (HeapTupleIsValid(tup = systable_getnext(scan))) CatalogTupleDelete(*depRel, &tup->t_self);deleteSharedDependencyRecordsFor(object->classId, object->objectId, object->objectSubId);DeleteComments(...); DeleteSecurityLabel(object); DeleteInitPrivs(object);CommandCounterIncrement();doDeletion은 classId로 분기하는 큰 switch문으로, 올바른 카탈로그별 루틴에 디스패치한다. 여기서 의존성 추적이 오브젝트별 DDL 해체(postgres-ddl-execution.md)로 넘어간다.
// doDeletion — src/backend/catalog/dependency.cswitch (object->classId){ case RelationRelationId: /* index_drop / heap_drop_with_catalog */ case ProcedureRelationId: RemoveFunctionById(object->objectId); break; case TypeRelationId: RemoveTypeById(object->objectId); break; case ConstraintRelationId: RemoveConstraintById(object->objectId); break; case TriggerRelationId: RemoveTriggerById(object->objectId); break; /* ... ~30 catalog classes ... */}공유 카탈로그: pg_shdepend.c
섹션 제목: “공유 카탈로그: pg_shdepend.c”pg_shdepend는 pg_depend를 미러링하지만 클러스터 공유 참조 오브젝트(롤, 테이블스페이스)를 위한 것이다. depender가 속한 데이터베이스를 스탬프하는 추가 dbid 컬럼을 가진다. recordSharedDependencyOn이 기본 연산이고, recordDependencyOnOwner는 롤에 SHARED_DEPENDENCY_OWNER 에지를 기록하는 공통 래퍼다.
// recordDependencyOnOwner — src/backend/catalog/pg_shdepend.creferenced.classId = AuthIdRelationId;referenced.objectId = owner;recordSharedDependencyOn(&myself, &referenced, SHARED_DEPENDENCY_OWNER);changeDependencyOnOwner(ALTER ... OWNER TO가 사용)는 shdepChangeDep을 구동한다. shdepChangeDep은 기존 소유자/테이블스페이스 에지를 찾아 제자리에서 업데이트하거나, 새 참조 오브젝트가 핀되어 있는지 여부에 따라 삽입/삭제한다. checkSharedDependencies는 DROP ROLE / DROP TABLESPACE 게이트다. reference 인덱스로 pg_shdepend를 스캔하고, depender를 로컬/공유/원격으로 분류해서, 각 데이터베이스에 연결하지 않고도 “롤이 다른 N개 데이터베이스에서 오브젝트를 소유하고 있다”는 상세 메시지를 구성한다.
// checkSharedDependencies — src/backend/catalog/pg_shdepend.cif (sdepForm->dbid == MyDatabaseId || sdepForm->dbid == InvalidOid) /* local or shared: add to objects[] for detailed report */;else /* remote: tally per-database count in remDeps list */;deleteSharedDependencyRecordsFor(deleteOneObject에서 호출)와 shdepDropDependency는 삭제된 오브젝트의 공유 에지를 제거한다.
위치 힌트 (2026-06-05 기준, REL_18 273fe94)
섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”| 심볼 | 파일 | 행 |
|---|---|---|
DependencyType enum | src/include/catalog/dependency.h | 31 |
SharedDependencyType enum | src/include/catalog/dependency.h | 78 |
PERFORM_DELETION_INTERNAL | src/include/catalog/dependency.h | 92 |
recordDependencyOn | src/backend/catalog/pg_depend.c | 51 |
recordMultipleDependencies | src/backend/catalog/pg_depend.c | 63 |
recordDependencyOnCurrentExtension | src/backend/catalog/pg_depend.c | 206 |
deleteDependencyRecordsFor | src/backend/catalog/pg_depend.c | 314 |
changeDependencyFor | src/backend/catalog/pg_depend.c | 470 |
isObjectPinned | src/backend/catalog/pg_depend.c | 729 |
dependencyLockAndCheckObject | src/backend/catalog/pg_depend.c | 752 |
getExtensionOfObject | src/backend/catalog/pg_depend.c | 865 |
getOwnedSequences | src/backend/catalog/pg_depend.c | 1140 |
get_index_constraint | src/backend/catalog/pg_depend.c | 1192 |
deleteObjectsInList | src/backend/catalog/dependency.c | 185 |
performDeletion | src/backend/catalog/dependency.c | 273 |
performMultipleDeletions | src/backend/catalog/dependency.c | 332 |
findDependentObjects | src/backend/catalog/dependency.c | 432 |
reportDependentObjects | src/backend/catalog/dependency.c | 980 |
deleteOneObject | src/backend/catalog/dependency.c | 1246 |
doDeletion | src/backend/catalog/dependency.c | 1352 |
AcquireDeletionLock | src/backend/catalog/dependency.c | 1496 |
recordDependencyOnExpr | src/backend/catalog/dependency.c | 1553 |
eliminate_duplicate_dependencies | src/backend/catalog/dependency.c | 2398 |
object_address_comparator | src/backend/catalog/dependency.c | 2458 |
recordSharedDependencyOn | src/backend/catalog/pg_shdepend.c | 125 |
recordDependencyOnOwner | src/backend/catalog/pg_shdepend.c | 168 |
shdepChangeDep | src/backend/catalog/pg_shdepend.c | 206 |
changeDependencyOnOwner | src/backend/catalog/pg_shdepend.c | 316 |
checkSharedDependencies | src/backend/catalog/pg_shdepend.c | 676 |
deleteSharedDependencyRecordsFor | src/backend/catalog/pg_shdepend.c | 1047 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”모든 주장은 /data/hgryoo/references/postgres, 브랜치 REL_18_STABLE, 커밋 273fe94852b3a7e34fd171e8abdf1481beb302fa(PostgreSQL 18.x)에 대해 검증했다.
-
카탈로그 파일이 존재하고 인용된 루틴을 소유한다.
dependency.c(2838행),pg_depend.c(1295행),pg_shdepend.c(1759행) 모두src/backend/catalog/<file>을 명시하는IDENTIFICATION배너를 가진다. 위치 힌트 표의 모든 심볼은grep -n으로 표시된 행에서 확인했다. -
두 열거형은 그대로다.
DependencyType('n' 'a' 'i' 'P' 'S' 'e' 'x')과SharedDependencyType('o' 'a' 'i' 'r' 't' 0)은src/include/catalog/dependency.h31행과 78행의 것과 일치한다.DEPFLAG_*비트마스크(0x0001~0x0100)는dependency.c상단에 정의되어 있다. -
삭제 순서 불변식은 코드 자체의 계약이다.
findDependentObjects헤더 주석은 “An object’s dependencies will be placed into targetObjects before the object itself; this means that the finished list’s order represents a safe deletion order”라고 명시한다. 함수는 의존자에 대한 재귀 루프 후에만add_exact_object_address_extra(object, ...)를 호출한다. 구조적으로 확인했다. -
internal 에지를 통한 사이클 끊기는 같은 함수의 주석(“the check for internal dependency below guarantees that we will not break a loop at an internal dependency”)에 문서화되어 있으며,
owningObject에DEPFLAG_REVERSE로 재귀하는DEPENDENCY_INTERNALswitch 분기에 구현되어 있다. -
RESTRICT 게이트는
reportDependentObjects에 있다.DEPFLAG_AUTO | DEPFLAG_INTERNAL | DEPFLAG_PARTITION | DEPFLAG_EXTENSION테스트는 DEBUG2로 다운그레이드하고, NORMAL 전용 경로에서만DROP_RESTRICT가ok = false→ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST로 이어진다. -
이 문서의 범위.
dependency.c,pg_depend.c,pg_shdepend.c만 권위 있는 범위로 다뤘다.doDeletion을 통해 도달하는 오브젝트별 해체 루틴(예:heap_drop_with_catalog,RemoveFunctionById)과objectaddress.c의ObjectAddress해석 기계는 범위 밖이며postgres-ddl-execution.md와postgres-system-catalogs.md로 위임한다.contrib/코드는 참조하지 않는다. 익스텐션 멤버십 메커니즘은 여기서 요약하고postgres-extensions.md에서 상세히 다룬다.
PostgreSQL 너머 — 비교 설계와 연구 최전선
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 최전선”PostgreSQL의 의존성 추적은 넓은 설계 공간의 한 지점이다. 다른 엔진과 연구 문헌과 비교하면 본질적인 것과 PostgreSQL 고유의 선택이 선명하게 갈린다.
카탈로그-as-테이블 vs. 하드코딩된 의존성 규칙
섹션 제목: “카탈로그-as-테이블 vs. 하드코딩된 의존성 규칙”PostgreSQL의 정의적 선택은 의존성을 오브젝트 타입별 C 규칙이 아닌 pg_depend의 카탈로그 데이터로 저장하는 것이다. 이는 System R(Astrahan et al., 1976; research/dbms-papers/systemr.md에 수록)의 자기 기술적 철학과 같은 계보다. System R의 SYSCATALOG/SYSCOLUMNS 카탈로그는 그 자체가 일반 접근 경로로 쿼리되는 관계였다. 얻는 것은 범용성이다. 새 오브젝트 타입(publication, statistics 오브젝트, 접근 방법)은 CREATE 경로에서 recordDependencyOn을 호출하는 순간 CASCADE에 참여한다. 순회 엔진을 바꿀 필요가 없다. 치르는 비용은 DROP 시점에 라이브 카탈로그 스캔을 실행해야 한다는 것, 그리고 의존성 그래프의 정확성이 모든 CREATE 경로의 규율에 달려 있다는 것이다.
많은 엔진은 반대 방향을 택한다. 일부 임베디드 및 역사적 시스템은 “테이블을 삭제하면 인덱스와 트리거도 삭제된다”를 DROP TABLE 핸들러에 하드코딩한다. 일반 에지 카탈로그가 없다. 일반적인 케이스에서는 단순하고 빠르지만, 임의 오브젝트 간 참조(다른 스키마 컬럼에서 사용되는 함수, 함수가 사용하는 타입)에는 대응하지 못한다. pg_depend가 존재하는 이유가 바로 그 케이스들이다.
위상 정렬로서의 삭제 순서 결정
섹션 제목: “위상 정렬로서의 삭제 순서 결정”삭제 순서 문제는 유도 부분 그래프의 위상 정렬이다. PostgreSQL은 이를 암묵적으로 푼다. DFS 중 후위(post-order) 추가가 역위상 순서를 만들어내므로, 앞에서 뒤로 순회하면 의존자보다 의존 대상이 나중에 삭제된다. 교과서의 DFS 기반 위상 정렬(예: Cormen et al.)과 같은 알고리즘이다. PostgreSQL은 순수 알고리즘이 다루지 않는 두 가지를 추가한다.
-
사이클이 합법적이며 임의의 역방향 에지가 아닌 internal 에지에서 결정론적으로 끊어야 한다.
findDependentObjects가 교과서의 비순환 위상 정렬이 될 수 없는 이유다. 소유자로의 재지정 로직이 필요하다. -
테스트 가능성을 위한 결정론. 의존자 수집 단계에서의 OID 내림차순
qsort는DROP CASCADE출력을 재현 가능하게 만들기 위한 것이다. 대부분의 위상 정렬 형식화는 동률 처리에 무관심하다. 회귀 테스트 슈트를 운영하는 프로덕션 카탈로그 엔진은 무관심할 수 없다.
강도 분류 에지 vs. 단일 에지
섹션 제목: “강도 분류 에지 vs. 단일 에지”PostgreSQL의 네 가지 이상의 의존성 강도(normal/auto/internal/extension, 그리고 partition pri/sec와 auto-extension)는 SQL 표준의 데이터 수준 외래 키가 가진 이진 “RESTRICT vs. CASCADE 참조 동작”보다 풍부하다. 가장 가까운 표준 유사체는 SQL:2016의 DROP ... RESTRICT|CASCADE 스키마 오브젝트 동작이지만, 표준은 internal 강도를 명시하지 않는다. 두 카탈로그 오브젝트가 하나의 논리 단위임을 나타내는 개념, 즉 하나를 다른 것 없이 삭제하지 못하게 막는 개념이 표준에는 없다. 이 개념이 PostgreSQL로 하여금 제약을 지원하는 인덱스에 대한 DROP INDEX를 금지하면서도 올바르게 캐스케이드할 수 있게 한다. 하나의 논리 오브젝트(기본 키)가 여러 카탈로그 행(pg_constraint 행 하나, pg_class 인덱스 하나, pg_attribute 컬럼들)으로 표현되는 구현 필연이다.
공유/전역 의존성 추적
섹션 제목: “공유/전역 의존성 추적”pg_shdepend 분리 — 두 번째, 클러스터 전역 카탈로그에 dbid를 스탬프 — 는 롤과 테이블스페이스가 모든 데이터베이스에서 보이지만 그 의존자는 데이터베이스별 카탈로그에 분산되어 있다는 사실에 대한 PostgreSQL의 답이다. 그렇지 않으면 DROP ROLE이 소유 오브젝트를 확인하기 위해 모든 데이터베이스에 연결해야 한다. Database Internals(Petrov)는 데이터 사전을 엔진이 참조하는 메타데이터 계층으로 프레임한다. 공유 카탈로그 변형은 메타데이터 계층이 멀티 데이터베이스 클러스터에서 데이터베이스 간 격리 경계를 넘어야 할 때 나타나는 것이다. 서버 인스턴스당 하나의 데이터베이스(또는 전역 네임스페이스)를 쓰는 엔진은 이 문제에 직면하지 않으며 의존성 카탈로그가 하나면 충분하다.
연구 최전선
섹션 제목: “연구 최전선”-
증분/캐시된 의존성 폐쇄.
findDependentObjects는 매DROP마다 폐쇄를 처음부터 재계산한다. 스키마가 무거운 워크로드(수천 개의 파티션, 생성된 테이블)에서는 카탈로그에 대한 반복 그래프 순회가 된다. 증분 뷰 유지 및 구체화된 그래프 도달 가능성에 대한 연구가 직접 관련된다.pg_depend위의 유지되는 전이적 폐쇄 인덱스는 매CREATE/ALTER에 인덱스를 유지하는 비용으로DROP CASCADE와 RESTRICT 확인을 준선형으로 만들 수 있다. -
온라인/동시 스키마 변경. 의존성 엔진은 모든 삭제 대상 오브젝트에
AccessExclusiveLock(AcquireDeletionLock)을 잡아DROP을 모든 접근에 대해 직렬화한다. 비블로킹 DDL(Google F1의 스키마 변경 프로토콜; 온라인 스키마 변경 도구)을 추구하는 시스템은 동시 읽기 하에서 의존성 에지를 추론해야 한다.dependencyLockAndCheckObject의 “참조된 오브젝트가 동시에 삭제됨” 재확인만으로는 부분적으로만 해결된다. -
불변식으로서의 의존성 그래프 정확성. 그래프는 모든
CREATE경로의 규율에 달려 있으므로, 누락되거나 오래된pg_depend에지는 잠재적 버그의 한 유형이다. 삭제된 오브젝트가 끊어진 참조를 남길 수 있다. “모든 참조에는 기록된 에지가 있다”를 검증 가능한 속성으로 다루는 카탈로그 불변식 검사에 대한 형식 방법 연구는 엔진이 그래프를 신뢰하는 대신 검증할 수 있게 해줄 것이다.
소스 목록
섹션 제목: “소스 목록”PostgreSQL 소스 (REL_18_STABLE, 커밋 273fe94, PG 18.x), /data/hgryoo/references/postgres:
src/backend/catalog/dependency.c— 순회/삭제 엔진:performDeletion,performMultipleDeletions,findDependentObjects,reportDependentObjects,deleteOneObject,doDeletion,recordDependencyOnExpr,eliminate_duplicate_dependencies,object_address_comparator,AcquireDeletionLock.src/backend/catalog/pg_depend.c— 로컬 카탈로그 CRUD 및 조회:recordDependencyOn,recordMultipleDependencies,recordDependencyOnCurrentExtension,deleteDependencyRecordsFor,changeDependencyFor,dependencyLockAndCheckObject,isObjectPinned,getExtensionOfObject,getOwnedSequences,get_index_constraint.src/backend/catalog/pg_shdepend.c— 클러스터 공유 카탈로그:recordSharedDependencyOn,recordDependencyOnOwner,shdepChangeDep,changeDependencyOnOwner,checkSharedDependencies,deleteSharedDependencyRecordsFor.src/include/catalog/dependency.h—DependencyType,SharedDependencyType,PERFORM_DELETION_*플래그.
교과서/논문 앵커 (knowledge/research/ 하위):
- Database System Concepts (Silberschatz, Korth, Sudarshan) —
research/dbms-general/database-system-concepts.md— 참조 무결성과 CASCADE/RESTRICT 참조 동작. - Database Internals (Petrov) —
research/dbms-general/database-internals.md— 엔진의 메타데이터 계층으로서의 데이터 사전/카탈로그. - System R (Astrahan et al., 1976) —
research/dbms-papers/systemr.md— PostgreSQL의pg_depend가 계승하는 카탈로그-as-관계 설계.
교차 참조 (같은 폴더의 형제 문서):
postgres-system-catalogs.md—ObjectAddress, 카탈로그 OID,objectaddress.c해석.postgres-ddl-execution.md—doDeletion을 통해 도달하는 오브젝트별 해체 루틴.postgres-extensions.md—DEPENDENCY_EXTENSION멤버십 메커니즘과CREATE/DROP EXTENSION.