이메일 열에 UNIQUE를 걸었다. 같은 이메일을 막으려던 목적대로라면, 값이 없는 회원도 한 명만 들어가야 할까? SQLite에서 NULL인 회원 두 명을 넣었더니 둘 다 저장됐다. 제약조건이 잠깐 쉬는 건 아니다. 이메일 값의 중복과 이메일 값이 없음은 서로 다른 상태다.
같은 테이블에서 허용되는 입력과 거부되는 입력을 차례로 확인해 보자.
UNIQUE인데 NULL 두 건이 왜 저장될까?
다음 테이블의 id는 회원 행의 기본키이고, email은 입력할 수도 있고 비워 둘 수도 있는 고유값이다. 여기서 NULL은 빈 문자열 ''이 아니라 값이 아직 없다는 표시다.
도식은 식별자인 id와 선택 입력인 email이 서로 다른 제약을 가진다는 점을 보여 준다. 테이블을 만든 뒤 NULL과 중복 이메일을 각각 넣어 결과를 구분해 보자.
CREATE TABLE accounts (
id INTEGER PRIMARY KEY,
email TEXT UNIQUE
);
INSERT INTO accounts (id, email) VALUES
(1, NULL),
(2, NULL),
(3, 'a@x.test');
SELECT id, email FROM accounts ORDER BY id;| id | |
|---|---|
| 1 | NULL |
| 2 | NULL |
| 3 | a@x.test |
SQLite의 UNIQUE(email)은 두 NULL을 서로 다른 값으로 취급해 이 입력을 허용한다. 그래서 UNIQUE만으로는 '이메일이 꼭 있어야 한다'는 요구사항을 표현하지 못한다. 이 설명은 SQLite의 동작이다. 다른 DBMS까지 NULL 처리 방식이 같다고 단정하지 말자.
같은 이메일과 빈 문자열은 왜 거부될까?
이어서 같은 이메일을 한 번 더 넣으면 오류가 난다.
INSERT INTO accounts (id, email)
VALUES (4, 'a@x.test');
-- UNIQUE constraint failed: accounts.emailNULL과 달리 'a@x.test'는 비교할 수 있는 실제 문자열이다. 3번 행과 4번 행의 이메일이 같으므로 4번 행 전체가 저장되지 않는다. 빈 문자열도 '값 없음'이 아니라 길이가 0인 문자열 값이다. email=''인 행을 두 번 넣으면 두 번째는 같은 UNIQUE 오류를 낸다. 화면에서 둘 다 비어 보인다고 NULL과 ''을 섞어 저장하면, 운영 중에는 꽤 다른 결과가 돌아온다.
GROUP BY에는 NULL 2건이 보이는데 중복 위반일까?
중복 점검 쿼리로 이메일별 행 수를 세면 NULL이 한 그룹으로 묶여 2건으로 보인다.
SELECT email, COUNT(*) AS rows_in_group
FROM accounts
GROUP BY email;| rows_in_group | |
|---|---|
NULL | 2 |
a@x.test | 1 |
GROUP BY가 두 NULL을 한 그룹으로 출력하는 일과 UNIQUE가 NULL 입력을 허용하는 일은 규칙이 다르다. 이 결과만 보고 제약조건이 고장 났다고 판단하면 안 된다. 실제 이메일 값의 중복을 조사하려면 값이 없는 행을 제외하고 확인한다.
SELECT email, COUNT(*) AS n
FROM accounts
WHERE email IS NOT NULL
GROUP BY email
HAVING COUNT(*) > 1;
-- 이 예제에서는 결과 행 없음이메일이 필수라면 NOT NULL을 함께 써야 한다
가입에 이메일이 반드시 필요하다면 제약을 두 가지로 나눠 적는다. NOT NULL은 누락을, UNIQUE는 같은 실제 값의 재사용을 막는다.
CREATE TABLE required_accounts (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL UNIQUE
);
INSERT INTO required_accounts (id, email) VALUES (1, NULL);
-- NOT NULL constraint failed: required_accounts.email단, NOT NULL도 ''까지 막지는 않는다. 공백만 입력한 이메일이나 대소문자만 다른 이메일을 같은 것으로 볼지는 별도의 입력·정규화 규칙이다. UNIQUE 하나로 회원 가입 정책 전체를 해결하려 하지 말자.
PRIMARY KEY와 UNIQUE는 같은 제약일까?
이 예제의 id INTEGER PRIMARY KEY는 각 행을 식별하는 기본키다. 1번 id로 다른 행을 다시 넣으면 이메일이 NULL이어도 accounts.id 중복 오류가 난다. email UNIQUE는 이메일의 고유성을 따로 지킬 뿐 기본키를 바꾸지 않는다. 한 테이블에는 기본키 정의가 하나지만, 여러 열에 각각 UNIQUE를 둘 수 있다.
SQLite에는 일반 테이블의 일부 문자형 PRIMARY KEY가 NULL을 허용하는 예외도 있다. 여기서는 그 예외와 다른 INTEGER PRIMARY KEY를 사용했다. 다른 키 형태로 바꿀 때는 SQLite의 제약조건 설명을 확인하는 편이 안전하다.
핵심 요약
SQLite에서 UNIQUE(email)은 같은 이메일 문자열을 거부하지만 여러 NULL은 허용한다. NULL은 빈 문자열과 다르고, GROUP BY에서 한 그룹으로 보인다고 고유성 위반도 아니다. 이메일이 필수라면 NOT NULL UNIQUE를 함께 쓰고, 행 식별자는 별도의 PRIMARY KEY로 명확히 정하자.
작성자
기초 개념을 구현과 검증, 실제 운영 판단까지 연결해 기록합니다.

