Order By 导致对大表进行扫描

Tom*_*Tom 14 sql-server index-tuning query-performance

我有以下查询;

SELECT TOP 100 ID
FROM [dbo].[TableName] WITH (NOLOCK)
WHERE TypeId = 2
    AND DateTimeUTC < '2022-Aug-04 07:02:40'
    AND DateTimeUTC > '4/26/2022 7:36:36 AM'
ORDER BY ID ASC
Run Code Online (Sandbox Code Playgroud)

表 [dbo].[TableName](顺便说一句,不是它的真实名称)有超过 1.18 亿行。

我在此表上创建了以下索引;

CREATE INDEX [ix_TableName_DateTimeUTC_TypeId] 
ON [dbo].[TableName] (DateTimeUTC, TypeId)
    WITH FILLFACTOR = 90;
Run Code Online (Sandbox Code Playgroud)

如果我运行此查询(不包括ORDER BY),该查询会对上述索引执行 SEEK,并立即完成。然而,一旦我包含ORDER BY,查询就会在 PK 上执行 SCAN,读取所有 118+ 百万行。正如您可以想象的那样,这会降低性能并且查询需要很长时间才能完成。

解决此问题的最简单方法是完全删除该ORDER BY子句,但我认为这是不可能的,因为应用程序(进行此调用)要求按顺序返回数据。

关于如何改进这一点有什么建议吗?

Eri*_*ing 20

出击

\n

我会将索引更改为如下所示:

\n
CREATE INDEX \n    [TypeId_Id_DateTimeUTC] \nON [dbo].[TableName] \n(\n    TypeId, \n    Id, \n    DateTimeUTC\n)\nWITH \n(\n    FILLFACTOR = 100,\n    SORT_IN_TEMPDB = ON\n);\n
Run Code Online (Sandbox Code Playgroud)\n

这个想法是使初始数据定位和排序变得自由,并且还支持范围谓词。

\n

拥有Id第二列是为了TypeId = 2可以查找该部分,然后索引行按逻辑顺序排序Id- SQL Server 然后只需要按现有顺序读取索引行,直到DateTimeUTC读取与谓词匹配的 100 行。即它是为了避免任何排序操作。

\n

我在这些博客文章中详细讨论了这一点:

\n

让\xe2\x80\x99s 一起设计一个 SQL Server 索引第 1 部分第 2 部分第 3 部分

\n

实际上,避免排序通常比避免剩余谓词更好。

\n


Mar*_*ith 12

您应该对日期时间文字使用一致且明确的格式。>和谓词有两种完全不同的格式很奇怪<

DateTimeUTC, TypeId不是该索引的最佳顺序。

相等条件中使用的列应首先列出,因此如果该索引专门用于优化该查询,则应首先列出 TypeId ( TypeId, DateTimeUTC)。否则,它能做的最好的事情就是对日期部分和残差谓词进行范围搜索。

如果您确实进行了索引更改,但仍然看到对聚集索引的扫描,这可能是因为 SQL Server 认为从已按所需顺序包含它们的源中读取它们并丢弃不匹配的索引比从源中读取它们要快。在运行时对它们进行排序。由于TOP 100它只需要找到前100个匹配的就可以停止扫描。

您很可能遇到与此处问题类似的情况,其中日期在很大程度上与其相关而不是独立,因此它低估了在找到 100 个与谓词匹配的行之前id需要按顺序读取的行。id

假设ID是一个升序标识列,并且考虑到您的DateTimeUTC谓词今天结束,匹配的行可能全部位于索引的末尾,而不是均匀地分散在整个索引中,因此这几乎是最坏的情况。

可能要查看的查询提示是DISABLE_OPTIMIZER_ROWGOAL从 中删除行目标效果,TOP或者FORCESEEK只是告诉它无论如何都要使用查找


Joe*_*ish 6

您发布的查询模式(范围谓词和ORDER BY不同列上的 an)很难用传统索引覆盖。使用典型的方法,您必须在消除排序(埃里克的答案)或消除剩余谓词(马丁的答案)之间进行选择。ID假设和以及相对较短的日期范围筛选器之间存在强相关性DateTimeUTC,则使用 E​​rik 索引的查询性能会随着表中具有匹配值的总行数而变化IdType。使用 Martin 索引的查询的性能取决于对IdType具有所需日期范围内的匹配值的行进行扫描和排序。根据您的数据情况,这两种解决方案都可能无法获得可接受的性能。

考虑以下替代算法:

  • 对于日期范围内的每个日历日期,获取该日期 ID 最小的 100 行
  • 合并所有行并排序以获得全局 ID 最小的 100 行

