web-dev-qa-db-ja.com

MySQL階層再帰クエリを作成する方法

私は以下のMySQLテーブルを持っています。

id | name        | parent_id
19 | category1   | 0
20 | category2   | 19
21 | category3   | 20
22 | category4   | 21
......

今、私は単純にidを提供する単一のMySQLクエリが欲しいのですが(例えば 'id = 19'と言います)、そのすべての子IDを取得するべきです。結果は、ID '20、21、22']を持つ必要があります。また、子の階層は、それが変化することが知られていません....

また、私はすでにforループを使用した解決策を持っています.....可能であれば単一のMySQLクエリを使用して同じことを達成する方法を教えてください。

194
Tarun Parswani

MySQL 8を使用している場合は、再帰的な with 句を使用します。

with recursive cte (id, name, parent_id) as (
  select     id,
             name,
             parent_id
  from       products
  where      parent_id = 19
  union all
  select     p.id,
             p.name,
             p.parent_id
  from       products p
  inner join cte
          on p.parent_id = cte.id
)
select * from cte;

parent_id = 19で指定された値は、すべての子孫を選択したい親のidに設定する必要があります。

MySQL 8より前

Common Table ExpressionsをサポートしていないMySQLバージョン(バージョン5.7まで)の場合は、次のクエリを使用してこれを実現できます。

select  id,
        name,
        parent_id 
from    (select * from products
         order by parent_id, id) products_sorted,
        (select @pv := '19') initialisation
where   find_in_set(parent_id, @pv)
and     length(@pv := concat(@pv, ',', id))

これが フィドル です。

ここで、@pv := '19'で指定された値は、すべての子孫を選択したい親のidに設定されるべきです。

これは、親が multiple childrenを持っている場合にも機能します。ただし、各レコードがparent_id < id条件を満たしている必要があります。そうでなければ結果は完全にはなりません。

クエリ内の変数代入

このクエリは特定のMySQL構文を使用します。変数は実行中に割り当てられ変更されます。実行順序については、いくつかの前提条件があります。

  • from節が最初に評価されます。だから@pvが初期化されるのです。
  • where節は、fromエイリアスからの検索順に各レコードに対して評価されます。そのため、ここでは、親がすでに子孫ツリー内にあると識別されているレコードのみを含めるように条件を設定します(プライマリ親のすべての子孫は@pvに徐々に追加されます)。
  • このwhere節の条件は順番に評価され、結果全体が確実になると評価は中断されます。したがって、idが親リストに追加されるため、2番目の条件は2番目になければなりません。これは、idが最初の条件を通過した場合にのみ発生します。 length関数は、たとえpv文字列が何らかの理由で誤った値を生成する場合でも、この条件が常に真になるようにするためにのみ呼び出されます。

全体として、これらの仮定を当てにするにはリスクが高すぎると感じるかもしれません。 ドキュメント は警告します:

期待通りの結果が得られるかもしれませんが、これは保証されていません[...]ユーザ変数を含む式の評価の順序は未定義です。

そのため、上記のクエリと一貫して動作しても、条件を追加したり、このクエリを大きなクエリのビューまたはサブクエリとして使用したりすると、評価の順序が変わる可能性があります。これは 将来のMySQLリリースでは削除される予定です という「機能」です。

MySQLの以前のリリースでは、SET以外のステートメントでユーザー変数に値を割り当てることが可能でした。この機能は下位互換性のためにMySQL 8.0でサポートされていますが、MySQLの将来のリリースで削除される可能性があります。

上で述べたように、MySQL 8.0以降では再帰的なwith構文を使うべきです。

効率

非常に大きなデータセットの場合、 find_in_set 操作はリスト内の数を見つけるための最も理想的な方法ではなく、同じ桁数のサイズに達するリストではないので、この解決方法は遅くなります。返されたレコード数として。

代替案1:with recursiveconnect by

ますます多くのデータベースが再帰的なクエリのために SQL:1999 ISO標準のWITH [RECURSIVE]構文 を実装しています(例: Postgres 8.4+SQL Server 2005+DB2Oracle 11g Release 2 +SQLite 3.8.4以降Firebird 2.1以降H2HyperSQL 2.1.0以降)TeradataMariaDB 10.2.2以降 )。そして バージョン8.0、MySQLでもサポートされています 以降。使用する構文については、この回答の先頭を参照してください。

データベースによっては、 OracleDB2Informixで利用可能なCONNECT BY句のような、階層ルックアップのための代替の非標準構文があります。 _ cubrid _ と他のデータベース。

