Який найефективніший / елегантний спосіб розібрати плоский стіл на дерево?


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] КОРИНА
                          / \ 
              [1] Вузол 1 [3] Вузол 2
              / \ \
    [2] Вузол 1.1 [6] Вузол 1.2 [5] Вузол 2.1
          /          
 [4] Вузол 1.1.1

Який мінімалістичний підхід ви б використали для виведення цього HTML (або тексту для цього питання) як правильно впорядкованого, правильно відрізаного дерева?

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

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

Питання про бонус: Чи існує принципово кращий спосіб збереження такої структури дерева в RDBMS?


ЗМІНИ ТА ДОПОМОГИ

Щоб відповісти на запитання одного коментатора ( Марка Бессея ): Корінний вузол не потрібен, оскільки він ніколи не відображатиметься. ParentId = 0 - це умова для вираження "це верхній рівень". Стовпець "Порядок" визначає, як будуть сортуватися вузли з тим самим батьком.

"Набір результатів", про який я говорив, можна зобразити як масив хешмапів (щоб залишитися в цій термінології). Бо мій приклад мав на увазі бути вже там. Деякі відповіді проходять додаткову милю і споруджують її спочатку, але це добре.

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

Не помиляйтеся моїм вибором іменування вузла ("Вузол 1.1.1"), на що можна покластися. Вузли однаково добре можна назвати "Френк" або "Боб", структура імен не передбачається, це було просто для того, щоб зробити його читабельним.

Я розмістив своє власне рішення, щоб ви, хлопці, могли його розрізати.


2
"жодних фантазійних об'єктів із посиланнями батьків / дітей" - чому б і ні? Створення базового об'єкта Node за допомогою методу .addChild (), .getParent () дозволяє досить добре моделювати зв'язок між вузлами.
мат б

2
Це звичайне (n дітей, де n може бути> 2) дерево або двійкове дерево (у вузлі може бути 0, 1 або 2 дітей)?
BKimmel

Оскільки ви можете реалізувати належну структуру даних вузла за допомогою хешмапу, реального обмеження тут немає, просто більше роботи.
Svante

... і саме це ви зробили.
Svante

Відповіді:


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 у своїй презентації " Рекрусивне запит", що знижується у 2017 році.

Нижче моя оригінальна відповідь з 2008 року:


Існує кілька способів зберігання структурованих деревом даних у реляційній базі даних. Те, що ви показуєте у своєму прикладі, використовує два способи:

  • Список суміжності (стовпець "батьків") та
  • Перерахування шляху (пунктирні числа у стовпці вашого імені).

Інше рішення називається Nested Sets , і воно може зберігатися і в одній таблиці. Прочитайте " Дерева та ієрархії в SQL для розумних " Джо Челко для отримання додаткової інформації про ці проекти.

Зазвичай я віддаю перевагу дизайну під назвою " Таблиця закриття" (він же "співвідношення суміжності") для зберігання структурованих деревом даних. Для цього потрібна інша таблиця, але тоді запити дерев досить легко.

Я висвітлюю Таблицю закриття в моїй презентації Моделі ієрархічних даних із 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-satis щодо найближчих дітей (або безпосереднього батька). Ви можете додати 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;

Повторити коментар від @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 b.path_length, f.name, мабуть, щоб переконатися, що замовлення відповідає ієрархії. Але це не працює, тому що він буде замовляти "Вузол 1.1.1" після "Вузла 1.2".

Якщо ви хочете, щоб замовлення відповідало ієрархії розумним чином, це можливо, але не просто шляхом впорядкування за довжиною шляху. Наприклад, дивіться мою відповідь до ієрархічної бази даних таблиці закриття MySQL - Як вивести інформацію у правильному порядку .


6
Це дуже елегантно, дякую. Бонусний бал присуджується ;-) Я бачу один невеликий недолік - оскільки він зберігає дочірнє відношення явно і неявно, вам потрібно зробити багато ретельного оновлення навіть для невеликого зрушення структури дерева.
Томалак

16
Правда, кожен метод зберігання деревних структур у базі даних вимагає певної роботи, або під час створення або оновлення дерева, або під час запитів дерев та підрядів. Виберіть дизайн, на основі якого ви хочете бути простішим: писати чи читати.
Білл Карвін

