为什么我的索引未在SELECT TOP中使用?


15

总结:我正在执行选择查询。WHEREand ORDER BY子句中的每一列都在单个非聚集索引中IX_MachineryId_DateRecorded,可以作为键的一部分,也可以作为INCLUDE列。我选择了所有列,因此将导致书签查找,但我只是在考虑TOP (1),因此可以肯定的是服务器可以告诉查找,最后只需要执行一次。

最重要的是,当我强制查询使用index时IX_MachineryId_DateRecorded,它在不到一秒钟的时间内运行。如果我让服务器决定使用哪个索引,它将选择IX_MachineryId,最多需要一分钟。这确实向我表明,我已正确编制了索引,而服务器只是在做出错误的决定。为什么?

CREATE TABLE [dbo].[MachineryReading] (
    [Id]                 INT              IDENTITY (1, 1) NOT NULL,
    [Location]           [sys].[geometry] NULL,
    [Latitude]           FLOAT (53)       NOT NULL,
    [Longitude]          FLOAT (53)       NOT NULL,
    [Altitude]           FLOAT (53)       NULL,
    [Odometer]           INT              NULL,
    [Speed]              FLOAT (53)       NULL,
    [BatteryLevel]       INT              NULL,
    [PinFlags]           BIGINT           NOT NULL,
    [DateRecorded]       DATETIME         NOT NULL,
    [DateReceived]       DATETIME         NOT NULL,
    [Satellites]         INT              NOT NULL,
    [HDOP]               FLOAT (53)       NOT NULL,
    [MachineryId]        INT              NOT NULL,
    [TrackerId]          INT              NOT NULL,
    [ReportType]         NVARCHAR (1)     NULL,
    [FixStatus]          INT              DEFAULT ((0)) NOT NULL,
    [AlarmStatus]        INT              DEFAULT ((0)) NOT NULL,
    [OperationalSeconds] INT              DEFAULT ((0)) NOT NULL,
    CONSTRAINT [PK_dbo.MachineryReading] PRIMARY KEY CLUSTERED ([Id] ASC),
    CONSTRAINT [FK_dbo.MachineryReading_dbo.Machinery_MachineryId] FOREIGN KEY ([MachineryId]) REFERENCES [dbo].[Machinery] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_dbo.MachineryReading_dbo.Tracker_TrackerId] FOREIGN KEY ([TrackerId]) REFERENCES [dbo].[Tracker] ([Id]) ON DELETE CASCADE
);

GO
CREATE NONCLUSTERED INDEX [IX_MachineryId]
    ON [dbo].[MachineryReading]([MachineryId] ASC);

GO
CREATE NONCLUSTERED INDEX [IX_TrackerId]
    ON [dbo].[MachineryReading]([TrackerId] ASC);

GO
CREATE NONCLUSTERED INDEX [IX_MachineryId_DateRecorded]
    ON [dbo].[MachineryReading]([MachineryId] ASC, [DateRecorded] ASC)
    INCLUDE([OperationalSeconds], [FixStatus]);

该表被划分为月份范围(尽管我仍然不太了解那里到底发生了什么)。

ALTER PARTITION SCHEME PartitionSchemeMonthRange NEXT USED [Primary]
ALTER PARTITION FUNCTION [PartitionFunctionMonthRange]() SPLIT RANGE(N'2016-01-01T00:00:00.000') 

ALTER PARTITION SCHEME PartitionSchemeMonthRange NEXT USED [Primary]
ALTER PARTITION FUNCTION [PartitionFunctionMonthRange]() SPLIT RANGE(N'2016-02-01T00:00:00.000') 
...

CREATE UNIQUE CLUSTERED INDEX [PK_dbo.MachineryReadingPs] ON MachineryReading(DateRecorded, Id) ON PartitionSchemeMonthRange(DateRecorded)

我通常会运行的查询:

