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

это поможет вам

function CallPageSync(url, data) {
    var response;
    $.ajax({
        async: false,
        url: url,
        data: data,
        timeout: 4000,
        success: function(result) {
            response = result;
        },
        error: function(XMLHttpRequest, textStatus, errorThrown) {
            response = "err--" + XMLHttpRequest.status + " -- " + XMLHttpRequest.statusText;
        }
    });
    return response;
}

, и вы можете называть его как

response = CallPageSync(checkPageURL, "un=" + username);
501
задан Community 23 May 2017 в 12:18
поделиться

10 ответов

Теперь, когда 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 в моем представлении рекурсивный запрос Throwdown в 2017.

Ниже мой исходный ответ с 2008:

<час>

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

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

Другое решение называют Вложенные Наборы , и оно может быть сохранено в той же таблице также. Читайте" Деревья и Иерархии в SQL для Присяжных острословов " Joe Celko для намного большей информации об этих проектах.

я обычно предпочитаю дизайн, названный Таблица Закрытия (иначе столбец "Adjacency Relation") for storing tree-structured data. It requires another table, but then querying trees is pretty easy.

I cover Closure Table in my presentation Models for Hierarchical Data with SQL and PHP and in my book SQL Antipatterns: Avoiding the Pitfalls of Database Programming .

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

Store all paths in the Closure Table, where there is a direct ancestry from one node to another. Include a row for each node to reference itself. For example, using the data set you showed in your question:

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

Now you can get a tree starting at node 1 like this:

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

The output (in MySQL client) looks like the following:

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

In other words, nodes 3 and 5 are excluded, because they are part of a separate hierarchy, not descending from node 1.


Re: comment from e-satis about immediate children (or immediate parent). You can add a "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, который содержит другие атрибуты узла такой как [1 112], и вид именем.

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, по-видимому, чтобы удостовериться, что упорядочивание соответствует иерархии. Но это не работает, потому что это заказало бы "Узел 1.1.1" после "Узла 1.2".

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

435
ответ дан Bill Karwin 23 May 2017 в 12:18
поделиться

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

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

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

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

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

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

1
ответ дан bluish 23 May 2017 в 12:18
поделиться

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

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

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

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

1
ответ дан tchen 23 May 2017 в 12:18
поделиться

Если элементы находятся в древовидном порядке, как показано в Вашем примере, можно использовать что-то как следующий пример 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)

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

, Если Вы хотите произвести расположение с отступом использования дерева, а не вложенные элементы, можно просто пропустить операторы печати, чтобы распечатать отделения и распечатать много пробелов, равных некоторым несколько из размера стека перед каждым объектом. Например, в 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
ответ дан Nick Johnson 23 May 2017 в 12:18
поделиться

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

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
ответ дан wcm 23 May 2017 в 12:18
поделиться

Хорошо, учитывая выбор, я использовал бы объекты. Я создал бы объект для каждой записи, где каждый объект имеет 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. Это противно . , Если у Вас есть выбор, пойдите путем ООП.

7
ответ дан Oli 23 May 2017 в 12:18
поделиться

С Oracle 9i, можно использовать ПОДКЛЮЧЕНИЕ.

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'
18
ответ дан Eric Weilnau 23 May 2017 в 12:18
поделиться

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

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

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

3
ответ дан Mark Bessey 23 May 2017 в 12:18
поделиться

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

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

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

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;
    }
}
5
ответ дан matt b 23 May 2017 в 12:18
поделиться

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

Для [1 117] 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, столбцы должны быть скорректированы соответственно, когда эти операции происходят, чтобы создать или преодолеть разрывы. Я сделал [приблизительно 1 118] примечания разработки в то время, когда я пытался обернуть голову вокруг запросов, требуемых для каждой операции.

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

[еще 1133] информация о MPTT:

55
ответ дан Jonny Buchanan 23 May 2017 в 12:18
поделиться
Другие вопросы по тегам:

Похожие вопросы: