SSブログ

WITH 再帰クエリ SQLポケリ [SQLポケリ]

前回は、WITH句と再帰クエリについて紹介した。今回は、その続きである。

再帰処理とツリー構造
再帰処理は、ツリー構造のデータ構造を扱う際に、よく使用される。XMLの要素はツリー構造になっている。各要素をくまなく回ってみていく際に、再帰処理を使うと簡単に書けたりする。

SQLでは表形式のデータを扱うことが多いので、再帰処理はあまり得意ではない。SELECT命令にしても、ループ処理をするための制御命令を便利にしただけという印象。

最近拡張された、WITHによって「再帰クエリ」が可能になった、というわけである。

表形式のデータでもツリー構造にすることができる。簡単に実装する方法に、親へのポインタを持つ方法がある。C
言語ならポインタとなるが、テーブルなら行番号を持たせれば良いであろう。テーブルを作成するとしたら、以下のようになる。

CREATE TABLE TREE_DATA (
 NODE_NO INTEGER NOT NULL PRIMARY KEY,
 PARENT_NO INTEGER,
 NODE_DATA INTEGER
);

INSERT INTO TREE_DATA VALUES(1, NULL, 100);
INSERT INTO TREE_DATA VALUES(2, 1, 200);
INSERT INTO TREE_DATA VALUES(3, 1, 300);
INSERT INTO TREE_DATA VALUES(4, 2, 100);


図にしたら以下のような感じ。

 1
 ┣ 2
 ┃ ┗ 4
 ┗ 3

NODE_NO=1の行には親が存在しない。従って、PARENT_NOの列はNULLとなっている。
NODE_NO=1の行には、子のノードとして、NODE_NO=2と3の二つがぶら下がっている。PARENT_NOの列がどちらも1になっている。
NODE_NO=2の行には、子ノードNODE_NO=4がある。
NODE_NO=3と4の行には、子ノードが存在しない。

PARENT_NOがNULLである行は、子ノードを持たない、いわゆるルートノードである。これは以下のようなSELECT命令で取得できる。

SELECT * FROM TREE_DATA WHERE PARENT_NO IS NULL

NODE_NO  PARENT_NO  NODE_DATA
-----------------------------
1        NULL       100


ルートノードは、NODE_NO=1の行が一つだけということがわかる。
ノード1の子ノードを列挙しなさい、と言われたら、以下のSELECT命令で取得できる。

SELECT * FROM TREE_DATA WHERE PARENT_NO = 1

NODE_NO  PARENT_NO  NODE_DATA
-----------------------------
2        1          200
3        1          300


NODE_NO=2と3の二つの子ノードがある。
さらに、ノード2の子ノードを列挙するなら以下とすればOK

SELECT * FROM TREE_DATA WHERE PARENT_NO = 2

NODE_NO  PARENT_NO  NODE_DATA
-----------------------------
4        2          100


そんなに難しくない。
ノード3なら以下。

SELECT * FROM TREE_DATA WHERE PARENT_NO = 3

NODE_NO  PARENT_NO  NODE_DATA
-----------------------------


ノード3には子ノードが存在しないので、結果は空となる。

これらのSELECT命令を順に実行できれば、ツリー構造を「舐めた」と言えるのであるが、PARENT_NO=1とか、2とかの条件はツリー構造の図から人力で拾ってきた。ここをSELECT命令でなんとか書きたいっていうのが再帰クエリの主な目的。
WITHを使うとできるようになっちゃうのだが、この書き方がちょっと慣れが必要。

まず、WITH内のSELECT命令に、UNION ALLが必要。それに、初期化ブランチというものが必要であることは、前でも述べた。
初期化ブランチというのは、初期化を行うためのクエリで、「ツリー構造を舐める」クエリの場合、ルートノードを見つけるというのが初期化ブランチになる。ブランチ、と呼んでいるが、UNION ALLで繋げたクエリのどちらか、と考えるとスッキリするかも知れない。
初期化ブランチでは、再帰呼び出しをしてはいけない。再帰呼び出しをすると無限ループになるからね。

前記事でのWITH再帰クエリをもう一度見てみよう。

WITH vfoo(a) AS (
  SELECT a FROM vfoo
  UNION ALL
  SELECT a FROM vfoo
)
SELECT * FROM vfoo
 
 ORA-32043: 再帰的WITH句には初期化ブランチが必要です


エラーとなってしまうのは、UNION ALLで繋げた両方のクエリで再帰呼び出しになってしまっているから。以下のように変更して実行してみると、エラーの様子が変化する。