MySQLバージョン5.7はそのような機能を提供していません。データベースエンジンでこの構文が提供されている場合、またはその構文に移行できる場合は、それが確かに最善の選択肢です。そうでない場合は、以下の代替案も検討してください。

代替案2:パススタイル識別子

階層的な情報を含むidの値、つまりパスを代入すると、作業がずっと楽になります。たとえば、あなたの場合、これは次のようになります。

ID       | NAME
19       | category1   
19/1     | category2  
19/1/1   | category3  
19/1/1/1 | category4  

それであなたのselectはこのようになるでしょう:

select  id,
        name 
from    products
where   id like '19/%'

代替案3:繰り返し自己結合

階層ツリーの深さの上限を知っている場合は、次のような標準のsqlクエリを使用できます。

select      p6.parent_id as parent6_id,
            p5.parent_id as parent5_id,
            p4.parent_id as parent4_id,
            p3.parent_id as parent3_id,
            p2.parent_id as parent2_id,
            p1.parent_id as parent_id,
            p1.id as product_id,
            p1.name
from        products p1
left join   products p2 on p2.id = p1.parent_id 
left join   products p3 on p3.id = p2.parent_id 
left join   products p4 on p4.id = p3.parent_id  
left join   products p5 on p5.id = p4.parent_id  
left join   products p6 on p6.id = p5.parent_id
where       19 in (p1.parent_id, 
                   p2.parent_id, 
                   p3.parent_id, 
                   p4.parent_id, 
                   p5.parent_id, 
                   p6.parent_id) 
order       by 1, 2, 3, 4, 5, 6, 7;

こちらをご覧ください フィドル

where条件は、子孫を取得したい親を指定します。必要に応じてこのクエリをより多くのレベルで拡張できます。

259
trincot

ブログからMySQLで階層データを管理する

テーブル構造

+-------------+----------------------+--------+
| category_id | name                 | parent |
+-------------+----------------------+--------+
|           1 | ELECTRONICS          |   NULL |
|           2 | TELEVISIONS          |      1 |
|           3 | TUBE                 |      2 |
|           4 | LCD                  |      2 |
|           5 | PLASMA               |      2 |
|           6 | PORTABLE ELECTRONICS |      1 |
|           7 | MP3 PLAYERS          |      6 |
|           8 | FLASH                |      7 |
|           9 | CD PLAYERS           |      6 |
|          10 | 2 WAY RADIOS         |      6 |
+-------------+----------------------+--------+

クエリ:

SELECT t1.name AS lev1, t2.name as lev2, t3.name as lev3, t4.name as lev4
FROM category AS t1
LEFT JOIN category AS t2 ON t2.parent = t1.category_id
LEFT JOIN category AS t3 ON t3.parent = t2.category_id
LEFT JOIN category AS t4 ON t4.parent = t3.category_id
WHERE t1.name = 'ELECTRONICS';

出力

+-------------+----------------------+--------------+-------+
| lev1        | lev2                 | lev3         | lev4  |
+-------------+----------------------+--------------+-------+
| ELECTRONICS | TELEVISIONS          | TUBE         | NULL  |
| ELECTRONICS | TELEVISIONS          | LCD          | NULL  |
| ELECTRONICS | TELEVISIONS          | PLASMA       | NULL  |
| ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS  | FLASH |
| ELECTRONICS | PORTABLE ELECTRONICS | CD PLAYERS   | NULL  |
| ELECTRONICS | PORTABLE ELECTRONICS | 2 WAY RADIOS | NULL  |
+-------------+----------------------+--------------+-------+

ほとんどのユーザーは一度にSQLデータベースの階層データを扱い、階層データの管理はリレーショナルデータベースの目的ではないことを疑いなく知っていました。リレーショナルデータベースのテーブルは階層的ではなく(XMLのように)、単なるフラットリストです。階層データには、リレーショナルデータベーステーブルでは本来表現されていない親子関係があります。 続きを読む

詳細についてはブログを参照してください。

編集:

select @pv:=category_id as category_id, name, parent from category
join
(select @pv:=19)tmp
where parent=@pv

出力:

category_id name    parent
19  category1   0
20  category2   19
21  category3   20
22  category4   21

参照: Mysqlで再帰SELECTクエリを実行する方法?

77
Damodaran

これらを試してください:

テーブル定義:

