🧚 お知らせ!Beekeeper Studio は高速でモダンなオープンソースのデータベース GUI です ダウンロード
2026年1月7日 著者: Matthew Rathbone

一対多リレーションシップは、親テーブルの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

一対多リレーションシップについてさらに学ぶ

このブログの関連記事:

公式ドキュメント:

Beekeeper Studioは無料のオープンソースのデータベースGUIです

これまで使った中で最高のSQLクエリ&エディタツールです。データベースの管理に必要なものがすべて揃っています。 - ⭐⭐⭐⭐⭐ Mit

Beekeeper Studioは高速で直感的、そして使いやすいツールです。多数のデータベースに対応し、Windows、Mac、Linuxで快適に動作します。

BeekeeperのLinux版は100%フル機能。手抜きも機能の妥協も一切ありません。

Beekeeper Studio 利用者の声

★★★★★
「Beekeeper Studio は以前の SQL ワークフローを完全に置き換えてくれました。高速で直感的で、データベース作業がまた楽しくなりました。」
— Alex K.、データベース開発者
★★★★★
「多くのデータベース GUI を試してきましたが、Beekeeper は機能とシンプルさのバランスが完璧です。とにかくちゃんと動きます。」
— Sarah M.、フルスタックエンジニア