与日期比较的子查询效果不佳


15

当使用子查询查找具有匹配字段的所有先前记录的总数时,在只有5万条记录的表上,性能会很糟糕。没有子查询,查询将在几毫秒内执行。使用子查询,执行时间超过一分钟。

对于此查询,结果必须:

  • 仅包括给定日期范围内的那些记录。
  • 包括所有先前记录的计数,不包括当前记录,无论日期范围如何。

基本表架构

Activity
======================
Id int Identifier
Address varchar(25)
ActionDate datetime2
Process varchar(50)
-- 7 other columns

示例数据

Id  Address     ActionDate (Time part excluded for simplicity)
===========================
99  000         2017-05-30
98  111         2017-05-30
97  000         2017-05-29
96  000         2017-05-28
95  111         2017-05-19
94  222         2017-05-30

预期成绩

对于日期范围2017-05-29,以2017-05-30

Id  Address     ActionDate    PriorCount
=========================================
99  000         2017-05-30    2  (3 total, 2 prior to ActionDate)
98  111         2017-05-30    1  (2 total, 1 prior to ActionDate)
94  222         2017-05-30    0  (1 total, 0 prior to ActionDate)
97  000         2017-05-29    1  (3 total, 1 prior to ActionDate)

记录96和95从结果中排除,但包含在PriorCount子查询中

当前查询

select 
    *.a
    , ( select count(*) 
        from Activity
        where 
            Activity.Address = a.Address
            and Activity.ActionDate < a.ActionDate
    ) as PriorCount
from Activity a
where a.ActionDate between '2017-05-29' and '2017-05-30'
order by a.ActionDate desc

当前指数

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON [dbo].[Activity]
(
    [ActionDate] ASC
)
INCLUDE ([Address]) WITH (
    PAD_INDEX = OFF, 
    STATISTICS_NORECOMPUTE = OFF, 
    SORT_IN_TEMPDB = OFF, 
    DROP_EXISTING = OFF, 
    ONLINE = OFF, 
    ALLOW_ROW_LOCKS = ON, 
    ALLOW_PAGE_LOCKS = ON
)

  • 可以使用哪些策略来改善此查询的性能?

编辑1
回答关于可以在DB上进行哪些修改的问题:我可以修改索引,而不能修改表结构。

编辑2
我现在在该Address列上添加了一个基本索引,但是似乎并没有太大改善。我目前发现通过创建临时表并插入不带值的值PriorCount,然后使用其特定计数更新每一行,可以得到更好的性能。

编辑3
发现问题的索引后台处理程序Joe Obbish(可接受的答案)。一旦添加了新内容nonclustered index [xyz] on [Activity] (Address) include (ActionDate),查询时间就从一分钟以上减少到不到一秒,而无需使用临时表(请参见编辑2)。

Answers:


17

使用for的索引定义IDX_my_nme,SQL Server将能够使用该ActionDate列而不是该Address列进行查找。索引包含了覆盖子查询所需的所有列,但是对于该子查询而言,它可能不是非常有选择性。假设表中几乎所有数据的ActionDate值都早于'2017-05-30'。搜寻ActionDate < '2017-05-30'将返回索引中几乎所有的行,从索引中提取行后将进一步过滤掉这些行。如果查询返回200行,则可能会对进行近200次完整索引扫描IDX_my_nme,这意味着您将从索引中读取大约50000 * 200 = 1000万行。

Address尽管您尚未向我们提供有关该查询的完整统计信息,但搜索对您的子查询可能会更具选择性,这是我的假设。但是,假设您在just上创建了一个索引,Address并且表具有的10k唯一值Address。使用新索引,SQL Server每次执行子查询仅需要从索引中查找5行,因此您将从索引中读取大约200 * 5 = 1000行。

我正在针对SQL Server 2016进行测试,因此可能存在一些小的语法差异。以下是一些示例数据,其中我对数据分配做出了与上述类似的假设:

CREATE TABLE #Activity (
    Id int NOT NULL,
    [Address] varchar(25) NULL,
    ActionDate datetime2 NULL,
    FILLER varchar(100),
    PRIMARY KEY (Id)
);

INSERT INTO #Activity WITH (TABLOCK)
SELECT TOP (50000) -- 50k total rows
x.RN
, x.RN % 10000 -- 10k unique addresses
, DATEADD(DAY, x.RN / 100, '20160201') -- 100 rows per day
, REPLICATE('Z', 100)
FROM
(
    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) x;

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON #Activity
([ActionDate] ASC) INCLUDE ([Address]);

我已经按照问题中的说明创建了您的索引。我正在针对此查询进行测试,该查询返回的数据与问题中的数据相同:

select 
    a.*
    , ( select count(*) 
        from #Activity Activity
        where 
            Activity.[Address] = a.[Address]
            and Activity.ActionDate < a.ActionDate
    ) as PriorCount
from #Activity a
where a.ActionDate between '2017-05-29' and '2017-05-30'
order by a.ActionDate desc;

我得到一个索引后台处理程序。从根本上讲,这意味着查询优化器会动态构建临时索引,因为针对该表的现有索引都不适合。

索引假脱机

查询仍然对我快速完成。也许您没有在系统上进行索引假脱机优化,或者表定义或查询有所不同。出于教育目的,我可以使用未记录的功能OPTION (QUERYRULEOFF BuildSpool)来禁用索引假脱机。该计划如下所示:

不良索引搜寻

不要被简单的索引查找的外观所迷惑。SQL Server从索引中读取了近一千万行:

索引中有1000万行

如果我要不止一次地运行查询,那么查询优化器每次运行时创建索引可能就没有意义了。我可以预先创建一个索引,该索引对于此查询将更具选择性:

CREATE NONCLUSTERED INDEX [IDX_my_nme_2] ON #Activity
([Address] ASC) INCLUDE (ActionDate);

该计划与之前类似:

索引搜寻

但是,使用新索引,SQL Server仅从索引读取1000行。返回800行进行计数。可以将索引定义为更具选择性,但这可能足以满足您的数据分布。

好寻求

如果您无法在表上定义任何其他索引,则可以考虑使用窗口函数。以下内容似乎起作用:

SELECT t.*
FROM
(
    select 
        a.*
        , -1 + ROW_NUMBER() OVER (PARTITION BY [Address] ORDER BY ActionDate) PriorCount
    from #Activity a
) t
where t.ActionDate between '2017-05-29' and '2017-05-30'
order by t.ActionDate desc;

该查询只对数据进行一次扫描,但进行了昂贵的排序,并ROW_NUMBER()为表中的每一行计算了函数,因此感觉这里需要做一些额外的工作:

不好的排序

但是,如果您真的喜欢该代码模式,则可以定义一个索引以使其更有效:

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON #Activity
([Address], [ActionDate]) INCLUDE (FILLER);

这样一来,排序就可以便宜得多了:

好排序

如果以上方法均无济于事,则您需要向该问题添加更多信息,最好包括实际的执行计划。


1
您找到的索引假脱机就是问题所在。一旦添加了新nonclustered index [xyz] on [Activity] (Address) include (ActionDate)的查询,查询时间就会从一分钟以上减少到不到一秒钟。如果可以的话,+ 10。谢谢!
大都会蓝精灵
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.