DROP TABLE IF EXISTS category;
CREATE TABLE category (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(20),
    parent_id INT,
    CONSTRAINT fk_category_parent FOREIGN KEY (parent_id)
    REFERENCES category (id)
) engine=innodb;

実験行

INSERT INTO category VALUES
(19, 'category1', NULL),
(20, 'category2', 19),
(21, 'category3', 20),
(22, 'category4', 21),
(23, 'categoryA', 19),
(24, 'categoryB', 23),
(25, 'categoryC', 23),
(26, 'categoryD', 24);

再帰ストアドプロシージャ:

DROP PROCEDURE IF EXISTS getpath;
DELIMITER $$
CREATE PROCEDURE getpath(IN cat_id INT, OUT path TEXT)
BEGIN
    DECLARE catname VARCHAR(20);
    DECLARE temppath TEXT;
    DECLARE tempparent INT;
    SET max_sp_recursion_depth = 255;
    SELECT name, parent_id FROM category WHERE id=cat_id INTO catname, tempparent;
    IF tempparent IS NULL
    THEN
        SET path = catname;
    ELSE
        CALL getpath(tempparent, temppath);
        SET path = CONCAT(temppath, '/', catname);
    END IF;
END$$
DELIMITER ;

ストアドプロシージャのラッパー関数

DROP FUNCTION IF EXISTS getpath;
DELIMITER $$
CREATE FUNCTION getpath(cat_id INT) RETURNS TEXT DETERMINISTIC
BEGIN
    DECLARE res TEXT;
    CALL getpath(cat_id, res);
    RETURN res;
END$$
DELIMITER ;

例を選択してください:

SELECT id, name, getpath(id) AS path FROM category;

出力:

+----+-----------+-----------------------------------------+
| id | name      | path                                    |
+----+-----------+-----------------------------------------+
| 19 | category1 | category1                               |
| 20 | category2 | category1/category2                     |
| 21 | category3 | category1/category2/category3           |
| 22 | category4 | category1/category2/category3/category4 |
| 23 | categoryA | category1/categoryA                     |
| 24 | categoryB | category1/categoryA/categoryB           |
| 25 | categoryC | category1/categoryA/categoryC           |
| 26 | categoryD | category1/categoryA/categoryB/categoryD |
+----+-----------+-----------------------------------------+

特定のパスで行をフィルタリングする:

SELECT id, name, getpath(id) AS path FROM category HAVING path LIKE 'category1/category2%';

出力:

+----+-----------+-----------------------------------------+
| id | name      | path                                    |
+----+-----------+-----------------------------------------+
| 20 | category2 | category1/category2                     |
| 21 | category3 | category1/category2/category3           |
| 22 | category4 | category1/category2/category3/category4 |
+----+-----------+-----------------------------------------+
8
Fandi Susanto

ここで別の待ち行列のために同じことをしました

MySQLの選択再帰的に複数のレベルを持つすべての子を取得

クエリは次のようになります。

SELECT GROUP_CONCAT(lv SEPARATOR ',') FROM (
SELECT @pv:=(SELECT GROUP_CONCAT(id SEPARATOR ',') FROM table WHERE parent_id IN (@pv)) AS lv FROM table 
JOIN
(SELECT @pv:=1)tmp
WHERE parent_id IN (@pv)) a;
7

私が思い付いた最善のアプローチは

  1. 系統を使用して\ sort\traceの木を保存し​​ます。それは十分すぎるほどであり、他のどのアプローチよりも数千倍も早く読むことができます。また、DBが変更されてもそのパターンを維持することができます(ANY dbではそのパターンを使用できます)
  2. 特定のIDの系統を決定する関数を使用してください。
  3. あなたが望むようにそれを使ってください(選択で、またはCUD操作で、あるいは仕事でさえ)。

系統アプローチたとえば、 Here または here など、どこにでも見つけることができます。機能として - that は私を奮い立たせたものです。

結局、多かれ少なかれ単純で、比較的速く、そして単純な解決策を得ました。

機能の本体

-- --------------------------------------------------------------------------------
-- Routine DDL
-- Note: comments before and after the routine body will not be stored by the server
-- --------------------------------------------------------------------------------
DELIMITER $$

CREATE DEFINER=`root`@`localhost` FUNCTION `get_lineage`(the_id INT) RETURNS text CHARSET utf8
    READS SQL DATA