SELECT TOP (1) [Id], [Location], [Latitude], [Longitude], [Altitude], [Odometer], [ReportType], [FixStatus], [AlarmStatus], [Speed], [BatteryLevel], [PinFlags], [DateRecorded], [DateReceived], [Satellites], [HDOP], [OperationalSeconds], [MachineryId], [TrackerId]
    FROM [dbo].[MachineryReading]
    --WITH(INDEX(IX_MachineryId_DateRecorded)) --This makes all the difference
    WHERE ([MachineryId] = @p__linq__0) AND ([DateRecorded] >= @p__linq__1) AND ([DateRecorded] < @p__linq__2) AND ([OperationalSeconds] > 0)
    ORDER BY [DateRecorded] ASC

查询计划:https : //www.brentozar.com/pastetheplan/?id=r1c-RpxNx

具有强制索引的查询计划:https : //www.brentozar.com/pastetheplan/?id=SywwTagVe

包括的计划是实际的执行计划,但在登台数据库上(大约实时大小的1/100)。我犹豫要如何修改实时数据库,因为我大约一个月前才在这家公司工作。

我有一种感觉是因为分区,而且我的查询通常跨越每个分区(例如,当我想获取OperationalSeconds一台机器的第一个或最后一个记录时)。但是,我一直在手工编写的查询都比EntityFramework生成的查询快10到100倍,因此,我将创建一个存储过程。


1
嗨@AndrewWilliamson,这可能是统计问题。如果您从非强制性计划中看到实际计划,则估计的行数为1.22,而实际的行数为19039。这依次导致您在计划的稍后部分中看到键查找。您是否尝试过更新统计信息?如果不是,请尝试对登台数据库进行全面扫描。
jesijesi

Answers:


21

如果我让服务器决定使用哪个索引,它将选择IX_MachineryId,最多需要一分钟。

该索引未分区,因此优化程序可以识别出它可以用于提供查询中指定的排序而不进行排序。作为非唯一的非聚集索引,它还具有聚集索引的键作为子键,因此该索引可用于搜索on MachineryIdDateRecorded范围:

索引搜寻

索引不包含OperationalSeconds,因此计划必须在(分区的)聚集索引中逐行查找该值,以便进行测试OperationalSeconds > 0

抬头

优化程序估计需要从非聚集索引中读取一行,并进行查询以满足该需求。 TOP (1)。此计算基于行目标(快速查找一行),并假设值的分布均匀。

从实际计划中,我们可以看到1行的估计是不准确的。实际上,必须处理19,039行才能发现没有行满足查询条件。对于行目标优化,这是最坏的情况(估计1行,实际需要所有行):

实际/估计

您可以使用跟踪标志4138禁用行目标。这很可能导致SQL Server选择其他计划,可能是您强制执行的计划。在任何情况下,IX_MachineryId通过包含OperationalSeconds

具有非对齐的非聚集索引(索引以与基表不同的方式进行分区,包括完全不分区)是非常不寻常的。

这确实向我表明,我已正确编制了索引,而服务器只是在做出错误的决定。为什么?

像往常一样,优化器会选择它认为最便宜的计划。

IX_MachineryId计划的估计成本为0.01个成本单位,基于(不正确的)行目标假设,即将测试并返回一行。

IX_MachineryId_DateRecorded计划的估计成本要高得多,为0.27个单位,主要是因为它希望从索引中读取5,515行,对其进行排序,然后返回排序最低的行(按DateRecorded):

前N个排序

该索引是分区的,不能DateRecorded直接按顺序返回行(请参阅下文)。它可以寻求MachineryIdDateRecorded范围在每个分区内,但需要一个类别:

分区搜寻

如果未对该索引进行分区,则将不需要排序,并且该索引将与其他(未分区)具有额外包含列的索引非常相似。未分区的过滤索引仍然会稍微更有效。


您应该更新源查询,以便数据类型的中@From@To参数匹配DateRecorded列(datetime)。目前,由于运行时类型不匹配,SQL Server正在计算动态范围(使用Merge Interval运算符及其子树):

