检索下一个队列项

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)

SQLFiddle 突出显示了这一点

Pau*_*ite 7

仅使用标识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,并filesequenceINCLUDE列表索引键。新索引仍然支持旧索引所做的所有查询,并且作为更改的副作用,重新定义的索引现在可以标记为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,它引入了LAGLEAD窗口函数

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)

此查询还需要对现有索引进行调整,这次是通过添加processedfailed包含列:

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 之间存在语义差异。我的理解是您对第一个匹配行感兴趣;该行不一定是最高filesequencetestrunident

  • 我想补充一下 re:unsafe,SQL Server 中内置了 Service Broker 形式的排队。一旦您完成初始设置,它就会运行得非常好,并且非常稳定并且本机支持多线程。 (2认同)