强制流量不同


19

我有一张这样的桌子:

CREATE TABLE Updates
(
    UpdateId INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
    ObjectId INT NOT NULL
)

本质上跟踪ID不断增加的对象的更新。

该表的使用者将选择一个由100个不同的对象ID组成的块,这些ID UpdateId由一个特定的并从其开始UpdateId。从本质上讲,跟踪它停止的位置,然后查询任何更新。

我发现这是一个有趣的优化问题,因为我只能通过编写恰好由于索引而做我想要做的查询的查询来生成一个最大最优查询计划,但不能保证我想要的:

SELECT DISTINCT TOP 100 ObjectId
FROM Updates
WHERE UpdateId > @fromUpdateId

@fromUpdateId存储过程参数在哪里。

有以下计划:

SELECT <- TOP <- Hash match (flow distinct, 100 rows touched) <- Index seek

由于UpdateId正在使用对索引的查找,因此结果已经不错,并且可以按照我想要的那样从最低更新ID到最高更新ID进行排序。这会生成一个流程明确的计划,这正是我想要的。但是排序显然不能保证行为,所以我不想使用它。

此技巧还导致了相同的查询计划(尽管具有冗余的TOP):

WITH ids AS
(
    SELECT ObjectId
    FROM Updates
    WHERE UpdateId > @fromUpdateId
    ORDER BY UpdateId OFFSET 0 ROWS
)
SELECT DISTINCT TOP 100 ObjectId FROM ids

不过,我不确定(也不确定)这是否真的可以保证订购。

我希望SQL Server能够简化它的一个查询就是这样,但是最终生成了一个非常糟糕的查询计划:

SELECT TOP 100 ObjectId
FROM Updates
WHERE UpdateId > @fromUpdateId
GROUP BY ObjectId
ORDER BY MIN(UpdateId)

有以下计划:

SELECT <- Top N Sort <- Hash Match aggregate (50,000+ rows touched) <- Index Seek

我试图找到一种方法来生成具有索引查找功能的最佳计划,UpdateId并以独特流程删除重复的ObjectIds。有任何想法吗?

如果需要,请提供样本数据。对象很少会有一个以上的更新,并且在一组100行中几乎不应该有一个以上的更新,这就是为什么我追求流程的不同,除非有我不知道的更好的东西?但是,不能保证单个ObjectId表中的行数不会超过100。该表有超过1,000,000行,并且有望快速增长。

假设用户对此有另一种查找合适的next的方法@fromUpdateId。无需在此查询中返回它。

Answers:


15

由于Hash Match Flow Distinct运算符不保留顺序,因此SQL Server优化器无法生成您需要的执行计划。

不过,我不确定(也不确定)这是否真的可以保证订购。

您可能会在许多情况下观察到订单保留,但这是一个实现细节。没有保证,所以您不能依靠它。与往常一样,表示顺序只能由顶层ORDER BY子句来保证。

下面的脚本显示哈希匹配流区分不保留顺序。它将问题表格设置为两列中的匹配数字为1-50,000:

IF OBJECT_ID(N'dbo.Updates', N'U') IS NOT NULL
    DROP TABLE dbo.Updates;
GO
CREATE TABLE Updates
(
    UpdateId INT NOT NULL IDENTITY(1,1),
    ObjectId INT NOT NULL,

    CONSTRAINT PK_Updates_UpdateId PRIMARY KEY (UpdateId)
);
GO
INSERT dbo.Updates (ObjectId)
SELECT TOP (50000)
    ObjectId =
        ROW_NUMBER() OVER (
            ORDER BY C1.[object_id]) 
FROM sys.columns AS C1
CROSS JOIN sys.columns AS C2
ORDER BY
    ObjectId;

测试查询是:

DECLARE @Rows bigint = 50000;

-- Optimized for 1 row, but will be 50,000 when executed
SELECT DISTINCT TOP (@Rows)
    U.ObjectId 
FROM dbo.Updates AS U
WHERE 
    U.UpdateId > 0
OPTION (OPTIMIZE FOR (@Rows = 1));

估计的计划显示出不同的索引查找和流向:

预估计划

输出当然似乎从以下命令开始:

