Day5 前半のゴール
「“DBが仕様を守ってくれる”感覚を、PostgreSQL流でつかむ」
今日は「制約」の話です。
SQLite ではかなりゆるく、MySQL でも設定次第で挙動が変わるところですが、PostgreSQLはここがかなり“真面目”です。
前半のゴールはこうです。
CHECK制約で「値の範囲・パターン」をDB側に守らせるイメージを持つ。
UNIQUE制約で「重複禁止」をアプリ任せにせず、DBに任せる感覚をつかむ。
外部キーが「“存在しない親”を絶対に許さない門番」だと理解する。
「アプリでチェックするからいいや」ではなく、「DBにも守らせる」という発想に切り替えていきます。
制約とは何か
「“このテーブルはこういうルールでしかデータを受け付けません”という契約」
制約(constraint)は、一言で言うと「このテーブルに入っていいデータのルール」です。
NOT NULL も制約の一種ですが、今日は特に次の3つにフォーカスします。
CHECK制約
UNIQUE制約
外部キー制約(FOREIGN KEY)
PostgreSQLは、これらをかなり厳密に守ります。
「変な値を入れようとしたら、その場でエラーにする」というスタンスです。
アプリ側でバリデーションを書くのはもちろん大事ですが、
DB側にもルールを持たせておくと、「バグっても壊れたデータが入らない」という最後の砦になります。
CHECK制約のイメージ
「“このカラムはこの条件を満たしていないとダメ”をDBに書く」
CHECK制約は、「この行が有効であるための条件」をSQLで書く仕組みです。
例えば、年齢を持つテーブルを考えます。
CREATE TABLE people (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER NOT NULL
);
SQLここに、「年齢は0〜150の間に限定したい」というルールをDBに持たせたいとします。
CHECK制約を使うと、こう書けます。
CREATE TABLE people (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER NOT NULL CHECK (age >= 0 AND age <= 150)
);
SQLこの状態で、次のINSERTは通ります。
INSERT INTO people (name, age) VALUES ('Taro', 20);
SQLしかし、こう書くとエラーになります。
INSERT INTO people (name, age) VALUES ('Baby', -1); -- age < 0
INSERT INTO people (name, age) VALUES ('Old', 200); -- age > 150
SQLPostgreSQLは、「CHECK条件を満たしていないから、この行は受け付けない」と言ってくれます。
ここでの重要ポイントは、「ビジネスルールをDBに書ける」ということです。
「年齢は0〜150歳まで」というのは、アプリの仕様書に書くべき内容ですが、
それをそのままDBの定義に埋め込めるのがCHECK制約です。
CHECK制約のもう少し実務寄りな例
「ステータス値・割合・フラグの組み合わせ」
CHECK制約は、数値の範囲だけでなく、いろいろな場面で使えます。
例えば、注文のステータスを表すカラムを考えます。
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
status TEXT NOT NULL
);
SQLstatus に入る値を 'new', 'paid', 'canceled' の3つに限定したいとします。
CHECK制約でこう書けます。
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
status TEXT NOT NULL CHECK (status IN ('new', 'paid', 'canceled'))
);
SQLこれで、status = 'hoge' のような謎の値はDBが拒否してくれます。
あるいは、「割引率は0〜100の間」というルールもCHECKで書けます。
CREATE TABLE coupons (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
discount_pct INTEGER NOT NULL CHECK (discount_pct >= 0 AND discount_pct <= 100)
);
SQLCHECK制約は、「このカラムはこういう値しか受け付けない」というルールを、
SQLの条件式としてそのまま書けるのが強みです。
MySQLでもCHECKは書けますが、バージョンや設定によっては無視されたり挙動が違ったりします。
PostgreSQLはここがかなり真面目で、「書いたCHECKはちゃんと守る」前提で設計できます。
UNIQUE制約のイメージ
「“同じ値は2回入れないで”をDBに任せる」
次に UNIQUE制約です。
これは、「このカラム(またはカラムの組み合わせ)は重複してはいけない」というルールです。
一番分かりやすいのは、ユーザーのメールアドレスです。
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE
);
SQLこの定義だと、同じemailを持つ行を2つ入れようとすると、エラーになります。
INSERT INTO users (email) VALUES ('a@example.com'); -- OK
INSERT INTO users (email) VALUES ('a@example.com'); -- エラー(重複)
SQLアプリ側で「登録前にSELECTして重複チェックする」という書き方もできますが、
マルチスレッド・マルチプロセスの世界では、「チェックしている間に別のリクエストが同じメールで登録する」というレースコンディションが起きます。
UNIQUE制約をDB側に置いておけば、
「最終的にDBが絶対に重複を許さない」状態になります。
ここでの重要ポイントは、「一意性はDBに守らせる」という発想です。
アプリ側のチェックは「ユーザーに優しいエラーメッセージを出すため」であり、
「データの一貫性を守る最後の砦」はUNIQUE制約です。
複数カラムのUNIQUE制約
「“この組み合わせ”は一意、というルールも書ける」
UNIQUE制約は、1カラムだけでなく、「カラムの組み合わせ」に対しても設定できます。
例えば、「1人のユーザーは、同じ商品をお気に入りに登録できるのは1回だけ」という仕様を考えます。
CREATE TABLE favorites (
user_id INTEGER NOT NULL,
product_id INTEGER NOT NULL
);
SQLここで、「(user_id, product_id) の組み合わせは一意」というUNIQUE制約を付けます。
CREATE TABLE favorites (
user_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
UNIQUE (user_id, product_id)
);
SQLこれで、同じユーザーが同じ商品を2回お気に入りに追加しようとすると、DBがエラーにしてくれます。
MySQLでも複合UNIQUEは使えますが、PostgreSQLは制約まわりがかなり安定しているので、
「こういう一意性ルールはどんどんDBに書いていい」という感覚で使えます。
外部キーの“厳密性”のイメージ
「“存在しない親”を絶対に許さない門番」
最後に、外部キー(FOREIGN KEY)の話に触れておきます。
前半ではイメージ中心、後半で動き方を深掘りします。
外部キーは、「このカラムの値は、必ず別のテーブルの主キー(など)に存在していなければならない」という制約です。
例えば、ユーザーと注文の関係を考えます。
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL
);
SQLここに、「orders.user_id は必ず users.id に存在する値でなければならない」というルールを付けたいとします。
外部キー制約を使うと、こう書けます。
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id)
);
SQLこの状態だと、次のようなINSERTはエラーになります。
INSERT INTO orders (user_id) VALUES (9999); -- users に 9999 がいなければエラー
SQLPostgreSQLは、「そんなユーザーいないよ」と言ってくれます。
これが「外部キーの厳密性」です。
SQLiteでは外部キー制約がデフォルトで無効だったり、
MySQLでも設定やストレージエンジンによって挙動が変わったりしますが、
PostgreSQLはここがかなりしっかりしていて、「外部キーを書いたらちゃんと守られる」前提で設計できます。
Day5 前半のまとめ
PostgreSQLの制約は、「アプリの仕様をDBに埋め込む」ための強力な仕組みで、CHECK 制約では age >= 0 AND age <= 150 や status IN ('new','paid','canceled') のように値の範囲・パターンをSQLの条件式として書き、変な値が入ろうとした瞬間にDBがエラーで止めてくれる。UNIQUE 制約は、メールアドレスのような単一カラムの一意性だけでなく、UNIQUE (user_id, product_id) のように「この組み合わせは一意」という複合ルールもDB側に持たせることで、アプリ側の事前チェックに頼らず「最終的な重複禁止」を保証できる。
外部キー制約は、user_id REFERENCES users(id) のように「この値は必ず親テーブルに存在していなければならない」という関係をDBに宣言し、「存在しない親への参照」を絶対に許さない門番として働くため、SQLiteやMySQLよりも“制約がちゃんと効く”PostgreSQLでは、「仕様=制約」として積極的にDBにルールを書いていくのが Day5 前半の着地点になる。
