【问题标题】:How can two hierarchies be efficiently merged in SQL Server?如何在 SQL Server 中有效地合并两个层次结构?
【发布时间】:2011-10-26 20:42:48
【问题描述】:

我有两个带有 hierarchyid 字段的表,其中一个是一个暂存表,其中包含需要合并到另一个中的新数据(即需要添加到主树中的一组节点,其中一些可能已经在那里)。

除了定义树结构(父/子关系)的hierarchyid 列。每个表都有一个单独的列,其中包含一个唯一标识每个节点的节点标识符。也就是说,判断临时表中的节点是否已经在主表中的方法是通过节点 ID,而不是通过 hierarchyid 列。

必要时,需要执行的处理如下所示:

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 Server 中实现此目的的唯一方法是在暂存表(已排序)上使用游标并为每一行调用一个存储过程,该过程基本上完全按照上述说明完成,完成使用 SELECT MAX() 来查找已经作为 PM 的子级存在的最高层次结构 ID,以便可以唯一地添加子级。

不过,这是一种非常低效的方法,而且对于我的目的来说太慢了。有没有更好的办法?

作为背景,这是我正在做的一种可行性检查。我需要弄清楚我是否可以在 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

请注意,暂存表中具有hierarchy_id /1/1/ 的节点对应于目标表中具有hiearchy_id /1/2/ 的节点(这就是为什么node_id 很重要——不能只是复制hierarchy_id 值) .另请注意,将 node_id 为 6 的新节点添加为正确父节点的子节点,即 node_id 为 3 的子节点,这就是为什么 hierarchy_id 很重要 - 它定义了任何新节点的树结构(父/子关系)。任何解决方案都需要考虑这两个方面。

【问题讨论】:

  • 有效的第一步是删除目标表中已经存在相同节点 ID 的所有行吗?似乎这样可以消除在合并过程中进行重复检查的需要
  • @OMG Ponies,没有子表和父表。有两个表,每个表都有一个 hierarchyid 列 (msdn.microsoft.com/en-us/library/bb677290.aspx)。节点 ID 建立了两个表中节点的等价性,但是在目标表中尚不存在节点的情况下,仅复制其节点 ID 是不够的 - 它还需要添加到目标表中的适当位置由目标表的 hierarchyid 列定义的层次结构。
  • 顺便说一下,我对不使用hierarchyid 的解决方案持开放态度。然而,我的理解是,在没有它们的情况下处理任意深度的树需要公共表表达式,这在语法上看起来很可怕,而且我读过的资料表明它们比层次结构要慢。
  • @Neil N,我可以从源表中删除此类记录,尽管这可能会使我以后要做的一些事情变得复杂。不过,重复检查并不是真正的问题——如果我能用 MERGE 语句实现这一点,那就太好了。
  • 我认为如果您给我们一个示例表结构和预期结果会有所帮助。我很难理解为什么你不能用一个简单的MERGE 语句来做到这一点;你不允许改变hierarchyid或者你会有重复的hierarchyid吗?我已经构建了一个自我维护的 hierarchyid/邻接列表混合体,这可能是您正在寻找的,但从问题中很难判断。

标签: sql sql-server sql-server-2008 hierarchical-data hierarchyid


【解决方案1】:

这是一个将行从源 @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/

【讨论】:

    【解决方案2】:

    以这种方式建模您的层次结构会导致问题。 hierarchy_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
    

    您将对此使用递归查询,您可能会惊讶于执行计划的效率。如果您必须拥有扁平的层次结构列,您可能可以使用递归查询创建索引视图。

    【讨论】:

    • 有趣的想法。我的合并是序列化的,所以我不太担心这个分数,但这看起来确实会使合并更容易。我想这是以使查询变得更棘手和更慢为代价的。我试试看,谢谢!
    • 这当然要快得多,尽管对于我的用例来说还不够快。至少通过这种方法,我认为我不会人为地阻碍 SQL 服务器。如果像这样的直接合并太慢,我怀疑我必须寻找其他地方。
    【解决方案3】:

    我们一直在开发需要类似解决方案的产品。在对这种方法和其他方法进行了大量研究之后,我们得出结论,hierarchyID 方法不适合我们。

    因此,直接回答您的问题:使用这种方法没有更好的方法。

    看看Nested Set ModelsAdjacency List Model

    对于这一特定的设计挑战,这两者都是更加优雅和高效的解决方案。

    编辑: 作为事后的想法,如果您没有与 SQL 结婚 - 这个问题可以使用非关系数据库更好地解决。 我们不能那样做,因为没有人在设计非关系数据库方面拥有足够的专业知识,但如果 SQL 是可选的,那么您可以在 MongoDB 中以更好、更有效的方式使用当前方法。

    【讨论】:

    • 谢谢 - 我是这样倾斜的。我有一个使用非关系数据库(基于 HDF5)的原型,但由于它是一个保守的企业环境,我想确保我已经给了更传统/受支持的技术一个公平的鞭子。
    • 感谢您的链接,顺便说一句。嵌套集模型对我来说看起来有问题,因为每次合并一组新节点时它都需要重新编号目标树。对于允许任意深度层次结构的邻接表模型(我有),我相信我需要 Common Table表情,看起来很可怕。我在层次结构 ID 上看到的文档声称它们比公用表表达式更快。
    • 嘿,我听到你的声音了。保守的客户会浪费我的时间。为什么合并时重新编号会出现问题?此外,请参阅 joe 的邻接列表上的 this link。模式的创建者写了一本书,其中的摘录描述了您可以应用于简单邻接模式以使其更持久的修复。另外,我认为这里不需要 CTE,我错过了什么吗?
    • @Tom:如果你必须坚持这一点,我有一个可行的解决方案,但我并不为此感到自豪。我只保留了一张桌子。没有自增PK。没有父 ID,没有子 ID。这很简单。我将 nodeID 存储为来自常规有序列表的字符串。所以我有 nodeID = 1、nodeID = 1.1、nodeID = 1.1.1、nodeID = 1.1.2 等。这是使用我发现的令人麻木的(当时)查询完成的。也许你可以工作:)
    猜你喜欢
    • 2021-03-24
    • 1970-01-01
    • 1970-01-01
    • 2019-08-10
    • 2020-12-19
    • 1970-01-01
    • 2021-04-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多