一対多リレーションシップは、親テーブルの1行を子テーブルの任意の数の行に結び付けます。顧客とその注文がその一例です。子テーブルに親を指す外部キー列を作ることで構築します。サンプルは PostgreSQL、MySQL、SQLite、SQL Server で動作します。
一対多リレーションシップのモデル化方法
「多」側である子テーブルに、親の主キーを保持する列を追加し、外部キーとして宣言します。各注文は注文した顧客の id を保持し、customers テーブルは一切変更しません。
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
-- Each order stores the id of the customer who placed it
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
order_date DATE NOT NULL,
total DECIMAL(10,2) NOT NULL,
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
外部キーで2つのテーブルを結合すると、各顧客とその注文を確認できます。
-- Input: customers
-- id | name
-- ---+------
-- 1 | Alice
-- 2 | Bob
-- 3 | Carol
-- Input: orders
-- id | customer_id | order_date | total
-- ---+-------------+------------+-------
-- 1 | 2 | 2026-03-02 | 42.50
-- 2 | 1 | 2026-03-05 | 18.00
-- 3 | 2 | 2026-04-11 | 120.00
-- 4 | 2 | 2026-05-20 | 64.99
SELECT c.name, o.order_date, o.total
FROM customers c
JOIN orders o ON c.id = o.customer_id
ORDER BY c.name, o.order_date;
-- Output:
-- name | order_date | total
-- ------+------------+-------
-- Alice | 2026-03-05 | 18.00
-- Bob | 2026-03-02 | 42.50
-- Bob | 2026-04-11 | 120.00
-- Bob | 2026-05-20 | 64.99
Bob には3件の注文があり、各注文の顧客は1人です。
知っておくべきルール
-
外部キーは子テーブルに置きます。各注文が自分の顧客を記録します。
customersに注文 ID を列挙する列を持たせてはいけません。 - 通常の JOIN では子を持たない親が除外されます。上の出力には Carol が含まれていません。すべての親を表示したい場合は、後述の例のように LEFT JOIN を使います。
-
外部キーにはインデックスを作成します。顧客による結合や検索には
orders.customer_idのインデックスが必要です(MySQL では外部キーと一緒に自動作成されます)。
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
-
外部キーは両方のテーブルを保護します。存在しない顧客の注文を拒否し、デフォルトではまだ注文が残っている顧客の削除も拒否します。顧客と一緒に注文も削除するには
ON DELETE CASCADEを、顧客なしで注文を残すにはON DELETE SET NULLを追加します(この場合、列は NULL を許可する必要があります)。
-- Input: the customers and orders tables above
INSERT INTO orders VALUES (5, 99, '2026-06-01', 10.00);
-- ERROR: insert or update on table "orders" violates foreign key constraint "orders_customer_id_fkey"
DELETE FROM customers WHERE id = 1;
-- ERROR: update or delete on table "customers" violates foreign key constraint "orders_customer_id_fkey" on table "orders"
-- With FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE
DELETE FROM customers WHERE id = 1;
-- Output (orders after the DELETE):
-- id | customer_id | order_date | total
-- ---+-------------+------------+-------
-- 1 | 2 | 2026-03-02 | 42.50
-- 3 | 2 | 2026-04-11 | 120.00
-- 4 | 2 | 2026-05-20 | 64.99
-
SQLite では有効にするまで外部キーが無視されます。接続ごとに
PRAGMA foreign_keys = ON;を実行してください。そうしないと、SQLite は存在しない顧客の注文を受け付けてしまいます。
別の構文
同じキーを宣言する方法は、ほかに3つあります。
-- Inline on the column (MySQL 8.0 accepts this but silently ignores it)
customer_id INTEGER NOT NULL REFERENCES customers(id)
-- Named, so errors and later changes can refer to it
CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id) REFERENCES customers(id)
-- Added to an existing table (SQLite can't add a foreign key this way)
ALTER TABLE orders ADD CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers(id);
データベースに id を自動採番させるには、親のキーを PostgreSQL では SERIAL PRIMARY KEY、MySQL では INT AUTO_INCREMENT PRIMARY KEY、SQLite では INTEGER PRIMARY KEY、SQL Server では INT IDENTITY PRIMARY KEY として宣言します。外部キー列は通常の整数のままです。
一対多リレーションシップはいつ使うべきか?
各子がちょうど1つの親に属する場合に使います。顧客と注文、投稿とコメント、部署と従業員、カテゴリと商品などです。複数のコースを受講する学生のように、子が複数の親を持てる場合は、代わりに多対多リレーションシップが必要です。以下は、一対多リレーションシップで解決できる問題です。
各親を子と一緒に表示する
請求書とその明細、投稿とそのコメントなど。外部キーで子テーブルを親に結合します。親の列は子の各行で繰り返し表示されます。
各親の子の数を数える
各顧客の注文数、各著者の記事数など。LEFT JOIN し、親でグループ化して子の列をカウントします。* ではなく子の列をカウントすることで、子を持たない親は 0 になります。
子を持たない親を見つける
一度も注文していない顧客、プロジェクトのない部署、誰もレビューしていない商品など。LEFT JOIN は一致しない場合に子の列を NULL で埋めるため、子の id が NULL の行を抽出します。
子の値を集計する
顧客ごとの売上、カテゴリごとの在庫金額、プロジェクトごとの作業時間など。親でグループ化し、子の行に対して SUM、AVG、MIN、MAX を使います。子を持たない親は NULL になりますが、COALESCE で 0 に変換できます。
1つのテーブルに階層を格納する
従業員とその上司、フォルダとサブフォルダ、カテゴリとサブカテゴリなど。外部キーは同じテーブルを参照し、NULL がツリーの最上位を示します。テーブルを自己結合すると、各行を親と並べて表示できます。
一対多リレーションシップの実例
各顧客の最新の注文を表示する
ROW_NUMBER() で各顧客の注文に新しい順に番号を付け、1番を残します。
-- Input: customers
-- id | name
-- ---+------
-- 1 | Alice
-- 2 | Bob
-- 3 | Carol
-- Input: orders
-- id | customer_id | order_date | total
-- ---+-------------+------------+-------
-- 1 | 2 | 2026-03-02 | 42.50
-- 2 | 1 | 2026-03-05 | 18.00
-- 3 | 2 | 2026-04-11 | 120.00
-- 4 | 2 | 2026-05-20 | 64.99
SELECT name, order_date, total
FROM (
SELECT c.name, o.order_date, o.total,
ROW_NUMBER() OVER (PARTITION BY c.id ORDER BY o.order_date DESC) AS rn
FROM customers c
JOIN orders o ON c.id = o.customer_id
) latest
WHERE rn = 1
ORDER BY name;
-- Output:
-- name | order_date | total
-- ------+------------+------
-- Alice | 2026-03-05 | 18.00
-- Bob | 2026-05-20 | 64.99
注文数で顧客をランク付けする
-- Input: the customers and orders tables above
SELECT c.name, COUNT(o.id) AS order_count
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name
ORDER BY order_count DESC;
-- Output:
-- name | order_count
-- ------+------------
-- Bob | 3
-- Alice | 1
-- Carol | 0
一度も注文していない顧客を見つける
-- Input: the customers and orders tables above
SELECT c.name
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.id IS NULL;
-- Output:
-- name
-- -----
-- Carol
NOT EXISTS でも同じことができ、問いに近い形で読めます。
-- Input: the customers and orders tables above
SELECT c.name
FROM customers c
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.id
);
-- Output:
-- name
-- -----
-- Carol
購入額の多い顧客を見つける
行がない場合の SUM は NULL になるため、COALESCE で Carol の合計を 0 に変換します。
-- Input: the customers and orders tables above
SELECT c.name, COALESCE(SUM(o.total), 0) AS total_spent
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name
ORDER BY total_spent DESC;
-- Output:
-- name | total_spent
-- ------+------------
-- Bob | 227.49
-- Alice | 18.00
-- Carol | 0
組織図を上から順にたどる
各従業員の manager_id は別の従業員を指しています。再帰クエリは上司のいない人から始め、部下の階層を1段ずつ追加していきます。
CREATE TABLE employees (
id INTEGER PRIMARY KEY,
name VARCHAR(100) NOT NULL,
manager_id INTEGER, -- NULL for the person at the top
FOREIGN KEY (manager_id) REFERENCES employees(id)
);
-- Input: employees
-- id | name | manager_id
-- ---+-------+-----------
-- 1 | Maya | NULL
-- 2 | Raj | 1
-- 3 | Lena | 1
-- 4 | Tom | 2
-- 5 | Sofia | 2
-- In SQL Server, write WITH instead of WITH RECURSIVE
WITH RECURSIVE org_chart AS (
-- Start at the top: the employee with no manager
SELECT id, name, 0 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- Then add each employee's direct reports, one level at a time
SELECT e.id, e.name, oc.level + 1
FROM employees e
JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT name, level
FROM org_chart
ORDER BY level, name;
-- Output:
-- name | level
-- ------+------
-- Maya | 0
-- Lena | 1
-- Raj | 1
-- Sofia | 2
-- Tom | 2
一対多リレーションシップについてさらに学ぶ
このブログの関連記事:
- データベースリレーションシップガイド:一対一、一対多、多対多の比較
- 多対多リレーションシップガイド:両側が複数を持つ場合の中間テーブルの使い方
- 一対一のデータベースリレーションシップ:各行がちょうど1つの相手を持つ場合
- PostgreSQL で外部キーを使う方法:外部キーの作成と、親が変更されたときの動作の選択
- SQL JOIN 図解ガイド:親テーブルと子テーブル間の INNER JOIN と LEFT JOIN
- SQL GROUP BY ガイド:親ごとに子の行をカウント・合計する
- SQL CTE とサブクエリの比較ガイド:再帰 CTE を含む CTE の解説
公式ドキュメント:
Beekeeper Studioは無料のオープンソースのデータベースGUIです
これまで使った中で最高のSQLクエリ&エディタツールです。データベースの管理に必要なものがすべて揃っています。 - ⭐⭐⭐⭐⭐ Mit
Beekeeper Studioは高速で直感的、そして使いやすいツールです。多数のデータベースに対応し、Windows、Mac、Linuxで快適に動作します。
Beekeeper Studio 利用者の声
「Beekeeper Studio は以前の SQL ワークフローを完全に置き換えてくれました。高速で直感的で、データベース作業がまた楽しくなりました。」
「多くのデータベース GUI を試してきましたが、Beekeeper は機能とシンプルさのバランスが完璧です。とにかくちゃんと動きます。」