<ScalarOperator ScalarString="GetRangeWithMismatchedTypes([@From],NULL,(22))">
<ScalarOperator ScalarString="GetRangeWithMismatchedTypes([@To],NULL,(22))">

这种转换使优化程序无法正确地推断升序分区IDDateRecorded以升序覆盖一系列值)与不等式谓词之间的关系DateRecorded

分区ID是分区索引的隐式前导键。通常,优化器可以看到按分区ID排序(其中,升序ID映射为的升序,不相交值DateRecordedDateRecordedDateRecorded单独排序相同(假定MachineryID为常数)。类型转换破坏了这种推理链。

演示版

一个简单的分区表和索引:

CREATE PARTITION FUNCTION PF (datetime)
AS RANGE LEFT FOR VALUES ('20160101', '20160201', '20160301');

CREATE PARTITION SCHEME PS AS PARTITION PF ALL TO ([PRIMARY]);

CREATE TABLE dbo.T (c1 integer NOT NULL, c2 datetime NOT NULL) ON PS (c2);

CREATE INDEX i ON dbo.T (c1, c2) ON PS (c2);

INSERT dbo.T (c1, c2) 
VALUES (1, '20160101'), (1, '20160201'), (1, '20160301');

查询匹配类型

-- Types match (datetime)
DECLARE 
    @From datetime = '20010101',
    @To datetime = '20090101';

-- Seek with no sort
SELECT T2.c2 
FROM dbo.T AS T2 
WHERE T2.c1 = 1 
AND T2.c2 >= @From
AND T2.c2 < @To
ORDER BY 
    T2.c2;

没排序

查询类型不匹配

-- Mismatched types (datetime2 vs datetime)
DECLARE 
    @From datetime2 = '20010101',
    @To datetime2 = '20090101';

-- Merge Interval and Sort
SELECT T2.c2 
FROM dbo.T AS T2 
WHERE T2.c1 = 1 
AND T2.c2 >= @From
AND T2.c2 < @To
ORDER BY 
    T2.c2;

合并间隔和排序


5

该索引对于查询而言似乎相当不错,但我不确定为什么优化器没有选择它(统计信息?分区?天蓝色限制?实际上不知道。)

但是,如果索引> 0是一个固定值,并且不会从一个查询的执行变为另一个查询,则过滤后的索引对于特定查询会更好:

CREATE NONCLUSTERED INDEX IX_MachineryId_DateRecorded_filtered
    ON dbo.MachineryReading
        (MachineryId, DateRecorded) 
    WHERE (OperationalSeconds > 0) ;

OperationalSeconds第3列的索引与过滤后的索引有两个区别:

  • 首先,过滤后的索引在宽度(较窄)和行数上都较小。
    通常,由于SQL Server需要较少的空间将其保留在内存中,因此通常使筛选后的索引更有效。

  • 其次,对于查询而言,这更为微妙和重要,因为它只有与查询中使用的过滤器匹配的行。这可能非常重要,具体取决于第三列的值。
    例如一组特定的参数用于MachineryIdDateRecorded可产生1000行。如果所有或几乎所有这些行都与(OperationalSeconds > 0)过滤器匹配,则两个索引都将表现良好。但是,如果与过滤器匹配的行很少(或者只有最后一个或根本没有),则第一个索引将必须遍历全部或全部1000行,直到找到匹配项。另一方面,过滤后的索引只需要一个查找即可找到匹配的行(或返回0行),因为仅存储与过滤器匹配的行。


1
添加索引是否使查询更有效?
ypercubeᵀᴹ

不需要登台数据库(它确实需要更多数据才能正确测试),但我还没有实时尝试过,新索引需要一个多小时才能建立。我也很犹豫对实时数据库执行任何操作,因为它已经运行缓慢。我们需要一个更好的系统来将直播克隆到舞台上。
安德鲁·威廉姆森
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.