Back to blog

PostgreSQL 18에서의 NOT NULL 변경 사항

August 10, 2026

글쓴이: Álvaro Herrera
발행일: 2025년 9월 23일

PostgreSQL 18에서는 오랫동안 기다려온 기능 중 하나인 NOT NULL 제약 조건 관리 개선이 드디어 구현되었습니다. 이 변화는 단순히 제약 조건에 이름을 붙이는 수준을 넘어, 대규모 운영 환경에서도 더 안전하고 유연한 데이터베이스 관리가 가능하도록 만든 중요한 진전입니다. 이번 글에서는 PostgreSQL 커뮤니티와 EDB 엔지니어들이 함께 기여한 이 개선 사항을 자세히 살펴보겠습니다.


NOT NULL 제약 조건에 이름 부여

PostgreSQL 18부터는 NOT NULL 제약 조건에도 이름이 부여됩니다. 이제 psql에서 \d+ 명령을 사용하면 각 제약 조건의 이름을 확인할 수 있습니다.

=# \d+ aircraft
                                       Table "postgres_air.aircraft"
  Column  │  Type   │ Collation │ Nullable │ Default │ Storage  │ 
──────────┼─────────┼───────────┼──────────┼─────────┼──────────┼─
 model    │ text    │           │          │         │ extended │ 
 range    │ numeric │           │ not null │         │ main     │ 
 class    │ integer │           │ not null │         │ plain    │ 
 velocity │ numeric │           │ not null │         │ main     │ 
 code     │ text    │           │ not null │         │ extended │ 
Indexes:
    "aircraft_pkey" PRIMARY KEY, btree (code)
