PostgreSQL 18에서의 NOT NULL 변경 사항

글쓴이: Á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 시스템 카탈로그에 저장되기 때문입니다.

기존에는 단순히 컬럼 카탈로그 pg_attribute.attnotnull에만 저장했는데, 여기는 추가 속성을 담을 공간이 없습니다. 그래서 Postgres 18에서는 각 NOT NULL 제약 조건에 대해 pg_constraint 행을 추가하도록 바꿨습니다.

이게 말은 쉽지만 실제 구현은 쉽지 않았습니다.


구현 난제

  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 Karage가 시간을 들여 ALTER 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에서 진행되는 많은 과제에도 기여하고 있어 정말 기쁩니다.


협업과 앞으로의 발전

이번 프로젝트에는 EDB 엔지니어들이 적극적으로 참여했습니다. 특히 새로 합류한 Jian He는 단순히 테스트만 하던 단계에서 실제 C 코드까지 작성하며 PostgreSQL 글로벌 개발팀에 큰 기여를 했습니다.

또한 이번 개선은 끝이 아니라 시작입니다.

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

사용자에게 드리는 부탁

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

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

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


맺음말

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

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

메일: salesinquiry@enterprisedb.com