WITH vfoo(a) AS (
  SELECT a FROM foo WHERE a = 'A' -- 再帰しない
  UNION ALL
  SELECT a FROM vfoo
)
SELECT * FROM vfoo

 ORA-32044: 再帰的WITH問合せの実行中にサイクルが検出されました


しかし、これでもエラーになってしまっている。「サイクルが検出」というのは、無限ループに陥ってますよ、と言うこと。UNION ALLで繋げた後の方のクエリで再帰している。
ここの再帰の様子を変更すれば、実行可能な再帰クエリとなる。TREE_DATAの例でやってみよう。

WITH td(NNO, PNO, NDATA) AS (
  SELECT NODE_NO, PARENT_NO, NODE_DATA
    FROM TREE_DATA WHERE PARENT_NO IS NULL
  UNION ALL
  SELECT TD2.NODE_NO, TD2.PARENT_NO, TD2.NODE_DATA
    FROM td INNER JOIN TREE_DATA TD2
     ON td.NNO = TD2.PARENT_NO
)
SELECT * FROM td

  NNO  PNO   NDATA
  -----------------
  1    NULL  100
  2    1     200
  3    1     300
  4    2     100


UNION ALLの前のクエリは、初期化ブランチである。切り出してよく見てみよう。

  SELECT NODE_NO, PARENT_NO, NODE_DATA
    FROM TREE_DATA WHERE PARENT_NO IS NULL


PARENT_NOがNULLである、ルートノードを取り出すためのクエリである。再帰となっていないため、単独で実行できる。
以下は、UNION ALLの後のクエリである。

  SELECT TD2.NODE_NO, TD2.PARENT_NO, TD2.NODE_DATA
    FROM td INNER JOIN TREE_DATA TD2
     ON td.NNO = TD2.PARENT_NO


tdは、WITHで定義したインラインビューである。自分を再帰的に呼び出している部分でもある。そのtdとTREE_DATAを結合している。TD2はTREE_DATAに付けた別名である。
tdを定義中の一部分であるため、取り出して単独で実行することはできない。

tdの定義で、列名を指定している。そのため、tdは以下の列を持っているようになる。

NNO
PNO
NDATA

仮想的な表、tdとTREE_DATAテーブルを結合している。tdの元は、TREE_DATAテーブルなので、自己結合しているようなものだが、単純に一つの同じテーブルを横に並べただけではなく、「再帰呼び出しで得られた結果のテーブルを結合」している感じになる。
結合条件が、td.NNO = TD2.PARENT_NOとなっていることが最大のポイントなわけである。

なんとなくわかってきたと思うが、まだしっかり把握できたわけではないと思うので、再帰処理を順を追ってみていくことにしよう。

再帰の1回目

WITH句の外で、tdを参照する。その最初の参照で、どうなるかというと、初期化ブランチだけの実行結果が一時表に格納される。初期化ブランチは以下のようになっていた。

  SELECT NODE_NO, PARENT_NO, NODE_DATA
    FROM TREE_DATA WHERE PARENT_NO IS NULL

  NNO  PNO   NDATA
  -----------------
  1    NULL  100


NODE_NO=1だけの行が一時表に格納される。初期化だからなんとなく納得頂けるのではないかと思う。

再帰の2回目

で、この一時表の結果をさらに、WITH内のクエリにかけるのである。この時、tdの内容が仮に、一時表のものとなる。

  SELECT NODE_NO, PARENT_NO, NODE_DATA
    FROM TREE_DATA WHERE PARENT_NO IS NULL
  UNION ALL
  SELECT TD2.NODE_NO, TD2.PARENT_NO, TD2.NODE_DATA
    FROM td INNER JOIN TREE_DATA TD2                -- tdの内容は再帰の1回目の実行結果となる
     ON td.NNO = TD2.PARENT_NO

  NNO  PNO   NDATA    td.NNO td.PNO td.NDATA
  -----------------
  1    NULL  100     -- 初期化ブランチで得られる
  2    1     200      1      NULL   100
  3    1     300      1      NULL   100


td.NNO、td.PNO、td.NDATAは、参考情報である。クエリで取得していないため、戻されることはない。
UNION ALLの後のクエリでPARENT_NO=1となっている行が結合条件に引っかかり、NNO=2と3の二つの行が増えている。一時表には、増えた分の行だけが追加されていくことになる。UNION ALLだからと言って、重複して結果が戻されることはない。

再帰の3回目

