【Oracle】階層問い合わせの使い方完全ガイド|START WITH・CONNECT BY・LEVEL・SYS_CONNECT_BY_PATH・CONNECT_BY_ROOTまで解説

【Oracle】階層問い合わせの使い方完全ガイド|START WITH・CONNECT BY・LEVEL・SYS_CONNECT_BY_PATH・CONNECT_BY_ROOTまで解説 Oracle

組織図・カテゴリツリー・部品表(BOM)のように、データが親子関係で連なる構造を扱うとき、Oracle固有の階層問い合わせ(Hierarchical Query)が非常に強力です。START WITHCONNECT BY PRIORの組み合わせにより、再帰的なJOINを書かずに木構造を一発で展開できます。本記事ではサンプルデータを使いながら、基本構文からLEVELSYS_CONNECT_BY_PATHCONNECT_BY_ROOTNOCYCLEなどの応用機能まで体系的に解説します。

この記事でわかること

  • 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)との使い分け
スポンサーリンク

階層問い合わせとは

リレーショナルデータベースのテーブルは本来フラットな表形式ですが、parent_idのような自己参照列を使って「木(ツリー)構造」を表現することがよくあります。次のような場面で頻繁に登場します。

  • 組織図・部署ツリー:経営管理部 → 営業本部 → 営業1部 のような構造
  • 商品カテゴリ:電化製品 → スマートフォン → iPhoneシリーズ
  • 部品表(BOM):製品 → ユニット → 部品 → サブ部品
  • コメントスレッド:親コメント → 返信 → 返信の返信

このようなデータを取得するには、通常は再帰的なJOINや複数回のクエリが必要になります。OracleはSQLにSTART WITHCONNECT 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
PRIOR の位置に注意
CONNECT BY PRIOR id = parent_id は「親の id」と「子の parent_id」をつなぐ意味です。PRIOR は必ず 親側のキー に付けます。逆に書くと、次に説明するボトムアップになります。

下から上へ(ボトムアップ)の展開

PRIOR を子キー側(parent_id)に置くことで、指定ノードからルートへ向かう方向に展開できます。特定ノードの「上位組織を辿る」場面で使います。

ボトムアップ階層問い合わせ(id=8 から根へ辿る)
SELECT
    LEVEL,
    name
FROM dept_tree
START WITH id = 8                  -- 「モバイル班」(末端ノード)から開始
CONNECT BY PRIOR parent_id = id;   -- 「親のparent_id」=「子のid」で逆方向に展開

結果として「モバイル班 → アプリチーム → 開発本部 → 経営管理部」と根まで辿れます。

LEVEL と LPAD でツリー表示する

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(列名, 区切り文字) を使うと、ルートから現在ノードまでのパス文字列を自動的に生成できます。カテゴリのパンくずリスト生成や、データの階層を視覚的に確認するのに便利です。

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 列名 を使うと、そのノードのツリー上の最上位(ルート)の値を取得できます。「この部署のトップは誰か」を一発で調べるのに役立ちます。

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 を返します。末端ノードのみに処理を行いたい場合に使います。

CONNECT_BY_ISLEAF ── 末端ノードの判定
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 を使います。

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 キーワードを追加することで、循環を検出しながら処理を継続できます。

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_ISCYCLE1 の行が循環参照を起こしているノードです。本番データの整合性チェックとしても活用できます。

WHERE と CONNECT BY 条件の違い(行除外 vs 枝除外)

特定ノードを除外したい場合、WHERE 句と CONNECT BY 条件では動作が異なります。この違いは実務で頻繁に誤解されるポイントです。

WHERE 句での除外(行のみ除外、子孫は残る)

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 条件での除外(枝ごと除外)

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 を追加して深さを制限できます。

LEVEL で深さを2階層に制限する
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つの書き方を比較します。

CONNECT BY での記述
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句(再帰CTE)での同等記述
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 列にインデックスを付ける

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 WITHCONNECT BY PRIORという2つの句だけで木構造の展開を実現できる、非常に実用的な機能です。

  • トップダウン展開は CONNECT BY PRIOR 親キー = 子キー、ボトムアップは逆に PRIOR 子キー = 親キー と書く
  • LEVELSYS_CONNECT_BY_PATHCONNECT_BY_ROOTCONNECT_BY_ISLEAF を組み合わせると多彩な情報が取れる
  • ORDER SIBLINGS BY で兄弟ノードの並び順を制御できる(ORDER BY だと階層が崩れる)
  • 循環参照があるデータには NOCYCLE を付けて安全に処理する
  • WHERE 句と CONNECT BY 条件では除外範囲が異なる(行のみ vs 枝ごと)
  • 移植性が必要な場合は WITH句(再帰CTE)を検討する

階層構造を持つデータはどのシステムにも登場します。CONNECT BY をマスターしておくと、複雑な組織図や部品表の集計・表示が格段に楽になります。