본문 바로가기
빌드노트 검색

빌드노트

PostgreSQL에서 끝이 다른 긴 테이블 이름이 중복으로 충돌할 때

PostgreSQL에서 긴 테이블 이름의 끝만 바꾸면 길이 제한 밖의 차이가 사라져 중복으로 충돌할 수 있다. 실제 서버 제한과 카탈로그 이름을 조회하고, PostgreSQL 18.6의 NOTICE·SQLSTATE·짧은 이름 대조로 확인한다.

PostgreSQL에서 끝을 _a와 _b로 다르게 붙인 긴 테이블 이름 두 개를 만들었는데, 두 번째 CREATE TABLE이 이미 존재하는 이름이라며 실패할 수 있다. 서버가 이름의 뒷부분을 잘라 두 입력을 같은 식별자로 처리하기 때문이다. 오류가 난 서버의 max_identifier_length와 실제 저장된 이름을 확인하고, 제한 안에서 앞부분부터 구분되는 짧은 이름을 정한다.

PostgreSQL 18의 식별자 문서 (새 탭에서 열림)는 제한보다 긴 이름을 SQL에 쓸 수 있지만 실제로는 잘린다고 설명한다. 기본 제한은 63바이트다. 긴 이름이 처음 생성될 때 오류 대신 NOTICE와 함께 성공할 수 있어서, 두 번째 생성의 중복 오류에서야 문제를 알아차리기 쉽다.

입력한 이름 대신 저장된 이름을 본다

실패한 SQL을 실행한 DB에 접속해 아래 조회를 실행한다. 'length_lab'은 이 글의 실습 스키마이므로 실제 확인할 스키마 이름으로 바꾼다. 서버의 버전·인코딩·제한을 읽고, 그 스키마의 객체 이름과 길이를 함께 확인하는 읽기 전용 조회다.

SHOW server_version;
SHOW server_encoding;
SHOW max_identifier_length;

SELECT n.nspname AS schema_name, c.relname AS stored_name,
       octet_length(c.relname::text) AS stored_bytes, c.relkind
FROM pg_catalog.pg_class AS c
JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace
WHERE n.nspname = 'length_lab'
ORDER BY c.relname;

max_identifier_length는 바이트 단위다. 서버 설정 문서 (새 탭에서 열림)에 따르면 서버 빌드 때 정해지는 읽기 전용 값이므로, 기본값을 외우기보다 접속한 서버에서 읽는다. 아래 재현에 쓴 서버는 18.6, 인코딩은 UTF8, 제한은 63이었다.

두 번째 생성이 실패한 직후 카탈로그에는 다음 한 행이 있었다. psql의 비정렬 출력이며 r은 일반 테이블을 뜻한다.

schema_name|stored_name|stored_bytes|relkind
length_lab|monthly_orders_archive_for_regional_sales_reconciliation_export|63|r
(1 row)

stored_name에는 _a도 _b도 없다. pg_class.relname은 실제 객체 이름이고, relnamespace가 해당 스키마를 가리킨다. 인덱스·뷰 등도 이 카탈로그에 있으므로 relkind를 함께 읽어 충돌한 대상의 종류를 확인한다. pg_class의 이름과 종류 (새 탭에서 열림), pg_namespace의 스키마 이름 (새 탭에서 열림)

긴 원문을 조건에 넣어 조회됐더라도 이름이 온전히 저장됐다는 뜻은 아니다. relname은 name 타입이다. PostgreSQL 18.6에서 c.relname = 'monthly_orders_archive_for_regional_sales_reconciliation_export_a'는 참이었고, 끝을 _b로 바꿔도 참이었다. 타입을 지정하지 않은 문자열 상수가 name으로 해석되면서 같은 접두부로 잘렸기 때문이다. 반면 같은 원문을 c.relname::text와 비교하거나 상수에 ::text를 붙인 비교는 모두 거짓이었다. 따라서 조건의 일치 여부만 보지 말고, 위 조회가 반환한 stored_name을 직접 읽는다. 연산자 비교의 타입 선택 (새 탭에서 열림)

조회가 0행이라면 이 원인으로 확정하지 말고 접속 DB와 스키마, 생성이 커밋됐는지를 다시 확인한다. 명시적 트랜잭션에서 오류가 나 후속 조회도 거부된다면, 먼저 그 트랜잭션을 정리해야 한다.

끝의 _a와 _b가 사라졌다

따로 만든 PostgreSQL 서버에서 합성 테이블로 비교했다. 직접 재현하려면 스키마 생성 권한이 있는 실습용 DB와 psql이 필요하다. length_lab이 아직 없는 DB에서, 자동 커밋을 켜고 BEGIN 없이 아래 입력을 한 문장씩 실행한다. 스키마 생성이나 첫 테이블 생성부터 실패하면 그 원인을 확인한 뒤 진행한다. \echo는 psql 전용 명령이다.

다음 두 테이블 이름은 각각 65바이트인 ASCII 문자열이며 마지막 글자만 다르다. 앞의 length_lab.은 스키마 지정이고, 테이블 이름 자체의 길이와 구분된다. 첫 설정은 이 연결에서 NOTICE가 보이도록 한다.

