排序sql树层次结构

Posted

技术标签:

【中文标题】排序sql树层次结构【英文标题】:order sql tree hierarchy 【发布时间】:2013-01-31 03:54:08 【问题描述】:

像这样对表格进行排序的最佳方法是什么:

CREATE TABLE category(
    id INT(10),
    parent_id INT(10),
    name VARCHAR(50)
);

INSERT INTO category (id, parent_id, name) VALUES
(1, 0, 'pizza'),        --node 1
(2, 0, 'burger'),       --node 2
(3, 0, 'coffee'),       --node 3
(4, 1, 'piperoni'),     --node 1.1
(5, 1, 'cheese'),       --node 1.2
(6, 1, 'vegetariana'),  --node 1.3
(7, 5, 'extra cheese'); --node 1.2.1

idname分层排序: 'pizza' //节点 1 'piperoni' //节点 1.1 '奶酪' //节点 1.2 '额外的奶酪' //节点 1.2.1 'vegetariana' //节点 1.3 'burger' //节点 2 'coffee' //节点 3

编辑:名称末尾的数字是为了更好地可视化结构,而不是用于排序。

编辑 2: 多次提到...name "cheese 1.2" 末尾的数字仅用于可视化目的,不用于排序.我把它们作为cmets移动了,太多人混淆了,对不起。

【问题讨论】:

Oracle 有一种方法可以使用START WITH parent_id = 0 CONNECT BY PRIOR id = parent_id ORDER SIBLINGS BY id ASC。我认为mysql没有这样的分层查询。 @Benoit:实际上几乎所有的 DBMS except 对于少数(包括 MySQL)都可以使用递归公用表表达式来执行类似的操作。 表格结构是否已经定义或者您正处于规划阶段并且可以选择其他结构?您计划在表中包含多少条目?是经常修改还是对它有很多读取访问权限很重要? @t.niese 可以添加更多列(top_parent_id,depth_level ..),但如果可能的话,我正在寻找这种结构中的解决方案。可以添加深度(树不限于 3 级),但通常不应超过 50-100 个条目,并且此特定查询的速度不是问题。 查看Managing Hierarchical Data in MySQL 了解一些食谱。 【参考方案1】:

通过添加路径列和触发器,这可以相当容易地完成。

首先添加一个 varchar 列,该列将包含从根到节点的路径:

ALTER TABLE category ADD path VARCHAR(50) NULL;

然后添加一个触发器,计算插入时的路径:

(简单地将新的 id 与父路径连接起来)

CREATE TRIGGER set_path BEFORE INSERT ON category
  FOR EACH ROW SET NEW.path = 
  CONCAT(IFNULL((select path from category where id = NEW.parent_id), '0'), '.', New.id);

然后只需按路径选择顺序:

SELECT name, path FROM category ORDER BY path;

结果:

pizza         0.1
piperoni      0.1.4
cheese        0.1.5
extra cheese  0.1.5.7
vegetariana   0.1.6
burger        0.2
coffee        0.3

见fiddle。

这种方式维护成本也最低。路径字段在插入时隐藏,通过触发器计算。删除一个节点没有开销,因为该节点的所有子节点也都被删除了。唯一的问题是更新节点的 parent_id 时;好吧,不要那样做! :)

【讨论】:

感谢您的建设性回答。其优雅的解决方案无需额外的代码和支持。 从来没有想过这个——非常优雅而且非常有用。 如果任何节点的子节点超过 9 个,则无法正确排序。它是按字符串排序的,而不是数字的【参考方案2】:

嵌套树集level 列相结合是一种非常好的技术,用于读取和排序基于树的结构。很容易选择一棵子树,将结果限制在某个级别,并在一个查询中进行排序。但是插入和删除整体的成本相对较高,因此如果您更频繁地查询数据然后写入它们并且读取性能很重要,则应该使用它。 (对于 50-100,移除、插入或移动元素的时间应该没有问题,即使是 1000 也应该没有问题)。