2
@buffer, є шанс створити невідповідності, коли ви створюєте всі рядки для ієрархії. У списку суміжності ( parent_id) є лише один рядок для вираження кожного стосунка батько-дитина, але Таблиця закриття містить багато.
Білл Карвін

1
@BillKarwin Ще одне - це таблиці закриття, придатні для графіка з декількома шляхами до будь-якого заданого вузла (наприклад, ієрархія категорій, де будь-який вузол із листям або не листком може належати більше ніж одному з батьків)
користувач

2
@Reza, так що якщо ви додасте новий дочірній вузол, ви можете запитувати всіх нащадків (1), і це предки нової дитини.
Білл Карвін

58

Якщо ви використовуєте вкладені набори (іноді їх називають Модифікованим попереднім замовленням дерева), ви можете витягти всю структуру дерева або будь-яке підкреслене дерево в ньому в порядку дерева за допомогою одного запиту, що коштує, щоб вставки були дорожчими, як вам потрібно керуйте стовпцями, які описують шлях порядку в структурі дерева.

Для django-mptt я використовував таку структуру:

id parent_id tree_id рівень 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 | |
| | ___________ ___________ | | ___________ ___________ | |
| | | 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:


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-вузли;
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
| Поле | Тип | Нульовий | Ключові | За замовчуванням | Додатковий |
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
| id | int (11) | НІ | ПРІ | NULL | auto_increment |
| назва | варчар (255) | ТАК | | NULL | |
| leftSibling | int (11) | НІ | | 0 | |
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
3 рядки в наборі (0,00 сек)

mysql> desc суміжності;
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
| Поле | Тип | Нульовий | Ключові | За замовчуванням | Додатковий |
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
| відношенняId | int (11) | НІ | ПРІ | NULL | auto_increment |
| батьківський | int (11) | НІ | | NULL | |
| дитина | int (11) | НІ | | NULL | |
| pathLen | int (11) | НІ | | NULL | |
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
4 ряди в наборі (0,00 сек)

Детальніше та код SQL в моєму блозі .

Дякую Біллу, ваша відповідь була корисною для початку!


7

З урахуванням вибору, я б використовував об'єкти. Я б створив об'єкт для кожної записи, де кожен об’єкт має childrenколекцію, і зберігаю їх у масиві допоміжних (/ хештелів), де ключ є ключем. І промайструйте колекцію один раз, додавши дітей до відповідних дитячих полів. Простий.

Але оскільки вам не забавно, обмеживши використання хорошого ООП, я, мабуть, повторюю:

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. Це противно . Якщо у вас є вибір, вирушайте по маршруту OOP.


Це те, що я мав на увазі під "без рамки" - ви використовуєте LINQ, чи не так? Щодо першого абзацу: Набір результатів вже є, навіщо спочатку копіювати всю інформацію в нову структуру об'єкта? (Мені було недостатньо зрозуміло з цього факту, вибачте)
Томалак

Томалак - жоден код не є псевдокодом. Звичайно, вам доведеться розбити речі на правильний вибір та ітератори ... та справжній синтаксис! Чому OOP? Тому що ви можете точно відобразити структуру. Це добре тримає речі, і воно просто виявляється більш ефективним (лише один вибір)
Олі,

У мене теж не було повторних виборів. Стосовно OOP: Марк Бессі у своїй відповіді сказав: "Ви можете імітувати будь-яку іншу структуру даних за допомогою хешмапу, тому це не страшне обмеження". Ваше рішення є правильним, але, я думаю, є певне покращення навіть без OOP.
Томалак

5

Це було написано швидко, і не є ні гарним, ні ефективним (до того ж це багато автобоксу, перетворюючи між собою intі Integerдратує!), Але це працює.

Це, ймовірно, порушує правила, оскільки я створюю свої власні об'єкти, але ей, я роблю це як відволікання від реальної роботи :)

Це також передбачає, що результатSet / таблиця повністю зчитується в якусь структуру перед тим, як почати створювати Node, що було б не найкращим рішенням, якщо у вас є сотні тисяч рядків.

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 і являє собою "наступну дитину" tw вузла.

Ось приклад 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 гілки поза діапазоном, тому необхідно також відключити обмеження сторонніх ключів при переміщенні гілки. Для переміщення філії потрібно, по суті, чотири запити:

  • Перемістіть гілку поза діапазоном.
  • Закрийте проміжок, який він залишив. (дерево, що залишилося, зараз нормалізується).
  • Відкрийте проміжок, куди він піде.
  • Перемістіть гілку на нове місце.

