Как можно эффективно объединить две иерархии в SQL Server?

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

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

В обязательном порядке обработка, которую необходимо выполнить, будет выглядеть примерно так:

For each row, RS, in the staging table:
    If there is not already a row with the same Id as RS in the main table:
         Find the parent, PS, of the staging row
         Find the row, PM, in the main table that has the same node ID as PS
         Create a new child, RM of row PM
         Set PM's ID equal to the ID of RS

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

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

Однако это ужасно неэффективный подход и слишком медленный для моих целей. Есть ли лучший способ?

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

Обновить

Как и просили, вот конкретный пример.

Таблицы «staging» и «main» имеют одни и те же два столбца:

   hierarchy_id of type hierarchyid
   node_id of type bigint

Исходное содержание

основной:

 hierarchy_id    node_id
 /1/             1
 /1/1/           2
 /1/2/           3
 /1/3/           4

постановка:

 hierarchy_id    node_id
 /1/             1
 /1/1/           3
 /1/2/           5
 /1/1/1/         6

Желаемый контент

основной:

 hierarchy_id    node_id
 /1/             1
 /1/1/           2
 /1/2/           3
 /1/3/           4
 /1/4/           5
 /1/2/1/         6

Обратите внимание, что узел в промежуточной таблице с иерархией_id /1/1/ соответствует узлу с hiearchy_id /1/2/ в целевой таблице (вот почему node_id важен — нельзя просто копировать значения иерархии_ид). Также обратите внимание, что новый узел с node_id 6 добавляется как дочерний элемент правильного родителя, узла с node_id 3, поэтому иерархия_id важна — она определяет древовидную структуру (отношения родитель/потомок) для любых новых узлов. Любое решение должно учитывать оба аспекта.


person Tom    schedule 14.08.2011    source источник
comment
Будет ли допустимым первым шагом удалить все строки, которые уже имеют такой же идентификатор узла, существующий в целевой таблице? Похоже, что это устранит необходимость проверки дубликатов в процессе слияния.   -  person Neil N    schedule 14.08.2011
comment
@OMG Пони, нет дочерней и родительской таблиц. Есть две таблицы, каждая со столбцом иерархии (msdn.microsoft.com/en -us/library/bb677290.aspx). Идентификатор узла устанавливает эквивалентность узлов в двух таблицах, но если узел еще не существует в целевой таблице, недостаточно просто скопировать его идентификатор узла — его также необходимо добавить в соответствующее место в таблице. иерархия, определяемая столбцом иерархии целевой таблицы.   -  person Tom    schedule 14.08.2011
comment
Между прочим, я открыт для решений, которые не используют иерархию. Однако, насколько я понимаю, для обработки произвольно глубоких деревьев без них требуются общие табличные выражения, которые кажутся синтаксически отвратительными, а источники, которые я читал, предполагают, что они медленнее, чем иерархии.   -  person Tom    schedule 14.08.2011
comment
@Neil N, я мог бы удалить такие записи из исходной таблицы, хотя это может усложнить некоторые вещи, которые мне придется делать позже. Однако проверка дубликатов на самом деле не является проблемой - если я смогу добиться этого с помощью оператора MERGE, это было бы здорово.   -  person Tom    schedule 14.08.2011
comment
Я думаю, было бы полезно, если бы вы дали нам пример структуры таблицы и ожидаемый результат. Мне трудно понять, почему вы не можете сделать это с помощью простого оператора MERGE; вам не разрешено изменять иерархию или у вас будут дубликаты иерархии таким образом? Я построил самоподдерживающийся гибрид списка иерархии/смежности, который может быть тем, что вы ищете, но это немного сложно сказать по вопросу.   -  person Aaronaught    schedule 14.08.2011
comment
@Aaronought - я добавил пример. Вполне возможно, что это можно сделать с помощью простого оператора MERGE (я не эксперт в SQL). Однако я не могу просто объединить столбец иерархии. Было бы недопустимо копировать значения иерархии, поскольку значения в промежуточной таблице определяют только древовидную структуру в промежуточной таблице. По сути, для уже существующих узлов ничего делать не нужно. Любую ветвь, которая еще не существует, необходимо скопировать с неповрежденной иерархической структурой и соединить с соответствующим узлом в целевом объекте. Надеюсь, это прояснит ситуацию — сложно описать.   -  person Tom    schedule 14.08.2011
comment
Есть ли в промежуточной таблице только добавления узлов или ее можно использовать для переопределения родителя для уже существующего узла? Как добавить новый корневой узел для всего дерева?   -  person Mikael Eriksson    schedule 14.08.2011
comment
@Mikael Eriksson, промежуточная таблица будет содержать только новые ветки и узлы, которые уже существуют в целевой таблице. Кроме того, в промежуточной таблице не будет узлов без родителей. Каждый узел в промежуточной таблице будет иметь неповрежденный путь узлов до корня, присутствующего вместе с ним.   -  person Tom    schedule 14.08.2011
comment
Хм. Хорошо, я попробую задать вопрос еще раз, потому что я все еще не уверен. Может ли в промежуточной таблице быть узел, который уже существует в целевой таблице, но родительский узел для этого узла отличается в промежуточной и целевой? Это означает, что существующий узел в цели должен быть обновлен новым родителем?   -  person Mikael Eriksson    schedule 14.08.2011
comment
Ах, извините - неправильно понял ваш вопрос. Нет, это невозможно.   -  person Tom    schedule 14.08.2011


Ответы (3)