您存储的每个条目是 levelleftright 的值,在下面的示例中它是:(left,right,level) 如果您只想选择1.2 和它的后代你会做的:

 SELECT * FROM table WHERE left >=7 AND right <=16

如果您只想选择孩子,那么

 SELECT * FROM table WHERE left >=7 AND right <=16 AND level=2

如果你想排序,你可以这样做

 SELECT * FROM table WHERE left >=7 AND right <=16 ORDER BY left

在保持层次结构分组的同时按其他字段排序可能会出现问题,具体取决于您希望如何排序。

                               1 (0,17,0)
                                   |
                                   |
                   +---------------+---------------------------------------+
                   |                                                       |
              1.1 (1,6,1)                                            1.2 (7,16,1)
                   |                                                       |
      +------------+-------+                  +-------------------+--------+----------------+
      |                    |                  |                   |                         |
  1.1.1 (2,3,2)      1.1.2 (4,5,2)      1.2.1 (8,9,2)       1.2.2 (10,13,2)         1.2.2 (14,15,2)
                                                                  |
                                                                  |
                                                                  |
                                                            1.2.2.1 (11,12,3)

闭包表(用于完成,但我不建议您的用例使用)。它将所有路径存储在树中,因此如果您有许多级别,层次结构所需的存储空间将增长得非常快。

路径枚举在那里你存储每个元素的路径与条目/0//0/1/ 在那里查询路径很容易,但是对于排序它不是那么灵活。

对于少量的整体,我会使用 嵌套树集。 遗憾的是,我没有一个很好的参考页面来描述这些技术并进行比较。

【讨论】:

【参考方案3】:

如果只有 3 层嵌套,你可以这样做

SELECT c1.name FROM category as c1 LEFT JOIN category as c2
   ON c1.parent_id = c2.id OR (c1.parent_id = 0 AND c1.id = c2.id) 
   ORDER BY c2.parent_id, c2.id, c1.id; 

如果你有更多的嵌套级别,那就更棘手了

更多嵌套级别可以编写函数

delimiter ~
DROP FUNCTION getPriority~

CREATE FUNCTION getPriority (inID INT) RETURNS VARCHAR(255) DETERMINISTIC
begin
  DECLARE gParentID INT DEFAULT 0;
  DECLARE gPriority VARCHAR(255) DEFAULT '';
  SET gPriority = inID;
  SELECT parent_id INTO gParentID FROM category WHERE ID = inID;
  WHILE gParentID > 0 DO
    SET gPriority = CONCAT(gParentID, '.', gPriority);
    SELECT parent_id INTO gParentID FROM category WHERE ID = gParentID;
  END WHILE;
  RETURN gPriority;
end~

delimiter ;

所以我现在开始

SELECT * FROM category ORDER BY getPriority(ID);

我有

+------+-----------+--------------------+
| ID   | parent_id | name               |
+------+-----------+--------------------+
|    1 |         0 | pizza 1            |
|    4 |         1 | piperoni 1.1       |
|    5 |         1 | cheese 1.2         |
|    7 |         5 | extra cheese 1.2.1 |
|    6 |         1 | vegetariana 1.3    |
|    2 |         0 | burger 2           |
|    3 |         0 | coffee 3           |
+------+-----------+--------------------+

【讨论】:

2levels 很容易.. 如您所见,示例是 3。您的查询抛出“'name' is ambiguous”,即使固定为c2.name,订单也不起作用 抱歉,SELECT c1.name FROM category as c1 LEFT JOIN category as c2 ON c1.parent_id = c2.id OR (c1.parent_id = 0 AND c1.id = c2.id) ORDER BY c2.id, c1.id; 我只能看到 2 个级别(额外的奶酪指的是比萨饼(1),而不是奶酪(5)) 这个函数是个好主意,虽然我会担心性能。反正想法不错。 @d.raev 已编辑,它有效,但我对您的建议是在某些列中保存优先级,您可以在添加新行时通过我的函数计算它 为什么函数会影响性能?这似乎是一个很棒的解决方案,并且比其他解决方案更清洁,但我想知道它会对性能产生什么影响。【参考方案4】:

