なぜ僕らは「UNIQUE制約」を愛するのか?―データベースの信頼性を守る最後の砦
エンジニアの皆さん、お疲れ様です。
今日はPostgreSQLの「UNIQUE制約」について話をしようと思う。正直、「そんなの教科書に書いてあるよ」と思うかもしれない。でもね、現場で長くデータベースを触っていると、「ここを甘く見ていたせいで、後のデータクレンジングで地獄を見た」という悲鳴を何度聞いたことか。
UNIQUE制約は単なる「重複禁止ルール」じゃない。君が書いたアプリケーションのロジックがどんなにバグっても、データだけは守り抜くという、データベースからの「最後の警告」なんだ。
1. UNIQUE制約の本質:インデックスという名の「番人」
まず、基本をおさらいしよう。UNIQUE制約を設定すると、PostgreSQLは裏で自動的に「B-treeインデックス」を作成する。
ここが重要なんだけど、これは単に「重複を防ぐためだけの機能」じゃないんだ。「そのカラムで検索した時に爆速で結果を返す」ためのインデックスが自動的に付いてくるということ。
つまり、`email`カラムにUNIQUE制約を張ることは、データ整合性を担保するだけでなく、ログイン処理や検索処理のパフォーマンスを担保することと同義なんだよ。
— 基本の形
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE, — 重複を許さない
username TEXT NOT NULL
);
2. 「カラムの組み合わせ」で制約を作る現場の知恵
実務でよくあるのが、「単一カラムじゃなくて、組み合わせで重複を防ぎたい」というケースだ。例えば、ユーザーごとの「お気に入りリスト」。
「ユーザーID(user_id)」と「商品ID(product_id)」の組み合わせは一つであるべきだよね。これをアプリケーション側の`if`文でチェックするのはやめよう。それはRace Condition(競合状態)の温床だ。必ずデータベース側で制約をかける。
CREATE TABLE favorites (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
product_id INT NOT NULL,
— 組み合わせで一意にする
UNIQUE (user_id, product_id)
);
こうしておけば、仮に複数のリクエストが同時に飛んできても、PostgreSQLがしっかり弾いてくれる。安心感が段違いだよ。
3. 注意点:NULLの扱いは「優等生」ではない
ここで一つ、現場でハマりやすい罠を教えておくね。
実は、PostgreSQLにおいて「NULLはNULLと等しくない」んだ。
だから、`UNIQUE(email)`を張ったカラムに、NULLを2回入れることは可能だよ。
- 1回目:NULL → OK
- 2回目:NULL → OK
- 3回目:NULL → OK
「えっ、UNIQUEなのに重複してるじゃん!」と焦る後輩を何人も見てきた。もしNULLも含めて「一つしか許さない」という仕様にするなら、`UNIQUE`制約じゃなくて、`EXCLUDE`句を使った特殊な制約が必要になる。ここは設計時に一度立ち止まって考えてみてほしい。
4. 現場で役立つ「後付け」テクニック
すでに運用中のテーブルにUNIQUE制約を後付けしたいとき、データがすでに汚れていたらどうするか?
`ALTER TABLE`で制約をかけようとすると、重複データがある場合はエラーで止まってしまう。そんなときは、まず重複を特定して掃除してからだ。
— 重複しているデータを特定する
SELECT email, COUNT()
FROM users
GROUP BY email
HAVING COUNT() > 1;
クリーンアップが終わってから、満を持して制約をかける。これがエンジニアの作法だよ。
ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email);
最後に:制約は「コードのゴミ」を減らす
最後にアドバイス。
アプリケーションコードの中に、複雑な「重複チェックロジック」を書いていないかな? もしそうなら、それはデータベースに任せていい仕事かもしれない。
データベースが制約で弾いてくれるなら、アプリケーション側は「エラーが起きたら例外をキャッチして、ユーザーに分かりやすく伝える」というシンプルな設計に集中できる。
データベースの制約を「邪魔者」と思わず、「自分のコードを助けてくれる相棒」だと思って付き合ってみてほしい。そうすれば、君が書くシステムはもっと堅牢で、もっと美しいものになるはずだよ。
それじゃ、また現場で会おう。いいコードを!
コメント