该方法根据日期范围内的天数(大约 100)以及表中仅一天的行数(考虑到开始日期不是从午夜开始)进行缩放。通过创建计算列并将该列添加到索引,可以在 SQL Server 中有效地实现此算法:

ALTER TABLE Q315205 ADD DateUTC AS CAST(DateTimeUTC AS DATE);
 
CREATE INDEX [TypeId_DateUTC_ID_INCLUDE_DatetimeUTC] ON [dbo].Q315205
(TypeId, DateUTC, ID)
INCLUDE (DatetimeUTC);
 
GO 
 
DECLARE @StartDate DATE = '20220426';
DECLARE @EndDate DATE = '20220805';

SELECT TOP (100) ca.ID
FROM ( -- derived table of numbers, can replace with something else
    SELECT TOP (DATEDIFF(DAY, @StartDate, @EndDate)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) q
CROSS APPLY (
    SELECT TOP (100) ID
    FROM [dbo].Q315205 WITH (NOLOCK)
    WHERE TypeId = 2
    AND DateTimeUTC < '20220804 07:02:40'
    AND DateTimeUTC > '20220426 07:36:36' -- causes a residual predicate for one day
    AND DateUTC = DATEADD(DAY, q.RN - 1, @StartDate)
    ORDER BY ID
) ca (ID)
ORDER BY ca.ID
OPTION (NO_PERFORMANCE_SPOOL);
Run Code Online (Sandbox Code Playgroud)

查询计划如下所示:

在此输入图像描述

绿色部分生成过滤器范围所需的日期值(全部 101 个),红色部分使用索引使用新索引获取每个日期的最小 ID,蓝色部分对 10100 行进行排序以获得全球最小的ID。

我创建了一个大小为 28 GB、包含 1.2 亿行的测试表。的值DateTimeUTC在 2018-01-01 和 2022-08-05 之间均匀分布。所有数据的 TypeId 均为 2。公平地说,这是 Erik 索引的最坏情况。以下是我的本地计算机上不同方法的计时:

+------------------------------+---------------+
|          Algorithm           | CPU Time (ms) |
+------------------------------+---------------+
| Clustered index scan         |         13858 |
| Erik's index (no sort)       |          3222 |
| Martin's index (no residual) |          1044 |
| Computed column skip scan    |             6 |
+------------------------------+---------------+
Run Code Online (Sandbox Code Playgroud)


Pau*_*ite 6

如果您需要一个方案来查找按 排序的给定时间范围内的行ID,但不想创建可能大于原始表的计算列或索引,则有一种方法使用索引视图,假设某些之间的相关性ID值和日期

\n

下面解决方案的总体思路是使用索引视图(始终与 SQL Server 中的基表同步)维护每个日期和关联ID 范围的不同列表。TypeID将 ID 聚合到范围中意味着索引视图可以比基表小得多。

\n

给定要查询的日期范围,我们可以从索引视图中找到需要检查的 ID范围,然后使用对现有聚集索引的查找来检查基表中的精确匹配。按ID 范围对小视图列表进行排序使我们能够有效地找到有序的匹配行。

\n

当每天 和 出现某个范围内的多个 ID 时,此方案效果最佳TypeID。最坏的情况是所有 ID 与每天的其他值之间的间隔超过范围大小TypeID。本例中使用的范围大小为 1000。

\n

除了小型索引视图上所需的唯一聚集索引之外,此解决方案不需要新列或索引。

\n

测试表及数据

\n

该脚本创建84,067,200 行数据,需要1GB的页面压缩存储空间。2020 年 1 月 1 日至 2022 年 8 月 31 日期间每秒有一行。

\n
CREATE TABLE dbo.TableName\n(\n    ID bigint IDENTITY NOT NULL,\n    TypeID integer NOT NULL,\n    DateTimeUTC datetime NOT NULL,\n\n    CONSTRAINT [PK dbo.TableName ID]\n        PRIMARY KEY CLUSTERED (ID)\n        WITH (DATA_COMPRESSION = PAGE)\n);\n\n-- Add 84,067,200 rows\nWITH N AS\n(\n    SELECT TOP \n    (\n        DATEDIFF(SECOND,\n            CONVERT(datetime, \'2020-01-01\', 120),\n            CONVERT(datetime, \'2022-08-31\', 120)) \n    )\n        n = ROW_NUMBER() OVER (ORDER BY @@SPID) \n    FROM sys.all_columns AS AC1\n    CROSS JOIN sys.all_columns AS AC2\n    ORDER BY n\n)\nINSERT dbo.TableName WITH (TABLOCK)\n    (TypeID, DateTimeUTC)\nSELECT \n    TypeID = 2,\n    DateTimeUTC =\n        DATEADD(SECOND, N.n, \n            CONVERT(datetime, \'2020-01-01\', 120))\nFROM N;\n
Run Code Online (Sandbox Code Playgroud)\n

索引视图

\n

TypeID该视图对于每个、日期和最多 1000 个 ID 的范围包含一行。1000 数字\xe2\x80\x94 没有什么特别之处,您可以通过更改常量和重建视图来尝试不同的范围大小。

\n
CREATE OR ALTER VIEW dbo.TableNameSummary\nWITH SCHEMABINDING AS\nSELECT \n    TN.TypeID,\n    DateOnly = CONVERT(date, TN.DateTimeUTC),\n    IDrange1K = FLOOR(TN.ID / 1000) * 1000,\n    NumRows = COUNT_BIG(*)\nFROM dbo.TableName AS TN\nGROUP BY\n    TN.TypeID,\n    CONVERT(date, TN.DateTimeUTC), \n    FLOOR(TN.ID / 1000) * 1000;\nGO\nCREATE UNIQUE CLUSTERED INDEX \n    [CUQ dbo.TableNameSummary TypeID, DateOnly, IDrange1K]\nON dbo.TableNameSummary \n    (TypeID, DateOnly, IDrange1K)\nWITH (DATA_COMPRESSION = PAGE);\n
Run Code Online (Sandbox Code Playgroud)\n

对于本示例,该视图包含84,847 行,大小为952KB 。

\n

询问

\n
DECLARE \n    @Rows bigint = 100,\n    @StartDate datetime = CONVERT(datetime, \'2022-04-26 07:36:36\', 120),\n    @EndDate datetime = CONVERT(datetime, \'2022-08-04 07:02:40\', 120);\n
Run Code Online (Sandbox Code Playgroud)\n
WITH Ranges AS\n(\n    -- Low end of ID ranges from the indexed view.\n    -- DISTINCT because a range may have data for several days.\n    SELECT DISTINCT TNS.IDrange1K\n    FROM dbo.TableNameSummary AS TNS WITH (NOEXPAND)\n    WHERE\n        TNS.TypeID = 2\n        AND TNS.DateOnly >= CONVERT(date, @StartDate)\n        AND TNS.DateOnly <= CONVERT(date, @EndDate)\n)\nSELECT TOP (@Rows)\n    MR.ID\nFROM Ranges AS R\nCROSS APPLY \n(\n    -- Find matching rows in each ID range\n    SELECT TOP (@Rows) TN.ID\n    FROM dbo.TableName AS TN\n    WHERE \n        -- Match ID group from indexed view\n        TN.ID >= R.IDrange1K\n        AND TN.ID < (R.IDrange1K + 1000)\n        -- Must check individual rows match\n        -- the exact date & time range\n        AND TN.DateTimeUTC > @StartDate\n        AND TN.DateTimeUTC < @EndDate\n    ORDER BY\n        TN.ID ASC\n) AS MR\nORDER BY\n    R.IDrange1K ASC,\n    MR.ID ASC;\n
Run Code Online (Sandbox Code Playgroud)\n

使用串行行模式执行计划可在11 毫秒内返回结果:

\n

执行计划

\n

在此示例中,不同排序应用于索引视图中的 8,807 行。在应用残差谓词以获得精确的日期和时间范围匹配之前,基表查找会找到 27,897 行。这是因为第一个日期的许多行在开始时间之前落在所需范围的低端。

\n

执行计划示例

\n

原始查询:

\n

原来的

\n

根据埃里克的索引:

\n

艾瑞克

\n

丹的指数:

\n

伊丹

\n

使用索引视图(如上所述):

\n

索引视图

\n

使用乔的方案:

\n

乔

\n

业绩总结

\n

在 SQL Server 2019 CU16-GDR 上测试。并行性在有利的地方启用,MAXDOP 12。

\n

表大小(带页面压缩)1,026,312KB

\n

每组的最佳结果以粗体显示。

\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n\n
作者时间(毫秒)额外索引大小 (KB)压缩平行线
原来的第1364章是的
埃里克第359章2,170,296没有任何是的
埃里克5901,493,784是的
埃里克第974章1,026,000是的
2752,170,296没有任何是的
3111,493,784是的
第387章1,026,000是的
保罗102,712没有任何
保罗111,344
保罗11第952章
42,418,640没有任何
51,742,936
61,027,456
\n