私は以下の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クエリを使用して同じことを達成する方法を教えてください。
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
に設定する必要があります。
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
操作はリスト内の数を見つけるための最も理想的な方法ではなく、同じ桁数のサイズに達するリストではないので、この解決方法は遅くなります。返されたレコード数として。
with recursive
、connect by
ますます多くのデータベースが再帰的なクエリのために SQL:1999 ISO標準のWITH [RECURSIVE]
構文 を実装しています(例: Postgres 8.4+ 、 SQL Server 2005+ 、 DB2 、 Oracle 11g Release 2 + 、 SQLite 3.8.4以降 、 Firebird 2.1以降 、 H2 、 HyperSQL 2.1.0以降) 、 Teradata 、 MariaDB 10.2.2以降 )。そして バージョン8.0、MySQLでもサポートされています 以降。使用する構文については、この回答の先頭を参照してください。
データベースによっては、 Oracle 、 DB2 、 Informix 、 で利用可能なCONNECT BY
句のような、階層ルックアップのための代替の非標準構文があります。 _ cubrid _ と他のデータベース。
MySQLバージョン5.7はそのような機能を提供していません。データベースエンジンでこの構文が提供されている場合、またはその構文に移行できる場合は、それが確かに最善の選択肢です。そうでない場合は、以下の代替案も検討してください。
階層的な情報を含む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/%'
階層ツリーの深さの上限を知っている場合は、次のような標準の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
条件は、子孫を取得したい親を指定します。必要に応じてこのクエリをより多くのレベルで拡張できます。
ブログから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
これらを試してください:
テーブル定義:
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 |
+----+-----------+-----------------------------------------+
ここで別の待ち行列のために同じことをしました
クエリは次のようになります。
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;
私が思い付いた最善のアプローチは
系統アプローチたとえば、 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)
誰かに役立つことを願っています:)
高速の読み取り速度が必要な場合は、クロージャーテーブルを使用するのが最善の選択肢です。クロージャーテーブルには、それぞれの先祖/子孫ペアの行が含まれています。したがって、あなたの例では、クロージャテーブルは次のようになります。
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
もちろん、このような非正規化データを使用するときはいつでも大きなマイナス面があります。あなたはあなたのカテゴリテーブルと一緒にクロージャテーブルを維持する必要があります。最善の方法はおそらくトリガーを使用することですが、クロージャー表の挿入/更新/削除を正しく追跡するのはやや複雑です。他と同じように、あなたはあなたの要件を見て、あなたにとってどのアプローチが最適かを決める必要があります。
編集 :質問を参照してください。 リレーショナルデータベースに階層データを格納するためのオプションは何ですか? より多くのオプションのために。状況ごとに異なる最適な解決策があります。
最初の再帰の子供をリストするための簡単な問い合わせ:
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 を使用してオンラインでテストし、すべての結果を確認してください。
あなたは他のデータベースで再帰的な問い合わせ(パフォーマンス上のYMMV)を使って非常に簡単にこれをすることができます。
これを行うもう1つの方法は、2つの追加ビットのデータ、左右の値を格納することです。左右の値は、表現しているツリー構造の事前順序のトラバースから導き出されます。
これはModified Preorder Tree Traversalとして知られており、すべての親の値を一度に取得するために簡単なクエリを実行することを可能にします。 「入れ子集合」という名前でもあります。
ちょっとトリッキーな、それがあなたのために働いているかどうかこれをチェックしてください
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
フィールドとテーブル名を適切に置き換えます。
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);
...
ここで述べられていないことは、受け入れられた答えの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 _
一つのアイテムの後退した先祖のためにパスを分割するための関数や手続きを構築する
これは私のために働く、これがあなたのためにも働くことを願っています。それはあなたに任意の特定のメニューのために子供に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;
私はそれがより簡単に見つけた:
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));