【问题标题】:Query an adjacency list in SQL via a materialized path通过物化路径查询SQL中的邻接表
【发布时间】:2020-12-21 02:25:12
【问题描述】:

我使用邻接列表模型(例如,IdParentId)在 Microsoft SQL Server (2019) 中存储了一个大型层次结构(2,500 多条记录)。我正在寻找一种有效的方法来根据层次结构中的特定路径查找记录。换句话说,给定一个路径(例如/Root/FolderA/SubfolderA),我想检索与最终节点关联的Id(在这种情况下为SubfolderA)。

注意:节点名称不是全局唯一的。即,我们不能只查找SubfolderA 并假设它映射到/Root/FolderA/SubfolderA。层次结构中可能有多个名为SubfolderA 的节点。

设置

层次结构

/Root
  /FolderA
    /SubfolderA
    /SubfolderB
  /FolderB
    /SubfolderA
    /SubfolderB

结构

CREATE 
TABLE   [dbo].[Tree] (
        [Id]            INT             NOT NULL PRIMARY KEY, 
        [ParentId]      INT             NULL, 
        [Name]          VARCHAR(255)    NOT NULL, 
        CONSTRAINT      [FK_Hierarchy]  
        FOREIGN KEY     (ParentId) 
        REFERENCES      [Tree]([Id])
)

数据

INSERT INTO Tree VALUES (1,    NULL, 'Root');
INSERT INTO Tree VALUES (2,    1,    'FolderA');
INSERT INTO Tree VALUES (3,    2,    'SubfolderA');
INSERT INTO Tree VALUES (4,    2,    'SubfolderB');
INSERT INTO Tree VALUES (5,    1,    'FolderB');
INSERT INTO Tree VALUES (6,    5,    'SubfolderA');
INSERT INTO Tree VALUES (7,    5,    'SubfolderB');

天真的方法

关于如何将邻接列表转换为物化路径有很多线程,包括:

查看

我们可以使用这些方法之一,使用 rCTE 将整个邻接列表转换为物化路径:

CREATE 
VIEW            [dbo].[MaterializedPaths]
WITH            SCHEMABINDING
AS 

WITH RCTE AS (

  SELECT        Id,
                ParentId,
                CAST('/' + Name AS VARCHAR(255)) AS Path
  FROM          [dbo].[Tree] root
  WHERE         root.Id = 1

  UNION ALL

  SELECT        this.Id,
                this.ParentId,
                CAST(parent.Path + '/' + this.Name AS VARCHAR(255)) AS Path
  FROM          [dbo].[Tree] AS this
  INNER JOIN    RCTE parent
    ON          this.ParentId = parent.Id
)
SELECT          Id,
                Path
FROM            RCTE as hierarchy

输出

这会产生以下输出:

Id    Path
1     /Root
2     /Root/FolderA
3     /Root/FolderA/SubfolderA
4     /Root/FolderA/SubfolderB
5     /Root/FolderB
6     /Root/FolderB/SubfolderA
7     /Root/FolderB/SubfolderB

查询

我们可以使用简单的WHERE 子句过滤该输出:

SELECT          Id
FROM            MaterializedPaths
WHERE           Path = '/Root/FolderA/SubfolderA'

问题

天真的方法效果很好。问题在于它对于查询大型层次结构难以置信效率低下,因此速度很慢,因为它需要在每次调用时动态重建整个物化路径集。就我而言,这需要 8-9 秒。显然,我可以将这些数据存储在一个表中,并在数据更改的任何时候通过触发器重新生成它。但我宁愿找到更有效的查询并避免额外的复杂性。

问题

构建此查询的有效方法是什么?或者,冒着使这成为 XY 问题的风险,有没有办法限制 rCTE,使其只需要评估层次结构中的节点,而不是每次都重建 整个 层次结构?

【问题讨论】:

    标签: sql sql-server recursive-query adjacency-list materialized-path-pattern


    【解决方案1】:

    有没有办法限制 rCTE,使其只需要评估层次结构中的节点,而不是每次都重建整个层次结构?

    限制 rCTE

    有几种方法可以限制每个递归查询的范围,以便它只评估层次结构中的相关节点。一种相当有效的方法是简单地将 rCTE 限制为源路径(我们称之为@Path)以以下开头的记录:

    INNER JOIN  RCTE recursive
      ON        this.ParentId = recursive.Id
      AND       @Path LIKE CAST(recursive.Path + '/' + this.Name AS VARCHAR(MAX)) + '%'
    

    这会将查询限制为路径中的每条记录:

    Id    Path
    1     /Root
    2     /Root/FolderA
    3     /Root/FolderA/SubfolderA
    

    然后可以根据简单的WHERE 子句轻松过滤到最终记录:

    WHERE Path = @Path
    

    把它打包成一个函数

    我们可以将它与原始 rCTE 组合成一个函数。把它们放在一起,它可能看起来像:

    CREATE
    FUNCTION        [dbo].[GetIdFromPath]
    (
        @Path       VARCHAR(MAX)
    )
    RETURNS         INT
    AS
    
    BEGIN
    
      DECLARE       @Id         INT = -1
    
    
      ;WITH RCTE AS (
    
        SELECT      Id,
                    ParentId,
                    CAST('/' + Name AS VARCHAR(MAX)) AS Path
        FROM        [dbo].[Tree] root
        WHERE       root.Id = 1
    
        UNION ALL
    
        SELECT      this.Id,
                    this.ParentId,
                    CAST(parent.Path + '/' + this.Name AS VARCHAR(MAX)) AS Path
        FROM        [dbo].[Tree] AS this
        INNER JOIN  RCTE parent
          ON        Tree.ParentId = parent.Id
          AND       @Path LIKE CAST(parent.Path + '/' + this.Name AS VARCHAR(MAX)) + '%'
      )
      SELECT        @Id = Id
      FROM          RCTE as hierarchy
      WHERE         Path = @Path
    
      RETURN        @Id
    
    END
    

    按路径查询

    鉴于上述函数,您现在可以通过简单地将完整路径传递给GetIdFromPath() 函数来查询邻接列表:

    SELECT          dbo.GetIdFromPath('/Root/FolderA/SubfolderA') AS Id
    

    根据原始帖子中的示例数据,这将返回 3

    性能

    我已针对具有 2,500 条样本记录的相当大小的表测试了这种方法,并且它始终在不到一秒的时间内执行,这与幼稚的方法相比有了显着改进。显然,您需要根据自己的数据库和性能要求对此进行评估,以确定其效率是否足够

    【讨论】:

      猜你喜欢
      • 2023-03-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多