Оптимизированный SQL для древовидных структур

Как бы вы получили древовидные данные из базы данных с лучшей производительностью? Например, предположим, что у вас есть иерархия папок в базе данных. Где папка-база данных-строка имеет ID, имя и атрибутом parentId столбцы.

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

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

может быть, есть разные ответы, основанные на количестве X строк базы данных, глубине иерархии или что-то еще?

редактировать: Я использую Microsoft SQL Server, но ответы из других точек зрения тоже интересны.

12 ответов


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


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

один умный метод-дать каждому узлу идентификатор строки, где идентификатор родителя является предсказуемой подстрокой ребенка. Например, родитель может быть '01', а дети будут '0100', '0101', '0102', etc. Таким образом, вы можете выбрать все поддерево из базы данных сразу:

SELECT * FROM treedata WHERE id LIKE '0101%';

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


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

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

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

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

мое исследование этого метода началось с этой статьи:управление иерархическими данными в MySQL.


в продукте, над которым я работаю, мы имеем некоторые древовидные структуры, хранящиеся в SQL Server, и используем упомянутый выше метод для хранения иерархии узла в записи. т. е.

tblTreeNode
TreeID = 1
TreeNodeID = 100
ParentTreeNodeID = 99
Hierarchy = ".33.59.99.100."
[...] (actual data payload for node)

поддержание иерархии-это сложный бит, конечно, и использует триггеры. Но создание его на insert/delete / move никогда не является рекурсивным, потому что иерархия родителя или ребенка имеет всю необходимую информацию.

вы можете получить всех потомков узла так:

SELECT * FROM tblNode WHERE Hierarchy LIKE '%.100.%'

вот триггер вставки:

--Setup the top level if there is any
UPDATE T 
SET T.TreeNodeHierarchy = '.' + CONVERT(nvarchar(10), T.TreeNodeID) + '.'
FROM tblTreeNode AS T
    INNER JOIN inserted i ON T.TreeNodeID = i.TreeNodeID
WHERE (i.ParentTreeNodeID IS NULL) AND (i.TreeNodeHierarchy IS NULL)

WHILE EXISTS (SELECT * FROM tblTreeNode WHERE TreeNodeHierarchy IS NULL)
    BEGIN
        --Update those items that we have enough information to update - parent has text in Hierarchy
        UPDATE CHILD 
        SET CHILD.TreeNodeHierarchy = PARENT.TreeNodeHierarchy + CONVERT(nvarchar(10),CHILD.TreeNodeID) + '.'
        FROM tblTreeNode AS CHILD 
            INNER JOIN tblTreeNode AS PARENT ON CHILD.ParentTreeNodeID = PARENT.TreeNodeID
        WHERE (CHILD.TreeNodeHierarchy IS NULL) AND (PARENT.TreeNodeHierarchy IS NOT NULL)
    END

и вот триггер обновления:

--Only want to do something if Parent IDs were changed
IF UPDATE(ParentTreeNodeID)
    BEGIN
        --Update the changed items to reflect their new parents
        UPDATE CHILD
        SET CHILD.TreeNodeHierarchy = CASE WHEN PARENT.TreeNodeID IS NULL THEN '.' + CONVERT(nvarchar,CHILD.TreeNodeID) + '.' ELSE PARENT.TreeNodeHierarchy + CONVERT(nvarchar, CHILD.TreeNodeID) + '.' END
        FROM tblTreeNode AS CHILD 
            INNER JOIN inserted AS I ON CHILD.TreeNodeID = I.TreeNodeID
            LEFT JOIN tblTreeNode AS PARENT ON CHILD.ParentTreeNodeID = PARENT.TreeNodeID

        --Now update any sub items of the changed rows if any exist
        IF EXISTS (
                SELECT * 
                FROM tblTreeNode 
                    INNER JOIN deleted ON tblTreeNode.ParentTreeNodeID = deleted.TreeNodeID
            )
            UPDATE CHILD 
            SET CHILD.TreeNodeHierarchy = NEWPARENT.TreeNodeHierarchy + RIGHT(CHILD.TreeNodeHierarchy, LEN(CHILD.TreeNodeHierarchy) - LEN(OLDPARENT.TreeNodeHierarchy))
            FROM tblTreeNode AS CHILD 
                INNER JOIN deleted AS OLDPARENT ON CHILD.TreeNodeHierarchy LIKE (OLDPARENT.TreeNodeHierarchy + '%')
                INNER JOIN tblTreeNode AS NEWPARENT ON OLDPARENT.TreeNodeID = NEWPARENT.TreeNodeID

    END

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

ALTER TABLE [dbo].[tblTreeNode]  WITH NOCHECK ADD  CONSTRAINT [CK_tblTreeNode_TreeNodeHierarchy] CHECK  
((charindex(('.' + convert(nvarchar(10),[TreeNodeID]) + '.'),[TreeNodeHierarchy],(charindex(('.' + convert(nvarchar(10),[TreeNodeID]) + '.'),[TreeNodeHierarchy]) + 1)) = 0))

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

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



существует несколько распространенных типов запросов к иерархии. Большинство других видов запросов являются вариациями этих.

  1. от родителя найдите всех детей.

    a. На определенную глубину. Например, учитывая моего непосредственного родителя, все дети до глубины 1 будут моими братьями и сестрами.

    b. На дно дерева.

  2. У ребенка, найти всех родителей.

    a. На определенную глубину. Для например, мой непосредственный родитель родители на глубину 1.

    b. На неограниченную глубину.

случаи (a) (определенная глубина) проще в SQL. Частный случай (depth=1) тривиален в SQL. Ненулевая глубина сложнее. Конечная, но ненулевая глубина может быть выполнена через конечное число соединений. Случаи (b) с неопределенной глубиной (сверху, снизу) действительно сложны.

Если вы дерево огромный (миллионы узлов) тогда вы в мире боли, независимо от того, что вы пытаетесь сделать.

Если ваше дерево находится под миллионом узлов, просто извлеките все это в память и работайте над ним там. В мире ОО Жизнь намного проще. Просто извлеките строки и создайте дерево по мере возврата строк.

Если у вас огромный елка, у вас есть два варианта.

  • рекурсивные курсоры для обработки неограниченной выборки. Это значит что обслуживание структуры О (1) -- просто обновите несколько узлов и все. Однако выборка O(n*log (n)), потому что вы должны открыть курсор для каждого узла с дочерними элементами.

  • умные алгоритмы "нумерации кучи" могут кодировать происхождение каждого узла. Как только каждый узел правильно пронумерован, тривиальный SQL SELECT может использоваться для всех четырех типов запросов. Однако изменения древовидной структуры требуют перенумерования узлов, что делает стоимость изменения довольно высокой по сравнению со стоимостью поиск.


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

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

Если вы храните только одно дерево, ваш вопрос становится одним из запросов только поддеревьев, и применялся второй ответ.


Google для "материализованного пути"или" генетических деревьев"...


в Oracle есть SELECT ... Инструкция CONNECT BY для извлечения деревьев.


Я поклонник простого метода хранения идентификатора, связанного с его parentID:

ID     ParentID
1      null
2      null
3      1
4      2
...    ...

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


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


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

ID | ParentCommentID

вы также можете хранить TopCommentID который представляет собой самый верхний комментарий:

ID | ParentCommentID | TopCommentID

здесь TopCommentID и ParentCommentID are null или 0 когда это самый верхний комментарий. Для комментариев ребенка,ParentCommentID указывает на комментарий над ним, и TopCommentID указывает на самый верхний родитель.