Какой самый эффективный / элегантный способ разбить плоский стол на дерево?

517

Предположим, у вас есть плоская таблица, в которой хранится иерархия упорядоченного дерева:

Id   Name         ParentId   Order
 1   'Node 1'            0      10
 2   'Node 1.1'          1      10
 3   'Node 2'            0      20
 4   'Node 1.1.1'        2      10
 5   'Node 2.1'          3      10
 6   'Node 1.2'          1      20

Вот схема, где мы имеем [id] Name. Корневой узел 0 вымышленный.

                       [0] ROOT
                          / \ 
              [1] Узел 1 [3] Узел 2
              / \ \
    [2] Узел 1.1 [6] Узел 1.2 [5] Узел 2.1
          /          
 [4] Узел 1.1.1

Какой минималистический подход вы бы использовали для вывода этого в HTML (или текст, если на то пошло) как правильно упорядоченное, правильно отступающее дерево?

Предположим далее, что у вас есть только базовые структуры данных (массивы и хэш-карты), нет причудливых объектов со ссылками родитель / потомок, нет ORM, нет фреймворка, только ваши две руки. Таблица представлена ​​в виде набора результатов, к которому можно получить произвольный доступ.

Псевдокод или простой английский в порядке, это чисто концептуальный вопрос.

Бонусный вопрос: существует ли принципиально лучший способ хранить древовидную структуру, подобную этой, в СУБД?


ИЗМЕНЕНИЯ И ДОПОЛНЕНИЯ

Чтобы ответить на вопрос одного комментатора ( Марка Бесси ): корневой узел не нужен, потому что он все равно никогда не будет отображаться. ParentId = 0 - это соглашение для выражения «это верхний уровень». Столбец «Порядок» определяет порядок сортировки узлов с одним и тем же родителем.

«Набор результатов», о котором я говорил, можно представить в виде массива хэш-карт (чтобы остаться в этой терминологии). Для моего примера должно было быть уже там. Некоторые ответы проходят лишнюю милю и сначала ее строят, но это нормально.

Дерево может быть сколь угодно глубоким. Каждый узел может иметь N детей. Хотя я не имел в виду дерево «миллионов записей».

Не путайте мой выбор имен узлов ('Node 1.1.1') с тем, на что можно положиться. С таким же успехом узлы можно было бы назвать «Фрэнк» или «Боб», структура имен не подразумевалась, это было сделано лишь для того, чтобы сделать его читаемым.

Я опубликовал свое собственное решение, чтобы вы, ребята, могли разобрать его на части.

Томалак
источник
2
«нет причудливых объектов с родительскими / дочерними ссылками» - почему бы и нет? Создание базового объекта Node с помощью методов .addChild (), .getParent () позволяет довольно хорошо моделировать отношения узлов.
Мэтт б
2
Это регулярное (n дочерних элементов, где n может быть> 2) дерево или двоичное дерево (узел может иметь 0, 1 или 2 дочерних элемента)?
BKimmel
Поскольку вы можете реализовать правильную структуру данных узла с помощью хэш-карты, здесь нет никаких реальных ограничений, просто больше работы.
Сванте
... и это именно то, что вы сделали.
Сванте

Ответы:

451

Теперь, когда MySQL 8.0 поддерживает рекурсивные запросы , мы можем сказать, что все популярные базы данных SQL поддерживают рекурсивные запросы в стандартном синтаксисе.

WITH RECURSIVE MyTree AS (
    SELECT * FROM MyTable WHERE ParentId IS NULL
    UNION ALL
    SELECT m.* FROM MyTABLE AS m JOIN MyTree AS t ON m.ParentId = t.Id
)
SELECT * FROM MyTree;

Я тестировал рекурсивные запросы в MySQL 8.0 в своей презентации Recursive Query Throwdown в 2017 году.

Ниже мой оригинальный ответ от 2008 года:


Существует несколько способов хранения древовидных данных в реляционной базе данных. То, что вы показываете в своем примере, использует два метода:

  • Список смежности (родительский столбец) и
  • Перечисление пути (точечные числа в вашем столбце имени).

Другое решение называется Nested Sets , и оно также может храниться в той же таблице. Прочтите « Деревья и иерархии в SQL для умников » Джо Селко, чтобы узнать больше об этих проектах.

Я обычно предпочитаю проект под названием Closure Table (он же «Соотношение смежности») для хранения данных с древовидной структурой. Требуется другая таблица, но тогда запросить деревья довольно просто.

Я закрываю таблицу замыканий в своей презентации « Модели для иерархических данных с использованием SQL и PHP» и в своей книге « Антипаттерны SQL: предотвращение ловушек программирования баз данных» .

