-
UNIQUE를 걸었는데 빈 값이 여러 개 들어가는 이유Data Analysis 2026. 10. 5. 00:21728x90반응형

단일 컬럼의 유일성을 비교한 예시입니다. 다른 제약은 만족한다고 가정합니다. 별칭 컬럼에
UNIQUE를 걸었는데 별칭 없는 회원이 여러 명 저장됩니다. PostgreSQL의 기본 규칙에서는 정상입니다. 같은 문자열의 중복은 막지만NULL두 개를 같은 값으로 비교하지 않기 때문입니다. NULL을 여러 개 허용할지 하나로 제한할지 업무 의미부터 정하고 PostgreSQL의 UNIQUE NULL 처리 방식을 명시하세요.선택 입력인 별칭이라면 아직 정하지 않은 사람이 여럿이어도 괜찮습니다. 반대로 “그룹마다 이름 없는 기본 설정은 하나만 둔다”는 규칙이라면 NULL의 중복까지 막아야 합니다. 필수 입력이 목적이라면
NOT NULL이 필요하고요. 이 세 요구를 같은 “빈 값 금지”로 부르면 다른 제약을 고르게 됩니다.아래 SQL은 PostgreSQL 18 문서에 따른 가상 설계 예시입니다. 2026년 9월 28일 문서를 확인했으며 실제 DB 실행 출력이나 운영 마이그레이션 결과는 아닙니다.
화면의 빈칸이 무엇으로 저장됐는지 확인합니다
빈 입력은 애플리케이션에서
NULL, 빈 문자열'', 공백이 든 문자열' '가운데 하나로 저장될 수 있습니다. 화면에서는 비슷해 보여도 DB의 비교 대상은 다릅니다.가상의 회원 테이블을 다음처럼 잡겠습니다. 여기서
NULL은 별칭을 아직 선택하지 않았다는 뜻입니다.CREATE TABLE sample_members ( id integer PRIMARY KEY, alias text UNIQUE );다음 두 입력은 별칭의 유일성 규칙과 충돌하지 않습니다.
INSERT INTO sample_members (id, alias) VALUES (1, NULL), (2, NULL);NULL두 개를 하나의 같은 별칭으로 세지 않는 규칙입니다. 실제 별칭인'river'는 다르게 처리합니다.INSERT INTO sample_members (id, alias) VALUES (3, 'river'); -- 같은 별칭의 중복 입력 INSERT INTO sample_members (id, alias) VALUES (4, 'river');두 번째 문장이 실패하는 것은
river라는 값이 이미 있기 때문입니다. 같은 방식으로 빈 문자열도 저장된 문자열 값이므로 두 번 넣으면 충돌합니다.UNIQUE가 모든 화면의 빈칸을 같은 규칙으로 다루는 것은 아닙니다. PostgreSQL의 유일성 제약설명용 문장을 직접 시험한다면 서로 분리해서 실행하세요. 오류가 예상되는 문장을 하나의 큰 INSERT나 트랜잭션에 섞으면, 실패 범위 때문에 다른 행까지 저장되지 않아 NULL 규칙을 잘못 읽을 수 있습니다.
NULL을 하나만 허용하려면 비교 규칙을 바꿉니다
UNIQUE NULLS NOT DISTINCT는 유일성을 판단할 때 NULL도 서로 같은 값처럼 취급합니다.CREATE TABLE sample_single_alias ( id integer PRIMARY KEY, alias text UNIQUE NULLS NOT DISTINCT );첫 NULL은 들어갈 수 있지만 같은 컬럼에 두 번째 NULL을 넣으면 충돌합니다. NULL을 전부 금지하는 설정이 아니라 중복을 비교하는 방식을 바꾼 것입니다.
표는
alias한 컬럼에 대한 규칙입니다.NOT NULL을 써도 빈 문자열이 자동으로 금지되지는 않습니다. 빈 문자열과 공백도 막아야 한다면 애플리케이션의 정규화와 별도의 검사 조건을 정해야 합니다. NOT NULL 제약선택 입력인 회원 별칭에 두 번째 규칙을 적용하면 별칭을 정하지 않은 두 번째 회원부터 가입이 막힐 수 있습니다. “더 엄격한 UNIQUE”라는 설명보다, 실제로 어떤 입력 두 개가 충돌하는지 적어 보는 편이 정확합니다.
복합키에서는 같은 그룹 안에서 비교합니다
NULL을 하나만 허용하는 규칙이 쓸모 있는 예로 테넌트별 설정을 생각할 수 있습니다. 이름 있는 설정은 여러 개, 이름 없는 기본 설정은 테넌트마다 하나만 허용한다고 가정하겠습니다.
CREATE TABLE sample_settings ( tenant_id integer NOT NULL, name text, value text, UNIQUE NULLS NOT DISTINCT (tenant_id, name) );(tenant_id, name)전체가 비교 대상입니다. 아래 세 입력을 각각 생각해 보면 범위가 보입니다.(10, NULL)이 이미 있을 때 다른(10, NULL)은 충돌합니다.(10, NULL)이 있어도(20, NULL)은 다른 테넌트이므로 허용됩니다.(10, 'theme')과(10, 'language')는 이름이 달라 함께 저장할 수 있습니다.
NULL이 표 전체에서 딱 하나라는 뜻은 아닙니다. 같은 테넌트에서 같은 이름 조합을 중복시키지 않는 규칙입니다. 기본
UNIQUE (tenant_id, name)을 쓰면name이 NULL인 두 행은 같은 테넌트 안에서도 허용될 수 있으므로, 여기서는 요구와 달라집니다. 복합 유일성 제약의 비교tenant_id에NOT NULL을 붙인 것도 의도한 선택입니다. 어떤 테넌트의 설정인지 없는 행을 허용할 이유가 없기 때문입니다. 복합키를 설계할 때는 각 컬럼의 NULL 허용 여부와 조합의 유일성을 각각 정해야 합니다.기존 테이블이라면 규칙을 바꾸기 전에 겹치는 행을 찾습니다
새 테이블에서는 규칙을 먼저 정하면 되지만, 기존 테이블에는 새 규칙과 충돌하는 자료가 이미 있을 수 있습니다. 위 설정 테이블로 이전할 계획이라면 다음처럼 조합별 개수를 읽어볼 수 있습니다.
SELECT tenant_id, name, count(*) AS row_count FROM sample_settings GROUP BY tenant_id, name HAVING count(*) > 1;이 결과에서 이름이 NULL인 그룹도 확인 대상입니다. 예를 들어 테넌트 10의 NULL 행이 둘이면 새 제약을 넣기 전에 어느 설정을 남길지 결정해야 합니다. SQL이 오래된 행과 최신 행의 업무상 의미를 알아서 정해 주지는 않습니다.
여기서 곧바로 중복 행을 지우는 명령까지 이어 붙이지 않는 이유가 있습니다. 같은 이름이어도 값이 서로 다를 수 있고, 다른 테이블이 특정 행을 참조할 수도 있기 때문입니다. 조회 결과를 기준으로 보존 규칙과 참조 관계를 확인한 뒤 변경 계획을 잡습니다.
정규화도 먼저 합의해야 합니다. 별칭 앞뒤 공백을 제거할 계획이라면 지금은 서로 다른 두 문자열이 정리 후 같은 값이 될 수 있습니다. 제약 추가와 입력 정규화를 별개로 진행하면 어느 쪽에서 충돌이 생겼는지 헷갈리기 쉽습니다.
다른 DB로 옮길 때도 기본값을 확인합니다
PostgreSQL 문서는 UNIQUE의 기본 NULL 처리가 다른 DB 구현과 다를 수 있다고 명시합니다. 따라서 ORM에
unique=True처럼 짧게 선언했다고 모든 DB에서 같은 동작을 기대하면 안 됩니다.실제 변경을 검토할 때는 NULL 두 개, 같은 실제 값 두 개, 빈 문자열 두 개를 입력 사례로 적어 두세요. 복합키라면 같은 테넌트와 다른 테넌트도 나눕니다. 그 사례의 허용·거부가 서비스 요구와 맞는지 확인한 다음 제약 문법을 선택하면 됩니다.
728x90반응형'Data Analysis' 카테고리의 다른 글
JSON에 같은 키가 두 번 있으면 어느 값이 남을까 (0) 2026.10.06 제외 목록에 NULL 하나 넣었더니 SQL 결과가 사라지는 이유 (1) 2026.10.05 SQLite에 외래 키를 썼는데 잘못된 참조가 들어가는 이유 (0) 2026.10.05 timestamptz에 저장한 시간대 이름은 어디로 갔을까 (0) 2026.10.05 DuckDB-Wasm이란? CSV를 서버에 올리지 않고 분석하는 구조 (0) 2026.09.11