问题描述
我使用邻接表模型(例如port: 80
,Id
)在Microsoft sql Server(2019)中存储了较大的层次结构(2,500多个记录)。我正在寻找一种有效的方法来根据层次结构中的特定路径查找记录。换句话说,给定路径(例如ParentId
),我想检索与最终节点(在这种情况下为/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])
)
天真的方法
关于如何将邻接表转换为具体路径的线程很多,包括:
- SQL - Convert non-null adjacency list to path
- Build Enumeration Path from Adjacency List in SQL
- Flatten Adjacency List Hierarchy To A List Of All Paths
- Load hierarchical data from MSSQL with recursive common table expressions
查看
我们可以使用以下一种方法,使用rCTE将整个邻接表转换为具体路径:
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,'SubfolderB');
INSERT INTO Tree VALUES (5,'FolderB');
INSERT INTO Tree VALUES (6,5,'SubfolderA');
INSERT INTO Tree VALUES (7,'SubfolderB');
输出
这将产生以下输出:
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
问题
幼稚的方法行之有效。问题在于,查询大型层次结构效率极低,因此速度很慢,因为它需要动态地重建每个调用的实体路径的整个集。就我而言,这需要8-9秒。显然,我可以将这些数据存储在一个表中,并在数据更改时通过触发器重新生成它。但我会宁可找到一个更有效的查询,并避免额外的复杂性。
问题
构造此查询的有效方法是什么?或者,冒着使这成为XY问题的风险,是否有办法限制rCTE,使其仅需要评估层次结构中的节点,而不是每次都重新构建 entire 层次结构? / p>
解决方法
是否有一种方法来限制rCTE,使其仅需要评估层次结构中的节点,而不是每次都重新构建 entire 层次结构?
限制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
。
性能
我已经在具有2500个样本记录的可比较大小的表上测试了这种方法,并且它在一秒钟之内都能很好地执行,这比单纯的方法有了很大的改进。显然,您需要根据自己的数据库和性能要求对此进行评估,以确定其有效程度足够。