CREATE TABLE ClosureTable (
  ancestor_id   INT NOT NULL REFERENCES FlatTable(id),
  descendant_id INT NOT NULL REFERENCES FlatTable(id),
  PRIMARY KEY (ancestor_id, descendant_id)
);

Сохраните все пути в таблице закрытия, где существует прямое происхождение от одного узла к другому. Включите строку для каждого узла, чтобы ссылаться на себя. Например, используя набор данных, который вы указали в своем вопросе:

INSERT INTO ClosureTable (ancestor_id, descendant_id) VALUES
  (1,1), (1,2), (1,4), (1,6),
  (2,2), (2,4),
  (3,3), (3,5),
  (4,4),
  (5,5),
  (6,6);

Теперь вы можете получить дерево, начинающееся с узла 1 следующим образом:

SELECT f.* 
FROM FlatTable f 
  JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1;

Вывод (в клиенте MySQL) выглядит следующим образом:

+----+
| id |
+----+
|  1 | 
|  2 | 
|  4 | 
|  6 | 
+----+

Другими словами, узлы 3 и 5 исключены, потому что они являются частью отдельной иерархии, а не нисходящими от узла 1.


Re: комментарий от e-satun о непосредственных детях (или непосредственных родителях). Вы можете добавить path_lengthстолбец " " в, ClosureTableчтобы упростить запрос специально для непосредственного потомка или родителя (или любого другого расстояния).

INSERT INTO ClosureTable (ancestor_id, descendant_id, path_length) VALUES
  (1,1,0), (1,2,1), (1,4,2), (1,6,1),
  (2,2,0), (2,4,1),
  (3,3,0), (3,5,1),
  (4,4,0),
  (5,5,0),
  (6,6,0);

Затем вы можете добавить термин в свой запрос для запроса непосредственных потомков данного узла. Это потомки которых path_length1.

SELECT f.* 
FROM FlatTable f 
  JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
  AND path_length = 1;

+----+
| id |
+----+
|  2 | 
|  6 | 
+----+

Комментарий от @ashraf: «Как насчет сортировки всего дерева [по имени]?»

Вот пример запроса, чтобы вернуть все узлы, которые являются потомками узла 1, соединить их с FlatTable, который содержит другие атрибуты узла, такие как name, и отсортировать по имени.

SELECT f.name
FROM FlatTable f 
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
ORDER BY f.name;

Re комментарий от @Nate:

SELECT f.name, GROUP_CONCAT(b.ancestor_id order by b.path_length desc) AS breadcrumbs
FROM FlatTable f 
JOIN ClosureTable a ON (f.id = a.descendant_id) 
JOIN ClosureTable b ON (b.descendant_id = a.descendant_id) 
WHERE a.ancestor_id = 1 
GROUP BY a.descendant_id 
ORDER BY f.name

+------------+-------------+
| name       | breadcrumbs |
+------------+-------------+
| Node 1     | 1           |
| Node 1.1   | 1,2         |
| Node 1.1.1 | 1,2,4       |
| Node 1.2   | 1,6         |
+------------+-------------+

Пользователь предложил изменить сегодня. ТАК модераторы одобрили редактирование, но я изменил его.

Редактирование предположило, что ORDER BY в последнем запросе выше ORDER BY b.path_length, f.name, вероятно, должен убедиться, что порядок соответствует иерархии. Но это не работает, потому что он будет заказывать «Node 1.1.1» после «Node 1.2».

Если вы хотите, чтобы порядок соответствовал иерархии разумным образом, это возможно, но не просто путем упорядочения по длине пути. Например, см. Мой ответ на иерархическую базу данных MySQL Closure Table - Как вытащить информацию в правильном порядке .

Билл Карвин
источник
6
Это очень элегантно, спасибо. Бонусный балл присуждается. ;-) Я вижу один небольшой недостаток - поскольку он хранит дочерние отношения явно и неявно, вам нужно сделать много тщательного ОБНОВЛЕНИЯ даже для небольшого сдвига в древовидной структуре.
Томалак
16
Правда, каждый метод хранения древовидных структур в базе данных требует некоторой работы, либо при создании или обновлении дерева, либо при запросе деревьев и поддеревьев. Выберите дизайн, на основе которого вы хотели бы быть проще: написание или чтение.
Билл Карвин
2
@buffer, есть шанс создать несоответствия, когда вы создаете все строки для иерархии. Список смежности ( parent_id) имеет только одну строку, чтобы выразить каждое отношение родитель-потомок, но у таблицы закрытия есть много.
Билл Карвин
1
@BillKarwin Еще одна вещь, это таблицы закрытия, подходящие для графа с несколькими путями к любому данному узлу (например, иерархия категорий, где любой листовой или неконечный узел может принадлежать более чем одному родительскому элементу)
пользователь
2
@Reza, так что если вы добавляете новый дочерний узел, вы можете запросить всех потомков (1), и они являются предками нового дочернего узла.
Билл Карвин
58