Referenced by:
    TABLE "flight" CONSTRAINT "aircraft_code_fk" FOREIGN KEY (airc...
Not-null constraints:
    "aircraft_range_not_null" NOT NULL "range"
    "aircraft_class_not_null" NOT NULL "class"
    "aircraft_velocity_not_null" NOT NULL "velocity"
    "aircraft_code_not_null" NOT NULL "code"
Access method: heap

이는 단순한 변화처럼 보이지만, 앞으로 제약 조건을 삭제(drop)하는 것뿐 아니라 NOT VALID 옵션으로 추가한 뒤 나중에 검증(validate)하거나, 상속/비상속(INHERIT/NO INHERIT)과 같은 세밀한 관리가 가능해진다는 점에서 큰 의미가 있습니다.

 


pg_constraintNOT NULL

그렇다면 제약 조건 이름과 NOT VALID 속성이 어떤 관계가 있는지 의문이 생길 수 있습니다. 모든 제약 조건의 유효성 플래그(validity flag)는 이름과 같은 다른 속성과 함께 pg_constraint 시스템 카탈로그에 저장되기 때문입니다.

기존에는 NOT NULL 여부를 컬럼 카탈로그의 pg_attribute.attnotnull에만 저장했지만, 여기에는 추가 속성을 담을 공간이 없습니다. 그래서 PostgreSQL 18에서는 각 NOT NULL 제약 조건에 대해 pg_constraint 행을 추가하도록 변경했습니다.

개념은 간단해 보이지만 실제 구현 과정에서는 여러 과제를 해결해야 했습니다.


NOT NULL 제약 조건 구현 시 해결해야 했던 과제

  1. pg_upgrade 호환성
    • 기존 데이터베이스에서 제약 조건이 올바르게 업그레이드되도록 보장해야 했습니다.
    • 특히 파티셔닝(버전 10 이후 도입)이나 상속(inheritance) 같은 구조에서 다양한 경우를 처리해야 했습니다.
    • 파티셔닝은 부모 테이블의 NOT NULL 제약이 자식 테이블에도 반드시 존재하도록 강제되므로 비교적 간단했습니다.
    • 그러나 상속은 더 복잡했습니다. 부모에는 있는데 자식에는 없는 경우가 있었고, pg_upgrade에서 이런 경우 오류를 발생시키도록 했습니다. 사실상 이전부터 버그로 여겨지던 부분이었으나, 이번 작업을 통해 제대로 해결할 수 있었습니다.
       
  2. 성능 및 호환성
    • 실행 시 성능 저하 없이 동작해야 했습니다.
    • 또한 pgAdmin 같은 서드파티 소프트웨어가 attnotnull 변경으로 영향을 받지 않도록 호환성을 유지해야 했습니다.
    • 결국 내부 코드에서는 attnotnull에 의존하지 않도록 변경하면서도, 외부 소프트웨어는 기존과 같이 정상적으로 작동하도록 처리했습니다.

NOT VALID

이제 앞에서 언급한 기능 중 하나인 NOT NULL 제약 조건을 NOT VALID 옵션과 함께 추가하는 기능을 살펴보겠습니다. 이 부분의 코드는 오랫동안 EDB의 EPAS(Postgres 포크)를 유지해 온 Rushabh Lathia가 기여했습니다.

서드파티 애플리케이션의 경우 pg_attribute.attnotnull은 주로 특정 컬럼에 NULL 값을 넣을 수 있는지 여부를 표시하는 용도로 사용됩니다. 따라서 사용자가 NOT VALID 제약 조건을 추가하면 실제 데이터에 NULL 값이 존재할 수 있더라도 해당 컬럼은 not-nullable로 표시해 주어야 합니다.

이는 매우 중요한 문제입니다. 쿼리 최적화기(optimizer)가 컬럼의 nullability에 대해 다르게 판단할 수 있기 때문입니다. 이를 잘못 처리하면 잘못된 결과를 내는 쿼리 플랜이 생성될 수도 있습니다.


NOT VALID 제약 조건 예시

=# CREATE TABLE person (id bigserial PRIMARY KEY, lastname text, firstname text); 
CREATE TABLE

-- 가장 간단하게 not-valid 제약을 추가하는 방법
=# ALTER TABLE person ADD NOT NULL lastname NOT VALID;
ALTER TABLE

-- 원하는 이름을 지정할 수도 있다
=# ALTER TABLE person ADD CONSTRAINT "AnotherOne" NOT NULL firstname NOT VALID;
ALTER TABLE

-- 이름을 사용해 제약 조건을 검증할 수 있다. 이 과정은 테이블을 스캔하지만
-- DML(I/U/D) 작업을 막지는 않는다.
=# ALTER TABLE person VALIDATE CONSTRAINT "AnotherOne";
ALTER TABLE

-- 최종적으로 테이블은 대략 이렇게 보인다
=# \d+ person
                                                     Table "postgres_air.person"
  Column   │  Type  │ Collation │ Nullable │              Default               │
───────────┼────────┼───────────┼──────────┼────────────────────────────────────┼─
 id        │ bigint │           │ not null │ nextval('person_id_seq'::regclass) │ 
 lastname  │ text   │           │ not null │                                    │
 firstname │ text   │           │ not null │                                    │
Indexes:
    "person_pkey" PRIMARY KEY, btree (id)
Not-null constraints:
    "person_id_not_null" NOT NULL "id"
    "person_lastname_not_null" NOT NULL "lastname" NOT VALID
    "AnotherOne" NOT NULL "firstname"
Access method: heap

이 구현에서는 David Rowley의 커밋 “Introduce CompactAttribute array in TupleDesc, take 2를 활용할 수 있었습니다. 이 커밋은 최적화기가 소위 튜플 디스크립터(tuple descriptor)를 처리하는 방식을 변경했습니다.

이전까지 튜플 디스크립터는 단순히 pg_attribute 행 전체의 복사본이었지만, Rowley는 CompactAttribute 구조체를 도입해 실행기가 저장된 튜플에서 필요한 정보를 훨씬 더 빠르게 추출할 수 있도록 했습니다.

이 덕분에 CompactAttribute를 활용해 다음 세 가지 경우를 명확히 구분할 수 있었습니다.

  1. 제약 조건이 전혀 없는 경우
  2. 무효(NOT VALID)한 제약 조건이 있는 경우
  3. 유효한 제약 조건이 있는 경우

그와 동시에 서드파티 애플리케이션들이 여전히 attnotnull을 통해 단순히 제약 조건 존재 여부를 확인할 수 있도록 유지했습니다. 또한 이 방식 덕분에 pg_constraint를 한 번만 효율적으로 스캔해 올바른 nullability 정보를 수집할 수 있었습니다.


주의할 점

CREATE TABLE에서 제약 조건을 정의할 때 NOT VALID을 명시하더라도 실제로는 유효(validated) 상태로 생성됩니다. 테이블이 비어 있다는 사실을 이미 알고 있기 때문에 제약 조건을 위반하는 행이 존재할 수 없기 때문입니다. 따라서 이를 NOT VALID 상태로 두는 것은 의미가 없습니다.

즉, NOT VALID 제약 조건은 ALTER TABLE을 통해서만 만들 수 있습니다.


INHERITNO INHERIT

또 다른 변화는 제약 조건의 자식 테이블 상속 여부와 관련되어 있습니다. 대부분의 제약 조건은 기본적으로 상속 가능하기 때문에 자식 테이블에도 그대로 적용됩니다. 하지만 특정 제약 조건에 NO INHERIT 플래그를 지정하면 자식 테이블에 해당 제약이 적용되지 않을 수 있습니다. 이 정보는 pg_constraint.connoinherit에 저장됩니다.

이 방식만으로도 정상적으로 동작하지만, 이후 사용자가 설정을 변경해야 하는 경우가 있습니다. 특히 나중에 기본 키(primary key)를 추가하려는 경우 중요합니다. NO INHERIT 제약이 있다면 기본 키 생성은 실패합니다. 기본 키에는 상속 가능한 제약이 필요하기 때문입니다. 기존에는 제약을 삭제하고 다시 추가하는 것이 유일한 해결 방법이었습니다.

이를 개선하기 위해 Suraj KarageALTER TABLE ... ALTER CONSTRAINT ... INHERIT 명령을 추가했습니다. 이 명령은 NO INHERIT 제약을 상속 가능하도록 변경하며, 자식 테이블이 존재하는 경우에는 제약을 자식에게도 추가해 일관성을 보장합니다. 처음에는 SET INHERIT 구문으로 추가했으나, 이후 Amul Sul이 문법 관련 리팩터링을 진행하면서 SET 키워드를 제거했습니다.


NO INHERIT 제약 조건 예시


-- In our vegetables table, we add a not-null constraint that doesn't apply
-- to child tables.
=# create table vegetables (veggie text, 
   kcals_per_kg int constraint shortnm not null no inherit);
