Chr*_*s L 2 performance sql-server sql-server-2012 queue query-performance
我在 SQL Server 2012 中有一个简单的表,它实现了一个处理队列。随着数据的插入,检索下一个项目的查询从 <100 毫秒变为恒定的 5-6 秒。如果有人能指出性能突然下降的原因,我将不胜感激。(这似乎几乎是一夜之间的下跌)。
这是表定义:
CREATE TABLE [dbo].[messagequeue] (
[id] INT IDENTITY (1, 1) NOT NULL,
[testrunident] VARCHAR (255) NOT NULL,
[filesequence] INT NOT NULL,
[processed] BIT NOT NULL,
[dateentered] DATETIME NULL,
[filedata] VARBINARY (MAX) NULL,
[retries] INT NOT NULL,
[failed] BIT NOT NULL,
[msgobject] VARBINARY (MAX) NULL,
[errortext] VARCHAR (MAX) NULL,
[sourcefilename] VARCHAR (MAX) NULL,
[xmlsource] VARCHAR (MAX) NULL,
[messagetype] VARCHAR (255) NULL
);
CREATE NONCLUSTERED INDEX [messagequeue_sequenc_failed_idx]
ON [dbo].[messagequeue]([processed] ASC, [failed] ASC)
INCLUDE([id], [testrunident], [filesequence]);
CREATE NONCLUSTERED INDEX [messagequeue_sequence_idx]
ON [dbo].[messagequeue]([testrunident] ASC, [processed] ASC)
INCLUDE([filesequence]);
CREATE UNIQUE NONCLUSTERED INDEX [IXd_testrun_sequence]
ON [dbo].[messagequeue]([testrunident] ASC, [filesequence] ASC);
Run Code Online (Sandbox Code Playgroud)
这是用于检索要处理的下一行的查询:
select messagequeue.id, messagequeue.testrunident, messagequeue.filesequence,
messagequeue.processed, messagequeue.filedata, messagequeue.retries, messagequeue.failed,
messagequeue.msgobject, messagequeue.xmlsource
from messagequeue where id = (
select top 1 id from messagequeue mqouter
where processed = 0
AND failed = 0
AND (filesequence = 0 OR
filesequence = (
select max (filesequence) + 1
from messagequeue mqinner
where mqinner.testrunident = mqouter.testrunident
and mqinner.processed = 1
)
)
order by testrunident, filesequence
)
Run Code Online (Sandbox Code Playgroud)
有多行具有相同的testrunident
,每行都有一个filesequence
应该是顺序的,但是有些可能会丢失,因此查询应该只返回前一行所在的 NEXT 行,processed = 1
或者filesequence = 0
表示这是组中的第一行testrunident
。
这是一个提供想法的 SQLFiddle:SQL Fiddle
查询计划:查询计划 XML
有没有更好的方法来编写查询?
编辑 1 - 确保在选择行之前处理前一行的示例:
Where `id` = testrunident and `fs` = filesequence
id | fs | processed
1 | 0 | 1
1 | 1 | 1
1 | 2 | 1
1 | 4 | 0 -- this shouldn't be next as no row with seqeuence = 3 and processed = 1
2 | 0 | 0 --this should be the next row
2 | 1 | 0
Run Code Online (Sandbox Code Playgroud)
仅使用标识id
要返回的行的核心部分,以下查询封装了所需的逻辑:
SELECT TOP (1)
MQ.id
FROM dbo.messagequeue AS MQ
WHERE
-- Current row
MQ.processed = 0
AND MQ.failed = 0
AND
(
EXISTS
(
-- Previous row in strict sequence
SELECT *
FROM dbo.messagequeue AS MQ2
WHERE
MQ2.testrunident = MQ.testrunident
AND MQ2.processed = 1
AND MQ2.failed = 0
AND MQ2.filesequence = MQ.filesequence - 1
)
OR MQ.filesequence = 0
)
ORDER BY
MQ.testrunident ASC,
MQ.filesequence ASC;
Run Code Online (Sandbox Code Playgroud)
有效地执行此查询需要对现有索引进行小的更改,其定义当前为:
CREATE NONCLUSTERED INDEX [messagequeue_sequenc_failed_idx]
ON [dbo].[messagequeue]([processed] ASC, [failed] ASC)
INCLUDE([id], [testrunident], [filesequence]);
Run Code Online (Sandbox Code Playgroud)
这种变化涉及移动testrunident
,并filesequence
从INCLUDE
列表索引键。新索引仍然支持旧索引所做的所有查询,并且作为更改的副作用,重新定义的索引现在可以标记为UNIQUE
。以下脚本将执行此更改(ONLINE
如果您运行的是企业版,则可以执行此操作):
CREATE UNIQUE NONCLUSTERED INDEX [messagequeue_sequenc_failed_idx]
ON [dbo].[messagequeue]
(
[processed] ASC,
[failed] ASC,
testrunident ASC,
filesequence ASC
)
INCLUDE
(
[id]
)
WITH
(
DROP_EXISTING = ON
--, ONLINE = ON
);
Run Code Online (Sandbox Code Playgroud)
有了这个索引,修改后的查询的执行计划是:
要从标识的行返回数据,最终查询是一个简单的扩展:
SELECT
MQ3.id,
MQ3.testrunident,
MQ3.filesequence,
MQ3.processed,
MQ3.filedata,
MQ3.retries,
MQ3.failed,
MQ3.msgobject,
MQ3.xmlsource
FROM dbo.messagequeue AS MQ3
WHERE
MQ3.id =
(
SELECT TOP (1)
MQ.id
FROM dbo.messagequeue AS MQ
WHERE
MQ.processed = 0
AND MQ.failed = 0
AND
(
EXISTS
(
SELECT *
FROM dbo.messagequeue AS MQ2
WHERE
MQ2.testrunident = MQ.testrunident
AND MQ2.filesequence = MQ.filesequence - 1
AND MQ2.processed = 1
AND MQ2.failed = 0
)
OR MQ.filesequence = 0
)
ORDER BY
MQ.testrunident ASC,
MQ.filesequence ASC
);
Run Code Online (Sandbox Code Playgroud)
还有另一种选择,因为您使用的是 SQL Server 2012,它引入了LAG
和LEAD
窗口函数:
SELECT TOP (1)
ML.id
FROM
(
SELECT
M.id,
M.testrunident,
M.filesequence,
M.processed,
M.failed,
PreviousProcessed = LAG(M.processed) OVER (
ORDER BY M.testrunident, M.filesequence),
PreviousFailed = LAG(M.failed) OVER (
ORDER BY M.testrunident, M.filesequence),
PreviousFileSequence = LAG(M.filesequence) OVER (
ORDER BY M.testrunident, M.filesequence)
FROM dbo.messagequeue AS M
) AS ML
WHERE
-- Current row
ML.processed = 0
AND ML.failed = 0
-- Previous row in strict order
AND ML.PreviousProcessed = 1
AND ML.PreviousFailed = 0
AND ML.PreviousFileSequence = ML.filesequence - 1
ORDER BY
ML.testrunident,
ML.filesequence;
Run Code Online (Sandbox Code Playgroud)
此查询还需要对现有索引进行调整,这次是通过添加processed
和failed
包含列:
CREATE UNIQUE NONCLUSTERED INDEX [IXd_testrun_sequence]
ON [dbo].[messagequeue]
(
[testrunident] ASC,
[filesequence] ASC
)
INCLUDE
(
[processed],
[failed]
)
WITH
(
DROP_EXISTING = ON
--, ONLINE = ON
);
Run Code Online (Sandbox Code Playgroud)
有了这个索引,执行计划是:
我还应该提到,如果有多个进程同时在同一个队列上工作,那么您的一般方法将是不安全的。有关此和一般队列表设计的更多信息,请参阅 Remus Rusanu 的优秀文章Using Tables As Queues。
原始执行计划显示 SQL Server 预期处理的行数与执行期间实际遇到的行数之间存在很大差异。使用SQL Sentry Plan Explorer打开计划可以清楚地显示这些差异:
SQL Server 选择了一种执行策略,如果行数真的像它估计的一样小,它就会很好地工作:
不幸的是,当估计结果太低时,所选择的策略不能很好地扩展。部分执行计划实际上执行了 458,260 次 - 不是立竿见影的秘诀。如果 SQL Server 优化器知道真实数字,它可能会选择不同的策略。
人们很容易认为估计行数和实际行数之间的差异一定是由于不准确的统计信息造成的,但您很有可能在这些表上拥有最新的单列自动统计信息。可以通过提供额外的统计信息来进行改进,但在这种情况下,估计不准确的根本原因是完全不同的。
默认情况下,当 SQL Server 为查询选择执行计划时,它假定所有潜在的结果行都将返回给客户端。但是,当 SQL Server 看到TOP
运算符时,它在估计基数时正确地考虑了指定的行数。这种行为称为设置行目标。
从本质上讲,行目标意味着优化器会缩减 Top 运算符下的估计值,以反映所需行数少于结果集的全部潜在大小这一事实。这种缩放隐含地假设感兴趣的值在集合中均匀分布。
例如,假设您有一个包含 1000 行的表,其中 10 行满足某个查询谓词。使用统计信息,SQL Server 知道 1% 的行符合条件。如果编写查询以返回匹配的第一行,SQL Server 假定它需要读取表的 1%(= 10 行)才能找到第一个匹配项。在最坏的情况下,符合条件的 10 行可能会出现在最后(以它们搜索的任何顺序),因此在 SQL Server 遇到您想要的行之前将读取 990 行不匹配。
改进后的查询(使用改进后的索引)在一定程度上仍然存在这个问题,但效果明显不那么明显:
我们在这里对优化器的行目标逻辑提出的基本问题是:在找到第一个(根据 order by 子句规范)按顺序排列、未处理且未标记为具有的行之前,我们需要检查多少行失败的。基于 SQL Server 保留的有限统计数据,这是一个很难回答的问题。
事实上,我们几乎无法将这个复杂问题的所有细微差别有效地传达给优化器。我们能做的最好的事情就是为它提供一个有效的索引,我们希望尽可能早地定位符合条件的行。
在这种情况下,这意味着提供一个索引,以order by
子句顺序返回未处理、未失败的条目。我们希望在找到按顺序排列的第一个之前必须检查相对较少的这些(即前一行存在并且被标记为已处理而不是被标记为失败)。
上面显示的两种解决方案都消除了在原始查询中看到的键查找操作,因为新索引现在包括(覆盖)所有需要的列。此外,新索引可能会更快地找到目标行。原始执行计划IXd_testrun_sequence
按 ( testrunident, filesequence
) 顺序扫描索引,这意味着它会首先遇到旧的测试运行,其中大部分将被标记为已处理。我们在外部查询中寻找未处理的行,因此这种策略效率低下(我们最终对 458,260 行执行序列检查)。
最后,检查特定序列值比查找潜在大集合的最大值要有效得多。这是我在前面的代码中强调的不同之处,即按严格顺序查找前一行。这两个查询与MAX
问题中显示的解决方案 using 之间存在语义差异。我的理解是您对第一个匹配行感兴趣;该行不一定是最高filesequence
的testrunident
。