Если вы используете вложенные наборы (иногда называемые измененным обходом дерева предзаказа), вы можете извлечь всю древовидную структуру или любое поддерево в ней в древовидном порядке с помощью одного запроса, за счет того, что вставки обходятся дороже, так как вам нужно управлять столбцами, которые описывают путь по порядку через древовидную структуру.

Для django-mptt я использовал такую ​​структуру:

id parent_id tree_id level lft rght
- --------- ------- ----- --- ----
 1 ноль 1 0 1 14
 2 1 1 1 2 7
 3 2 1 2 3 4
 4 2 1 2 5 6
 5 1 1 1 8 13
 6 5 1 2 9 10
 7 5 1 2 11 12

Который описывает дерево, которое выглядит следующим образом (с idпредставлением каждого элемента):

 1
 + - 2
 | + - 3
 | + - 4
 |
 + - 5
     + - 6
     + - 7

Или, как диаграмма вложенного множества, которая делает более очевидным, как работают значения lftи rght:

 __________________________________________________________________________
| Корень 1 |
| ________________________________ ________________________________ |
| | Ребенок 1.1 | | Детский 1.2 | |
| | ___________ ___________ | | ___________ ___________ | |
| | | С 1.1.1 | | С 1.1.2 | | | | С 1.2.1 | | С 1.2.2 | | |
1 2 3___________4 5___________6 7 8 9___________10 11__________12 13 14
| | ________________________________ | | ________________________________ | |
| __________________________________________________________________________ |

Как вы можете видеть, чтобы получить все поддерево для данного узла в древовидном порядке, вам просто нужно выбрать все строки, которые имеют lftи rghtзначения между его lftи rghtзначениями. Также легко получить дерево предков для данного узла.

levelКолонка немного денормализации для удобства более чем что - либо , а tree_idстолбец позволяет перезапустить lftи rghtнумерацию для каждого узла верхнего уровня, что уменьшает количество столбцов , затронутых вставки, движения и удаления, как lftи rghtстолбцы должны быть скорректированы соответственно, когда эти операции имеют место, чтобы создать или закрыть пробелы. Я сделал несколько замечаний по разработке в то время, когда пытался сосредоточиться на запросах, необходимых для каждой операции.

Что касается фактической работы с этими данными для отображения дерева, я создал tree_item_iteratorвспомогательную функцию, которая для каждого узла должна предоставлять вам достаточную информацию для генерирования любого вида отображения, который вы хотите.

Больше информации о MPTT:

Джонни Бьюкенен
источник
9
Я хотел бы, чтобы мы прекратили использовать сокращения, такие как lftи rghtдля имен столбцов, я имею в виду, сколько символов нам не нужно было вводить? один?!
orustammanapov
21

Это довольно старый вопрос, но, поскольку у него много мнений, я думаю, что стоит представить альтернативное и, на мой взгляд, очень элегантное решение.

Чтобы прочитать древовидную структуру, вы можете использовать рекурсивные выражения общих таблиц (CTE). Это дает возможность извлекать всю древовидную структуру сразу, иметь информацию об уровне узла, его родительском узле и порядке внутри дочерних узлов родительского узла.

Позвольте мне показать вам, как это будет работать в PostgreSQL 9.1.

  1. Создать структуру

    CREATE TABLE tree (
        id int  NOT NULL,
        name varchar(32)  NOT NULL,
        parent_id int  NULL,
        node_order int  NOT NULL,
        CONSTRAINT tree_pk PRIMARY KEY (id),
        CONSTRAINT tree_tree_fk FOREIGN KEY (parent_id) 
          REFERENCES tree (id) NOT DEFERRABLE
    );
    
    
    insert into tree values
      (0, 'ROOT', NULL, 0),
      (1, 'Node 1', 0, 10),
      (2, 'Node 1.1', 1, 10),
      (3, 'Node 2', 0, 20),
      (4, 'Node 1.1.1', 2, 10),
      (5, 'Node 2.1', 3, 10),
      (6, 'Node 1.2', 1, 20);
  2. Написать запрос

    WITH RECURSIVE 
    tree_search (id, name, level, parent_id, node_order) AS (
      SELECT 
        id, 
        name,
        0,
        parent_id, 
        1 
      FROM tree
      WHERE parent_id is NULL
    
      UNION ALL 
      SELECT 
        t.id, 
        t.name,
        ts.level + 1, 
        ts.id, 
        t.node_order 
      FROM tree t, tree_search ts 
      WHERE t.parent_id = ts.id 
    ) 
    SELECT * FROM tree_search 
    WHERE level > 0 
    ORDER BY level, parent_id, node_order;

    Вот результаты:

     id |    name    | level | parent_id | node_order 
    ----+------------+-------+-----------+------------
      1 | Node 1     |     1 |         0 |         10
      3 | Node 2     |     1 |         0 |         20
      2 | Node 1.1   |     2 |         1 |         10
      6 | Node 1.2   |     2 |         1 |         20
      5 | Node 2.1   |     2 |         3 |         10
      4 | Node 1.1.1 |     3 |         2 |         10
    (6 rows)

    Узлы дерева упорядочены по уровню глубины. В конечном результате мы представим их в следующих строках.

    Для каждого уровня они упорядочены по parent_id и node_order внутри родителя. Это говорит нам о том, как представить их в узле выходной ссылки для родителя в этом порядке.

    Имея такую ​​структуру, было бы нетрудно сделать действительно хорошую презентацию в HTML.

    Рекурсивные CTE доступны в PostgreSQL, IBM DB2, MS SQL Server и Oracle .

    Если вы хотите узнать больше о рекурсивных SQL-запросах, вы можете проверить документацию вашей любимой СУБД или прочитать две мои статьи на эту тему:

