MySQLで自己結合(self join)を使う:社員階層と行比較

black laptop computer turned-on displaying source code on table SQL

SQLの自己結合(self join)とは、同じテーブルに2つの異なるエイリアスを割り当て、1つのクエリの中で結合する技法です。「社員テーブルから各社員と直属の上司の名前を並べて取得したい」「同じテーブル内で条件を満たす行どうしを横に比較したい」――そんな場面で自己結合が活躍します。特別な新構文はなく、通常のJOINにテーブルエイリアスを付けるだけです。本記事では、従業員テーブルを題材に基本構文・LEFT JOINとの使い分け・行比較の実用例・よくある落とし穴まで、MySQL 8.0の実例で順を追って解説します。

このシナリオで考える

以下の employees テーブルを使います。manager_id 列が同じテーブルの employee_id を参照する自己参照構造で、社員と上司の関係を1テーブルだけで表現しています。こうした「自分自身を外部キーで参照する」テーブル設計は、組織図・カテゴリツリー・コメントの返信スレッドなど、階層データを扱う場面でよく登場します。

CREATE TABLE employees (
  employee_id   INT PRIMARY KEY,
  employee_name VARCHAR(50)    NOT NULL,
  manager_id    INT,           -- NULLはトップ(社長)を示す
  salary        DECIMAL(10,2),
  FOREIGN KEY (manager_id) REFERENCES employees(employee_id)
);

INSERT INTO employees VALUES
(1, '鈴木一郎',  NULL,   200000),  -- 社長
(2, '田中花子',  1,      120000),  -- 営業部長
(3, '佐藤健太',  1,      130000),  -- 開発部長
(4, '山田次郎',  2,       75000),  -- 営業担当
(5, '伊藤美咲',  2,       78000),  -- 営業担当
(6, '小林翔',    3,       80000);  -- 開発担当

6名のうち鈴木一郎だけが manager_id = NULL(トップ)で、残りは誰かの部下です。このデータを使って以降のクエリを確認していきます。

INNER JOINで社員と上司を取得する

自己結合の核心は「同じテーブルに別々のエイリアスを付ける」ことです。employeese(社員側)と m(上司側)という2つのインスタンスとして扱い、e.manager_id = m.employee_id で結びつけます。

SELECT
  e.employee_id,
  e.employee_name AS 社員名,
  m.employee_name AS 上司名
FROM employees AS e
INNER JOIN employees AS m
  ON e.manager_id = m.employee_id
ORDER BY e.employee_id;
+-------------+-----------+-----------+
| employee_id | 社員名    | 上司名    |
+-------------+-----------+-----------+
|           2 | 田中花子  | 鈴木一郎  |
|           3 | 佐藤健太  | 鈴木一郎  |
|           4 | 山田次郎  | 田中花子  |
|           5 | 伊藤美咲  | 田中花子  |
|           6 | 小林翔    | 佐藤健太  |
+-------------+-----------+-----------+

鈴木一郎(社長)が結果に含まれていないことに気づきましたか。manager_idNULL のため、ON e.manager_id = m.employee_id の評価が UNKNOWN となり、INNER JOIN の条件を満たさないためです。SQLでは NULL = <任意の値> は常に UNKNOWN で、TRUE にはなりません。これは三値論理と呼ばれるSQLの基本原則です。

LEFT JOINで全員を取得する

社長(ルートノード)も含めた全社員を取得するには、INNER JOIN を LEFT JOIN に切り替えます。LEFT JOIN は結合先に一致する行がなくても、結合先の列を NULL で埋めて行を残してくれます。

SELECT
  e.employee_id,
  e.employee_name                   AS 社員名,
  COALESCE(m.employee_name, 'なし') AS 上司名
FROM employees AS e
LEFT JOIN employees AS m
  ON e.manager_id = m.employee_id
ORDER BY e.employee_id;
+-------------+-----------+-----------+
| employee_id | 社員名    | 上司名    |
+-------------+-----------+-----------+
|           1 | 鈴木一郎  | なし      |
|           2 | 田中花子  | 鈴木一郎  |
|           3 | 佐藤健太  | 鈴木一郎  |
|           4 | 山田次郎  | 田中花子  |
|           5 | 伊藤美咲  | 田中花子  |
|           6 | 小林翔    | 佐藤健太  |
+-------------+-----------+-----------+

