通过删除执行计划中的排序运算符来优化 SQL 查询

Posted

技术标签:

【中文标题】通过删除执行计划中的排序运算符来优化 SQL 查询【英文标题】:Optimizing SQL queries by removing Sort operator in Execution plan 【发布时间】:2011-05-14 10:23:55 【问题描述】:

我刚刚开始考虑通过索引优化我的查询,因为 SQL 数据正在快速增长。我查看了优化器如何通过 SSMS 中的执行计划处理我的查询,并注意到正在使用排序运算符。我听说排序运算符表示查询中的错误设计,因为可以通过索引过早地进行排序。所以这里有一个类似于我正在做的示例表和数据:

IF OBJECT_ID('dbo.Store') IS NOT NULL DROP TABLE dbo.[Store]
GO

CREATE TABLE dbo.[Store]
(
    [StoreId] int NOT NULL IDENTITY (1, 1),
    [ParentStoreId] int NULL,
    [Type] int NULL,
    [Phone] char(10) NULL,
    PRIMARY KEY ([StoreId])
)

INSERT INTO dbo.[Store] ([ParentStoreId], [Type], [Phone]) VALUES (10, 0, '2223334444')
INSERT INTO dbo.[Store] ([ParentStoreId], [Type], [Phone]) VALUES (10, 0, '3334445555')
INSERT INTO dbo.[Store] ([ParentStoreId], [Type], [Phone]) VALUES (10, 1, '0001112222')
INSERT INTO dbo.[Store] ([ParentStoreId], [Type], [Phone]) VALUES (10, 1, '1112223333')
GO

这是一个示例查询:

SELECT [Phone]
FROM [dbo].[Store]
WHERE [ParentStoreId] = 10
AND ([Type] = 0 OR [Type] = 1)
ORDER BY [Phone]

我创建了一个非聚集索引来帮助加快查询速度:

CREATE NONCLUSTERED INDEX IX_Store ON dbo.[Store]([ParentStoreId], [Type], [Phone])

为了构建 IX_Store 索引,我从简单的谓词开始

[ParentStoreId] = 10
AND ([Type] = 0 OR [Type] = 1)

然后我为 ORDER BY 添加[Phone] 列并覆盖 SELECT 输出

所以即使建立了索引,优化器仍然使用排序运算符(而不是索引排序),因为[Phone][ParentStoreId][Type] 之后排序。如果我从索引中删除 [Type] 列并运行查询:

SELECT [Phone]
FROM [dbo].[Store]
WHERE [ParentStoreId] = 10
--AND ([Type] = 0 OR [Type] = 1)
ORDER BY [Phone]

那么优化器当然不会使用排序运算符,因为[Phone] 是按[ParentStoreId] 排序的。

所以问题是如何创建一个涵盖查询的索引(包括[Type] 谓词)并且不让优化器使用排序?

编辑:

我正在使用的表有超过 2000 万行

【问题讨论】:

您确实应该将[StoreId] 设为主键(顺便说一下,它也默认为集群),而不仅仅是添加唯一索引。 您可以通过在 Phone 列上创建 second 索引来解决此问题。 @Lucero,我修改了我的帖子,将[StoreId]标记为主键,虽然我认为这不会解决排序问题 @zespri,我刚刚注意到我正在使用的表格很大。创建新索引会占用大量硬盘空间 @jodev,我的评论不是对您问题的回答,而是更一般的设计建议。拥有一个集群的、小的、连续的主键可以帮助提高系统的整体性能,这就是为什么这是一个很好的开始。 【参考方案1】:

首先,您应该验证排序实际上是性能瓶颈。排序的持续时间将取决于要排序的元素的数量,并且特定父存储的存储数量可能很小。 (假设在应用 where 子句后应用了排序运算符)。

我听说排序运算符表明查询中的设计不好,因为可以通过索引过早地进行排序

这是一个过度概括。通常,排序运算符可以很容易地移到索引中,并且,如果只获取结果集的前几行,则可以大大降低查询成本,因为数据库不再需要获取所有匹配的行(并对它们进行排序) all) 查找第一个,但可以按结果集顺序读取记录,一旦找到足够的记录就停止。

在您的情况下,您似乎正在获取整个结果集,因此排序不太可能使事情变得更糟(除非结果集很大)。此外,在您的情况下,构建有用的排序索引可能并非易事,因为 where 子句包含一个或。

现在,如果您仍想摆脱该排序运算符,您可以尝试:

SELECT [Phone]
FROM [dbo].[Store]
WHERE [ParentStoreId] = 10
AND [Type] in (0, 1)
ORDER BY [Phone]    

或者,您可以尝试以下索引:

CREATE NONCLUSTERED INDEX IX_Store ON dbo.[Store]([ParentStoreId], [Phone], [Type])

尝试让查询优化器仅对ParentStoreId 进行索引范围扫描,然后扫描索引中所有匹配的行,如果Type 匹配,则输出它们。但是,这可能会导致更多的磁盘 I/O,因此会减慢而不是加快查询速度。

编辑:作为最后的手段,您可以使用

SELECT [Phone]
FROM [dbo].[Store]
WHERE [ParentStoreId] = 10
AND [Type] = 0
ORDER BY [Phone]

UNION ALL

SELECT [Phone]
FROM [dbo].[Store]
WHERE [ParentStoreId] = 10
AND [Type] = 1
ORDER BY [Phone]

CREATE NONCLUSTERED INDEX IX_Store ON dbo.[Store]([ParentStoreId], [Type], [Phone])

并在应用程序服务器上对两个列表进行排序,您可以在其中合并(如在合并排序中)预排序的列表,从而避免完全排序。但这确实是一个微优化,虽然将排序本身加快了一个数量级,但不太可能对查询的总执行时间产生太大影响,因为我预计瓶颈是网络和磁盘 I/O,尤其是考虑到磁盘将执行大量随机访问,因为索引不是集群的。

【讨论】:

我使用的表有超过 2000 万行。大约有 50 个不同的“[ParentStoreId]”值和 8 个不同的“[Type]”值。最后,我要对大约 200K 行进行排序,这似乎会减慢查询速度。你的信息很有用,我会试试看 @meriton 你能解释一下为什么使用“输入(0,1)”会有所不同吗?这与“或”有何不同?为什么电话号码会在没有排序运算符的情况下自动排序?据我了解,如果索引是在 ParentStoreId、Type、Phone 上创建的,那么电话号码是否会为每种类型单独排序?

以上是关于通过删除执行计划中的排序运算符来优化 SQL 查询的主要内容,如果未能解决你的问题,请参考以下文章

SQL Server执行计划

MySQL执行计划

通过分析SQL语句的执行计划优化SQL

MySQL执行计划都有那些规划?

增加索引分析

Mysql学会查看sql的执行计划