Menu

SQLite UNIQUE 제약조건: 단일·복합 컬럼과 NULL 처리

SQLite의 UNIQUE 제약조건을 컬럼 단위, 테이블 단위, 복합 키, 그리고 NULL 처리까지 한 번에 정리하고, 제약 위반이 발생했을 때 해결법까지 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 — 편집하고 실행하면 결과를 바로 볼 수 있습니다.

UNIQUE 제약조건: "중복은 허용하지 않는다"

UNIQUE 제약조건은 특정 컬럼(혹은 여러 컬럼의 조합)에 들어가는 값이 행끼리 겹치면 안 된다고 SQLite에게 알려주는 장치입니다. "두 사용자가 같은 이메일을 쓸 수 없다"거나 "상품 코드는 단 한 번만 등장해야 한다" 같은 규칙을 표현할 때 바로 이 sqlite unique 제약조건을 사용합니다.

세 번째 INSERT는 UNIQUE constraint failed: users.email 오류와 함께 실패합니다. SQLite는 쓰기가 일어날 때마다 제약조건을 검사해서 중복이 생길 만한 입력은 막아버립니다. 처음 두 행은 정상적으로 저장되지만, 세 번째 행은 아예 들어가지 못합니다.

내부적으로 UNIQUE는 unique index로 구현됩니다. SQLite가 빠른 조회에 쓰는 바로 그 자료구조죠. 그래서 검사 비용이 거의 들지 않고, 해당 컬럼에는 인덱스가 자동으로 만들어집니다.

컬럼 레벨 vs 테이블 레벨 문법

UNIQUE는 두 가지 방식으로 작성할 수 있습니다. 컬럼 정의 옆에 인라인으로 붙이거나, 테이블 정의 마지막에 별도 절로 분리해서 쓰는 방법입니다:

단일 컬럼이라면 두 방식 모두 동일하게 동작하니, 가독성이 더 좋은 쪽으로 고르면 됩니다. 다만 두 개 이상의 컬럼에 걸쳐 유일성을 보장해야 하는 순간부터는 테이블 레벨 형태가 꼭 필요해집니다.

SQLite 복합 UNIQUE: 여러 컬럼을 묶어서 제약하기

어떤 컬럼은 그 자체로는 중복이 허용되어야 하지만, _컬럼들의 조합_은 유일해야 하는 경우가 있습니다. 예를 들어 한 사용자가 여러 강의를 수강할 수 있고, 한 강의에도 여러 사용자가 등록될 수 있죠. 하지만 동일한 (user_id, course_id) 쌍이 두 번 등장해서는 안 됩니다:

제약은 두 컬럼을 묶은 _쌍_에 걸려 있고, 어느 한 컬럼에 단독으로 걸린 게 아닙니다. 그래서 사용자 1번은 여러 강의를 들을 수 있고, 강의 100번에는 여러 사용자가 등록할 수 있죠. 다만 같은 조합은 딱 한 번만 가능합니다.

다대다 관계의 조인 테이블에서 가장 흔하게 쓰는 패턴이 바로 이것입니다.

sqlite UNIQUE vs PRIMARY KEY 차이

이름도 비슷하고 역할도 닮아 보이지만, 둘은 분명히 다릅니다.

  • 한 테이블에 PRIMARY KEY딱 하나만 둘 수 있습니다. 반면 UNIQUE 제약은 여러 개 걸 수 있죠.
  • PRIMARY KEY는 그 행의 정체성입니다. 외래 키가 참조하는 대상이자, rowid의 별칭이 되는 키죠.
  • UNIQUE는 그저 "이 값(또는 조합)은 중복되지 않는다"라는 뜻일 뿐입니다.
  • 일반 테이블에서 UNIQUE 컬럼은 NULL을 허용하지만, PRIMARY KEY는 그렇지 않습니다(역사적인 예외가 하나 있긴 하지만 여기서는 넘어갈게요).

자주 보이는 형태는 이렇습니다:

id는 데이터베이스의 다른 테이블들이 참조하는 식별자입니다. email이나 username이 unique한 건 정체성 때문이 아니라 애플리케이션 요구사항 때문이죠. 사용자가 이메일을 바꿔도 id는 그대로 유지됩니다 — 두 개를 분리하는 이유가 바로 이겁니다.