BEGIN

 DECLARE v_rec INT DEFAULT 0;

 DECLARE done INT DEFAULT FALSE;
 DECLARE v_res text DEFAULT '';
 DECLARE v_papa int;
 DECLARE v_papa_papa int DEFAULT -1;
 DECLARE csr CURSOR FOR 
  select _id,parent_id -- @n:=@n+1 as rownum,T1.* 
  from 
    (SELECT @r AS _id,
        (SELECT @r := table_parent_id FROM table WHERE table_id = _id) AS parent_id,
        @l := @l + 1 AS lvl
    FROM
        (SELECT @r := the_id, @l := 0,@n:=0) vars,
        table m
    WHERE @r <> 0
    ) T1
    where T1.parent_id is not null
 ORDER BY T1.lvl DESC;
 DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
    open csr;
    read_loop: LOOP
    fetch csr into v_papa,v_papa_papa;
        SET v_rec = v_rec+1;
        IF done THEN
            LEAVE read_loop;
        END IF;
        -- add first
        IF v_rec = 1 THEN
            SET v_res = v_papa_papa;
        END IF;
        SET v_res = CONCAT(v_res,'-',v_papa);
    END LOOP;
    close csr;
    return v_res;
END

そしてあなただけ

select get_lineage(the_id)

誰かに役立つことを願っています:)

7
Der Zinger

高速の読み取り速度が必要な場合は、クロージャーテーブルを使用するのが最善の選択肢です。クロージャーテーブルには、それぞれの先祖/子孫ペアの行が含まれています。したがって、あなたの例では、クロージャテーブルは次のようになります。

ancestor | descendant | depth
0        | 0          | 0
0        | 19         | 1
0        | 20         | 2
0        | 21         | 3
0        | 22         | 4
19       | 19         | 0
19       | 20         | 1
19       | 21         | 3
19       | 22         | 4
20       | 20         | 0
20       | 21         | 1
20       | 22         | 2
21       | 21         | 0
21       | 22         | 1
22       | 22         | 0

このテーブルを取得すると、階層クエリは非常に簡単かつ高速になります。カテゴリ20のすべての子孫を取得するには

SELECT cat.* FROM categories_closure AS cl
INNER JOIN categories AS cat ON cat.id = cl.descendant
WHERE cl.ancestor = 20 AND cl.depth > 0

もちろん、このような非正規化データを使用するときはいつでも大きなマイナス面があります。あなたはあなたのカテゴリテーブルと一緒にクロージャテーブルを維持する必要があります。最善の方法はおそらくトリガーを使用することですが、クロージャー表の挿入/更新/削除を正しく追跡するのはやや複雑です。他と同じように、あなたはあなたの要件を見て、あなたにとってどのアプローチが最適かを決める必要があります。

編集 :質問を参照してください。 リレーショナルデータベースに階層データを格納するためのオプションは何ですか? より多くのオプションのために。状況ごとに異なる最適な解決策があります。

4
Justin Howard

最初の再帰の子供をリストするための簡単な問い合わせ:

select @pv:=id as id, name, parent_id
from products
join (select @pv:=19)tmp
where parent_id=@pv

結果:

id  name        parent_id
20  category2   19
21  category3   20
22  category4   21
26  category24  22

...左結合を使って

select
    @pv:=p1.id as id
  , p2.name as parent_name
  , p1.name name
  , p1.parent_id
from products p1
join (select @pv:=19)tmp
left join products p2 on p2.id=p1.parent_id -- optional join to get parent name
where p1.parent_id=@pv

すべての子を一覧表示する@tincotの解決策:

select  id,
        name,
        parent_id 
from    (select * from products
         order by parent_id, id) products_sorted,
        (select @pv := '19') initialisation
where   find_in_set(parent_id, @pv) > 0
and     @pv := concat(@pv, ',', id)

Sql Fiddle を使用してオンラインでテストし、すべての結果を確認してください。

http://sqlfiddle.com/#!9/a318e3/4/0

4
lynx_74

あなたは他のデータベースで再帰的な問い合わせ(パフォーマンス上のYMMV)を使って非常に簡単にこれをすることができます。

これを行うもう1つの方法は、2つの追加ビットのデータ、左右の値を格納することです。左右の値は、表現しているツリー構造の事前順序のトラバースから導き出されます。

これはModified Preorder Tree Traversalとして知られており、すべての親の値を一度に取得するために簡単なクエリを実行することを可能にします。 「入れ子集合」という名前でもあります。

3
Phil John

ちょっとトリッキーな、それがあなたのために働いているかどうかこれをチェックしてください

