「とりあえずUNIQUE」で満足してない?PostgreSQLのUNIQUE制約とインデックスの深い話
やあ。データベース周りの設計で悩んでる後輩くん、今日もコード書いてる?
今日は、みんなが当たり前のように使っている「UNIQUE制約」について少し掘り下げて話そうと思うんだ。初心者のうちは「重複を防ぐための便利ツール」くらいに思われがちだけど、PostgreSQLの内部構造やパフォーマンスを意識し始めると、この制約の使い方はグッと深みが出てくるんだよ。
現場で「なぜその設計にしたの?」と聞かれたとき、自信を持って答えられるようになろう。
—
そもそもUNIQUE制約って何をしてるの?
結論から言うと、「UNIQUE制約を貼る=裏で一意インデックス(Unique Index)が作られる」。これに尽きる。
PostgreSQLは、挿入や更新のたびに「この値、もうテーブルの中に存在しないかな?」ってチェックしなきゃいけない。もし制約がなかったら、毎回テーブルを全件走査(フルスキャン)することになる。そんなの、データが数万件を超えた瞬間にシステムが悲鳴を上げるよね。
だから、PostgreSQLは自動的にB-treeインデックスを作成して、そのインデックスツリーを辿ることで「爆速で重複チェック」を済ませているわけだ。
基本的な書き方(おさらい)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE, — ここで制約+インデックスが自動生成
username TEXT,
CONSTRAINT unique_username UNIQUE (username) — 複数列や明示的な命名にはこちら
);
ここまでは教科書通り。でも、実務で差が出るのはここからだ。
—
実務で意識すべき「NULL」の罠
これ、意外とハマるポイントなんだけど、PostgreSQLのUNIQUE制約では「NULLは重複とみなされない」んだ。
例えば、`deleted_at`(削除日時)を持つ論理削除テーブルで、「有効なデータだけメールアドレスを重複させたくない」という要件があったとする。
— よくあるミス
CREATE TABLE users (
email TEXT UNIQUE,
deleted_at TIMESTAMP
);
これだと、`deleted_at` がNULLならいくらでも同じメールアドレスが登録できてしまう。もし「削除済みも含めてメールアドレスは一意にしたい」なら、アプリケーション側で制御するか、あるいは部分インデックス(Partial Index)を使うのがスマートだ。
— 「削除されていないデータ」かつ「メールアドレス」で一意にする
CREATE UNIQUE INDEX idx_unique_active_email
ON users (email)
WHERE deleted_at IS NULL;
こうすることで、不要なNULL値までインデックスに含めず、かつ要件を満たすクリーンなインデックスが作れる。これぞDBエンジニアの腕の見せ所だね。
—
インデックスは「貼りすぎ」も禁物
「安全のためにとりあえずUNIQUE!」と何でもかんでもインデックスを貼る人がいるけれど、それ、書き込み(INSERT/UPDATE)のパフォーマンスを確実に殺しているよ。
インデックスはデータが書き込まれるたびに更新される。UNIQUE制約が多ければ多いほど、そのたびにツリー構造の再構築や整合性チェックが走るんだ。
- 本当に一意性が必須か?
- 頻繁に検索される列か?
- (複合インデックスの場合)先頭の列を検索条件に使うか?
これらを天秤にかけて、「本当に必要なものだけ」に絞る。これが大規模システムを支えるエンジニアの矜持だ。
—
最後に:困ったら EXPLAIN ANALYZE
最後に一つアドバイス。自分の設計したUNIQUE制約が、実際のクエリでどう動いているか不安になったら、すぐに `EXPLAIN ANALYZE` を叩く癖をつけてほしい。
EXPLAIN ANALYZE SELECT FROM users WHERE email = ‘test@example.com’;
ここで `Index Scan` が発生していれば、君のUNIQUE制約は正しく機能して、パフォーマンスの向上に貢献している証拠だ。
データベースは、一度作ったら終わりじゃない。データが増えれば特性も変わる。常に「今、このインデックスは最適か?」と問いかけ続けることが、君を一段上のエンジニアに引き上げてくれるはずだよ。
また何か気になったら聞きに来てくれ。今日はこの辺で!
コメント