導入
「1つの注文には複数の商品が入る。1つの商品は複数の注文に登場する」――双方向に“多”な関係を 多対多(many-to-many, N:M) といいます。会員と商品も、注文を通して多対多です(1人が複数の商品を買い、1商品を複数人が買う)。この関係は、実は表2つだけでは表せません。ここに order_items という表が存在する理由があります。
説明
もし orders に「商品」列を1つだけ持たせたら、1注文=1商品になってしまい、「1注文で複数商品」を表せません。逆に商品側に注文を持たせても同じ問題が起きます。そこで、両者の組み合わせ1つを1行にする専用の表を挟みます。これを 中間テーブル(junction table/連結表) と呼びます。
flowchart LR
O["orders<br/>(注文)"] -->|"1:N"| OI["order_items(中間テーブル)<br/>order_id + product_id の組"]
P["products<br/>(商品)"] -->|"1:N"| OI
N["orders ↔ products は多対多。<br/>中間テーブルが2つの1対多に分解して橋渡しする"]
order_items の1行は「どの注文(order_id)に・どの商品(product_id)が・いくつ(quantity)」を表します。多対多は、こうして2つの1対多に分解して表現します。中間テーブルを介せば、「注文に含まれる商品一覧」も「その商品を含む注文一覧」も、どちらの向きにもたどれます。
-- 注文ごとに、含まれる商品を並べる(注文 → 商品の向き)
SELECT o.id AS 注文, p.name AS 商品
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.id
JOIN products AS p ON oi.product_id = p.id
ORDER BY o.id;
逆向きも同じ中間テーブルで書けます。「ある商品を買った会員」を探すなら、products → order_items → orders → users とたどります。
-- 「モニター」を注文した会員(商品 → 会員の向き)
SELECT DISTINCT u.name
FROM products AS p
JOIN order_items AS oi ON oi.product_id = p.id
JOIN orders AS o ON oi.order_id = o.id
JOIN users AS u ON o.user_id = u.id
WHERE p.name = 'モニター';
中間テーブルは、実務のあらゆる場面に出てきます(学生と授業、記事とタグ、ユーザーとロール…)。「多対多を見たら中間テーブル」と反射できれば、設計の力が一段上がります。
やってみよう
初期表示のクエリを実行し、1つの注文(たとえば id=5 や id=6)に複数の商品がぶら下がることを確認しましょう。次に上の「モニターを注文した会員」クエリを実行し、中間テーブルを逆向きにもたどれることを体感してください。
演習
中間テーブルを使って、「マウス」を注文した会員の名前を、重複なく(DISTINCT)取り出してください。
ヒント1を見る
products → order_items → orders → users と JOIN でたどり、WHERE p.name = 'マウス'。
ヒント2を見る
SELECT DISTINCT u.name FROM products AS p JOIN order_items AS oi ON oi.product_id = p.id JOIN orders AS o ON oi.order_id = o.id JOIN users AS u ON o.user_id = u.id WHERE p.name = 'マウス';