リレーションシップとカーディナリティ
「ユーザは注文を持つ」の一言では、NOT NULL を決められない
orders テーブルを作る手が止まります。user_id に NOT NULL を付けてよいのかが分からないからです。
付ければ「持ち主のいない注文」は作れなくなります。ゲスト購入を後から足したくなったとき、既に数万行入ったテーブルから NOT NULL を外す作業が待っています。付けなければ、user_id が NULL の行がいつか紛れ込み、集計のたびに「この行は誰の注文なのか」で止まります。
決め手になるのがカーディナリティ、つまり関連の両側で、それぞれ何件と対応するのかという数です。
両側から 1 問ずつ聞く
カーディナリティは、必ず両方向から数えます。ユーザと注文なら、次の 2 問です。
- ユーザ 1 人につき、注文は何件か
- 注文 1 件につき、ユーザは何人か
答えが「複数」と「1 人」なら、この関連は 1 対多です。「1 対多」という言葉は、片側から見た数だけを指しているのではなく、この 2 つの答えの組を指しています。答えが「1 件」と「1 人」なら 1 対 1、両方とも「複数」なら多対多で、1 対多はその組み合わせのひとつにすぎません。
さらに、それぞれの答えに最小値を添えます。「ユーザ 1 人につき注文は 0 件でもよいか」の答えは、たいてい「よい」です。登録しただけで一度も買っていない人は普通にいます。「注文 1 件につきユーザは 0 人でもよいか」の答えが「駄目」なら、最小は 1 です。ここまで揃って、はじめて設計が決まります。
答えは、多側の 1 本の列に載る
最大の答え (「複数」の側がどちらか) が、外部キーの置き場所を決めます。多側、つまり orders に列を置きます。最小の答えが、その列に NOT NULL を付けるかどうかを決めます。
SQL クエリ
CREATE TABLE users (
id INT PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT NOT NULL,
total INT NOT NULL
);user_id INT NOT NULL の 1 行が、「注文には必ず 1 人のユーザがいる」という業務ルールそのものです。ゲスト購入を許すなら NOT NULL を外します。列を見れば業務ルールが読めるのが、カーディナリティを決めてから書いた設計です。
一方、ユーザ側の最小 0 は、列にも制約にも現れません。それは「注文 0 件のユーザが存在する」という事実として、集計するときに効いてきます。
最小が 0 か 1 かは、仕様書に書かれていないことがほとんどです。業務担当に「絶対に空にならないと言い切れますか」と聞いてください。後から
NOT NULLを外すのは簡単ですが、NULL が入った後にNOT NULLを付けるのは、既存行の埋め方を決めるところから始まります。
テーブル構造
CREATE TABLE users (
id INT PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT NOT NULL,
total INT NOT NULL
);
INSERT INTO users (id, name) VALUES
(1, '田中'),
(2, '鈴木'),
(3, '佐藤'),
(4, '山田');
INSERT INTO orders (id, user_id, total) VALUES
(1, 1, 1000),
(2, 1, 2500),
(3, 2, 800),
(4, 2, 1200),
(5, 2, 600),
(6, 4, 3000);期待される出力
| name | order_count | total_amount |
|---|---|---|
| 田中 | 2 | 3500 |
| 鈴木 | 3 | 2600 |
| 佐藤 | 0 | 0 |
| 山田 | 1 | 3000 |