SQL Server 中的树结构查询

Posted

技术标签:

【中文标题】SQL Server 中的树结构查询【英文标题】:Tree Structure Query in SQL Server 【发布时间】:2021-02-24 18:53:50 【问题描述】:

使用 Azure SQL Server,我有一个表,将组织的树结构存储在一个表中,如下所示:

    CREATE TABLE [dbo].[UserManagement_GroupDef]
    (
        [ID] [bigint] IDENTITY(1,1) NOT NULL,
        [PortalId] [bigint] NOT NULL,
        [GroupType] [int] NOT NULL,
        [ParentGroupId] [bigint] NULL,
        [GroupName] [nvarchar](2048) NOT NULL,
        [IsDefault] [bit] NOT NULL,
        [Email] [nvarchar](256) NULL,
        [PhoneNumber] [varchar](10) NULL,
        [UserManagementId] [nvarchar](450) NULL,
        [Address] [nvarchar](450) NULL,
        [Suite] [nvarchar](450) NULL,
        [City] [nvarchar](450) NULL,
        [State] [nvarchar](450) NULL,
        [Position] [nvarchar](450) NULL,
        [Zip] [nvarchar](450) NULL,
        [IsDeleted] [bit] NOT NULL,
CONSTRAINT [PK_UserManagement_GroupDef] PRIMARY KEY CLUSTERED     
   (
    [ID] ASC
   )WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
   ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

所有列都被索引。

[ParentGroupId] 引用同一个表中的另一行。总而言之,该表代表了一个树形结构。

在下面的查询中,我试图查找组 ID 155 的所有后代子代。

WITH tblChild AS
    (
        SELECT 
        ID
        ,ParentGroupId
        ,[IsDeleted]
        ,[PortalId]
        ,[GroupType]
        ,[GroupName]
        ,[IsDefault]
        ,[Email]
        ,[PhoneNumber]
        ,[UserManagementId]
        ,[Address]
        ,[Suite]
        ,[City]
        ,[State]
        ,[Position]
        ,[Zip]
            FROM UserManagement_GroupDef    
            WHERE ID = 155 AND IsDeleted = 0
        UNION ALL
        SELECT 
             UserManagement_GroupDef.ID,
             UserManagement_GroupDef.ParentGroupId, 
             UserManagement_GroupDef.[IsDeleted],
             UserManagement_GroupDef.[PortalId],
             UserManagement_GroupDef.[GroupType],
             UserManagement_GroupDef.[GroupName],
             UserManagement_GroupDef.[IsDefault],
             UserManagement_GroupDef.[Email],
             UserManagement_GroupDef.[PhoneNumber],
             UserManagement_GroupDef.[UserManagementId],
             UserManagement_GroupDef.[Address],
             UserManagement_GroupDef.[Suite],
             UserManagement_GroupDef.[City],
             UserManagement_GroupDef.[State],
             UserManagement_GroupDef.[Position],
             UserManagement_GroupDef.[Zip]

        FROM UserManagement_GroupDef  
        JOIN 
            tblChild  
                ON 
                    UserManagement_GroupDef.ParentGroupId = tblChild.ID
                WHERE tblChild.IsDeleted = 0        
    )

    SELECT 
            tblChild.ID,
             tblChild.ParentGroupId, 
             tblChild.[IsDeleted],
             tblChild.[PortalId],
             tblChild.[GroupType],
             tblChild.[GroupName],
             tblChild.[IsDefault],
             tblChild.[Email],
             tblChild.[PhoneNumber],
             tblChild.[UserManagementId],
             tblChild.[Address],
             tblChild.[Suite],
             tblChild.[City],
             tblChild.[State],
             tblChild.[Position],
             tblChild.[Zip]
        FROM tblChild       
    

该表目前只有不到 7000 条记录。这个特定的查询返回一个不到 2000 行的结果。结果最终是正确的,但需要大约 20 秒才能执行。

有没有办法加快这个查询或达到相同的结果,只是更快?

执行计划:

https://www.brentozar.com/pastetheplan/?id=S1k0Qdrzd

表格索引:

CREATE NONCLUSTERED INDEX [IX_UserManagement_GroupDef] ON [dbo].[UserManagement_GroupDef]
(
    [IsDeleted] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, DROP_EXISTING = OFF, ONLINE = OFF) ON [PRIMARY]

CREATE NONCLUSTERED INDEX [IX_UserManagement_GroupDef_2] ON [dbo].[UserManagement_GroupDef]
(
    [ParentGroupId] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, DROP_EXISTING = OFF, ONLINE = OFF) ON [PRIMARY]

CREATE NONCLUSTERED INDEX [IX_UserManagement_GroupDef_3] ON [dbo].[UserManagement_GroupDef]
(
    [IsDeleted] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, DROP_EXISTING = OFF, ONLINE = OFF) ON [PRIMARY]


CREATE NONCLUSTERED INDEX [umg_query_index] ON [dbo].[UserManagement_GroupDef]
(
    [ParentGroupId] ASC
)
INCLUDE(

[ID],
[IsDeleted],
[PortalId],
[GroupType],
[GroupName],
[IsDefault],
[Email],
[PhoneNumber],
[UserManagementId],
[Address],
[Suite],
[City],
[State],
[Position],
[Zip]
)

WHERE ([IsDeleted]=(0))

WITH (STATISTICS_NORECOMPUTE = OFF, DROP_EXISTING = OFF, ONLINE = OFF) ON [PRIMARY]

【问题讨论】:

为什么要使用 BIGINT 来处理 7k 条记录?你的桌子上有什么样的索引? 我最近想出了一种在 SQL 中管理分层数据的新方法,这将加快处理速度。如果我能找到时间,我会发布一个例子。 codingblackarts.com/2021/02/09/… 我建议您将IsDeleted 谓词推到CTE 内部。过滤索引ParentGroupId WHERE IsDeleted = 0 可能非常有用。请edit 您的问题包括所有 索引定义。也请通过pastetheplan.com分享查询计划 是的,肯定会将谓词推入 CTE 的递归部分,然后它将使用您的新索引 umg_query_index @Charlieface 你想添加它作为答案吗? 【参考方案1】:

看起来正在发生的事情是编译器无法将外部谓词IsDeleted = 0 下推到CTE。因此它不能使用O. Jones 推荐的umg_query_index

为什么会这样,是因为递归部分的每次运行都可能返回IsDeleted = 1的行,这些行需要反馈到下一次运行。虽然它们确实没有出现在最终结果中,但仍有可能这样的行可能有确实需要出现在最终结果集中的子行。所以没有办法从递归连接中消除它们。

你有两个选择:

    更改您的IX_UserManagement_GroupDef_2 索引,使其也包含IsDeleted 列,它不需要在键中,它可以是INCLUDE
CREATE NONCLUSTERED INDEX [IX_UserManagement_GroupDef_2] ON [dbo].[UserManagement_GroupDef]
([ParentGroupId] ASC) INCLUDE (IsDeleted)
WITH (DROP_EXISTING = ON, ONLINE = ON) ON [PRIMARY]
    可能是更好的选择,自己下推谓词。然后它将在 CTE 的两个部分使用 O. Jones 索引。
WITH tblChild AS
(
    SELECT *
    FROM UserManagement_GroupDef 
    WHERE ID = 155 AND IsDeleted = 0                

    UNION ALL

    SELECT 
        u.* 
    FROM 
        UserManagement_GroupDef u
    JOIN 
        tblChild ON u.ParentGroupId = tblChild.ID
    WHERE u.IsDeleted = 0
)
SELECT *
FROM tblChild 

此查询与您的原始查询具有不同的语义。它不会返回父级为IsDeleted = 1 的任何行。

此时您还可以将新索引更改为过滤索引,因为这意味着不会存储任何 IsDeleted 行。

CREATE NONCLUSTERED INDEX umg_query_index ON UserManagement_GroupDef
(ParentGroupId)  INCLUDE (IsDeleted) WHERE IsDeleted = 0
WITH (DROP_EXISTING = ON, ONLINE = ON) ON [PRIMARY];

您必须在索引中包含IsDeleted 列,否则优化器可能无法使用它,因为当前实现的逻辑错误。


进一步说明:单列索引通常没有那么有用,通常应避免使用。服务器尝试将两个索引合并在一起是不值得的,它会扫描聚集索引。

我建议您删除索引IX_UserManagement_GroupDefIX_UserManagement_GroupDef_3,因为它们现在完全是多余的。

【讨论】:

感谢您的详细解释。不幸的是,我看到执行时间的变化为零。事实上,如果我完全删除与 IsDeleted 相关的所有谓词,并且只有“ID = 155”谓词,则执行几乎相同。 你能通过pastetheplan.com分享这两个版本的查询计划吗? 和brentozar.com/pastetheplan一样吗? pastetheplan.com 不适合我。 是的,一样 brentozar.com/pastetheplan/?id=BkbzQPHzO 和 brentozar.com/pastetheplan/?id=SkkbEwHG_ 谢谢!【参考方案2】:

试试这个:

CREATE INDEX umg_query_index ON UserManagement_GroupDef (IsDeleted, ParentGroupId);

更好的是,在 SSMS 中运行查询。在你运行它之前,在查询窗口中右键单击并启用显示实际执行计划。

然后看执行计划。它可能会为您推荐正确的索引。

【讨论】:

以上是关于SQL Server 中的树结构查询的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 PHP 处理从 SQL 查询返回的树结构?

第一章: 在RDB中的树结构数据

SQL Server 查询树结构的表,查询一个节点的所有子节点

SQL Server 2008中的9种数据挖掘算法

Mina中的树结构

C#中的树数据结构