COALESCE(m.employee_name, 'なし') がポイントです。上司のいない行では m.employee_name が NULL になるため、COALESCE で代替文字列に置き換えています。INNER JOINとLEFT JOINの選択基準は次のとおりです。

目的 使うべきJOIN
上司が存在する社員だけを取得したい INNER JOIN
全社員(上司なしのトップも含む)を取得したい LEFT JOIN

実用例:同じテーブルの行どうしを比較する

自己結合は「同じテーブル内の2行を条件で突き合わせる」用途にも使えます。代表的な2パターンを見ていきます。

給与が上司を上回る社員を検出する

部下の給与が上司の給与を超えているケースを探すには、次のクエリを使います。INNER JOIN の ON 句で上司と部下をつなぎ、WHERE 句で給与大小を絞り込みます。

SELECT
  e.employee_name AS 社員名,
  e.salary        AS 社員給与,
  m.employee_name AS 上司名,
  m.salary        AS 上司給与
FROM employees AS e
INNER JOIN employees AS m
  ON e.manager_id = m.employee_id
WHERE e.salary > m.salary;

給与逆転が存在するときのみ行が返ります。同じ発想で「同じ部署の平均給与と各社員の給与を比較する」「同カテゴリ内の最高評価と各商品の評価を並べる」といった用途にも応用できます。

同じ上司を持つ社員をペアで取り出す

同じ上司の直属部下どうしを組み合わせで取得する例です。同じ条件の行どうしを「ペアリング」するパターンで、チームメンバーの相性分析や重複レコードの検出などに使えます。

SELECT
  e1.employee_name AS 社員A,
  e2.employee_name AS 社員B,
  e1.manager_id    AS 共通の上司ID
FROM employees AS e1
INNER JOIN employees AS e2
  ON  e1.manager_id  = e2.manager_id
  AND e1.employee_id < e2.employee_id;  -- 重複排除・自己ペア防止
+-----------+-----------+----------------+
| 社員A     | 社員B     | 共通の上司ID   |
+-----------+-----------+----------------+
| 田中花子  | 佐藤健太  |              1 |
| 山田次郎  | 伊藤美咲  |              2 |
+-----------+-----------+----------------+

e1.employee_id < e2.employee_id の条件が重要です。この条件がないと「田中-佐藤」と「佐藤-田中」という重複ペアに加え、「田中-田中」という自己ペアまで出現します。

よくある落とし穴と注意点

  • エイリアスを付け忘れる:同じテーブルを2回参照する際、エイリアスなしでは列名があいまいになりエラーになります。FROM employees AS e のように必ず別名を付けてください。
  • ON句が抜けてデカルト積が発生する:結合条件を省略または間違えると行数が n² に膨れ上がります。100行のテーブルなら10,000行です。実行前に EXPLAIN で行数の見積もりを確認する習慣が大切です。
  • NULL との等値比較NULL = NULL は TRUE ではなく UNKNOWN です。ON 句に NULL が混在する列を使うと、意図した行が結合されません。ルートノードの検出には WHERE manager_id IS NULL を使います。
  • パフォーマンス対策:ON 句で使う列(manager_id など)にインデックスを張ると、結合のコストを大きく下げられます。CREATE INDEX idx_manager ON employees(manager_id); を忘れずに。
  • 多段階の階層には再帰CTEを使う:自己結合は1ホップ(直属の上司)しか辿れません。「社長→部長→課長→担当→…」と多段階を再帰的に取得するには WITH RECURSIVE(再帰CTE)が必要です。

まとめ

自己結合は同じテーブルに2つのエイリアスを付けて結合する技法で、階層構造の取得や行どうしの比較に活躍します。上司のいないトップ行も含めたいときは LEFT JOIN を選び、COALESCE で NULL を補完しましょう。ON 句の条件もれによるデカルト積と NULL の扱いには特に注意が必要です。多段階の階層を辿る場面では、再帰CTEへの移行も検討してください。

参考リンク

アイキャッチ画像: Photo by Jantine Doornbos on Unsplash

タイトルとURLをコピーしました