빌드노트
LIKE로 복제한 PostgreSQL 테이블이 원본 시퀀스를 같이 쓴다
LIKE INCLUDING DEFAULTS로 serial 열을 복사하면 nextval 기본값이 원본 시퀀스를 계속 가리킨다. PostgreSQL 18.6에서 기본값·카탈로그·교차 INSERT를 비교하고, 복사본에 새 시퀀스를 연결하는 방법과 identity 복사의 독립 생성기를 확인한다.
PostgreSQL에서 serial 열을 CREATE TABLE ... (LIKE ... INCLUDING DEFAULTS)로 복사하면, 원본과 복사본의 ID가 같은 시퀀스에서 증가할 수 있다. 복사본만 1부터 시작할 것으로 기대했다면 복사된 기본값이 어느 시퀀스를 가리키는지 먼저 확인해야 한다. INCLUDING DEFAULTS는 nextval(...) 표현식을 복사하므로, 그 안의 원본 시퀀스 참조도 남는다.
serial은 독립된 자료형 이름으로 저장되는 기능이 아니다. PostgreSQL 18의 serial 설명 (새 탭에서 열림)에 따르면 정수 열, nextval 기본값, 열에 소유 관계로 연결된 시퀀스를 만드는 축약 문법이다. 정수 열과 기본값을 복사했다고 이 시퀀스 생성 절차까지 다시 실행되는 것은 아니다.
기본값에 적힌 시퀀스부터 확인한다
이미 복사한 테이블이 있다면 두 id 열의 기본값을 나란히 조회한다. 아래의 like_lab, orders, orders_copy를 자신의 스키마와 원본·복사본 이름으로 바꾼다. 해당 테이블의 열 정보가 보이는 계정으로 실행한다. 출력은 뒤의 격리 실험에서 만든 테이블을 조회한 결과다.
SELECT table_name, column_default, is_identity
FROM information_schema.columns
WHERE table_schema = 'like_lab'
AND table_name IN ('orders', 'orders_copy')
AND column_name = 'id'
ORDER BY table_name;
table_name | column_default | is_identity
orders | nextval('like_lab.orders_id_seq'::regclass) | NO
orders_copy | nextval('like_lab.orders_id_seq'::regclass) | NO
두 행 모두 like_lab.orders_id_seq를 가리킨다. 복사본에 INSERT해도 원본의 번호 발급기를 호출하는 구성이다. 실험에서는 pg_attrdef의 기본값과 pg_depend의 참조를 함께 읽어, 두 기본값이 같은 시퀀스 OID를 참조하는 것도 확인했다.
조회가 0행이면 스키마·테이블·열 이름과 조회 권한을 먼저 확인한다. 두 기본값이 다르거나 is_identity가 YES라면 이 예제와 생성 경로가 다르다. 그런 경우에는 실제 기본값이나 identity에 연결된 시퀀스를 따라가야 한다. ID가 띄엄띄엄 나온다는 증상만으로 공유 시퀀스를 단정할 수는 없다.
여기에는 조회 함수의 함정도 있다. 같은 실험에서 pg_get_serial_sequence('like_lab.orders', 'id')는 원본 시퀀스 이름을 반환했지만, 복사본에 같은 함수를 호출하면 NULL이었다. 이 NULL은 복사본의 기본값에 시퀀스 호출이 없다는 뜻이 아니다. 함수 문서 (새 탭에서 열림)는 열과 연결된 시퀀스를 찾는 기능을 설명하며, serial의 경우 OWNED BY 관계와 관련된다. 기본값의 참조와 소유 관계를 구분해서 읽어야 한다.
번갈아 INSERT하자 원본은 1·3, 복사본은 2·4를 받았다
다음은 실제 장애 기록이 아니라 PostgreSQL 18.6의 새 전용 DB에서 실행한 재현이다. CREATE SCHEMA와 CREATE TABLE 권한이 있는 빈 실습 DB에서 실행하며, like_lab이라는 스키마가 없어야 한다. 시작값 1·증가폭 1·캐시 1인 새 시퀀스와 한 연결을 사용했다. 아래 입력으로 원본과 복사본이 같은 번호 흐름을 나눠 쓰는지 확인할 수 있다.
CREATE SCHEMA like_lab;
CREATE TABLE like_lab.orders (id serial, note text);
CREATE TABLE like_lab.orders_copy
(LIKE like_lab.orders INCLUDING DEFAULTS);
INSERT INTO like_lab.orders(note) VALUES ('source-1') RETURNING id;
INSERT INTO like_lab.orders_copy(note) VALUES ('copy-1') RETURNING id;
INSERT INTO like_lab.orders(note) VALUES ('source-2') RETURNING id;
INSERT INTO like_lab.orders_copy(note) VALUES ('copy-2') RETURNING id;
각 RETURNING id는 실행 순서대로 1, 2, 3, 4였다. 원본에는 {1,3}, 복사본에는 {2,4}가 남았다. 앞의 기본값 조회를 이 상태에서 실행하면 두 열 모두 원본 시퀀스를 가리킨다. CREATE TABLE의 LIKE 옵션 설명 (새 탭에서 열림)도 nextval 같은 함수를 포함한 기본값을 복사하면 테이블 사이에 기능적 연결이 생길 수 있다고 명시한다.
여러 테이블이 공통 번호를 받는 설계라면 공유가 의도에 맞을 수 있다. 테이블마다 독립적으로 발급하려는 설계라면 복사본의 생성 경로를 바꿔야 한다. INCLUDING ALL로 옵션을 늘려도 이 문제는 해결되지 않는다. 이 옵션에는 기본값 복사도 포함된다. 별도로 만든 serial 복사본에서도 원본 시퀀스 참조가 그대로 남았다.
복사본만 새 시퀀스에 연결한다
현재의 serial 구성을 유지하면서 분리하려면 새 시퀀스를 만들고 복사본의 기본값을 그쪽으로 바꿀 수 있다. 아래 수정은 방금 만든 복사본에 ID 2와 4만 있고 다른 쓰기나 번호 발급이 없는 실험에 적용한 것이다. 새 시퀀스의 시작값 5는 이 데이터에 맞춘 값이다.
BEGIN;
CREATE SEQUENCE like_lab.orders_copy_id_seq AS integer START WITH 5
OWNED BY like_lab.orders_copy.id;
ALTER TABLE like_lab.orders_copy
ALTER COLUMN id SET DEFAULT nextval('like_lab.orders_copy_id_seq'::regclass);
COMMIT;
INSERT INTO like_lab.orders_copy(note) VALUES ('copy-3') RETURNING id;
INSERT INTO like_lab.orders(note) VALUES ('source-3') RETURNING id;
INSERT INTO like_lab.orders_copy(note) VALUES ('copy-4') RETURNING id;
INSERT INTO like_lab.orders(note) VALUES ('source-4') RETURNING id;
이번 반환값은 순서대로 5, 5, 6, 6이었다. 복사본의 INSERT가 원본 시퀀스를 소비하지 않게 됐다. 앞의 기본값 조회를 다시 실행하면 복사본만 nextval('like_lab.orders_copy_id_seq'::regclass)로 바뀐다. 독립 발급이므로 테이블 사이에 같은 ID 숫자가 생기는 것은 가능하다.
이 작업에는 테이블 변경과 시퀀스 생성 권한이 필요하다. CREATE SEQUENCE 문서 (새 탭에서 열림)의 OWNED BY는 시퀀스를 해당 열의 수명에 연결하는 설정이며, 같은 소유자와 스키마를 요구한다. 실제 INSERT 계정이 별도라면 nextval에 필요한 USAGE 또는 UPDATE 권한 (새 탭에서 열림)도 확인해야 한다. 이 실험에서는 생성·변경·INSERT를 모두 같은 계정으로 수행했다.
데이터가 있는 실제 복사본의 시작값을 5로 고정해서는 안 된다. 기존 ID와 증가 규칙을 확인하고, 복사본에 쓰거나 번호를 미리 확보하는 경로를 통제한 뒤 새 시퀀스의 시작값을 정해야 한다. 원본 시퀀스의 위치를 초기화해도 두 기본값의 연결은 분리되지 않는다. 위 SQL은 원본 시퀀스를 재설정하지 않았으며, 동시 쓰기가 있는 운영 전환 절차는 검증하지 않았다.
identity를 복사하면 새 시퀀스가 만들어진다
새 테이블 설계를 선택할 수 있다면 identity 속성을 복사하는 방법도 있다. 원본 열 자체가 identity여야 한다. 아래 입력은 별도의 새 테이블 두 개로 identity 복사 계약과 연결 시퀀스를 확인한다.
CREATE TABLE like_lab.identity_orders (
id integer GENERATED BY DEFAULT AS IDENTITY,
note text
);
CREATE TABLE like_lab.identity_orders_copy
(LIKE like_lab.identity_orders INCLUDING IDENTITY);
SELECT pg_get_serial_sequence('like_lab.identity_orders', 'id') AS source_seq,
pg_get_serial_sequence('like_lab.identity_orders_copy', 'id') AS copy_seq;
INSERT INTO like_lab.identity_orders(note) VALUES ('source-1') RETURNING id;
INSERT INTO like_lab.identity_orders_copy(note) VALUES ('copy-1') RETURNING id;
INSERT INTO like_lab.identity_orders(note) VALUES ('source-2') RETURNING id;
INSERT INTO like_lab.identity_orders_copy(note) VALUES ('copy-2') RETURNING id;
시퀀스 조회 결과는 like_lab.identity_orders_id_seq와 like_lab.identity_orders_copy_id_seq였다. 카탈로그에서도 서로 다른 시퀀스 OID가 각 테이블의 id 열에 내부 의존 관계로 연결돼 있었다. 이어진 INSERT는 순서대로 1, 1, 2, 2를 반환했고 두 테이블에 각각 {1,2}가 남았다. INCLUDING IDENTITY 문서 (새 탭에서 열림)는 복사한 identity 열마다 원본과 별개인 새 시퀀스를 만든다고 설명한다.
반대로 serial 원본에 INCLUDING IDENTITY만 붙인 대조에서는 identity로 변환되지 않았다. id는 NOT NULL인 정수 열로 남았고 기본값은 NULL, is_identity는 NO였다. ID를 생략한 INSERT는 SQLSTATE 23502의 NOT NULL 위반으로 실패했다. 옵션 이름만 바꿔 기존 serial 복사본을 자동 전환하는 방법으로 사용할 수는 없다.
또한 identity 열 설명 (새 탭에서 열림)에서 BY DEFAULT는 명시적 ID 입력을 허용한다. serial과 identity 모두 그 자체로 PK·UNIQUE를 보장하지 않으므로 필요한 유일성 제약은 별도로 설계한다. 여기서는 생성기 연결만 비교하려고 PK와 데이터 복사를 넣지 않았다.
직접 확인한 범위는 PostgreSQL 18.6의 새 합성 테이블, 기본 시퀀스 설정, 순차 INSERT와 위 카탈로그 조회다. 소유 열·테이블을 삭제하면 시퀀스도 삭제되는 관계는 공식 문서에서 확인했지만, 이 실험에서 DROP이나 삭제 연쇄를 실행하지는 않았다. 다른 계정의 권한, ORM의 별도 ID 발급, 기존 데이터 이관과 동시 쓰기 전환도 검증하지 않았다. 독립 ID가 필요할 때는 복사본의 기본값 참조를 분리하거나, identity 원본과 INCLUDING IDENTITY를 함께 사용하는 계약을 선택한다.