select a.id,if(a.parent = 0,@varw:=concat(a.id,','),@varw:=concat(a.id,',',@varw)) as list from (select * from recursivejoin order by if(parent=0,id,parent) asc) a left join recursivejoin b on (a.id = b.parent),(select @varw:='') as c  having list like '%19,%';

SQLフィドルリンク http://www.sqlfiddle.com/#!2/e3cdf/2

フィールドとテーブル名を適切に置き換えます。

1
senK

Mysqlの自己関連テーブルのmake treeには BlueM/tree phpクラスを使用するだけです。

TreeとTree\Nodeは、親ID参照を使用して階層的に構造化されたデータを処理するためのPHPクラスです。典型的な例は、各レコードの「親」フィールドが別のレコードの主キーを参照するリレーショナルデータベースのテーブルです。もちろん、Treeはデータベースから発生したデータだけを使用することはできません。あなたがデータを供給し、Treeはデータの出所や処理方法に関係なくそれを使用します。 続きを読む

これはBlueM/treeの使用例です。

<?php 
require '/path/to/vendor/autoload.php'; $db = new PDO(...); // Set up your database connection 
$stm = $db->query('SELECT id, parent, title FROM tablename ORDER BY title'); 
$records = $stm->fetchAll(PDO::FETCH_ASSOC); 
$tree = new BlueM\Tree($records); 
...
1
Saleh Mosleh

ここで述べられていないことは、受け入れられた答えの2番目の選択肢に少し似ていますが、大きな階層のクエリと簡単な(insert update delete)アイテムのための異なる低コストですが、各アイテムに永続パス列を追加することです。

いくつかのような:

id | name        | path
19 | category1   | /19
20 | category2   | /19/20
21 | category3   | /19/20/21
22 | category4   | /19/20/21/22

例:

-- get children of category3:
SELECT * FROM my_table WHERE path LIKE '/19/20/21%'
-- Reparent an item:
UPDATE my_table SET path = REPLACE(path, '/19/20', '/15/16') WHERE path LIKE '/19/20/%'

パスの長さとORDER BY pathを、base36エンコーディングの代わりに実際の数値のパスIDを使用して最適化します

 // base10 => base36
 '1' => '1',
 '10' => 'A',
 '100' => '2S',
 '1000' => 'RS',
 '10000' => '7PS',
 '100000' => '255S',
 '1000000' => 'LFLS',
 '1000000000' => 'GJDGXS',
 '1000000000000' => 'CRE66I9S'

https://en.wikipedia.org/wiki/Base36

固定長を使用し、エンコードされたIDにパディングすることによって、スラッシュ「/」区切り文字も抑制します。

ここで詳細な最適化の説明: https://bojanz.wordpress.com/2014/04/25/storing-hierarchical-data-materialized-path/

_ todo _

一つのアイテムの後退した先祖のためにパスを分割するための関数や手続きを構築する

0
MTK

enter image description here

これは category テーブルです。

SELECT  id,
        NAME,
        parent_category 
FROM    (SELECT * FROM category
         ORDER BY parent_category, id) products_sorted,
        (SELECT @pv := '2') initialisation
WHERE   FIND_IN_SET(parent_category, @pv) > 0
AND     @pv := CONCAT(@pv, ',', id)

Output ::enter image description here

0

これは私のために働く、これがあなたのためにも働くことを願っています。それはあなたに任意の特定のメニューのために子供にRecord set Rootを与えるでしょう。必要に応じてフィールド名を変更してください。

SET @id:= '22';

SELECT Menu_Name, (@id:=Sub_Menu_ID ) as Sub_Menu_ID, Menu_ID 
FROM 
    ( SELECT Menu_ID, Menu_Name, Sub_Menu_ID 
      FROM menu 
      ORDER BY Sub_Menu_ID DESC
    ) AS aux_table 
    WHERE Menu_ID = @id
     ORDER BY Sub_Menu_ID;
0
Monzur

私はそれがより簡単に見つけた:

1)アイテムが他のアイテムの親階層のどこかにあるかどうかをチェックする関数を作成します。このようなもの(私はこの関数を書かない、WHILE DOでそれを作る):

is_related(id, parent_id);

あなたの例では

is_related(21, 19) == 1;
is_related(20, 19) == 1;
is_related(21, 18) == 0;

2)このようなサブセレクトを使用します。

select ...
from table t
join table pt on pt.id in (select i.id from table i where is_related(t.id,i.id));
0
cripox