Михал Колодзейский
источник
18

Начиная с Oracle 9i, вы можете использовать CONNECT BY.

SELECT LPAD(' ', (LEVEL - 1) * 4) || "Name" AS "Name"
FROM (SELECT * FROM TMP_NODE ORDER BY "Order")
CONNECT BY PRIOR "Id" = "ParentId"
START WITH "Id" IN (SELECT "Id" FROM TMP_NODE WHERE "ParentId" = 0)

Начиная с SQL Server 2005, вы можете использовать рекурсивное общее табличное выражение (CTE).

WITH [NodeList] (
  [Id]
  , [ParentId]
  , [Level]
  , [Order]
) AS (
  SELECT [Node].[Id]
    , [Node].[ParentId]
    , 0 AS [Level]
    , CONVERT([varchar](MAX), [Node].[Order]) AS [Order]
  FROM [Node]
  WHERE [Node].[ParentId] = 0
  UNION ALL
  SELECT [Node].[Id]
    , [Node].[ParentId]
    , [NodeList].[Level] + 1 AS [Level]
    , [NodeList].[Order] + '|'
      + CONVERT([varchar](MAX), [Node].[Order]) AS [Order]
  FROM [Node]
    INNER JOIN [NodeList] ON [NodeList].[Id] = [Node].[ParentId]
) SELECT REPLICATE(' ', [NodeList].[Level] * 4) + [Node].[Name] AS [Name]
FROM [Node]
  INNER JOIN [NodeList] ON [NodeList].[Id] = [Node].[Id]
ORDER BY [NodeList].[Order]

Оба будут выводить следующие результаты.

имя
«Узел 1»
«Узел 1.1»
«Узел 1.1.1»
«Узел 1.2»
«Узел 2»
«Узел 2.1»
Эрик Вейльну
источник
CTE может использоваться как в SQLServer и оракулом @Eric Weilnau
Nisar
9

Ответ Билла чертовски хорош, этот ответ добавляет к нему некоторые вещи, которые заставляют меня желать, чтобы ТАК поддерживали многопоточные ответы.

В любом случае я хотел поддержать древовидную структуру и свойство Order. Я включил одно свойство в каждый названный узел, leftSiblingкоторое делает то же самое Order, что и в первоначальном вопросе (поддерживает порядок слева направо).

mysql> desc узлов;
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
| Поле | Тип | Null | Ключ | По умолчанию | Extra |
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
| id | int (11) | НЕТ | PRI | NULL | auto_increment |
| имя | Варчар (255) | ДА | | NULL | |
| leftSibling | int (11) | НЕТ | | 0 | |
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
3 ряда в наборе (0,00 сек)

mysql> desc смежности;
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
| Поле | Тип | Null | Ключ | По умолчанию | Extra |
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
| отношение ID | int (11) | НЕТ | PRI | NULL | auto_increment |
| родитель | int (11) | НЕТ | | NULL | |
| ребенок | int (11) | НЕТ | | NULL | |
| pathLen | int (11) | НЕТ | | NULL | |
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
4 ряда в наборе (0,00 сек)

Более подробно и код SQL на моем блоге .

Спасибо, Билл, ваш ответ был полезен для начала!

bobobobo
источник
7

Хорошо, если бы у меня был выбор, я бы использовал объекты. Я бы создал объект для каждой записи, где у каждого объекта есть childrenколлекция, и сохранил бы их все в ассоциированном массиве (/ hashtable), где Id - это ключ. И пролистайте коллекцию один раз, добавив детей в соответствующие дочерние поля. Просто.