我认为每个人都在过度设计解决方案。如果您的目标确实由您的示例表示,例如在虚拟*** 0 id 的 3 级中,这就足够了。

SELECT *
     , id AS SORT_KEY
  FROM category a
 WHERE parent_id = 0
UNION ALL
SELECT a.*
     , CONCAT(b.id, '.', a.id) AS SORT_KEY
  FROM category a
     , category b
 WHERE b.parent_id = 0
   and b.id = a.parent_id
UNION ALL
SELECT a.*
     , CONCAT(c.id,'.', b.id,'.', a.id) AS SORT_KEY
  FROM category a
     , category b
     , category c
 WHERE c.parent_id = 0
   and b.id = a.parent_id
   AND c.id = b.parent_id
ORDER BY sort_key

【讨论】:

有趣...但丑陋 :) 想象一下,如果您必须在此之上添加更多列、连接和过滤器......。 你需要 CTE,而 MySQL 没有。【参考方案5】:

一种方法是使用单独的字符串字段来存储任何节点的完整路径。 您需要在每次插入/更新/删除操作时维护此字段。

你可以有如下的字段值

CREATE TABLE category(
    id INT(10),
    parent_id INT(10),
    name VARCHAR(50),
    path VARCHAR(255)
);

INSERT INTO category (id, parent_id, name, path) VALUES
(1, 0, 'pizza 1','|1|'),
(2, 0, 'burger 2','|2|'),
(3, 0, 'coffee 3','|3|'),
(4, 1, 'piperoni 1.1','|1||4|'),
(5, 1, 'cheese 1.2','|1||5|'),
(6, 1, 'vegetariana 1.3','|1||6|'),
(7, 5, 'extra cheese 1.2.1','|1||5||1|');

您需要按路径字段排序才能使树按正确的排序顺序。

SELECT * FROM `category` ORDER BY `path`;

SqlFiddle Demo

这样您就不需要编程语言中的递归来以正确的排序顺序打印整个树。

Note:

此示例仅在您的最大 ID 为 9 时才有效,如 |1||11|将早于 |1||2|

要解决此问题,您需要根据您的应用程序预期的 ID 字段的最大值对构建字符串进行填充,如以下示例,预期最大值为 999(3 位)

|001||002|


根据我的经验,这个解决方案应该只适用于处理深度达到 7-8 级的树。

其他方式:Click Here

【讨论】:

如果我添加额外的字段......并且必须维护它......它可以简单地是一个 order 字段或类似的东西。这篇文章也很容易用递归代码完成,我正在寻找这个基本结构上的 SQL 变体。【参考方案6】:

SQL

WITH CTE_Category
    AS
    (
      SELECT id, parent_id, name
      , RIGHT(name,CHARINDEX(' ',REVERSE(RTRIM(name)))-1) as ordername
      FROM Category 
    )

    SELECT id, parent_id, name FROM CTE_Category ORDER BY ordername

MySql

SELECT id, parent_id, name
FROM Category ORDER BY SUBSTRING_INDEX(name,' ',-1)

【讨论】:

【参考方案7】:

在 SQL 查询结束时尝试ORDER BY name , id

这将按名称排序并使用 id 来解决任何关系。

【讨论】:

true .. 但名称是示例.. 更好地可视化结构。【参考方案8】:
SELECT * FROM category ORDER BY name, parent_id ASC

【讨论】:

以上是关于排序sql树层次结构的主要内容,如果未能解决你的问题,请参考以下文章

从父/子的平面列表构建层次结构对象

深层次两张图解经典6大排序与6大基础数据结构——学完这些,妈妈再也不用担心我的排序算法与数据结构

数据结构与算法系列研究五——树二叉树三叉树平衡排序二叉树AVL

数据结构(B树)

数据结构(B树)

2023数据结构考研复习-查找