结果开始

...但是进一步下降的值开始“丢失”:

模式分解

...并最终:

混乱爆发

在这种情况下的解释是散列运算符溢出:

执行后计划

一旦分区溢出,散列到同一分区的所有行也会溢出。溢出的分区将在以后进行处理,这打破了预期,遇到的不同值将按接收顺序立即发出。


有很多方法可以编写有效的查询以生成所需的有序结果,例如递归或使用游标。但是,不能使用哈希匹配流Distinct完成此操作。


11

我对这个答案不满意,因为我无法设法获得一个与众不同的运算符以及可以保证是正确的结果。但是,我有一个替代方法,它应该具有良好的性能以及正确的结果。不幸的是,它要求在表上创建非聚集索引。

我通过尝试考虑可以组合的列ORDER BY并在应用DISTINCT它们后获得正确的结果来解决此问题。的最小值UpdateIdObjectId沿着ObjectId就是这样的一个组合。但是,直接要求最小值UpdateId似乎导致读取表中的所有行。相反,我们可以间接要求UpdateId与另一个表联接的最小值。想法是Updates按顺序扫描表,丢弃UpdateId不是该行的最小值的任何行ObjectId,并保留前100行。根据您对数据分布的描述,我们不需要抛出很多行。

为了进行数据准备,我将一百万行放入一个表中,每个不同的ObjectId都有两行:

INSERT INTO Updates WITH (TABLOCK)
SELECT t.RN / 2
FROM 
(
    SELECT TOP 1000000 -1 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) t;

CREATE INDEX IX On Updates (Objectid, UpdateId);

在非聚集索引ObjectidUpdateId是很重要的。它使我们能够有效地抛出没有UpdateIdper 最小值的行Objectid。有多种方法可以编写与上面的描述匹配的查询。这是一种使用方法NOT EXISTS

DECLARE @fromUpdateId INT = 9999;
SELECT ObjectId
FROM (
    SELECT DISTINCT TOP 100 u1.UpdateId, u1.ObjectId
    FROM Updates u1
    WHERE UpdateId > @fromUpdateId
    AND NOT EXISTS (
        SELECT 1
        FROM Updates u2
        WHERE u2.UpdateId > @fromUpdateId
        AND u1.ObjectId = u2.ObjectId
        AND u2.UpdateId < u1.UpdateId
    )
    ORDER BY u1.UpdateId, u1.ObjectId
) t;

这是查询计划的图片:

查询计划

在最佳情况下,SQL Server将只对非聚集索引执行100次索引查找。为了模拟非常不幸的情况,我更改了查询以将前5000行返回给客户端。这样就产生了9999个索引查找,因此就像每个不重复的平均100行ObjectId。这是来自的输出SET STATISTICS IO, TIME ON

表“更新”。扫描计数10000,逻辑读取31900,物理读取0

SQL Server执行时间:CPU时间= 31毫秒,经过的时间= 42毫秒。


9

我喜欢这个问题-Flow Distinct是我最喜欢的运营商之一。

现在,保证是问题所在。当您考虑FD运算符以有序方式从Seek运算符中提取行,并在确定其唯一性时生成每一行时,这将为您提供正确顺序的行。但是很难知道在某些情况下FD一次不处理一行。

从理论上讲,FD可以从Seek请求100行,并按其需要的顺序生成它们。

查询提示OPTION (FAST 1, MAXDOP 1)可能会有所帮助,因为它将避免从Seek运算符获取过多的行。虽然可以保证吗?不完全的。它仍然可以决定一次拉出一行页面,或者类似的事情。

我认为使用时OPTION (FAST 1, MAXDOP 1),您的OFFSET版本可以使您对订单充满信心,但这不是保证。


据我了解,问题在于Flow Distinct运算符使用的哈希表可能会溢出到磁盘。发生溢出时,可以使用仍在RAM中的部分处理可立即处理的行,但直到从磁盘读回溢出的数据后,其他行才被处理。据我所知,由于哈希表的溢出行为,使用哈希表(例如哈希联接)的任何运算符都不能保证保留顺序。
sam.bishop

正确。请参阅保罗·怀特的答案。
罗布·法利
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.