Но поскольку вам не весело, ограничивая использование некоторых хороших ООП, я бы, вероятно, повторил на основе:

function PrintLine(int pID, int level)
    foreach record where ParentID == pID
        print level*tabs + record-data
        PrintLine(record.ID, level + 1)

PrintLine(0, 0)

Изменить: это похоже на пару других записей, но я думаю, что это немного чище. Я добавлю одну вещь: это чрезвычайно интенсивно использует SQL. Это противно . Если у вас есть выбор, пройдите маршрут ООП.

Oli
источник
Вот что я имел в виду под «без фреймворков» - ведь вы используете LINQ? Что касается вашего первого абзаца: результирующий набор уже есть, зачем сначала копировать всю информацию в новую структуру объекта? (Я не был достаточно ясен по этому факту, извините)
Томалак
Томалак - нет, код является псевдокодом. Конечно, вам придется разбить вещи на правильные селекторы и итераторы ... и настоящий синтаксис! Почему ООП? Потому что вы можете точно отражать структуру. Это делает вещи хорошими, и это, случается, более эффективно (только один выбор)
Оли
У меня также не было повторных выборов. Относительно ООП: Марк Бесси сказал в своем ответе: «Вы можете эмулировать любую другую структуру данных с помощью хэш-карты, так что это не страшное ограничение». Ваше решение правильное, но я думаю, что есть некоторые возможности для улучшения даже без ООП.
Томалак
5

Это было написано быстро, и не является ни красивым, ни эффективным (плюс это много автобоксов, преобразование между intиInteger раздражает!), Но это работает.

Возможно, это нарушает правила, так как я создаю свои собственные объекты, но эй, я делаю это как отвлечение от реальной работы :)

Это также предполагает, что resultSet / таблица полностью считывается в какую-то структуру перед тем, как вы начнете создавать узлы, что не будет лучшим решением, если у вас есть сотни тысяч строк.

public class Node {

    private Node parent = null;

    private List<Node> children;

    private String name;

    private int id = -1;

    public Node(Node parent, int id, String name) {
        this.parent = parent;
        this.children = new ArrayList<Node>();
        this.name = name;
        this.id = id;
    }

    public int getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    public void addChild(Node child) {
        children.add(child);
    }

    public List<Node> getChildren() {
        return children;
    }

    public boolean isRoot() {
        return (this.parent == null);
    }

    @Override
    public String toString() {
        return "id=" + id + ", name=" + name + ", parent=" + parent;
    }
}

public class NodeBuilder {

    public static Node build(List<Map<String, String>> input) {

        // maps id of a node to it's Node object
        Map<Integer, Node> nodeMap = new HashMap<Integer, Node>();

        // maps id of a node to the id of it's parent
        Map<Integer, Integer> childParentMap = new HashMap<Integer, Integer>();

        // create special 'root' Node with id=0
        Node root = new Node(null, 0, "root");
        nodeMap.put(root.getId(), root);

        // iterate thru the input
        for (Map<String, String> map : input) {

            // expect each Map to have keys for "id", "name", "parent" ... a
            // real implementation would read from a SQL object or resultset
            int id = Integer.parseInt(map.get("id"));
            String name = map.get("name");
            int parent = Integer.parseInt(map.get("parent"));

            Node node = new Node(null, id, name);
            nodeMap.put(id, node);

            childParentMap.put(id, parent);
        }

        // now that each Node is created, setup the child-parent relationships
        for (Map.Entry<Integer, Integer> entry : childParentMap.entrySet()) {
            int nodeId = entry.getKey();
            int parentId = entry.getValue();

            Node child = nodeMap.get(nodeId);
            Node parent = nodeMap.get(parentId);
            parent.addChild(child);
        }

        return root;
    }
}

public class NodePrinter {

    static void printRootNode(Node root) {
        printNodes(root, 0);
    }

    static void printNodes(Node node, int indentLevel) {

        printNode(node, indentLevel);
        // recurse
        for (Node child : node.getChildren()) {
            printNodes(child, indentLevel + 1);
        }
    }

    static void printNode(Node node, int indentLevel) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < indentLevel; i++) {
            sb.append("\t");
        }
        sb.append(node);

        System.out.println(sb.toString());
    }

    public static void main(String[] args) {

        // setup dummy data
        List<Map<String, String>> resultSet = new ArrayList<Map<String, String>>();
        resultSet.add(newMap("1", "Node 1", "0"));
        resultSet.add(newMap("2", "Node 1.1", "1"));
        resultSet.add(newMap("3", "Node 2", "0"));
        resultSet.add(newMap("4", "Node 1.1.1", "2"));
        resultSet.add(newMap("5", "Node 2.1", "3"));
        resultSet.add(newMap("6", "Node 1.2", "1"));

        Node root = NodeBuilder.build(resultSet);
        printRootNode(root);

    }

    //convenience method for creating our dummy data
    private static Map<String, String> newMap(String id, String name, String parentId) {
        Map<String, String> row = new HashMap<String, String>();
        row.put("id", id);
        row.put("name", name);
        row.put("parent", parentId);
        return row;
    }
}
Мэтт Б
источник
Мне всегда трудно отфильтровать специфичную для алгоритма часть от специфичной для реализации части, когда представлено много исходного кода. Вот почему я попросил решение, которое изначально не зависело от языка. Но это делает работу, так что спасибо за ваше время!
Томалак
Теперь я понимаю, что вы имеете в виду, если не очевидно, что основной алгоритм находится в NodeBuilder.build () - возможно, я мог бы лучше подвести итог.
Мэтт б
5

Есть действительно хорошие решения, которые используют внутреннее btree представление индексов sql. Это основано на некоторых замечательных исследованиях, проведенных в 1998 году.

Вот пример таблицы (в MySQL).

CREATE TABLE `node` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `tw` int(10) unsigned NOT NULL,
  `pa` int(10) unsigned DEFAULT NULL,
  `sz` int(10) unsigned DEFAULT NULL,
  `nc` int(11) GENERATED ALWAYS AS (tw+sz) STORED,
  PRIMARY KEY (`id`),
  KEY `node_tw_index` (`tw`),
  KEY `node_pa_index` (`pa`),
  KEY `node_nc_index` (`nc`),
  CONSTRAINT `node_pa_fk` FOREIGN KEY (`pa`) REFERENCES `node` (`tw`) ON DELETE CASCADE
)

Единственные поля, необходимые для представления дерева:

  • tw: индекс предварительного заказа DFS слева направо, где root = 1.
  • pa: ссылка (с использованием tw) на родительский узел, root имеет значение null.
  • sz: размер ветви узла, включая самого себя.
  • NC: используется в качестве синтаксического сахара. это tw + nc и представляет двойку «следующего потомка» узла.

Вот пример 24 узла населения, упорядоченный по tw:

+-----+---------+----+------+------+------+
| id  | name    | tw | pa   | sz   | nc   |
+-----+---------+----+------+------+------+
|   1 | Root    |  1 | NULL |   24 |   25 |
|   2 | A       |  2 |    1 |   14 |   16 |
|   3 | AA      |  3 |    2 |    1 |    4 |
|   4 | AB      |  4 |    2 |    7 |   11 |
|   5 | ABA     |  5 |    4 |    1 |    6 |
|   6 | ABB     |  6 |    4 |    3 |    9 |
|   7 | ABBA    |  7 |    6 |    1 |    8 |
|   8 | ABBB    |  8 |    6 |    1 |    9 |
|   9 | ABC     |  9 |    4 |    2 |   11 |
|  10 | ABCD    | 10 |    9 |    1 |   11 |
|  11 | AC      | 11 |    2 |    4 |   15 |
|  12 | ACA     | 12 |   11 |    2 |   14 |
|  13 | ACAA    | 13 |   12 |    1 |   14 |
|  14 | ACB     | 14 |   11 |    1 |   15 |
|  15 | AD      | 15 |    2 |    1 |   16 |
|  16 | B       | 16 |    1 |    1 |   17 |
|  17 | C       | 17 |    1 |    6 |   23 |
| 359 | C0      | 18 |   17 |    5 |   23 |
| 360 | C1      | 19 |   18 |    4 |   23 |
| 361 | C2(res) | 20 |   19 |    3 |   23 |
| 362 | C3      | 21 |   20 |    2 |   23 |
| 363 | C4      | 22 |   21 |    1 |   23 |
|  18 | D       | 23 |    1 |    1 |   24 |
|  19 | E       | 24 |    1 |    1 |   25 |
+-----+---------+----+------+------+------+

Каждый результат дерева может быть сделан не рекурсивно. Например, чтобы получить список предков узла в tw = '22 '

Предки

select anc.* from node me,node anc 
where me.tw=22 and anc.nc >= me.tw and anc.tw <= me.tw 
order by anc.tw;
+-----+---------+----+------+------+------+
| id  | name    | tw | pa   | sz   | nc   |
+-----+---------+----+------+------+------+
|   1 | Root    |  1 | NULL |   24 |   25 |
|  17 | C       | 17 |    1 |    6 |   23 |
| 359 | C0      | 18 |   17 |    5 |   23 |
| 360 | C1      | 19 |   18 |    4 |   23 |
| 361 | C2(res) | 20 |   19 |    3 |   23 |
| 362 | C3      | 21 |   20 |    2 |   23 |
| 363 | C4      | 22 |   21 |    1 |   23 |
+-----+---------+----+------+------+------+