前回の呼び出しと、今回の呼び出しで、行が増えている限り、再帰呼び出しが継続する。3回目の再帰処理も行われる。

  SELECT NODE_NO, PARENT_NO, NODE_DATA
    FROM TREE_DATA WHERE PARENT_NO IS NULL
  UNION ALL
  SELECT TD2.NODE_NO, TD2.PARENT_NO, TD2.NODE_DATA
    FROM td INNER JOIN TREE_DATA TD2                -- tdの内容は再帰の2回目の実行結果となる
     ON td.NNO = TD2.PARENT_NO

  NNO  PNO   NDATA    td.NNO td.PNO td.NDATA
  -----------------
  1    NULL  100     -- 初期化ブランチで得られる
  2    1     200      1      NULL   100    -- 2回目の実行で得られる
  3    1     300      1      NULL   100    -- 2回目の実行で得られる
  4    2     100      2      1      200


NNO=4の行が増えた。
増えたので、4回目の再帰も行われる。

再帰の4回目

  SELECT NODE_NO, PARENT_NO, NODE_DATA
    FROM TREE_DATA WHERE PARENT_NO IS NULL
  UNION ALL
  SELECT TD2.NODE_NO, TD2.PARENT_NO, TD2.NODE_DATA
    FROM td INNER JOIN TREE_DATA TD2                -- tdの内容は再帰の3回目の実行結果となる
     ON td.NNO = TD2.PARENT_NO

  NNO  PNO   NDATA    td.NNO td.PNO td.NDATA
  -----------------
  1    NULL  100     -- 初期化ブランチで得られる
  2    1     200      1      NULL   100    -- 2回目の実行で得られる
  3    1     300      1      NULL   100    -- 2回目の実行で得られる
  4    2     100      2      1      200    -- 3回目の実行で得られる


3回目でNNO=4の行が増えたが、PARENT_NO=4である行が存在しないので、結果として行が増えなかった。そのため、再帰呼び出しは、4回目で終了となる。

わかって頂けたであろうか。
これが、再帰クエリの基本である。

もう、「こういうパターンで再帰クエリは書くもの」と覚えてしまった方が良いかも知れない。
ポイントとしては、以下のようになる。

再帰クエリ

 1 WITH内には、UNION ALLで二つのSELECT命令を書く
 2 二つのSELECT命令のうち、一つは初期化ブランチなので、再帰呼び出しをしてはいけない
 3 二つのSELECT命令のうち、もう一つは再帰呼び出しをして良いが、呼び出し前の一時表結果と結合する
 4 呼び出し前の一時表結果との結合条件は、親子関係を示す「PARENT_NO = NODE_NO」のようなものとなる


しかしながら、得られた結果にどう言った意味があるのか?
少々疑問ではないだろうか。
単に、全レコードを取得できただけ?それならば、SELECT * FROM TREE_DATAで十分では?

 はい、このクエリではその通りです。

  えー、じゃあ再帰クエリを使う意味って...

大丈夫、再帰の場合は、計算方法が異なる。再帰処理の特徴を使えば、再帰処理ならではの計算ができるようになる。

まぁ、急ぐこともあるまい。次回に期待?して欲しい。






サイト内を検索

タグ:with SQL
nice!(0)  コメント(0) 
共通テーマ:携帯コンテンツ

nice! 0

コメント 0

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。



Copyright Atsushi Asai Google+朝井淳
[改訂第4版]SQLポケットリファレンス

[改訂第4版]SQLポケットリファレンス

  • 作者: 朝井 淳
  • 出版社/メーカー: 技術評論社
  • 発売日: 2017/02/18
  • メディア: 単行本(ソフトカバー)

イラストで理解 SQL はじめて入門

イラストで理解 SQL はじめて入門

  • 作者: 朝井 淳
  • 出版社/メーカー: 技術評論社
  • 発売日: 2019/05/16
  • メディア: 単行本(ソフトカバー)

[データベースの気持ちがわかる]SQLはじめの一歩 (WEB+DB PRESS plus)

[データベースの気持ちがわかる]SQLはじめの一歩 (WEB+DB PRESS plus)

  • 作者: 朝井 淳
  • 出版社/メーカー: 技術評論社
  • 発売日: 2015/03/03
  • メディア: 単行本(ソフトカバー)

Access クエリ 徹底活用ガイド ~仕事の現場で即使える

Access クエリ 徹底活用ガイド ~仕事の現場で即使える

  • 作者: 朝井 淳
  • 出版社/メーカー: 技術評論社
  • 発売日: 2018/05/25
  • メディア: 大型本

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。