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)
)
当前记录数:
ContactRecord
2000万
TransactionRecord
9000万
我的查询是:
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%确定这些简化的版本与原始版本一致。
最近我们 运行 遇到了 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)
)
当前记录数:
ContactRecord
2000万TransactionRecord
9000万
我的查询是:
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%确定这些简化的版本与原始版本一致。