미세한 변화: PostgreSQL의 NOT VALID와 NOT ENFORCED 제약 조건 이해하기
작성자: Amul Sul
작성일: 2026년 3월 27일
PostgreSQL은 뛰어난 데이터 무결성(Data Integrity)을 자랑하는 것으로 잘 알려져 있습니다. 하지만 데이터 세트가 테라바이트 단위로 커짐에 따라, 이러한 무결성을 유지하기 위한 비용이 시스템의 병목 현상(Bottleneck)을 유발하기도 합니다. 이러한 문제를 해결하기 위해 PostgreSQL은 제약 조건(Constraints)에 대한 다양한 “상태(States)”를 제공하고 있습니다.
많은 분들이 기존의 NOT VALID 옵션에는 익숙하시겠지만, 최근 SQL:2023 표준의 새로운 개념이 도입되었습니다. 바로 PostgreSQL 18에 공식적으로 추가된 NOT ENFORCED입니다. 이 두 가지는 이름만 보면 비슷하게 들릴 수 있지만, 데이터베이스의 라이프사이클 내에서 매우 다른 역할을 수행합니다.
💡 TL;DR (핵심 요약)
PostgreSQL에서 제약 조건은 단순히 엄격하게 “켜짐(On)” 또는 “꺼짐(Off)” 상태로만 존재하는 것은 아닙니다.
NOT VALID: 긴 잠금(Lock) 시간 없이 대용량 테이블에 제약 조건을 즉시 추가할 수 있게 해줍니다. 이 옵션은 나중에 수동으로 검증(Validation)하기 전까지는 기존 레거시 데이터에 대한 검사는 무시하되, 새로 추가되는 데이터에 대해서는 규칙을 엄격하게 강제합니다.NOT ENFORCED: 문서화 목적 등을 위해 제약 조건이 존재한다는 사실만을 PostgreSQL에 알려줍니다(원할 때 언제든 강제 상태로 활성화할 수 있습니다). 하지만 데이터베이스 엔진이 실제로 사용자의 데이터 규칙 위반을 차단하지는 않습니다.
참고: PostgreSQL 18 기준으로
NOT ENFORCED상태는CHECK및 외래 키(FOREIGN KEY) 제약 조건에 대해서만 지원됩니다.
이미 NOT VALID가 있는데, 왜 NOT ENFORCED가 필요할까요?
NOT ENFORCED의 필요성을 이해하려면, 먼저 기존 NOT VALID 기능의 한계점을 살펴보아야 합니다.
NOT VALID(임시 마이그레이션 상태): 제약 조건을NOT VALID로 추가하면, PostgreSQL은 다음과 같이 동작합니다. “당장 기존 데이터는 검사하지 않겠습니다(시간 절약). 하지만 지금 이 시점 이후로 추가되는 모든 새로운 행(Row)에 대해서는 이 규칙을 엄격하게 적용하겠습니다.” 궁극적으로 관리자는 제약 조건을 완전히 활성화하기 위해 유효성 검사 스캔(Validation Scan)을 실행해야 합니다.NOT ENFORCED(기능적 메타데이터 상태): 반면 이 옵션은 PostgreSQL에 다음과 같이 지시합니다. “문서화나 쿼리 최적화 목적으로 이 관계가 존재한다는 것은 알아두되, 새로운 데이터나 기존 데이터 그 어떤 것에 대해서도 검사를 수행하지 마세요.“
핵심 차이점: NOT VALID는 여전히 새로운 데이터를 검사하는 반면, NOT ENFORCED는 이러한 검사 자체를 완전히 건너뜁니다.
| 기능 (Feature) | NOT VALID | NOT ENFORCED |
| 새로운 데이터 검사 여부 | 예 (Yes) | 아니오 (강제 활성화 전까지) |
| 기존 데이터 검사 여부 | 아니오 (검증 스캔 전까지) | 아니오 (강제 활성화 전까지) |
| 시스템 오버헤드 | 높음 (검사 및 트리거 활성 유지) | 낮음 (검사 및 트리거 생략) |
외래 키(Foreign Keys)의 진화: 트리거 생략의 마법
이번 업데이트에서 가장 중요한 아키텍처적 변화 중 하나는, 외래 키(FK)가 NOT ENFORCED로 생성될 때의 동작 방식입니다.
CHECK 제약 조건은 데이터 무결성을 유지하기 위해 다른 데이터베이스 객체에 의존하지 않으므로 비교적 단순합니다. 반면에 표준 외래 키 제약 조건은 모든 INSERT, UPDATE, DELETE 작업 시 실행되는 내부 트리거(Internal Triggers)에 의존합니다.
외래 키가 NOT ENFORCED로 정의되면, PostgreSQL은 이러한 내부 트리거 생성을 완전히 건너뛰고 CHECK 제약 조건처럼 시스템 카탈로그(System Catalog)에 플래그만 설정합니다. 이로 인해 트리거 실행 비용이 사라지므로, 대용량 쓰기(High-volume Write) 작업에서 엄청난 성능 향상을 얻을 수 있습니다. 트리거는 나중에 제약 조건을 ENFORCED로 변경할 때만 자동으로 다시 생성됩니다.
‘Not Enforced’와 ‘Enforced’ 상태 간의 전환
그렇다면 데이터 무결성에 대한 책임을 다시 데이터베이스 엔진으로 넘기고 싶을 때는 어떻게 해야 할까요? 적용되지 않은 상태(Unenforced)에서 강제 적용 상태(Enforced)로 전환하려면 엄격한 검증 과정이 필요합니다.
1. Not Enforced로 제약 조건 생성하기
트리거로 인한 성능 저하(Penalty) 없이 테이블 간의 관계를 정의할 수 있습니다.
SQL
ALTER TABLE orders ADD CONSTRAINT fk_product FOREIGN KEY (product_no) REFERENCES products(product_no) NOT ENFORCED;
2. Enforced로 전환하기 (검증 스캔)
데이터베이스가 무결성 강제를 전담해야 한다고 결정했다면, 언제든 “스위치”를 켤 수 있습니다.
SQL
ALTER TABLE orders ALTER CONSTRAINT fk_product ENFORCED;
이 변경 과정에서 데이터 검증(Validation)이 수행됩니다. PostgreSQL은 제약 조건이 적용되지 않았던 기간 동안 추가된 데이터를 맹목적으로 신뢰할 수 없기 때문입니다. 따라서 제약 조건을 강제(Enforced)로 설정하기 전에, 전체 스캔(Full Scan)을 통해 모든 행이 규칙을 준수하는지 확인합니다.
외래 키 제약 조건의 경우 무결성 검사를 수행하는 데 필요한 내부 트리거가 생성되며, 유효성 확인을 위한 전체 스캔이 실행됩니다. 이때 단 하나의 유효하지 않은 행(Invalid row)이라도 발견되면 명령은 실패합니다.
참고: PostgreSQL 18에서 이러한 상태 변경(Alteration) 기능은 현재 외래 키 제약 조건에만 적용됩니다.
CHECK제약 조건의 강제 여부를 변경하는 지원 기능은 활발히 논의 중이며, 향후 버전을 위해 PostgreSQL 개발 메일링 리스트에 제안된 상태입니다.
3. 다시 Not Enforced로 돌아가기
대규모 벌크 로드(Bulk Load) 작업을 수행해야 해서 일시적으로 트리거 오버헤드를 비활성화하고 싶다면 다음과 같이 실행합니다.
SQL
ALTER TABLE orders ALTER CONSTRAINT fk_product NOT ENFORCED;
📝 요약: 상황별 추천 가이드
- 데이터베이스가 규칙을 엄격하게 강제하기를 원하지만, 장기적인 ‘접근 배타적 잠금(Access Exclusive Lock)’ 없이 초대용량 테이블에 제약 조건을 추가해야 할 때는
NOT VALID를 사용하세요. - 애플리케이션 계층에서 이미 데이터 무결성을 완벽하게 관리하고 있고, 데이터베이스의 검사 및 트리거 실행을 생략하여 극적인 성능 이점을 얻고 싶을 때는
NOT ENFORCED를 사용하세요. 현재 쿼리 옵티마이저(Query Optimizer)가SELECT쿼리 속도를 높이는 데 이 제약 조건을 직접 사용하지는 않지만, 이를 정의해 두는 것만으로도 필수적인 스키마 문서화(Schema Documentation) 역할을 수행하며, 향후 플래너 지원이 추가될 때를 대비한 훌륭한 미래 보장형(Future-proof) 설계가 됩니다.
메일: salesinquiry@enterprisedb.com