Налаштування запитів дерев

Відкриття / закриття прогалин у дереві є важливою підфункцією, яка використовується методами створення / оновлення / видалення, тому я включаю його сюди.

Нам потрібні два параметри - прапор, який відображає зменшення чи зменшення чи зменшення розміру, і 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;

Тоді нам потрібно відрегулювати батьківство для тих, у кого патронник вище, ніж гілка, яку потрібно видалити.

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)

3

Ви можете імітувати будь-яку іншу структуру даних за допомогою хешмапу, так що це не страшне обмеження. Скануючи зверху вниз, ви створюєте хеш-карту для кожного ряду бази даних із записом для кожного стовпця. Додайте кожну з цих хешмапів до "головного" хешмапу, введеного на ідентифікатор. Якщо будь-який вузол має "батьківський", якого ви ще не бачили, створіть для нього запис заповнювача у головній хешмапі та заповніть його, коли побачите фактичний вузол.

Щоб роздрукувати його, зробіть простий спочатку глибину проходження даних, відстежуючи рівень відступу на цьому шляху. Це можна зробити простіше, зберігаючи запис "дітей" для кожного рядка та заповнюючи його під час сканування даних.

Що стосується того, чи існує "кращий" спосіб зберігання дерева в базі даних, це залежить від того, як ви будете використовувати дані. Я бачив системи, які мали відому максимальну глибину, яка використовувала іншу таблицю для кожного рівня в ієрархії. Це має багато сенсу, якщо рівні в дереві зрештою не зовсім рівноцінні (категорії верхнього рівня відрізняються від листя).


1

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

Редагувати: Я спершу прочитав би всю таблицю в масив, тому вона не запитуватиме БД повторно. Звичайно, це не буде практично, якщо ваш стіл дуже великий.

Після побудови структури я повинен пройти глибину спочатку через неї та роздрукувати HTML.

Немає кращого принципового способу зберігання цієї інформації за допомогою однієї таблиці (я можу помилитися;), і я хотів би побачити краще рішення). Однак якщо ви створили схему для використання динамічно створених db-таблиць, ви відкрили цілий новий світ при жертві простоти та ризику пекла SQL;).


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)

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

Якщо ви хочете вивести дерево з використанням відступів, а не вкладених елементів, ви можете просто пропустити оператори друку, щоб надрукувати divs, і надрукувати кількість пробілів, рівних деякому кратному розміру стека перед кожним елементом. Наприклад, у 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, якщо ви не хочете отримати доступ до вузлів за їх ідентифікатором.


1

Для розширення SQL-рішення Білла ви можете зробити те ж саме, використовуючи плоский масив. Більше того, якщо всі ваші рядки мають однакову довжину і ваша максимальна кількість дітей відома (скажімо, у двійковому дереві), ви можете це зробити, використовуючи один рядок (символьний масив). Якщо у вас є довільна кількість дітей, це трохи ускладнює справи ... Мені доведеться перевірити свої старі записки, щоб побачити, що можна зробити.

Тоді, пожертвувавши трохи пам’яті, особливо якщо ваше дерево розріджене та / або незбалансоване, ви можете, з невеликою кількістю математичної індексу, отримати доступ до всіх рядків випадковим чином, зберігаючи дерево, ширина спочатку в масиві так (для двійкового дерево):

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

ти знаєш довжину струни, ти це знаєш

Я зараз на роботі, тому не можу витратити на це багато часу, але з інтересом я можу отримати трохи коду для цього.

Ми використовуємо це для пошуку в двійкових деревах, виготовлених з кодонів ДНК, процес побудував дерево, потім ми його згладили для пошуку тексту тексту і, коли його знайшли, хоча індексна математика (реверс зверху) ми повертаємо вузол назад ... дуже Швидке та ефективне, міцне наше дерево рідко має порожні вузли, але ми могли швидко шукати гігабайти даних.


0

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

Але використовуйте nosql лише для запитів рівня марта даних, а не для зберігання / підтримки транзакцій


Ознайомившись зі складнощами та можливостями SQL та "нетабличних" структур, це була і моя перша думка, nosql. Звичайно, існує стільки питань щодо експорту тощо. Крім того, ОП згадував лише таблиці. Ну добре. Я не експерт з питань БД, як очевидно.
Йозеф.Б.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.