CREATE TABLE

-- ... so this table doesn't have it.
=# create table nightshades () inherits (vegetables); 
CREATE TABLE

-- But this one does, because we declare it separately. Note we don't
-- declare the column itself, just the constraint on it (and we can choose
-- a different name if we so desire.)
CREATE TABLE tubers (type text, 
  CONSTRAINT yep NOT NULL kcals_per_kg) inherits (vegetables);
CREATE TABLE

-- Now we change our minds and set the constraint as inheritable in the
-- parent table:
=# ALTER TABLE vegetables ALTER CONSTRAINT shortnm INHERIT;
ALTER TABLE

-- Let's look at the child tables definitions. Here the constraint has the
-- name we gave it, and it's marked as locally defined as well as coming from
-- inheritance.
=# \d+ tubers
                Table "postgres_air.tubers"
    Column    │  Type   │ Collation │ Nullable │ Default │ 
──────────────┼─────────┼───────────┼──────────┼─────────┼─
 veggie       │ text    │           │          │         │ 
 kcals_per_kg │ integer │           │ not null │         │ 
 type         │ text    │           │          │         │ 
Not-null constraints:
    "yep" NOT NULL "kcals_per_kg" (local, inherited)
Inherits: vegetables
Access method: heap

-- In the other table it's only inherited.
=# \d+ nightshades 
                Table "postgres_air.nightshades"
    Column    │  Type   │ Collation │ Nullable │ Default │
──────────────┼─────────┼───────────┼──────────┼─────────┼
 veggie       │ text    │           │          │         │
 kcals_per_kg │ integer │           │ not null │         │
Not-null constraints:
    "shortnm" NOT NULL "kcals_per_kg" (inherited)
Inherits: vegetables
Access method: heap


저는 또한 최근에 합류한 엔지니어 Jian He의 도움을 꼭 언급하고 싶습니다. 그의 작업은 설계대로 동작하지 않던 미묘한 코너 케이스들을 발견하는 데 큰 도움이 되었습니다. 특히 인상적인 점은 단순히 테스트하는 데 그치지 않고 필요한 C 코드 일부를 직접 작성하기 시작했다는 것입니다. Jian은 PostgreSQL 글로벌 개발팀에 훌륭한 보탬이 되었고, EDB에서 진행되는 여러 과제에도 기여하고 있습니다.


협업과 앞으로의 발전

이번 개선은 끝이 아니라 앞으로 이어질 PostgreSQL 제약 조건 개선의 기반이 될 수 있습니다.

  • SQL 표준에서 새로 추가된 NOT ENFORCED 제약 조건을 PostgreSQL에 적용하는 작업이 이미 제안되었습니다.
  • 더 나아가 DEFERRED NOT NULL 제약 조건까지 구현될 가능성도 있습니다.

사용자에게 드리는 부탁

여러분의 테스트가 PostgreSQL의 완성도를 높이는 데 큰 도움이 됩니다.

  • pg_upgrade 후 데이터베이스가 정상적으로 업그레이드되는지,
  • 업그레이드된 데이터베이스가 pg_dump로 문제없이 덤프되는지,
  • 새로운 제약 기능들이 기대한 대로 작동하는지,
  • 그리고 문서 변경 사항이 명확하게 전달되는지 확인해 주십시오.

이 기능들은 화려하게 드러나는 기능은 아니지만, 데이터 무결성을 보장하는 데 매우 중요한 역할을 합니다. PostgreSQL 18은 이러한 개선을 통해 안정성과 운영 효율성을 한 단계 더 높였습니다.


맺음말

저는 이번 작업을 끝까지 완수할 수 있었다는 점에 큰 만족을 느낍니다. PostgreSQL 18의 NOT NULL 제약 조건 개선은 단순한 기능 추가가 아니라, 엔터프라이즈 운영 환경에서 Postgres가 한층 더 강력하고 유연해졌음을 보여주는 사례입니다.

그리고 이제, 다음 과제를 향해 나아갈 시간입니다!

 

 

메일: salesinquiry@enterprisedb.com

 

Share this