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_GroupDef
和IX_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 中的树结构查询的主要内容,如果未能解决你的问题,请参考以下文章