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

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

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] Node 1          [3] Node 2
              /       \                   \
    [2] Node 1.1     [6] Node 1.2      [5] Node 2.1
          /          
 [4] Node 1.1.1

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

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

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

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


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

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

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

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

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

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

Ответы [ 14 ]

428 голосов
/ 10 октября 2008

Теперь, когда 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 в своей презентации Рекурсивный сброс запросов в 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_length равно 1.

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;

Комментарий от @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 - Как извлечь информацию в правильном порядке .

55 голосов
/ 11 октября 2008

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

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

id  parent_id  tree_id  level  lft  rght
--  ---------  -------  -----  ---  ----
 1       null        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:

 __________________________________________________________________________
|  Root 1                                                                  |
|   ________________________________    ________________________________   |
|  |  Child 1.1                     |  |  Child 1.2                     |  |
|  |   ___________    ___________   |  |   ___________    ___________   |  |
|  |  |  C 1.1.1  |  |  C 1.1.2  |  |  |  |  C 1.2.1  |  |  C 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:

18 голосов
/ 13 марта 2014

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

Чтобы прочитать древовидную структуру, вы можете использовать рекурсивные выражения общих таблиц (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 внутри родителя. Это говорит нам о том, как представить их в узле output-link родительскому элементу в этом порядке.

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

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

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

18 голосов
/ 11 октября 2008

Начиная с 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]

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

Name
'Node 1'
'    Node 1.1'
'        Node 1.1.1'
'    Node 1.2'
'Node 2'
'    Node 2.1'
9 голосов
/ 22 декабря 2010

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

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

mysql> desc nodes ;
+-------------+--------------+------+-----+---------+----------------+
| Field       | Type         | Null | Key | Default | Extra          |
+-------------+--------------+------+-----+---------+----------------+
| id          | int(11)      | NO   | PRI | NULL    | auto_increment |
| name        | varchar(255) | YES  |     | NULL    |                |
| leftSibling | int(11)      | NO   |     | 0       |                |
+-------------+--------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

mysql> desc adjacencies;
+------------+---------+------+-----+---------+----------------+
| Field      | Type    | Null | Key | Default | Extra          |
+------------+---------+------+-----+---------+----------------+
| relationId | int(11) | NO   | PRI | NULL    | auto_increment |
| parent     | int(11) | NO   |     | NULL    |                |
| child      | int(11) | NO   |     | NULL    |                |
| pathLen    | int(11) | NO   |     | NULL    |                |
+------------+---------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

Подробнее и код SQL в моем блоге .

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

7 голосов
/ 10 октября 2008

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

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

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. Это противно . Если у вас есть выбор, пройдите маршрут ООП.

5 голосов
/ 10 октября 2008

Это было написано быстро, и не является ни привлекательным, ни эффективным (к тому же много автобоксов, преобразование между 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;
    }
}
3 голосов
/ 14 марта 2017

Существуют действительно хорошие решения, которые используют внутреннее представление дерева индексов 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 = 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 голосов
/ 10 октября 2008

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

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

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

3 голосов
/ 10 октября 2008

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

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