web-dev-qa-db-ja.com

クエリを使用して親を再帰的に検索

私はpostgresqlを使用しています。以下のようなテーブルがあります

parent_id    child_id
----------------------
101       102
103       104
104       105
105       106   

入力の最終的な親を与えるSQLクエリを書きたいのですが。

つまり、106を入力として渡すとすると、その出力は103になります。

(106 --> 105 --> 104 --> 103)
32
Avadhesh

これが完全な例です。まずDDL

test=> CREATE TABLE node (
test(>   id SERIAL,
test(>   label TEXT NOT NULL, -- name of the node
test(>   parent_id INT,
test(>   PRIMARY KEY(id)
test(> );
NOTICE:  CREATE TABLE will create implicit sequence "node_id_seq" for serial column "node.id"
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "node_pkey" for table "node"
CREATE TABLE

...そしていくつかのデータ...

test=> INSERT INTO node (label, parent_id) VALUES ('n1',NULL),('n2',1),('n3',2),('n4',3);
INSERT 0 4
test=> INSERT INTO node (label) VALUES ('garbage1'),('garbage2'), ('garbage3');
INSERT 0 3
test=> INSERT INTO node (label,parent_id) VALUES ('garbage4',6);
INSERT 0 1
test=> SELECT * FROM node;
id |  label   | parent_id 
----+----------+-----------
 1 | n1       |          
 2 | n2       |         1
 3 | n3       |         2
 4 | n4       |         3
 5 | garbage1 |          
 6 | garbage2 |          
 7 | garbage3 |          
 8 | garbage4 |         6
(8 rows)

これは、ノード内のすべてのIDに対して再帰クエリを実行します。

test=> WITH RECURSIVE nodes_cte(id, label, parent_id, depth, path) AS (
 SELECT tn.id, tn.label, tn.parent_id, 1::INT AS depth, tn.id::TEXT AS path 
 FROM node AS tn 
 WHERE tn.parent_id IS NULL
UNION ALL
 SELECT c.id, c.label, c.parent_id, p.depth + 1 AS depth, 
        (p.path || '->' || c.id::TEXT) 
 FROM nodes_cte AS p, node AS c 
 WHERE c.parent_id = p.id
)
SELECT * FROM nodes_cte AS n ORDER BY n.id ASC;
id |  label   | parent_id | depth |    path    
----+----------+-----------+-------+------------
 1 | n1       |           |     1 | 1
 2 | n2       |         1 |     2 | 1->2
 3 | n3       |         2 |     3 | 1->2->3
 4 | n4       |         3 |     4 | 1->2->3->4
 5 | garbage1 |           |     1 | 5
 6 | garbage2 |           |     1 | 6
 7 | garbage3 |           |     1 | 7
 8 | garbage4 |         6 |     2 | 6->8
(8 rows)

これにより、子孫WHERE node.id = 1がすべて取得されます。

test=> WITH RECURSIVE nodes_cte(id, label, parent_id, depth, path) AS (
 SELECT tn.id, tn.label, tn.parent_id, 1::INT AS depth, tn.id::TEXT AS path FROM node AS tn WHERE tn.id = 1
UNION ALL                   
 SELECT c.id, c.label, c.parent_id, p.depth + 1 AS depth, (p.path || '->' || c.id::TEXT) FROM nodes_cte AS p, node AS c WHERE c.parent_id = p.id
)                                                                
SELECT * FROM nodes_cte AS n;
id | label | parent_id | depth |    path    
----+-------+-----------+-------+------------
 1 | n1    |           |     1 | 1
 2 | n2    |         1 |     2 | 1->2
 3 | n3    |         2 |     3 | 1->2->3
 4 | n4    |         3 |     4 | 1->2->3->4
(4 rows)

以下は、ID 4のノードのパスを取得します。

test=> WITH RECURSIVE nodes_cte(id, label, parent_id, depth, path) AS (
 SELECT tn.id, tn.label, tn.parent_id, 1::INT AS depth, tn.id::TEXT AS path 
 FROM node AS tn 
 WHERE tn.parent_id IS NULL
UNION ALL
 SELECT c.id, c.label, c.parent_id, p.depth + 1 AS depth, 
        (p.path || '->' || c.id::TEXT) 
 FROM nodes_cte AS p, node AS c 
 WHERE c.parent_id = p.id
)
SELECT * FROM nodes_cte AS n WHERE n.id = 4;
id | label | parent_id | depth |    path    
----+-------+-----------+-------+------------
 4 | n4    |         3 |     4 | 1->2->3->4
(1 row)

また、depthが3未満の子孫に検索を制限するとします(depthはまだインクリメントされていないことに注意してください)。

test=> WITH RECURSIVE nodes_cte(id, label, parent_id, depth, path) AS (
  SELECT tn.id, tn.label, tn.parent_id, 1::INT AS depth, tn.id::TEXT AS path 
  FROM node AS tn WHERE tn.id = 1
UNION ALL
  SELECT c.id, c.label, c.parent_id, p.depth + 1 AS depth, 
         (p.path || '->' || c.id::TEXT) 
  FROM nodes_cte AS p, node AS c 
  WHERE c.parent_id = p.id AND p.depth < 2
)
SELECT * FROM nodes_cte AS n;
 id | label | parent_id | depth | path 
----+-------+-----------+-------+------
  1 | n1    |           |     1 | 1
  2 | n2    |         1 |     2 | 1->2
(2 rows)

「パス」を示すために、文字列ではなくARRAYデータ型を使用することをお勧めしますが、矢印は親<=>子の関係を示しています。

62
Sean

使用する - WITH RECURSIVE は、共通テーブル式(CTE)を作成します。非再帰的な用語については、子が親のすぐ下にある行を取得します。

SELECT
   c.child_id,
   c.parent_id
FROM
   mytable c
LEFT JOIN
   mytable p ON c.parent_id = p.child_id
WHERE
   p.child_id IS NULL

 child_id | parent_id
----------+-----------
      102 |       101
      104 |       103

再帰的な用語では、これらの子の子が必要です。

WITH RECURSIVE tree(child, root) AS (
   SELECT
      c.child_id,
      c.parent_id
   FROM
      mytable c
   LEFT JOIN
      mytable p ON c.parent_id = p.child_id
   WHERE
      p.child_id IS NULL
   UNION
   SELECT
      child_id,
      root
   FROM
      tree
   INNER JOIN
      mytable on tree.child = mytable.parent_id
)
SELECT * FROM tree;

 child | root
-------+------
   102 |  101
   104 |  103
   105 |  103
   106 |  103

CTEを照会するときに子をフィルタリングできます。

WITH RECURSIVE tree(child, root) AS (...) SELECT root FROM tree WHERE child = 106;

 root
------
  103
10
xn.