コマンド道場

再帰 CTE(WITH RECURSIVE で階層をたどる)の使い方

起点から 1 段ずつ増えていく

1 回目起点(親のない部署)

lvbusho_name
1経営

2 回目その子を足す

lvbusho_name
2開発
2営業
2管理

結果たどりきったところ

lvbusho_name
1経営
2開発
2営業
  • 起点(親のない部署) 1その子を足す 3たどりきったところ 7
  • 前の段の結果が次の段の入力になります。表が 1 つ増えたつもりで読んでください

1 部署から始めて、その子を足し、さらにその子を足して 7 部署になります。何回繰り返すかを人が指定していないことに注目してください。

WITH RECURSIVE は、自分自身を参照する問い合わせです。 「部署の親子」「上司の上司」のように何段あるか分からない階層をたどるための書き方で、ふつうの結合や派生表では代用できません。起点繰り返しの 2 つに分けて書きます。

この書き方が使えない製品があります そのまま使える: PostgreSQL / MySQL / MariaDB / SQLite

  • SQL Server では使えませんWITH RECURSIVE(再帰)
  • Oracle Database では使えませんWITH RECURSIVE(再帰)

未確認: IBM Db2

製品ごとの対応表(根拠つき)を見る

このページの実行結果と図は、すべて本物のデータベースに同じ SQL を流して得たものです。手で書いた「たぶんこうなる」ではありません。

01起点と繰り返しを UNION ALL でつなぐ

結論: WITH RECURSIVE 名前 AS (起点 UNION ALL 繰り返し) の形です。

2 つの部分に分かれます。

  • 起点 … 自分自身を参照しない問い合わせ。ここから始まる
  • 繰り返し自分自身の名前を参照する問い合わせ。前の回で増えた行を相手にする

動きは単純です。まず起点を実行し、次に「前の回で新しく増えた行」を相手に繰り返しを実行します。新しい行が 1 つも増えなくなったら終わりです。

⚠️ RECURSIVE を書き忘れると、自分自身の名前が見つからずエラーになります。名前を先に登録するための宣言だと考えてください。

1 行ずつ、何をしているか
  1. WITH RECURSIVE kaisou AS (★自分を参照すると宣言する
  2. SELECT busho_id, busho_name, 1 AS lv★起点
  3. FROM busho WHERE parent_busho_id IS NULL親のない部署から始める
  4. UNION ALL★ここでつなぐ
  5. SELECT c.busho_id, c.busho_name, k.lv + 1★繰り返し(段を 1 つ増やす)
  6. FROM busho c JOIN kaisou k自分自身(kaisou)を相手にする
  7. ON c.parent_busho_id = k.busho_id)前の回の行の子を取る
  8. SELECT lv, busho_name FROM kaisou本体はふつうに書ける
  9. ORDER BY lv, busho_id;結果の並べ方

繰り返しの側に kaisou が出てくるのが再帰の目印です。ここが無ければ、ふつうの WITH と同じです。

部署の階層をたどる
WITH RECURSIVE kaisou AS (
  SELECT busho_id, busho_name, 1 AS lv
  FROM busho
  WHERE parent_busho_id IS NULL
  UNION ALL
  SELECT c.busho_id, c.busho_name, k.lv + 1
  FROM busho c
    JOIN kaisou k ON c.parent_busho_id = k.busho_id
)
SELECT lv, busho_name
FROM kaisou
ORDER BY lv, busho_id;
lvbusho_name
1経営
2開発
2営業
2管理
3基盤
3法務
3マーケ

7

7 行です。経営が 1 段目、その子の開発・営業・管理が 2 段目、さらにその子の基盤・法務・マーケが 3 段目になっています。

⚠️ この書き方は SQL Server / Oracle Database では使えません(WITH RECURSIVE(再帰))。製品ごとの対応表

02何段あるかを知らなくてよい

結論: 繰り返す回数を書かないのが再帰の利点です。

