簡體   English   中英

如何創建 MySQL 分層遞歸查詢?

[英]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 或自連接。

MySQL 8+

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.x

對於不支持公用表表達式的 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操作不是在列表中查找數字的最理想方法,當然不是在達到與返回的記錄數相同數量級的列表中.

備選方案 1: with recursiveconnect by

越來越多的數據庫為遞歸查詢實現SQL:1999 ISO 標准WITH [RECURSIVE]語法(例如Postgres 8.4+、 SQL Server 2005 +、DB2Oracle 11gR2+SQLite 3.8.4 +、 Firebird 2.1 +、 H2HyperSQL 2.1。 0+, TeradataMariaDB 10.2.2+ 8.0 版開始,MySQL 也支持它 請參閱此答案的頂部以了解要使用的語法。

一些數據庫具有用於分層查找的替代非標准語法,例如OracleDB2InformixCUBRID和其他數據庫上可用的CONNECT BY子句。

MySQL 5.7 版不提供這樣的功能。 如果您的數據庫引擎提供了這種語法,或者您可以遷移到提供這種語法的引擎,那么這無疑是最好的選擇。 如果沒有,那么還要考慮以下替代方案。

備選方案 2:路徑式標識符

如果您分配包含分層信息的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/%'

備選方案 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條件指定要檢索其后代的父級。 您可以根據需要使用更多級別擴展此查詢。

來自博客在 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查詢?

試試這些:

表定義:

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

當然,每當您使用這樣的非規范化數據時,都會有一個很大的缺點。 您需要在類別表旁邊維護關閉表。 最好的方法可能是使用觸發器,但是正確跟蹤閉包表的插入/更新/刪除有點復雜。 與任何事情一樣,您需要查看您的要求並決定哪種方法最適合您。

編輯:請參閱問題在關系數據庫中存儲分層數據的選項是什么? 更多選擇。 不同的情況有不同的最優解。

我想出的最好的方法是

  1. 使用沿襲來存儲\排序\跟蹤樹。 這綽綽有余,而且閱讀速度比任何其他方法都要快數千倍。 即使數據庫發生變化,它也允許保持該模式(因為任何數據庫都允許使用該模式)
  2. 使用確定特定 ID 沿襲的函數。
  3. 隨心所欲地使用它(在選擇中,或在 CUD 操作中,甚至在作業中)。

沿襲方法描述。 可以在任何地方找到,例如Herehere 至於功能 - 就是啟發我的地方。

最后 - 獲得了或多或少簡單、相對快速且簡單的解決方案。

函數體

-- --------------------------------------------------------------------------------
-- 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 循環)

ERP授權矩陣

此示例將 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在線測試並查看所有結果。

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

這里沒有提到的東西,雖然有點類似於接受的答案的第二種選擇,但對於大層次結構查詢和簡單的(插入更新刪除)項目來說不同且成本低,將為每個項目添加一個持久路徑列。

一些喜歡:

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

適當地替換為您的字段和表名稱。

在此處輸入圖像描述

這是一個類別表。

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)

輸出:: 在此處輸入圖像描述

這對我有用,希望這對你也有用。 它將為您提供任何特定菜單的記錄集 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM