SQL 服务器查询间歇性性能问题

SQL Server Query intermittent performance Issue

最近我们 运行 遇到了 SQL 服务器 (2016) 上特定查询的性能问题。我看到的问题是性能问题非常不一致,我不确定如何改进它。

table详情:

CREATE TABLE ContactRecord 
(
  ContactSeq BIGINT NOT NULL 
, ApplicationCd VARCHAR(2) NOT NULL 
, StartDt DATETIME2 NOT NULL 
, EndDt DATETIME2 
, EndStateCd VARCHAR(3) 
, UserId VARCHAR(10) 
, UserTypeCd VARCHAR(2) 
, LineId VARCHAR(3) 
, CallingLineId VARCHAR(20) 
, DialledLineId VARCHAR(20) 
, ChannelCd VARCHAR(2) 
, SubChannelCd VARCHAR(2) 
, ServicingAgentCd VARCHAR(7) 
, EucCopyTimestamp VARCHAR(30) 
, PRIMARY KEY (ContactSeq)
, FOREIGN KEY (ApplicationCd) REFERENCES ApplicationType(ApplicationCd)
, FOREIGN KEY (EndStateCd) REFERENCES EndStateType(EndStateCd)
, FOREIGN KEY (UserTypeCd) REFERENCES UserType(UserTypeCd)
)

CREATE TABLE TransactionRecord 
(
  TransactionSeq BIGINT NOT NULL 
, ContactSeq BIGINT NOT NULL 
, TransactionTypeCd VARCHAR(3) NOT NULL 
, TransactionDt DATETIME2 NOT NULL 
, PolicyId VARCHAR(10) 
, ProductId VARCHAR(7) 
, EucCopyTimestamp VARCHAR(30) 
, Detail VARCHAR(1000) 
, PRIMARY KEY (TransactionSeq)
, FOREIGN KEY (ContactSeq) REFERENCES ContactRecord(ContactSeq)
, FOREIGN KEY (TransactionTypeCd) REFERENCES TransactionType(TransactionTypeCd)
)

当前记录数:

我的查询是:

select
   UserId,
   max(StartDt) as LastLoginDate 
from
   ContactRecord 
where
   ContactSeq in 
   (
      select
         ContactSeq 
      from
         TransactionRecord 
      where
         ContactSeq in 
         (
            select
               ContactSeq 
            from
               ContactRecord 
            where
               UserId in 
               (
                  '1234567890',
                  '1234567891' -- Etc.
               )
         )
         and TransactionRecord.TransactionTypeCd not in 
         (
            '122'
         )
   )
   and ApplicationCd not in 
   (
      '1',
      '4',
      '5'
   )
group by
   UserId;

现在查询不是很好,可以使用联接进行改进,但它确实有效。 我遇到的问题是我们的数据作业需要大约 7100 个 userIds 的输入。然后将它们分成 500 个组。对于每 500 个,这些用于此查询的 IN 子句中。 IN 子句中包含 500 个项目的此查询的前 14 次执行执行良好。每个结果大约在 15-20 秒内返回。

问题在于上次执行此查询的剩余 100 次取舍。它似乎永远不会完成。它只是挂起。在我们的数据作业中,它在 10 分钟后超时。我不知道为什么。我不是 SQL 服务器方面的专家,所以我不太确定如何调试它。我已经独立执行了每个子查询,然后用返回的数据替换了子查询的内容。对每个子查询执行此操作都很好。

在此非常感谢您的帮助,因为我不知道它是如何在大量参数下如此一致地工作的,但只对一小部分参数不起作用。

编辑

我这里有三个执行计划的例子。请注意,每一个都是在测试服务器上执行的,并且几乎都是立即执行的,因为这个测试等效项的数据非常少。

这是 500 个参数的执行计划,在生产环境中执行良好,大约在 15-20 秒内返回:

这是 119 个参数的执行计划,它在 10 分钟后在我们的数据作业中超时:

这是执行良好的 5 个参数的执行计划。此查询未在数据作业中明确执行,仅用于比较:

在所有情况下,SSMS 都给出了以下警告:

/*
Missing Index Details from SQLQuery2.sql
The Query Processor estimates that implementing the following index could improve the query cost by 26.3459%.
*/

/*
USE [CloasIvr]
GO
CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[TransactionRecord] ([TransactionTypeCd])
INCLUDE ([ContactSeq])
GO
*/

这是这个问题的根本原因吗?

如果没有看到发生了什么,就很难确切地知道发生了什么——尤其是那些失败的。 'good' 运行s 的执行计划可以提供一些帮助,但我们只是猜测在 运行s.

中出了什么问题

我最初的猜测(类似于我的评论)是它所期望的估计是非常错误的,并且它创建了一个非常糟糕的计划。

您的 TransactionRecord table 特别是 detail 列为 1000 个字符,可能会出现大量意外嵌套循环的大问题。

索引

我建议的第一件事是建立索引 - 特别是 a) 只包含这些数据所需的数据子集,以及 b) 以有用的方式对它们进行排序。

我建议以下两个索引似乎可以提供帮助

CREATE INDEX IX_ContactRecord_User ON ContactRecord 
    (UserId, ContactSeq) 
    INCLUDE (ApplicationCD, Startdt);

CREATE INDEX IX_TransactionRecord_ContactSeq ON TransactionRecord 
    (ContactSeq, TransactionTypeCd);

这些都是 'covering indexes',并且按照可以提供帮助的方式进行排序。 或者,您可以用稍作修改的版本替换第一个版本(在 ContactSeq 上首先排序),但我认为上面的版本会更有用。

CREATE INDEX IX_ContactRecord_User2 ON ContactRecord 
    (ContactSeq) 
    INCLUDE (ApplicationCD, Startdt, UserId);

此外,关于 TransactionRecord 上的索引 - 如果这是唯一使用该索引的查询,您可以通过创建以下索引来改进它

CREATE INDEX IX_TransactionRecord_ContactSeq_Filtered ON TransactionRecord
    (ContactSeq, TransactionTypeCd) 
    WHERE (TransactionTypeCD <> '122');

上面的 filtered index 与语句的 WHERE 子句中指定的内容相匹配。最重要的是它已经 a) 删除了类型 <> '122' 的记录,并且 b) 已经对 ContactSeq 上已有的记录进行了排序,因此很容易查找它们。

顺便说一下 - 鉴于您原则上询问过在外键上添加索引 - 这些的使用实际上取决于您如何读取数据。如果您只引用引用的 table(例如,您有一个状态 table 的 FK,并且只用它来用英语报告状态),那么原始索引table 的 Status_ID 无济于事。另一方面,如果您想找到 Status_ID = 4 的所有行,那么它会有所帮助。

为了帮助理解索引,我强烈推荐 Brent Ozar 的 How to think like an SQL Server Engine - 它确实帮助我理解了索引在实践中的工作原理。

使用排序的温度 table

这可能会有所帮助,但不太可能成为主要修复方法。如果您 pre-load 将相关的 UserIDs 变成一个临时的 table (在 UserID 上有一个主键)那么它可能有助于相关的 JOIN。修改每个 运行 而不是必须修改查询的中间部分也可能更容易。

CREATE TABLE #Users (UserId VARCHAR(10) PRIMARY KEY);
INSERT INTO #Users (UserID) VALUES
('1234567890'),
('1234567891');

然后将查询的中间部分替换为

      where
         ContactSeq in 
         (
            select
               ContactSeq 
            from
               ContactRecord CR
               INNER JOIN #Users U ON CR.UserID = U.UserID 
         )
         and TransactionRecord.TransactionTypeCd not in 
         (
            '122'
         )

简化查询

我试着简化了查询,结果变成了这样:

select  CR.UserId,
        max(CR.StartDt) as LastLoginDate 
from    ContactRecord CR
        INNER JOIN TransactionRecord TR ON CR.ContactSeq = TR.ContactSeq
where   TR.TransactionTypeCd not in ('122')
        AND CR.ApplicationCd not in ('1', '4', '5')
        AND CR.UserId in ('1234567890', '1234567891') -- etc
group by UserId;

或者(使用温度 table)

select  CR.UserId,
        max(CR.StartDt) as LastLoginDate 
from    ContactRecord CR
        INNER JOIN #Users U ON CR.UserID = U.UserID 
        INNER JOIN TransactionRecord TR ON CR.ContactSeq = TR.ContactSeq
where   TR.TransactionTypeCd not in ('122')
        AND CR.ApplicationCd not in ('1', '4', '5')
group by UserId;

简化查询的一个好处是,它还有助于 SQL 服务器获得良好的估计;这反过来又有助于它获得良好的执行计划。

当然,你需要在你的情况下测试以上returns完全相同的记录——我没有数据集可以测试,所以我不能100%确定这些简化的版本与原始版本一致。