UNIQUE制約は「重複NG」のサイン
UNIQUE制約は、特定のカラム(あるいは複数カラムの組み合わせ)で同じ値が重複しないようにSQLiteへ伝えるための仕組みです。「同じメールアドレスのユーザーを2人作らせない」「商品コードは1回しか登場しない」といったルールを表現するときに使います。
3 つ目の INSERT は UNIQUE constraint failed: users.email で失敗します。SQLite は書き込みのたびに UNIQUE 制約をチェックし、重複が発生する操作を拒否します。最初の 2 行は保存されますが、3 行目は登録されません。
内部的には、UNIQUE 制約はユニークインデックス(sqlite unique index)として実装されています。これは SQLite が高速検索に使うのと同じデータ構造なので、チェックのコストは低く、対象カラムには自動でインデックスが張られます。
カラム単位 vs テーブル単位の書き方
UNIQUE の書き方は 2 通りあります。カラム定義の横にインラインで書く方法と、テーブル定義の末尾に独立した句として書く方法です。
単一カラムであれば、両者は事実上同じなので読みやすい方を選べばOKです。ただし複数カラムにまたがる一意性を表現したい場合は、テーブルレベルの書き方が必須になります。
sqlite 複合ユニークキー:複数カラムを組み合わせる
カラム単体では重複してもよいけれど、組み合わせ としては一意であってほしいケースがあります。たとえば、1人のユーザーは複数のコースを受講でき、1つのコースにも複数のユーザーが登録できますが、同じ (user_id, course_id) のペアが2回現れてはいけません。
制約はペアに対してかかっていて、片方のカラム単体にかかっているわけではありません。ユーザー1は複数のコースを受講できるし、コース100にも複数のユーザーが登録できます。ただし、同じ組み合わせは1回だけ、というわけです。
これは多対多のリレーションで使う中間テーブル(join table)の定番パターンですね。
sqlite uniqueとprimary keyの違い
名前も似ているし関係も深いですが、UNIQUEとPRIMARY KEYはイコールではありません。
- 1つのテーブルに
PRIMARY KEYは1つだけ。一方でUNIQUE制約はいくつでも付けられます。 PRIMARY KEYは行のアイデンティティそのもの。外部キーの参照先になり、rowidのエイリアスとしても機能します。UNIQUEは単に「この値(または組み合わせ)が重複しない」という意味だけです。- 通常のテーブルでは、
UNIQUEカラムにはNULLを入れられますが、PRIMARY KEYには入れられません(歴史的な例外が1つありますが、ここでは触れません)。
よく見かける形はこんな感じです。
id はデータベースの他のテーブルから参照されるカラムです。一方で email や username がユニークなのは、アプリケーションの要件としてそうしているだけで、identity(同一性)を表すものではありません。ユーザーがメールアドレスを変更しても id は変わらない —— カラムを分けているのは、まさにこのためです。
sqlite unique と NULL の落とし穴
これは、ほぼ全員が最初にハマるポイントです。SQLite では、UNIQUE 制約のついたカラムでも NULL はいくつでも入れられます。
NULLが3つあっても問題なし。ところが'ada@example.com'が2つあると、これは衝突です。
理由はこうです。SQLではNULLは「未知の値」として扱われるため、未知同士を比較しても等しいとは判定されません。だからユニーク性チェックでも重複とは見なされないわけです。NULLを1つだけに制限したい場合は、NOT NULL UNIQUEを付けるのが一番すっきりします。NULLは許可しつつ、他のカラムとの組み合わせで1つだけにしたいなら、部分インデックス(partial index)の出番です(これは後のインデックスの章で扱います)。
衝突の処理:ON CONFLICTの使い方
デフォルトでは、UNIQUE制約に違反するとそのステートメントは中断されます。でも実際には、既存の行を置き換えたい、新しい行を無視したい、特定のカラムだけ更新したい、といったケースもありますよね。SQLiteではこういった挙動を2通りの方法で指定できます。
1つ目は、制約自体にON CONFLICTを組み込む書き方です:
themeを2回目に挿入したタイミングで、既存の行が削除され、新しい行に置き換わります。他に指定できるオプションはIGNORE(黙って無視)、ABORT(デフォルト)、FAIL、ROLLBACKです。
2つ目のやり方は、ステートメント単位で指定するsqlite upsert構文です。特定のカラムだけを更新できるので、こちらのほうが柔軟に使えます。
最初の INSERT で行が作成され、続く2回は UNIQUE 制約に引っかかって DO UPDATE 側に流れ、count がインクリメントされます。これがいわゆる INSERT ... ON CONFLICT を使った sqlite upsert 構文で、専用のページで後ほど詳しく取り上げます。
sqlite unique制約とunique indexの違い
CREATE UNIQUE INDEX は UNIQUE 制約と実質的に同じ役割を果たします。というより、UNIQUE 制約を定義すると裏側では sqlite unique index が自動的に作成されるので、見た目が違うだけで中身はほぼ同じ仕組みなんです。
どちらを選ぶべきか:
- 制約(CONSTRAINT) はユニーク性がテーブル定義そのものの一部である場合に使います。カラムの定義のすぐ隣に書かれるので、スキーマを読めば一目瞭然です。
- ユニークインデックス は、部分インデックス(
WHERE句付き)にしたい、特定の名前を付けたい、既存のテーブルを作り直さずに追加したい、といった場合に向いています。SQLite のALTER TABLEでは制約を追加できませんが、インデックスはいつでも後から追加できます。
書き込み時の挙動はまったく同じです。違いは、このルールをスキーマのどこに置きたいか、という点に尽きます。
既存テーブルに UNIQUE 制約を追加する
SQLite の ALTER TABLE は意図的に機能が絞られていて、ALTER TABLE ... ADD CONSTRAINT は用意されていません。実際に取れる方法は次の 2 つです:
方法その2 ―― どうしてもテーブル定義そのものに UNIQUE 制約を組み込みたい場合は、テーブルを作り直す手順を踏むことになります。制約付きの新しいテーブルを作って、データをコピーして、古いテーブルを削除し、リネームする、という流れです。詳しくは次のページで解説します。
ひとつ注意点があります。すでに重複データが入っているカラムにユニーク制約を後付けしようとすると、CREATE UNIQUE INDEX は失敗します。先に重複行を整理してから、インデックスを張りましょう。
UNIQUE制約に失敗したときのエラーの読み方
エラーメッセージを見れば、どの制約に引っかかったのかが一目でわかります。
Error: UNIQUE constraint failed: users.email
Error: UNIQUE constraint failed: enrollments.user_id, enrollments.course_id
1つ目は users.email という単一カラムに対する UNIQUE 制約、2つ目は複合ユニークキーです。複数カラムが並んでいるのは、組み合わせ として既に存在しているからですね。UNIQUE constraint failed に出くわしたら、こんな手順で対処します。
- どの行が衝突しているのかを特定する(
SELECT ... WHERE email = '...')。 - その行を更新するのか、INSERT をスキップするのか、別の値を使うのかを決める。
- 重複が 想定内 でマージしたいなら、
INSERT ... ON CONFLICT DO UPDATEを使った upsert 構文に切り替える。
このエラーがはっきり出てくれるのはありがたい話です。たいていの場合は本当に気づきたいエラーですし、書き込みが黙って失敗するより、重複が黙って通ってしまう方がずっと厄介ですから。
次回:テーブルの削除と変更
UNIQUE 制約は、既存テーブルに対して単純な ALTER TABLE で後付けすることができません。この制約があるため、SQLite ではスキーマ変更のときに独特の手順 ―― いわゆるテーブル再構築 ―― を踏むことになります。次のページでは、テーブルをきれいに削除する基本とあわせて、この再構築の流れを取り上げます。
よくある質問
SQLiteでUNIQUE制約はどう書けばいい?
単一カラムなら email TEXT UNIQUE のようにカラム定義に UNIQUE を付けるだけ。複数カラムをまとめてユニークにしたい場合は、テーブル定義の最後に UNIQUE(col1, col2) を書きます。SQLiteは内部でユニークインデックスを作って制約を管理しており、重複が発生する INSERT や UPDATE はそのまま弾かれます。
UNIQUEとPRIMARY KEYは何が違うの?
PRIMARY KEY はテーブルに1つしか持てませんが、UNIQUE はいくつでも付けられます。また PRIMARY KEY は NOT NULL を兼ねる扱い(STRICTテーブルや INTEGER PRIMARY KEY の場合)ですが、UNIQUE カラムには NULL を複数入れることができます。行を識別するキーには PRIMARY KEY、それ以外で重複を許したくないカラムには UNIQUE、と使い分けるのが基本です。
UNIQUE列なのにNULLが何個も入るのはなぜ?
SQLでは NULL は「値が不明」を表すため、NULL 同士を比べても等しいとは判定されません。だから UNIQUE カラムでも NULL の行はいくつでも入り、重複チェックの対象になるのは非NULLの値だけです。NULLも1件までに絞りたい場合は、NOT NULL を付けるか部分ユニークインデックスを使いましょう。
「UNIQUE constraint failed」エラーが出たときはどうする?
これは INSERT や UPDATE の結果、UNIQUE(または PRIMARY KEY)カラムで値が重複してしまうときに出るエラーです。挿入する値を変える、既存の行をあらかじめ削除する、あるいは INSERT ... ON CONFLICT(いわゆるupsert)で衝突時の挙動を明示する、のいずれかで対応します。