結合で階層をたどろうとすると、busho を何回結合すればよいかをあらかじめ決めなければなりません。3 段なら 3 回、4 段になったら書き換えが要ります。

再帰なら「子が無くなるまで」で済むので、データの深さが変わっても SQL は変わりません

段の番号(lv)は自分で作ります。起点で 1 を置き、繰り返しで + 1 するだけです。この列があると、どこまで下りたのかが結果から読み取れます。

段ごとに何部署あるか数える
WITH RECURSIVE kaisou AS (
  SELECT busho_id, 1 AS lv
  FROM busho
  WHERE parent_busho_id IS NULL
  UNION ALL
  SELECT c.busho_id, k.lv + 1
  FROM busho c
    JOIN kaisou k ON c.parent_busho_id = k.busho_id
)
SELECT lv, count(*) AS n
FROM kaisou
GROUP BY lv
ORDER BY lv;
lvn
11
23
33

3

1 段目が 1 部署、2 段目が 3 部署、3 段目が 3 部署でした。合計すると 7 部署で、上の例と一致します。

⚠️ この書き方は SQL Server / Oracle Database では使えません(WITH RECURSIVE(再帰))。製品ごとの対応表

03上へさかのぼることもできる

結論: たどる向きは結合条件で決まります。 子をたどれば下へ、親をたどれば上へ進みます。

社員表には上司の列があります。ある社員から始めて上司をたどっていけば、その人の上に何人いるかが分かります。

起点を「ある 1 人」にして、繰り返しで「前の回の人の上司」を取るだけです。上司がいない人まで来れば、新しい行が増えなくなって止まります。

⚠️ 起点の書き方で結果はまったく変わります。どこから始めるかを先に決めてください。

木村から上司をさかのぼる
WITH RECURSIVE up AS (
  SELECT shain_id, name, joushi_id, 1 AS lv
  FROM shain
  WHERE shain_id = 13
  UNION ALL
  SELECT s.shain_id, s.name, s.joushi_id, u.lv + 1
  FROM shain s
    JOIN up u ON s.shain_id = u.joushi_id
)
SELECT lv, name
FROM up
ORDER BY lv;
lvname
1木村
2井上
3山田
4鈴木

4

4 行です。木村 → 井上 → 山田 → 鈴木 とたどり、上司のいない鈴木で止まりました。

⚠️ この書き方は SQL Server / Oracle Database では使えません(WITH RECURSIVE(再帰))。製品ごとの対応表

04⚠️ よくある間違い:止まる条件は自分で作る

結論: 新しい行が増えなくなるまで繰り返します。 増え続ける書き方をすると止まりません。

階層をたどる場合は、いつか子(または親)が無くなるので自然に止まります。しかし数を増やしていくような書き方では、条件を書かない限り止まりません。

止める方法は 2 つです。

  • 繰り返しの側に WHERE で上限を書く
  • たどる相手がいつか尽きる構造にする

⚠️ データに輪(循環)があると止まりません。 「A の親が B、B の親が A」のような状態です。階層を扱うときは、そもそも輪ができない作りにしておくのが本筋です。

上限を書いて止める
WITH RECURSIVE kazu AS (
  SELECT 1 AS n
  UNION ALL
  SELECT n + 1 FROM kazu WHERE n < 5
)
SELECT n FROM kazu ORDER BY n;
n
1
2
3
4
5

5

5 行です。繰り返しの側の WHERE が無ければ、この文は永遠に行を増やし続けます。

⚠️ この書き方は SQL Server / Oracle Database では使えません(WITH RECURSIVE(再帰))。製品ごとの対応表

自分で打ってみる

このページの例と同じデータが入った本物のデータベースを、この場(あなたのブラウザの中)で起動できます。打った SQL がサーバーへ送られることはありません。

押すと初回だけ約 9MB を読み込みます(2 回目以降はキャッシュされます)。

解いてみる

読んだだけでは書けるようになりません。実行結果で採点します。

関連するトピック

根拠(一次情報)