組織図・カテゴリツリー・部品表(BOM)のように、データが親子関係で連なる構造を扱うとき、Oracle固有の階層問い合わせ(Hierarchical Query)が非常に強力です。START WITHとCONNECT BY PRIORの組み合わせにより、再帰的なJOINを書かずに木構造を一発で展開できます。本記事ではサンプルデータを使いながら、基本構文からLEVEL・SYS_CONNECT_BY_PATH・CONNECT_BY_ROOT・NOCYCLEなどの応用機能まで体系的に解説します。
- START WITH / CONNECT BY PRIOR の基本構文と動作の仕組み
- LEVEL 擬似列で階層の深さを取得する方法
- SYS_CONNECT_BY_PATH でフルパス文字列を生成する方法
- CONNECT_BY_ROOT で最上位ノードを取得する方法
- CONNECT_BY_ISLEAF で末端ノードを判定する方法
- ORDER SIBLINGS BY で兄弟ノードの並び順を制御する方法
- NOCYCLE で循環参照を検出・回避する方法
- WHERE と CONNECT BY 条件の違い(行除外 vs 枝除外)
- 組織図・部品表への実務適用パターン
- WITH句(再帰CTE)との使い分け
- 階層問い合わせとは
- サンプルデータの準備
- 基本構文:START WITH と CONNECT BY PRIOR
- 上から下へ(トップダウン)の展開
- 下から上へ(ボトムアップ)の展開
- LEVEL と LPAD でツリー表示する
- SYS_CONNECT_BY_PATH でフルパスを生成する
- CONNECT_BY_ROOT で最上位ノードを取得する
- CONNECT_BY_ISLEAF で末端ノードを判定する
- ORDER SIBLINGS BY で兄弟ノードの並び順を制御する
- NOCYCLE で循環参照を検出・回避する
- WHERE と CONNECT BY 条件の違い(行除外 vs 枝除外)
- 深さを制限して取得する(LEVEL <= n)
- 実務パターン:指定部門の配下を全て取得する
- 実務パターン:ルートまでのパスを辿る
- 実務パターン:同じルートに属するノード同士を横断検索する
- WITH句(再帰CTE)との使い分け
- パフォーマンスの注意点
- よく使う擬似列・関数まとめ
- まとめ
階層問い合わせとは
リレーショナルデータベースのテーブルは本来フラットな表形式ですが、parent_idのような自己参照列を使って「木(ツリー)構造」を表現することがよくあります。次のような場面で頻繁に登場します。
- 組織図・部署ツリー:経営管理部 → 営業本部 → 営業1部 のような構造
- 商品カテゴリ:電化製品 → スマートフォン → iPhoneシリーズ
- 部品表(BOM):製品 → ユニット → 部品 → サブ部品
- コメントスレッド:親コメント → 返信 → 返信の返信
このようなデータを取得するには、通常は再帰的なJOINや複数回のクエリが必要になります。OracleはSQLにSTART WITH・CONNECT BY句を追加することで、ツリー構造の展開を1つのSELECT文で実現できます。
サンプルデータの準備
以降の例で使用するサンプルテーブルを作成します。部門ツリーを想定した自己参照テーブルです。
CREATE TABLE dept_tree (
id NUMBER PRIMARY KEY,
name VARCHAR2(50) NOT NULL,
parent_id NUMBER
);
INSERT INTO dept_tree VALUES (1, '経営管理部', NULL);
INSERT INTO dept_tree VALUES (2, '営業本部', 1);
INSERT INTO dept_tree VALUES (3, '開発本部', 1);
INSERT INTO dept_tree VALUES (4, '営業1部', 2);
INSERT INTO dept_tree VALUES (5, '営業2部', 2);
INSERT INTO dept_tree VALUES (6, 'インフラチーム', 3);
INSERT INTO dept_tree VALUES (7, 'アプリチーム', 3);
INSERT INTO dept_tree VALUES (8, 'モバイル班', 7);
COMMIT;
このテーブルは次のような木構造を表しています。
経営管理部
├── 営業本部
│ ├── 営業1部
│ └── 営業2部
└── 開発本部
├── インフラチーム
└── アプリチーム
└── モバイル班
基本構文:START WITH と CONNECT BY PRIOR
階層問い合わせの基本構文は次のとおりです。
SELECT 列名, ... FROM テーブル名 [WHERE 条件] START WITH 開始条件 -- どのノードから展開を始めるか CONNECT BY PRIOR 親キー列 = 子キー列; -- 親子関係の定義
| 句 | 役割 |
|---|---|
| START WITH | ツリー展開の起点となるノードを指定する。ルートノードには通常 parent_id IS NULL を指定する。 |
| CONNECT BY PRIOR | 親子関係を定義する。PRIOR キーワードは「直前の(親の)行の値」を参照することを示す。 |
上から下へ(トップダウン)の展開
ルートノードから末端ノードに向かって展開するのが最も基本的なパターンです。PRIOR を親の主キー側に置きます。
SELECT
LEVEL,
id,
name,
parent_id
FROM dept_tree
START WITH parent_id IS NULL -- parent_idがNULLのレコード(ルート)から開始
CONNECT BY PRIOR id = parent_id; -- 「親のid」=「子のparent_id」の関係で展開
実行結果のイメージです。LEVELはツリーの深さを示す擬似列で、ルートが1になります。
| LEVEL | ID | NAME | PARENT_ID |
|---|---|---|---|
| 1 | 1 | 経営管理部 | (null) |
| 2 | 2 | 営業本部 | 1 |
| 3 | 4 | 営業1部 | 2 |
| 3 | 5 | 営業2部 | 2 |
| 2 | 3 | 開発本部 | 1 |
| 3 | 6 | インフラチーム | 3 |
| 3 | 7 | アプリチーム | 3 |
| 4 | 8 | モバイル班 | 7 |
CONNECT BY PRIOR id = parent_id は「親の id」と「子の parent_id」をつなぐ意味です。PRIOR は必ず 親側のキー に付けます。逆に書くと、次に説明するボトムアップになります。
下から上へ(ボトムアップ)の展開
PRIOR を子キー側(parent_id)に置くことで、指定ノードからルートへ向かう方向に展開できます。特定ノードの「上位組織を辿る」場面で使います。
SELECT
LEVEL,
name
FROM dept_tree
START WITH id = 8 -- 「モバイル班」(末端ノード)から開始
CONNECT BY PRIOR parent_id = id; -- 「親のparent_id」=「子のid」で逆方向に展開
結果として「モバイル班 → アプリチーム → 開発本部 → 経営管理部」と根まで辿れます。
LEVEL と LPAD でツリー表示する
LEVEL 擬似列と LPAD 関数を組み合わせると、インデント付きのツリー表示が簡単に作れます。
SELECT
LEVEL,
LPAD(' ', (LEVEL - 1) * 3, ' ') || name AS tree_view
FROM dept_tree
START WITH parent_id IS NULL
CONNECT BY PRIOR id = parent_id;
実行結果のイメージです。
LEVEL TREE_VIEW
----- -------------------------
1 経営管理部
2 営業本部
3 営業1部
3 営業2部
2 開発本部
3 インフラチーム
3 アプリチーム
4 モバイル班
SYS_CONNECT_BY_PATH でフルパスを生成する
SYS_CONNECT_BY_PATH(列名, 区切り文字) を使うと、ルートから現在ノードまでのパス文字列を自動的に生成できます。カテゴリのパンくずリスト生成や、データの階層を視覚的に確認するのに便利です。
SELECT
LEVEL,
name,
SYS_CONNECT_BY_PATH(name, ' / ') AS full_path
FROM dept_tree
START WITH parent_id IS NULL
CONNECT BY PRIOR id = parent_id;
| LEVEL | NAME | FULL_PATH |
|---|---|---|
| 1 | 経営管理部 | / 経営管理部 |
| 2 | 営業本部 | / 経営管理部 / 営業本部 |
| 3 | 営業1部 | / 経営管理部 / 営業本部 / 営業1部 |
| 4 | モバイル班 | / 経営管理部 / 開発本部 / アプリチーム / モバイル班 |
SYS_CONNECT_BY_PATH の第2引数(区切り文字)はパス中に登場しない文字列を選んでください。区切り文字がノード名と同じ文字を含む場合、パスが正しく解析できなくなります。
CONNECT_BY_ROOT で最上位ノードを取得する
CONNECT_BY_ROOT 列名 を使うと、そのノードのツリー上の最上位(ルート)の値を取得できます。「この部署のトップは誰か」を一発で調べるのに役立ちます。
SELECT
LEVEL,
name,
CONNECT_BY_ROOT name AS root_dept
FROM dept_tree
START WITH parent_id IS NULL
CONNECT BY PRIOR id = parent_id;
全行で root_dept に「経営管理部」が入ります。複数のルートがある場合、それぞれのルートが返ります。
CONNECT_BY_ISLEAF で末端ノードを判定する
CONNECT_BY_ISLEAF 擬似列は、そのノードが末端(子を持たないリーフノード)であれば 1、そうでなければ 0 を返します。末端ノードのみに処理を行いたい場合に使います。
SELECT
LEVEL,
name,
CONNECT_BY_ISLEAF AS is_leaf
FROM dept_tree
START WITH parent_id IS NULL
CONNECT BY PRIOR id = parent_id;
| NAME | IS_LEAF | 意味 |
|---|---|---|
| 経営管理部 | 0 | 子ノードあり(中間ノード) |
| 営業1部 | 1 | 子ノードなし(末端ノード) |
| モバイル班 | 1 | 子ノードなし(末端ノード) |
ORDER SIBLINGS BY で兄弟ノードの並び順を制御する
通常の ORDER BY を使うと階層構造が崩れてしまいます。同じ親を持つ兄弟ノード同士の並び順だけを変えたい場合は ORDER SIBLINGS BY を使います。
SELECT
LEVEL,
LPAD(' ', (LEVEL - 1) * 3, ' ') || name AS tree_view
FROM dept_tree
START WITH parent_id IS NULL
CONNECT BY PRIOR id = parent_id
ORDER SIBLINGS BY name; -- 兄弟間のみ name 順。親子の階層は維持される
ORDER BY name だけを書くと、名前のアルファベット順でフラットにソートされ、親子関係が無視されたバラバラな並び順になります。ツリー構造を保ったまま並び替えるには必ず ORDER SIBLINGS BY を使ってください。
NOCYCLE で循環参照を検出・回避する
データに誤りがあって循環参照(A → B → C → A のような輪)が存在すると、CONNECT BY は無限ループに陥り ORA-01436: CONNECT BY loop in user data エラーになります。NOCYCLE キーワードを追加することで、循環を検出しながら処理を継続できます。
SELECT
LEVEL,
name,
CONNECT_BY_ISCYCLE AS is_cycle -- 1=循環検出, 0=正常
FROM dept_tree
START WITH parent_id IS NULL
CONNECT BY NOCYCLE PRIOR id = parent_id;
CONNECT_BY_ISCYCLE が 1 の行が循環参照を起こしているノードです。本番データの整合性チェックとしても活用できます。
WHERE と CONNECT BY 条件の違い(行除外 vs 枝除外)
特定ノードを除外したい場合、WHERE 句と CONNECT BY 条件では動作が異なります。この違いは実務で頻繁に誤解されるポイントです。
WHERE 句での除外(行のみ除外、子孫は残る)
SELECT LEVEL, name FROM dept_tree WHERE name != '営業本部' -- 「営業本部」の行だけ消えるが、配下の「営業1部」「営業2部」は残る START WITH parent_id IS NULL CONNECT BY PRIOR id = parent_id;
CONNECT BY 条件での除外(枝ごと除外)
SELECT LEVEL, name FROM dept_tree START WITH parent_id IS NULL CONNECT BY PRIOR id = parent_id AND name != '営業本部'; -- 「営業本部」とその配下全体(営業1部・営業2部)が除外される
| 除外方法 | 該当行 | 子孫ノード | 使いどころ |
|---|---|---|---|
| WHERE 句 | 除外される | 残る(孤立して表示) | 特定行だけを非表示にしたいとき |
| CONNECT BY 条件 | 除外される | 一緒に除外される | サブツリーごと非表示にしたいとき |
深さを制限して取得する(LEVEL <= n)
大規模なツリーで全階層を取得するとデータが膨大になる場合、CONNECT BY 条件に LEVEL <= n を追加して深さを制限できます。
SELECT
LEVEL,
LPAD(' ', (LEVEL - 1) * 3, ' ') || name AS tree_view
FROM dept_tree
START WITH parent_id IS NULL
CONNECT BY PRIOR id = parent_id
AND LEVEL <= 2; -- 2階層まで展開(経営管理部・営業本部・開発本部のみ)
実務パターン:指定部門の配下を全て取得する
特定の部門を起点に、配下の全組織と深さ・パスを一覧で取得するパターンです。組織図レポートや権限管理で頻繁に使います。
SELECT
LEVEL - 1 AS depth,
name,
CONNECT_BY_ISLEAF AS is_leaf,
SYS_CONNECT_BY_PATH(name, ' > ') AS full_path
FROM dept_tree
START WITH id = 3 -- 「開発本部」を起点
CONNECT BY PRIOR id = parent_id
ORDER SIBLINGS BY id;
| DEPTH | NAME | IS_LEAF | FULL_PATH |
|---|---|---|---|
| 0 | 開発本部 | 0 | > 開発本部 |
| 1 | インフラチーム | 1 | > 開発本部 > インフラチーム |
| 1 | アプリチーム | 0 | > 開発本部 > アプリチーム |
| 2 | モバイル班 | 1 | > 開発本部 > アプリチーム > モバイル班 |
実務パターン:ルートまでのパスを辿る
末端ノードから根まで辿ることで「このノードの上位組織は何か」を一覧できます。申請承認フローで「承認者の上長を全て取得する」ような場面に適しています。
SELECT
LEVEL AS steps_to_root,
name,
CONNECT_BY_ROOT name AS root_name
FROM dept_tree
START WITH id = 8 -- 「モバイル班」から開始
CONNECT BY PRIOR parent_id = id -- 逆方向(子→親)に辿る
ORDER BY LEVEL;
実務パターン:同じルートに属するノード同士を横断検索する
あるノードと同じルートを持つ全ノードを取得したい場合、CONNECT_BY_ROOT で絞り込む方法が使えます。
-- 「営業1部」(id=4)と同じルートに属する全ノードを取得する
SELECT name, LEVEL
FROM dept_tree
START WITH parent_id IS NULL
CONNECT BY PRIOR id = parent_id
HAVING CONNECT_BY_ROOT id = (
SELECT CONNECT_BY_ROOT id
FROM dept_tree
START WITH id = 4
CONNECT BY PRIOR parent_id = id
WHERE parent_id IS NULL -- ルートノードだけ取得
);
WITH句(再帰CTE)との使い分け
Oracle 11g R2 以降では、ANSI SQL 標準の再帰WITH句(再帰CTE)でも階層問い合わせと同等の処理が書けます。WITH句の使い方については「【Oracle】WITH句(共通表式)の使い方完全ガイド」で詳しく解説しています。
同じ結果を得る2つの書き方を比較します。
SELECT
LEVEL,
name,
SYS_CONNECT_BY_PATH(name, ' / ') AS path
FROM dept_tree
START WITH parent_id IS NULL
CONNECT BY PRIOR id = parent_id;
WITH tree (id, name, parent_id, lvl, path) AS (
-- アンカー部分:ルートノードを取得
SELECT
id, name, parent_id,
1 AS lvl,
CAST(name AS VARCHAR2(500)) AS path
FROM dept_tree
WHERE parent_id IS NULL
UNION ALL
-- 再帰部分:子ノードを追加していく
SELECT
d.id, d.name, d.parent_id,
t.lvl + 1,
CAST(t.path || ' / ' || d.name AS VARCHAR2(500))
FROM dept_tree d
JOIN tree t ON d.parent_id = t.id
)
SELECT lvl AS "LEVEL", name, ' / ' || path AS path
FROM tree
ORDER BY path;
| 比較項目 | CONNECT BY | WITH句(再帰CTE) |
|---|---|---|
| Oracle対応バージョン | 全バージョン | 11g R2 以降 |
| ANSI SQL 標準 | Oracle固有 | 標準SQL(PostgreSQL・SQL Serverでも動く) |
| 記述量 | 少ない(簡潔) | 多い(冗長になりやすい) |
| パス生成(SYS_CONNECT_BY_PATH相当) | 関数1行で書ける | 再帰列として自分で計算が必要 |
| LEVEL擬似列 | 自動で利用可能 | 自分でカウント列を定義する必要あり |
| 循環参照の検出 | NOCYCLE で簡単に対応 | CYCLE句で対応(Oracle 18c+) |
| 移植性 | 低い(Oracle専用) | 高い(他DBMSに移行しやすい) |
Oracleのみで動かすことが確定している場合は
CONNECT BY が簡潔で高機能です。PostgreSQLやSQL Serverへの移植可能性が必要な場合、またはANSI標準に統一したい場合は WITH句(再帰CTE)を選んでください。
パフォーマンスの注意点
階層問い合わせはデータ量や深さによってパフォーマンスに差が出ます。以下の点を意識してください。
parent_id 列にインデックスを付ける
CREATE INDEX idx_dept_tree_parent
ON dept_tree (parent_id);
-- CONNECT BY が parent_id を頻繁に参照するため、インデックスが効果的
LEVEL で深さを制限する
深さが際限なく増えるデータ構造(または循環参照を含む可能性がある場合)では、LEVEL <= 最大深さ の条件を入れることで無限展開を防げます。
必要な列だけ SELECT する
階層展開ではすべての列を取得せず、必要な列に絞ることでI/Oコストを削減できます。特にテーブルに大きな文字列列やCLOB列がある場合は顕著に効いてきます。
実行計画を確認する
EXPLAIN PLAN FOR SELECT LEVEL, name FROM dept_tree START WITH parent_id IS NULL CONNECT BY PRIOR id = parent_id; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY); -- 「CONNECT BY WITH FILTERING」と表示されれば最適化されたアクセスパスが選ばれている
よく使う擬似列・関数まとめ
| 名前 | 種別 | 説明 |
|---|---|---|
LEVEL |
擬似列 | ルートを1とした階層の深さ |
CONNECT_BY_ISLEAF |
擬似列 | 末端ノードなら1、中間ノードなら0 |
CONNECT_BY_ISCYCLE |
擬似列 | 循環参照を検出したら1(NOCYCLEと併用) |
SYS_CONNECT_BY_PATH(列, 区切) |
関数 | ルートから現在ノードまでのパス文字列 |
CONNECT_BY_ROOT 列名 |
演算子 | 現在ノードのルートノードの列値を返す |
まとめ
Oracleの階層問い合わせは、START WITHとCONNECT BY PRIORという2つの句だけで木構造の展開を実現できる、非常に実用的な機能です。
- トップダウン展開は
CONNECT BY PRIOR 親キー = 子キー、ボトムアップは逆にPRIOR 子キー = 親キーと書く LEVEL・SYS_CONNECT_BY_PATH・CONNECT_BY_ROOT・CONNECT_BY_ISLEAFを組み合わせると多彩な情報が取れるORDER SIBLINGS BYで兄弟ノードの並び順を制御できる(ORDER BYだと階層が崩れる)- 循環参照があるデータには
NOCYCLEを付けて安全に処理する WHERE句とCONNECT BY条件では除外範囲が異なる(行のみ vs 枝ごと)- 移植性が必要な場合は WITH句(再帰CTE)を検討する
階層構造を持つデータはどのシステムにも登場します。CONNECT BY をマスターしておくと、複雑な組織図や部品表の集計・表示が格段に楽になります。