Братья и сестры и дети тривиальны - просто используйте порядок полей по tw.

Потомки

Например, набор (ветвь) узлов, которые имеют корни в tw = 17.

select des.* from node me,node des 
where me.tw=17 and des.tw < me.nc and des.tw >= me.tw 
order by des.tw;
+-----+---------+----+------+------+------+
| id  | name    | tw | pa   | sz   | nc   |
+-----+---------+----+------+------+------+
|  17 | C       | 17 |    1 |    6 |   23 |
| 359 | C0      | 18 |   17 |    5 |   23 |
| 360 | C1      | 19 |   18 |    4 |   23 |
| 361 | C2(res) | 20 |   19 |    3 |   23 |
| 362 | C3      | 21 |   20 |    2 |   23 |
| 363 | C4      | 22 |   21 |    1 |   23 |
+-----+---------+----+------+------+------+

Дополнительные замечания

Эта методология чрезвычайно полезна для случаев, когда число операций чтения намного больше, чем вставок или обновлений.

Поскольку для вставки, перемещения или обновления узла в дереве требуется корректировка дерева, необходимо заблокировать таблицу перед началом действия.

Стоимость вставки / удаления высока, потому что значения индекса tw и sz (размер ветви) необходимо будет обновить на всех узлах после точки вставки и для всех предков соответственно.

Перемещение веток включает в себя перемещение значения tw ветви за пределы диапазона, поэтому также необходимо отключить ограничения внешнего ключа при перемещении ветви. По сути, для перемещения ветки требуется четыре запроса:

  • Переместите ветку за пределы диапазона.
  • Закройте пробел, который он оставил. (оставшееся дерево теперь нормализовано).
  • Откройте пробел, куда он пойдет.
  • Переместите ветку в новую позицию.

Настройте запросы дерева

Открытие / закрытие пробелов в дереве является важной подфункцией, используемой методами create / update / delete, поэтому я включу ее здесь.

Нам нужны два параметра - флаг, показывающий, уменьшаем мы размер или нет, и индекс tw узла. Так, например, tw = 18 (который имеет размер ветви 5). Давайте предположим, что мы сокращаем (удаляем tw) - это означает, что мы используем '-' вместо '+' в обновлениях следующего примера.

Сначала мы используем (слегка измененную) функцию предка, чтобы обновить значение sz.

update node me, node anc set anc.sz = anc.sz - me.sz from 
node me, node anc where me.tw=18 
and ((anc.nc >= me.tw and anc.tw < me.pa) or (anc.tw=me.pa));

Затем нам нужно настроить tw для тех, чье tw больше, чем ветвь, которую нужно удалить.

update node me, node anc set anc.tw = anc.tw - me.sz from 
node me, node anc where me.tw=18 and anc.tw >= me.tw;

Затем нам нужно настроить родителя для тех, у кого pa больше чем ветвь, которую нужно удалить.

update node me, node anc set anc.pa = anc.pa - me.sz from 
node me, node anc where me.tw=18 and anc.pa >= me.tw;
Кёнчог
источник
3

Предполагая, что вы знаете, что корневые элементы равны нулю, вот псевдокод для вывода в текст:

function PrintLevel (int curr, int level)
    //print the indents
    for (i=1; i<=level; i++)
        print a tab
    print curr \n;
    for each child in the table with a parent of curr
        PrintLevel (child, level+1)


for each elementID where the parentid is zero
    PrintLevel(elementID, 0)
WCM
источник
3

Вы можете эмулировать любую другую структуру данных с помощью хэш-карты, так что это не страшное ограничение. Сканируя сверху вниз, вы создаете хэш-карту для каждой строки базы данных с записью для каждого столбца. Добавьте каждую из этих хеш-карт в «главную» хеш-карту, указав идентификатор. Если у какого-либо узла есть «родительский элемент», которого вы еще не видели, создайте для него запись-заполнитель в главной хэш-карте и заполните его, когда увидите реальный узел.

Чтобы распечатать его, сделайте простой проход в глубину по данным, отслеживая уровень отступа по пути. Это можно упростить, сохранив запись «children» для каждой строки и заполнив ее при сканировании данных.

Что касается того, существует ли «лучший» способ хранения дерева в базе данных, это зависит от того, как вы собираетесь использовать данные. Я видел системы с известной максимальной глубиной, которые использовали разные таблицы для каждого уровня в иерархии. Это имеет большой смысл, если уровни в дереве не совсем эквивалентны (категории верхнего уровня отличаются от листьев).