NULL 관련 함정

이 부분은 처음 접하는 분들이 거의 다 걸려 넘어집니다. SQLite에서 UNIQUE 컬럼은 NULL 값을 원하는 만큼 받아줍니다:

NULL 세 개는 괜찮지만, 'ada@example.com'이 두 개면 충돌입니다.

이유는 이렇습니다. SQL은 NULL을 "알 수 없는 값"으로 취급하기 때문에, 알 수 없는 값 두 개가 서로 같다고 단정할 수 없습니다. 그래서 유일성 검사에서도 중복으로 판정하지 못하죠. NULL을 최대 한 개로 제한하고 싶다면 NOT NULL UNIQUE로 묶는 게 가장 깔끔합니다. NULL은 허용하되 다른 컬럼 조합당 하나로 제한하고 싶다면 부분 인덱스(partial index)를 쓰면 되는데, 이건 뒤의 인덱스 장에서 다룹니다.

충돌 처리: ON CONFLICT 사용법

기본 동작은 UNIQUE 제약을 위반하는 순간 해당 문장을 중단하는 것입니다. 하지만 상황에 따라 기존 행을 교체하거나, 새 행을 무시하거나, 특정 컬럼만 갱신하고 싶을 때가 있죠. SQLite는 이런 동작을 지정할 수 있는 방법을 두 가지 제공합니다.

첫 번째는 제약조건 자체에 ON CONFLICT를 붙이는 방식입니다.

theme을 두 번째로 삽입하면 기존 행이 삭제되고 새 행이 그 자리를 차지합니다. 이 외에도 IGNORE(조용히 건너뛰기), ABORT(기본값), FAIL, ROLLBACK 옵션이 있습니다.

두 번째 방법은 문(statement) 단위로 처리하는 upsert 구문입니다. 특정 컬럼만 골라서 업데이트할 수 있어 보통 더 유연합니다:

첫 번째 INSERT는 행을 새로 만들고, 그다음 두 번은 UNIQUE 제약에 걸려 DO UPDATE 분기로 빠지면서 count가 1씩 증가합니다. 이게 바로 INSERT ... ON CONFLICT로 구현하는 upsert 패턴인데요, 자세한 사용법은 뒤에서 따로 다룹니다.

UNIQUE 제약조건 vs UNIQUE 인덱스

CREATE UNIQUE INDEXUNIQUE 제약조건과 사실상 같은 일을 합니다. 실제로 UNIQUE 제약조건을 걸면 내부적으로 unique 인덱스가 만들어지죠. 이름만 다르게 붙여놨을 뿐, 작동 방식은 거의 똑같다고 보시면 됩니다.

어떤 상황에서 무엇을 선택할지 정리하면 이렇습니다:

  • **제약조건(Constraint)**은 유일성이 테이블 정의의 일부일 때 사용합니다. 컬럼 바로 옆에 명시되어 있어 스키마만 봐도 의도가 드러납니다.
  • **유니크 인덱스(unique index)**는 부분 인덱스(WHERE 절)가 필요하거나, 인덱스 이름을 직접 지정하고 싶거나, 기존 테이블을 다시 만들지 않고 규칙을 추가하고 싶을 때 적합합니다. SQLite의 ALTER TABLE로는 제약조건을 추가할 수 없지만, 인덱스는 언제든 붙일 수 있거든요.

쓰기 동작 자체는 둘 다 똑같습니다. 결국 이 규칙을 스키마의 어느 위치에 두고 싶은가의 문제입니다.

기존 테이블에 UNIQUE 추가하기

SQLite의 ALTER TABLE은 의도적으로 기능이 제한되어 있어서 ALTER TABLE ... ADD CONSTRAINT 같은 문법은 지원하지 않습니다. 현실적인 방법은 두 가지입니다:

두 번째 방법은 — 진짜로 UNIQUE 절을 테이블 정의 안에 넣고 싶다면 — 테이블을 새로 만드는 우회 작업을 해야 한다. 제약조건이 들어간 새 테이블을 만들고, 데이터를 옮겨 담고, 기존 테이블을 드롭한 뒤 이름을 바꾸는 식이다. 이 과정은 다음 페이지에서 자세히 다룬다.