SET client_min_messages = notice;
CREATE SCHEMA length_lab;
CREATE TABLE length_lab.monthly_orders_archive_for_regional_sales_reconciliation_export_a (id integer);
\echo first=:SQLSTATE
CREATE TABLE length_lab.monthly_orders_archive_for_regional_sales_reconciliation_export_b (id integer);
\echo second=:SQLSTATE

서버와 psql 18.6에서 관찰한 NOTICE·완료 태그·SQLSTATE를 SQL 순서에 맞춰 함께 표시했다. psql이 붙인 입력 위치 접두부는 생략했다.

NOTICE:  identifier "monthly_orders_archive_for_regional_sales_reconciliation_export_a" will be truncated to "monthly_orders_archive_for_regional_sales_reconciliation_export"
CREATE TABLE
first=00000
NOTICE:  identifier "monthly_orders_archive_for_regional_sales_reconciliation_export_b" will be truncated to "monthly_orders_archive_for_regional_sales_reconciliation_export"
ERROR:  relation "monthly_orders_archive_for_regional_sales_reconciliation_export" already exists
second=42P07

첫 번째 명령도 원래 이름을 온전히 저장한 것은 아니었다. 두 NOTICE가 가리키는 잘린 이름이 같고, 뒤의 카탈로그 조회에서도 그 이름 하나만 남았다. 접미부 _a와 _b가 제한 밖에 있어서 두 번째 입력을 별도 이름으로 구별하지 못한 것이다.

psql의 SQLSTATE는 직전 SQL의 결과이며 성공이면 00000이다. 다른 SQL을 실행하기 전에 읽어야 한다. 42P07의 공식 조건명은 duplicate_table이다. 이 코드만으로 길이 문제가 확정되지는 않는다. 이번에는 잘림 NOTICE와 카탈로그의 이름까지 일치했기 때문에 원인을 좁힐 수 있었다. psql의 SQLSTATE (새 탭에서 열림), 오류 코드 (새 탭에서 열림)

따옴표보다 이름을 짧게 바꾼다

실패한 두 번째 이름을 큰따옴표로 감싼 length_lab."monthly_orders_archive_for_regional_sales_reconciliation_export_b"로 다시 요청해도 같은 NOTICE와 42P07이 나왔다. 별도의 빈 스키마에서 두 이름을 처음부터 모두 인용해 만든 대조에서도 첫 생성은 성공하고 두 번째는 같은 코드로 실패했다. 큰따옴표로 감싼 식별자에도 길이 제한은 적용된다. 대소문자를 보존하는 규칙으로 이 길이 충돌을 해결할 수는 없다. 인용 식별자의 제한 (새 탭에서 열림)

같은 length_lab에 두 대상을 짧게 구분하는 이름으로 생성하면 결과가 달라진다.

CREATE TABLE length_lab.orders_a_export (id integer);
CREATE TABLE length_lab.orders_b_export (id integer);

두 명령 모두 CREATE TABLE, SQLSTATE 00000으로 끝났다. 앞의 카탈로그 조회를 다시 실행하니 orders_a_export와 orders_b_export가 각각 15바이트로 저장돼 있었다. 기존의 잘린 테이블까지 합쳐 총 세 행이다. 이름 뒤에 구분자를 더 붙이는 대신, 구별할 부분이 서버 제한 안에 들어오도록 이름 전체를 줄인 결과다. 새 DDL과 그 테이블을 참조하는 SQL·매핑에서도 정한 이름을 일치시킨다.

비ASCII 이름에서는 문자 수로 제한을 판단하지 않는다. 같은 UTF8 서버에서 가를 22번 반복한 문자열은 22문자지만 66바이트였고, 그 이름으로 만든 테이블에는 21문자·63바이트가 저장됐다. 자신의 후보는 octet_length('사용할이름')으로 바이트 수를 확인한다. char_length()는 문자 수를 세므로 용도가 다르다. 모든 한글 이름이 일정한 문자 수까지 허용된다고 일반화하지 않는다. 문자 길이와 바이트 길이 함수 (새 탭에서 열림)

실습을 마치면 이 예제에서 만든 세 테이블만 지정해 정리할 수 있다. 아래는 방금의 실습 스키마에만 적용하며, 스키마에 다른 객체가 있으면 마지막 명령은 실패한다.

DROP TABLE length_lab.orders_a_export,
           length_lab.orders_b_export,
           length_lab.monthly_orders_archive_for_regional_sales_reconciliation_export;
DROP SCHEMA length_lab;

직접 확인한 범위는 PostgreSQL 18.6의 일반 테이블 생성, 인용 여부, ASCII와 한 가지 UTF8 입력이다. 기존 테이블의 이름을 바꾸거나 서버 빌드의 제한을 변경하지 않았다. ORM·마이그레이션 도구가 이름을 미리 줄이거나 해시를 붙이는 경우는 검증하지 않았으므로, 그런 도구를 쓴다면 선언한 이름과 함께 실제로 전송한 DDL도 대조한다.

← 목록으로 돌아가기