1条记录表索引扫描22亿次执行

Ace*_*ePL 6 performance sql-server sql-server-2012 query-performance

在我的查询中,我不确定如何解决某些问题。

一、定义:

快递服务表。有一个记录。

CREATE TABLE [dbo].[CS](
    [ServiceID] [int] IDENTITY(1,1) NOT NULL,
    [CSID] [nvarchar](6) NULL,
    [CSDescription] [varchar](50) NULL,
    [OperatingDays] [int] NULL,
    [DefaultService] [bit] NULL,
 CONSTRAINT [CourierServices_PK] PRIMARY KEY CLUSTERED 
(
    [ServiceID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF,
       ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90
) ON [PRIMARY]
) ON [PRIMARY]

GO
SET IDENTITY_INSERT [dbo].[CS] ON 

INSERT [dbo].[CS] ([ServiceID], [CSID], [OperatingDays], [DefaultService])
           VALUES (1, N'RM48', 2, 1)
SET IDENTITY_INSERT [dbo].[CS] OFF
SET ANSI_PADDING ON

GO
/****** Object:  Index [ix_CourierServices]    Script Date: 19/04/2017 14:27:03 ******/
CREATE NONCLUSTERED INDEX [ix_CourierServices] ON [dbo].[CS]
(
    [CSID] ASC,
    [DefaultService] ASC,
    [OperatingDays] ASC
)
INCLUDE (   [CSDescription]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
Run Code Online (Sandbox Code Playgroud)

日历数据库和表格,由Genius Jim Horn编写:

CREATE TABLE [dbo].[days](
    [PKDate] [date] NOT NULL,
    [calendar_year] [smallint] NULL,
    [calendar_quarter] [tinyint] NULL,
    [calendar_quarter_desc] [varchar](10) NULL,
    [calendar_month] [tinyint] NULL,
    [calendar_month_name_long] [varchar](30) NULL,
    [calendar_month_name_short] [varchar](10) NULL,
    [calendar_week_in_year] [tinyint] NULL,
    [calendar_week_in_month] [tinyint] NULL,
    [calendar_day_in_year] [smallint] NULL,
    [calendar_day_in_week] [tinyint] NULL,
    [calendar_day_in_month] [tinyint] NULL,
    [dmy_name_long] [varchar](30) NULL,
    [dmy_name_long_with_suffix] [varchar](30) NULL,
    [day_name_long] [varchar](10) NULL,
    [day_name_short] [varchar](10) NULL,
    [continuous_year] [tinyint] NULL,
    [continuous_quarter] [smallint] NULL,
    [continuous_month] [smallint] NULL,
    [continuous_week] [smallint] NULL,
    [continuous_day] [int] NULL,
    [description] [varchar](100) NULL,
    [is_weekend] [tinyint] NULL,
    [is_holiday] [tinyint] NULL,
    [is_workday] [tinyint] NULL,
    [is_event] [tinyint] NULL,
PRIMARY KEY CLUSTERED 
(
    [PKDate] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF,
 ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

/****** Object:  Index [ix_days]    Script Date: 19/04/2017 14:38:47 ******/
CREATE NONCLUSTERED INDEX [ix_days] ON [dbo].[days]
(
    [PKDate] ASC
)
INCLUDE (   [is_weekend],
    [is_holiday],
    [is_workday],
    [is_event]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
 SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF,
 ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
Run Code Online (Sandbox Code Playgroud)

现在,我正在运行一个查询,它根据代码位引用两个表:

Select
    OID
   ,case when
     Cast(o.[CreationDate] as time) > '16:00:00' 
        then (select top 1 [PKDate] from [calendar].[dbo].days
              where is_weekend <> 1 and is_holiday <>1 and 
              PKDate > cast(o.[CreationDate] as date)
              order by PKDate asc)
        else (select top 1 [PKDate] from [calendar].[dbo].days
              where is_weekend <> 1 and is_holiday <>1 and 
              PKDate >= Cast(o.[CreationDate] as date) 
              order by PKDate asc)
        end  OperatingDate
   ,case when
     Cast(o.[CreationDate] as time) > '16:00:00' 
        then (select top 1 [PKDate] from [calendar].[dbo].days
              where is_weekend <> 1 and is_holiday <>1 and 
              PKDate > dateadd(day,isnull(
                  (select top 1 [operatingdays]
                  from [dbo].[CS]
                  where DefaultService = 1)
                 ,2)+1,Cast(o.[CreationDate] as date))
                 order by PKDate asc)
            else (select top 1 [PKDate] from [calendar].[dbo].days
                  where is_weekend <> 1 and is_holiday <>1 and
                  PKDate > dateadd(day,isnull(
                      (select top 1 [operatingdays]
                       from [dbo].[CS]
                       where DefaultService = 1)
                      ,2), Cast(o.[CreationDate] as date))
                      order by PKDate asc)
            end EstimatedDeliveryDate
  ,(select dateadd(day,3,o.[CreationDate])) DeliveryDate
From o
Run Code Online (Sandbox Code Playgroud)

现在的问题是,与索引扫描和执行次数有关:为什么是 20 亿?还是60亿?诚然,整个查询的输出是 170 万行,但这并不能解释查询计划中显示的疯狂数字:

https://www.brentozar.com/pastetheplan/?id=H1iahxHAe

如果我可以将所有这些扫描打平,我可以显着减少查询时间,但首先:我如何解释这些数字以找到解决方案?

天表包含 7.6k 行(涵盖 2000-2020 年)。

Joe*_*ish 5

让我们从计划的右上角开始。那部分计算OperatingDate列:

营业日期

由于我们为外部行集取回了 1.72 M 行,我们可以预期大约 1.72 M 行索引针对ix_days。确实是这样。有 478k 行,o.[CreationDate] as time) > '16:00:00'因此该CASE语句将 478k 搜索发送到一个分支,其余的发送到另一个。

请注意,您拥有的索引对于此查询来说并不是最有效的索引。我们只能对 做一个搜索谓词PKDate。其余过滤器用作谓词。这意味着在找到匹配项之前,查找可能会遍历多行。我假设您日历表中的大多数日子不是周末或假期,因​​此对于此查询可能没有实际影响。但是,您可以在 上定义索引is_weekend, is_holiday, PKDate。这应该让你立即寻找你想要的第一行。

寻求与谓词

为了更清楚地说明这一点,让我们来看一个简单的例子:

-- does a scan
SELECT TOP 1 PkDate
FROM [Days]
WHERE is_weekend <> 1 AND is_holiday <> 1
AND PkDate >= '2000-04-01'
ORDER BY PkDate;

-- does a seek, reads 3 rows to return 1
SELECT TOP 1 PkDate
FROM [Days]
WHERE is_weekend = 0 AND is_holiday = 0
AND PkDate >= '2000-04-01'
ORDER BY PkDate;

-- create new index
CREATE NONCLUSTERED INDEX [ix_days_2] ON [dbo].[days]
(
    [is_weekend],
    [is_holiday],
    PkDate
)

-- does a seek, reads 1 row to return 1
SELECT TOP 1 PkDate
FROM [Days]
WHERE is_weekend = 0 AND is_holiday = 0
AND PkDate >= '2000-04-01'
ORDER BY PkDate;

DROP INDEX [days].[ix_days_2];
Run Code Online (Sandbox Code Playgroud)

让我们进入更有趣的部分,即计算DeliveryDate列的分支。我只会包括其中的一半:

DeliveryDate 分支

我怀疑您希望优化器做的是将其计算为标量:

dateadd(day,isnull(
                  (select top 1 [operatingdays]
                  from [dbo].[CS]
                  where DefaultService = 1)
                 ,2)+1,Cast(o.[CreationDate] as date))
Run Code Online (Sandbox Code Playgroud)

并使用它的值来使用ix_days. 不幸的是,优化器不会这样做。相反,它对索引应用行目标并进行扫描。对于从扫描返回的每一行,它会检查该值是否与针对 的过滤器匹配[dbo].[CS]。一旦找到匹配的一行,扫描就会停止。SQL Server 估计它平均只会从扫描中拉回 3.33 行,直到找到匹配项。如果这是真的,那么您将看到大约 150 万次针对[dbo].[CS]. 相反,优化器对表执行了 20 亿次,因此估计值相差 1000 多倍。

作为一般规则,您应该仔细检查嵌套循环内侧的任何扫描。当然,有些查询正是您想要的。并且仅仅因为您有一个搜索并不意味着查询将是有效的。例如,如果查找返回许多行,则可能与执行扫描没有太大区别。你没有在这里发布完整的查询,但我会讨论一些可能有帮助的想法。

这个查询有点奇怪:

select top 1 [operatingdays]
from [dbo].[CS]
where DefaultService = 1
Run Code Online (Sandbox Code Playgroud)

它是不确定的,因为您TOP没有ORDER BY. 但是,表本身有 1 行,您总是从o. 如果可能,我会尝试将这个查询的值保存到一个局部变量中,然后在查询中使用它。这应该再次为您节省总共 80 亿次扫描[dbo].[CS],我希望看到索引搜索而不是对ix_days. 我能够在我的机器上模拟一些数据。这是查询计划的一部分:

好的查询计划 1

现在我们有了所有的搜索,这些搜索不应该处理太多额外的行。但是,真正的查询可能比这更复杂,因此您可能无法使用变量。

假设我编写了一个不使用TOP. 相反,我将使用MIN. SQL Server 能够以更有效的方式处理该子查询。TOP 可以阻止某些查询转换。这是我的子查询:

WHERE PKDate > dateadd(day,isnull(
                      (select MIN([operatingdays])
                       from [dbo].[CS]
                       where DefaultService = 1)
                      ,2), Cast(o.[CreationDate] as date))
Run Code Online (Sandbox Code Playgroud)

该计划可能如下所示:

好计划2

现在我们将只对CS表格进行大约 150 万次扫描。我们还针对ix_days能够使用子查询结果的索引获得了更有效的索引查找:

不错的寻找

当然,我并不是说你应该重写你的代码来使用它。它可能会返回不正确的结果。重要的一点是您可以使用子查询获得所需的索引查找。您只需要以正确的方式编写子查询。

再举一个例子,让我们假设您绝对需要TOP在子查询中保留运算符。可以添加一个冗余过滤器PkDate来获得更好的性能。我将假设子查询的结果是非负的并且很小。这意味着这个查询将是等价的:

  PKDate > Cast(o.[CreationDate] as date) AND 
  PKDate > dateadd(day,isnull(
      (select top 1 [operatingdays]
      from [dbo].[CS]
      where DefaultService = 1)
     ,2)+1,Cast(o.[CreationDate] as date))
Run Code Online (Sandbox Code Playgroud)

这改变了使用寻求的计划:

再找

重要的是要意识到搜索可能只返回一行。重要的一点是 SQL Server 可以从o.[CreationDate]. 如果日期之间存在很大差距,那么索引查找将处理许多额外的行,并且查询效率将降低。