한 가지 주의할 점: 이미 중복 값이 들어 있는 컬럼에 유니크 제약을 걸려고 하면 CREATE UNIQUE INDEX가 실패한다. 먼저 중복 행을 정리한 다음에 인덱스를 추가해야 한다.

UNIQUE 제약조건 위반 시 에러 메시지 읽는 법

sqlite unique constraint failed 에러는 어떤 제약조건이 깨졌는지 정확히 알려준다:

エラー: UNIQUE制約に失敗しました: users.email
エラー: UNIQUE制約に失敗しました: enrollments.user_id, enrollments.course_id

첫 번째는 users.email 컬럼 하나에 걸린 제약이고, 두 번째는 복합 unique 제약입니다. 두 컬럼이 함께 표시되는 이유는 두 값의 조합 이 이미 존재하기 때문이죠. 이런 에러를 만났다면 다음 순서로 해결합니다.

  1. 어떤 행이 충돌을 일으키는지 먼저 확인합니다 (SELECT ... WHERE email = '...').
  2. 그 행을 업데이트할지, insert를 건너뛸지, 아니면 다른 값을 쓸지 결정합니다.
  3. 중복이 예상되는 상황이고 병합하고 싶다면 INSERT ... ON CONFLICT DO UPDATE 구문으로 바꾸세요.

이 에러가 시끄럽게 터지는 건 대부분의 경우 정말로 알아야 하기 때문입니다. 조용히 묻힌 중복이 실패한 쓰기보다 훨씬 위험하니까요.

다음 단계: 테이블 삭제와 변경

UNIQUE 제약은 단순한 ALTER TABLE로는 기존 테이블에 추가할 수 없습니다. 바로 이 제약 때문에 SQLite에는 스키마 변경을 위한 특유의 절차, 즉 테이블 재작성 이 필요합니다. 다음 페이지에서는 이 부분과 함께 테이블을 깔끔하게 삭제하는 기본기까지 함께 다루겠습니다.

자주 묻는 질문

SQLite에서 UNIQUE 제약조건은 어떻게 추가하나요?

컬럼 정의에 UNIQUE를 붙이거나(email TEXT UNIQUE), 여러 컬럼을 묶어 유일성을 보장하고 싶다면 테이블 레벨에 UNIQUE(col1, col2) 절을 작성하면 됩니다. SQLite는 내부적으로 unique 인덱스를 만들어 제약을 강제하며, 중복이 발생하는 INSERTUPDATE는 거부합니다.

SQLite에서 UNIQUE와 PRIMARY KEY는 뭐가 다른가요?

한 테이블에 PRIMARY KEY는 하나만 존재할 수 있지만, UNIQUE 제약조건은 여러 개를 둘 수 있습니다. 또한 PRIMARY KEYNOT NULL을 함께 의미하지만(STRICT 테이블이나 INTEGER PRIMARY KEY의 경우), UNIQUE 컬럼에는 NULL이 여러 개 들어갈 수 있습니다. 행을 식별하는 키는 PRIMARY KEY로, 그 외에 중복되면 안 되는 값은 UNIQUE로 잡아 주는 식으로 쓰면 됩니다.

왜 SQLite의 UNIQUE 컬럼에는 NULL이 여러 개 들어갈 수 있나요?

SQL 표준에서 NULL은 '알 수 없는 값'으로 취급되고, 알 수 없는 값끼리는 같다고 보지 않기 때문입니다. 그래서 UNIQUE 컬럼이라도 NULL은 몇 개든 허용되고, NULL이 아닌 값들만 서로 달라야 합니다. NULL을 하나만 허용하고 싶다면 NOT NULL을 함께 걸거나 partial unique index를 사용하세요.

'UNIQUE constraint failed' 오류는 어떻게 해결하나요?

이 에러는 INSERTUPDATEUNIQUE(혹은 PRIMARY KEY) 컬럼에 중복 값이 들어가려고 할 때 발생합니다. 입력하는 값을 다른 값으로 바꾸거나, 기존 행을 먼저 삭제하거나, INSERT ... ON CONFLICT 구문(이른바 upsert)을 사용해 충돌이 났을 때 어떻게 처리할지 SQLite에 알려 주면 됩니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기