Марк Бесси
источник
1

Если могут быть созданы вложенные хеш-карты или массивы, то я могу просто пойти вниз по таблице с самого начала и добавить каждый элемент во вложенный массив. Я должен проследить каждую строку до корневого узла, чтобы узнать, в какой уровень вложенного массива вставлять. Я могу использовать напоминание, чтобы мне не приходилось искать одного и того же родителя снова и снова.

Редактировать: я бы сначала прочитал всю таблицу в массив, чтобы он не запрашивал БД несколько раз. Конечно, это не будет практичным, если ваш стол очень большой.

После того, как структура построена, я должен сначала пройти через глубину и распечатать HTML.

Нет лучшего фундаментального способа хранения этой информации с использованием одной таблицы (хотя я могу ошибаться;), и я бы хотел найти лучшее решение). Однако, если вы создадите схему для использования динамически создаваемых таблиц БД, то вы откроете для себя целый новый мир, жертвуя простотой и риском адского SQL;).

tchen
источник
1
Я бы предпочел не менять структуру БД только потому, что необходим новый уровень подузлов. :-)
Томалак
1

Если элементы расположены в древовидном порядке, как показано в вашем примере, вы можете использовать что-то вроде следующего примера Python:

delimiter = '.'
stack = []
for item in items:
  while stack and not item.startswith(stack[-1]+delimiter):
    print "</div>"
    stack.pop()
  print "<div>"
  print item
  stack.append(item)

То, что это делает, поддерживает стек, представляющий текущую позицию в дереве. Для каждого элемента в таблице он выталкивает элементы стека (закрывая соответствующие элементы div), пока не найдет родителя текущего элемента. Затем он выводит начало этого узла и помещает его в стек.

Если вы хотите вывести дерево, используя отступы, а не вложенные элементы, вы можете просто пропустить операторы print, чтобы напечатать div, и вывести число пробелов, равное некоторому кратному размеру стека, перед каждым элементом. Например, в Python:

print "  " * len(stack)

Вы также можете легко использовать этот метод для создания набора вложенных списков или словарей.

Изменить: я вижу из вашего пояснения, что имена не были предназначены для пути узлов. Это предполагает альтернативный подход:

idx = {}
idx[0] = []
for node in results:
  child_list = []
  idx[node.Id] = child_list
  idx[node.ParentId].append((node, child_list))

Это создает дерево массивов кортежей (!). idx [0] представляет корень (и) дерева. Каждый элемент в массиве представляет собой 2-кортеж, состоящий из самого узла и списка всех его дочерних элементов. После создания вы можете удерживать idx [0] и сбрасывать idx, если только вы не хотите обращаться к узлам по их ID.

Ник Джонсон
источник
1

Чтобы расширить SQL-решение Билла, вы можете сделать то же самое, используя плоский массив. Более того, если все ваши строки имеют одинаковую длину и ваше максимальное число дочерних элементов известно (скажем, в двоичном дереве), вы можете сделать это, используя одну строку (массив символов). Если у вас произвольное количество детей, это немного усложняет ... Я должен был бы проверить мои старые записи, чтобы посмотреть, что можно сделать.

Затем, пожертвовав небольшим количеством памяти, особенно если ваше дерево редкое и / или несбалансированное, вы можете, с небольшим количеством математической индексации, получить доступ ко всем строкам случайным образом, сохраняя ваше дерево, сначала ширину в массиве, например (для двоичного файла) дерево)

String[] nodeArray = [L0root, L1child1, L1child2, L2Child1, L2Child2, L2Child3, L2Child4] ...

Вы знаете свою длину строки, вы знаете это

Я сейчас на работе, поэтому не могу тратить на это много времени, но с интересом могу принести немного кода для этого.

Мы используем это для поиска в двоичных деревьях, состоящих из кодонов ДНК, процесс строит дерево, затем мы сплющиваем его для поиска шаблонов текста, и когда найдем, хотя по индексу математики (наоборот) мы получаем узел обратно ... очень быстро и эффективно, хотя в нашем дереве редко были пустые узлы, но мы могли быстро найти гигабайты данных.

Newtopian
источник
0

Подумайте об использовании таких инструментов nosql, как neo4j, для иерархических структур. например, сетевое приложение, такое как linkedin, использует couchbase (другое решение nosql)

Но используйте nosql только для запросов уровня витрины данных, а не для хранения / поддержки транзакций

сринивасулу кандакуру
источник
Прочитав о сложностях и совершенстве структур SQL и «не таблиц», это была моя первая мысль, nosql. Конечно, существует много проблем с экспортом и т. Д. Кроме того, в ОП упоминаются только таблицы. Ну что ж. Я не эксперт по БД, как очевидно.
Josef.B