Моделирование вашей иерархии таким образом приведет к проблемам. Столбец Hierary_id нарушает первую нормальную форму, и процесс слияния будет склонен к обновлению аномалий, если вы не сериализуете доступ к узким местам.

Вы должны рассмотреть таблицу только с node_id и parent_id, посмотрите, как это упрощает вашу проблему слияния.

node_id   parent_id
1         NULL
2         1
3         2
4         3

node_id   parent_id
1         NULL
3         1
5         2
6         1

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

person gordy    schedule 14.08.2011
comment
Интересная мысль. Мои слияния сериализованы, поэтому я не слишком беспокоюсь на этот счет, но похоже, что это значительно облегчит слияние. Я предполагаю, что это происходит за счет того, что запросы становятся сложнее и медленнее. Я попробую, спасибо! - person Tom; 14.08.2011
comment
Это, безусловно, намного быстрее, хотя и недостаточно быстро для моего варианта использования. По крайней мере, при таком подходе я не думаю, что искусственно сковываю SQL-сервер. Если прямое слияние, подобное этому, слишком медленное, я подозреваю, что мне нужно искать в другом месте. - person Tom; 15.08.2011

Мы работали над продуктом, который требовал аналогичного решения. После долгих исследований этого и других подходов мы пришли к выводу, что метод иерархии ID не для нас.

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

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

Оба они являются гораздо более элегантными и эффективными решениями этой конкретной дизайнерской задачи.

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

person vvohra87    schedule 14.08.2011
comment
Спасибо - я склоняюсь к этому. У меня есть прототип, использующий нереляционную базу данных (на основе HDF5), но, поскольку это консервативная корпоративная среда, я хочу убедиться, что я дал более традиционным/поддерживаемым технологиям справедливый удар кнутом. - person Tom; 14.08.2011
comment
Спасибо за ссылки, кстати. Модель вложенного набора выглядит проблематичной для меня, поскольку она потребует перенумерации целевого дерева каждый раз, когда в него вливается новый набор узлов. Чтобы модель списка смежности допускала произвольно глубокие иерархии (которые у меня есть), я считаю, что мне понадобится общая таблица Выражения, которые выглядят отвратительно. Документация, которую я видел по идентификаторам иерархии, утверждает, что они быстрее, чем общие табличные выражения. - person Tom; 14.08.2011
comment
Эй, я слышу тебя, парень. Консервативные клиенты убивают мое время. Почему проблематично перенумеровать при слиянии? Также см. эту ссылку в списках смежности Джо. Создатель схемы написал книгу, отрывок из которой описывает исправления, которые можно применить к простой схеме смежности, чтобы сделать ее более надежной. Кроме того, я не вижу здесь необходимости в CTE, я что-то упустил? - person vvohra87; 14.08.2011
comment
@Tom: Если вам нужно придерживаться этого, у меня есть рабочее решение, которым я не очень горжусь. Я держал один стол. Нет автоматического увеличения PK. Нет идентификатора родителя, нет идентификатора ребенка. Это было просто. Я храню nodeID как строку, которая происходит из обычного упорядоченного списка. Итак, у меня был nodeID = 1, nodeID = 1.1, nodeID = 1.1.1, nodeID = 1.1.2 и т. д. Это было сделано с помощью того, что я нашел ошеломляющими (в то время) запросами. Может у вас получится :) - person vvohra87; 14.08.2011

Вот решение, которое перемещает строки из исходного @S в целевое @T на один уровень за раз. Чтобы немного упростить, я добавил корневой узел, чтобы всегда иметь родителя, который используется при создании нового HierarcyID.

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

-- Target table
declare @T table 
(
  hierarchy_id hierarchyid primary key,
  node_id bigint
)

insert into @T values
('/',             0), -- Needed for simplicity
('/1/',           1),
('/1/1/',         2),
('/1/2/',         3),
('/1/3/',         4)

-- Source table
declare @S table 
(
  hierarchy_id hierarchyid primary key,
  node_id bigint
)

insert into @S values
('/',               0),
('/1/',             1),
('/1/1/',           3),
('/1/2/',           5),
('/1/1/1/',         6)

declare @lvl int = 1

-- Move rows from @S to @T for each level
while exists(select *
             from @S
             where hierarchy_id.GetLevel() = @lvl)
begin

  insert into @T
  select T.hierarchy_id.GetDescendant(C.MaxID, null),
         S.node_id
  from (select S1.node_id, S2.node_id as ParentID
        from @S as S1
          inner join @S as S2
            on S1.hierarchy_id.GetAncestor(1) = S2.hierarchy_id
        where S1.hierarchy_id.GetLevel() = @lvl and
              S1.node_id not in (select node_id
                                 from @T)
       ) as S
    inner join @T as T
      on S.ParentID = T.node_id
    outer apply (select max(hierarchy_id) as MaxID
                 from @T as T2
                 where T.hierarchy_id = T2.hierarchy_id.GetAncestor(1)) as C       

    set @lvl = @lvl + 1
end

select *, hierarchy_id.ToString()
from @T
where hierarchy_id <> hierarchyid::GetRoot()  

Результат:

hierarchy_id  node_id  (No column name)
------------  -------  ----------------
0x58          1        /1/
0x5AC0        2        /1/1/
0x5B40        3        /1/2/
0x5B56        6        /1/2/1/
0x5BC0        4        /1/3/
0x5C20        5        /1/4/
person Mikael Eriksson    schedule 15.08.2011