[英]How to create a MySQL hierarchical recursive query?
我有一個 MySQL 表,如下所示:
ID | 姓名 | parent_id |
---|---|---|
19 | 類別1 | 0 |
20 | 類別2 | 19 |
21 | 類別3 | 20 |
22 | 類別4 | 21 |
... | ... | ... |
現在,我想要一個 MySQL 查詢,我只需提供 id [例如說id=19
] 然后我應該得到它的所有子 id [即結果應該有 id '20,21,22']... .
孩子的等級是未知的; 它可以變化....
我知道如何使用for
循環來做到這一點......但是如何使用單個 MySQL 查詢來實現相同的目標?
對於MySQL 8+:使用 recursive with
語法。
對於MySQL 5.x:使用內聯變量、路徑 ID 或自連接。
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 版本(最高版本 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
。
如果父母有多個孩子,這也將起作用。 但是,要求每條記錄都滿足條件parent_id < id
,否則結果將不完整。
此查詢使用特定的 MySQL 語法:在執行期間分配和修改變量。 對執行順序做了一些假設:
from
子句。 這就是@pv
被初始化的地方。from
別名中檢索的順序為每條記錄評估where
子句。 因此,這里設置了一個條件,只包括其父已被標識為在后代樹中的記錄(主要父代的所有后代都逐漸添加到@pv
)。where
子句中的條件是按順序求值的,一旦總的結果確定,求值就會中斷。 因此,第二個條件必須排在第二位,因為它將id
添加到父列表中,並且只有當id
通過第一個條件時才會發生這種情況。 僅調用length
函數以確保此條件始終為真,即使pv
字符串由於某種原因會產生虛假值。總而言之,人們可能會發現這些假設過於冒險而無法依賴。 文檔警告:
您可能會得到您期望的結果,但這不能保證 [...] 涉及用戶變量的表達式的評估順序是未定義的。
因此,即使它與上述查詢一致,評估順序仍可能發生變化,例如當您添加條件或將此查詢用作較大查詢中的視圖或子查詢時。 這是一個“特性”,將在未來的 MySQL 版本中刪除:
MySQL 的早期版本可以在
SET
以外的語句中為用戶變量賦值。 MySQL 8.0 支持此功能以實現向后兼容性,但在 MySQL 的未來版本中可能會被刪除。
如上所述,從 MySQL 8.0 開始,您應該使用 recursive with
語法。
對於非常大的數據集,此解決方案可能會變慢,因為find_in_set
操作不是在列表中查找數字的最理想方法,當然不是在達到與返回的記錄數相同數量級的列表中.
with recursive
, connect by
越來越多的數據庫為遞歸查詢實現SQL:1999 ISO 標准WITH [RECURSIVE]
語法(例如Postgres 8.4+、 SQL Server 2005 +、DB2 、 Oracle 11gR2+ 、 SQLite 3.8.4 +、 Firebird 2.1 +、 H2 、 HyperSQL 2.1。 0+, Teradata , MariaDB 10.2.2+ ) 。 從8.0 版開始,MySQL 也支持它。 請參閱此答案的頂部以了解要使用的語法。
一些數據庫具有用於分層查找的替代非標准語法,例如Oracle 、DB2 、 Informix 、 CUBRID和其他數據庫上可用的CONNECT BY
子句。
MySQL 5.7 版不提供這樣的功能。 如果您的數據庫引擎提供了這種語法,或者您可以遷移到提供這種語法的引擎,那么這無疑是最好的選擇。 如果沒有,那么還要考慮以下替代方案。
如果您分配包含分層信息的id
值,事情會變得容易得多:路徑。 例如,在您的情況下,這可能如下所示:
ID | 姓名 |
---|---|
19 | 類別1 |
19/1 | 類別2 |
19/1/1 | 類別3 |
19/1/1/1 | 類別4 |
然后您的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 |
+----+-----------+-----------------------------------------+
在這里對另一個問題做了同樣的事情
Mysql select recursive get all child with multiple level
查詢將是:
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;
如果您需要快速閱讀速度,最好的選擇是使用閉包表。 閉包表包含每個祖先/后代對的一行。 所以在你的例子中,閉包表看起來像
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
當然,每當您使用這樣的非規范化數據時,都會有一個很大的缺點。 您需要在類別表旁邊維護關閉表。 最好的方法可能是使用觸發器,但是正確跟蹤閉包表的插入/更新/刪除有點復雜。 與任何事情一樣,您需要查看您的要求並決定哪種方法最適合您。
編輯:請參閱問題在關系數據庫中存儲分層數據的選項是什么? 更多選擇。 不同的情況有不同的最優解。
我想出的最好的方法是
沿襲方法描述。 可以在任何地方找到,例如Here或here 。 至於功能 - 這就是啟發我的地方。
最后 - 獲得了或多或少簡單、相對快速且簡單的解決方案。
函數體
-- --------------------------------------------------------------------------------
-- 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)
希望它可以幫助某人:)
基於@trincot的回答,很好的解釋,我使用WITH RECURSIVE ()
語句使用當前頁面的id
創建一個面包屑,並在層次結構中向后查找route
表中的每個parent
級。
因此,@trincot 解決方案在這里以相反的方向進行調整,以找到父母而不是后代。
我還添加了depth
值,這對於反轉結果順序很有用(否則面包屑會顛倒)。
WITH RECURSIVE cte (
`id`,
`title`,
`url`,
`icon`,
`class`,
`parent_id`,
`depth`
) AS (
SELECT
`id`,
`title`,
`url`,
`icon`,
`class`,
`parent_id`,
1 AS `depth`
FROM `route`
WHERE `id` = :id
UNION ALL
SELECT
P.`id`,
P.`title`,
P.`url`,
P.`icon`,
P.`class`,
P.`parent_id`,
`depth` + 1
FROM `route` P
INNER JOIN cte
ON P.`id` = cte.`parent_id`
)
SELECT * FROM cte ORDER BY `depth` DESC;
在升級到 mySQL 8+ 之前,我使用的是 vars,但它已被棄用,並且不再使用我的 8.0.22 版本!
編輯 2021-02-19 :分層菜單示例
在@david 評論之后,我決定嘗試制作一個包含所有節點的完整分層菜單,並根據需要進行排序(使用sorting
列對每個深度中的項目進行排序)。 對我的用戶/授權矩陣頁面非常有用。
這確實簡化了我的舊版本,每個深度都有一個查詢(PHP 循環) 。
此示例將 INNER JOIN 與url
表集成以按網站過濾路由(多網站 CMS 系統)。
您可以看到包含CONCAT()
函數以正確方式對菜單進行排序的基本path
列。
SELECT R.* FROM (
WITH RECURSIVE cte (
`id`,
`title`,
`url`,
`icon`,
`class`,
`parent`,
`depth`,
`sorting`,
`path`
) AS (
SELECT
`id`,
`title`,
`url`,
`icon`,
`class`,
`parent`,
1 AS `depth`,
`sorting`,
CONCAT(`sorting`, ' ' , `title`) AS `path`
FROM `route`
WHERE `parent` = 0
UNION ALL SELECT
D.`id`,
D.`title`,
D.`url`,
D.`icon`,
D.`class`,
D.`parent`,
`depth` + 1,
D.`sorting`,
CONCAT(cte.`path`, ' > ', D.`sorting`, ' ' , D.`title`)
FROM `route` D
INNER JOIN cte
ON cte.`id` = D.`parent`
)
SELECT * FROM cte
) R
INNER JOIN `url` U
ON R.`id` = U.`route_id`
AND U.`site_id` = 1
ORDER BY `path` ASC
列出第一次遞歸的孩子的簡單查詢:
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在線測試並查看所有結果。
這里沒有提到的東西,雖然有點類似於接受的答案的第二種選擇,但對於大層次結構查詢和簡單的(插入更新刪除)項目來說不同且成本低,將為每個項目添加一個持久路徑列。
一些喜歡:
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/%'
使用 base36 編碼而不是實數路徑 id 優化路徑長度和ORDER BY path
// 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/
去做
構建一個函數或過程來分割路徑以檢索一個項目的祖先
您可以使用遞歸查詢(性能上的 YMMV)很容易地在其他數據庫中執行此操作。
另一種方法是存儲兩個額外的數據位,一個左值和右值。 左值和右值來自您所代表的樹結構的預遍歷。
這稱為修改的預排序樹遍歷,讓您可以運行一個簡單的查詢來一次獲取所有父值。 它也被稱為“嵌套集”。
只需使用BlueM/tree php 類在 mysql 中制作自關系表的樹。
Tree 和 Tree\Node 是 PHP 類,用於處理使用父 ID 引用分層結構的數據。 一個典型的例子是關系數據庫中的表,其中每條記錄的“父”字段引用另一條記錄的主鍵。 當然,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);
...
它有點棘手,檢查它是否適合你
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
適當地替換為您的字段和表名稱。
這對我有用,希望這對你也有用。 它將為您提供任何特定菜單的記錄集 Root to Child。 根據您的要求更改字段名稱。
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)使用 sub-select ,如下所示:
select ...
from table t
join table pt on pt.id in (select i.id from table i where is_related(t.id,i.id));
我已為您查詢。 這將為您提供帶有單個查詢的遞歸類別:
SELECT id,NAME,'' AS subName,'' AS subsubName,'' AS subsubsubName FROM Table1 WHERE prent is NULL
UNION
SELECT b.id,a.name,b.name AS subName,'' AS subsubName,'' AS subsubsubName FROM Table1 AS a LEFT JOIN Table1 AS b ON b.prent=a.id WHERE a.prent is NULL AND b.name IS NOT NULL
UNION
SELECT c.id,a.name,b.name AS subName,c.name AS subsubName,'' AS subsubsubName FROM Table1 AS a LEFT JOIN Table1 AS b ON b.prent=a.id LEFT JOIN Table1 AS c ON c.prent=b.id WHERE a.prent is NULL AND c.name IS NOT NULL
UNION
SELECT d.id,a.name,b.name AS subName,c.name AS subsubName,d.name AS subsubsubName FROM Table1 AS a LEFT JOIN Table1 AS b ON b.prent=a.id LEFT JOIN Table1 AS c ON c.prent=b.id LEFT JOIN Table1 AS d ON d.prent=c.id WHERE a.prent is NULL AND d.name IS NOT NULL
ORDER BY NAME,subName,subsubName,subsubsubName
這是一個小提琴。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.