为什么这个 CTE 会立即返回大部分结果,但需要几分钟才能完成?

JAF*_*JAF 5 performance sql-server-2008 sql-server recursive query-performance

我有一个大致像这样的架构(这是我实际架构的简化):

CREATE TABLE foo (
    key1 NUMERIC(6) NOT NULL,
    key2 VARCHAR(32) NOT NULL,
    val VARCHAR(255) NULL,
    CONSTRAINT foo_pk PRIMARY KEY (key1, key2) --PK on key1, key2
    )
GO
CREATE TABLE bar (
    key1 NUMERIC(6) NOT NULL,
    key2 VARCHAR(32) NOT NULL,
    val VARCHAR(255) NULL,
    CONSTRAINT bar_pk PRIMARY KEY (key1, key2) --PK on key1, key2
    )
GO
CREATE TABLE aliases (
    id VARCHAR(32) NOT NULL PRIMARY KEY,
    text VARCHAR(255) NOT NULL,
    CONSTRAINT aliases_uk UNIQUE (text) --PK on id, unique constraint on text
    )
GO

INSERT INTO aliases (id, text)
SELECT REPLACE(NEWID(), '-', ''), 'Text1' UNION ALL
SELECT REPLACE(NEWID(), '-', ''), 'Text2' UNION ALL
SELECT REPLACE(NEWID(), '-', ''), 'Text3' UNION ALL
SELECT REPLACE(NEWID(), '-', ''), 'Text4' UNION ALL
SELECT REPLACE(NEWID(), '-', ''), 'Text5' UNION ALL
SELECT REPLACE(NEWID(), '-', ''), 'Text6' UNION ALL
SELECT REPLACE(NEWID(), '-', ''), 'Text7' UNION ALL
SELECT REPLACE(NEWID(), '-', ''), 'Text8' UNION ALL
SELECT REPLACE(NEWID(), '-', ''), 'Text9' UNION ALL
SELECT REPLACE(NEWID(), '-', ''), 'Text10' UNION ALL
SELECT REPLACE(NEWID(), '-', ''), 'Text11' UNION ALL
SELECT REPLACE(NEWID(), '-', ''), 'Text12' UNION ALL
SELECT REPLACE(NEWID(), '-', ''), 'Text13' UNION ALL
SELECT REPLACE(NEWID(), '-', ''), 'Text14' UNION ALL
SELECT REPLACE(NEWID(), '-', ''), 'Text15' UNION ALL
SELECT REPLACE(NEWID(), '-', ''), 'Text16';
GO

BEGIN
    DECLARE @i INT = 1;
    WHILE (@i <= 234)
    BEGIN
        INSERT INTO foo (key1, key2, val) SELECT @i, id, id FROM aliases;
        SET @i = @i + 1;
    END;
    INSERT INTO bar (key1, key2, val) SELECT key1 + 234, key2, val FROM foo;
END;
GO
Run Code Online (Sandbox Code Playgroud)

我想获得所有key1key2对的列表(使用 的用户友好名称key2,将vals的用户友好别名连接起来。我最初的尝试如下所示:

WITH
foos_and_bars (key1, key2, val) AS (
    SELECT key1, key2, val FROM foo UNION ALL
    SELECT key1, key2, val FROM bar),
texts (key1, key2_name, val_text, rnum) AS (
    SELECT key1, a1.text, a2.text, ROW_NUMBER() OVER (PARTITION BY key1, key2 ORDER BY val)
    FROM foos_and_bars
    JOIN aliases a1 ON a1.id = foos_and_bars.key2
    LEFT JOIN aliases a2 ON a2.id = foos_and_bars.val),
partitioned (key1, key2_name, val_text, rnum, maxnum) AS (
    SELECT key1, key2_name, val_text, rnum, MAX(rnum) OVER (PARTITION BY key1, key2_name)
    FROM texts),
recurse (key1, key2_name, val_texts, rnum, maxnum) AS (
    SELECT key1, key2_name, val_text, rnum, maxnum
    FROM partitioned
    WHERE rnum = 1
    UNION ALL
    SELECT r.key1,
        r.key2_name, 
        CAST(r.val_texts + CHAR(13) + CHAR(10) + p.val_text AS VARCHAR(255)),
        p.rnum,
        r.maxnum
    FROM recurse r
    JOIN partitioned p
    ON p.key1 = r.key1
    AND p.key2_name = r.key2_name
    AND p.rnum = r.rnum + 1)
SELECT * FROM recurse WHERE rnum = maxnum;
Run Code Online (Sandbox Code Playgroud)

它有效,我在不到一秒的时间内获得了 7480 行,但随后 SSMS 运行了不少于 2 分 40 秒,让最后 8 行感到困惑。最后几行并不比第一行复杂(事实上,当前数据在系统中,rnum并且maxnum永远不会大于 1),并且由于这些表是专门为模拟这个 SE 问题而创建的,因此不应该有锁。

该问题似乎仅限于递归 CTE,因为从上到下进行选择以partitioned在不到一秒的时间内生成 7488 行。CTE 会卡在什么地方?

执行计划(使用与简化模式匹配的设置):

输出将进入一个视图,该视图将支持 Sybase 窗口,该窗口将用于搜索记录。因此,它需要能够检索所有行,尽管默认情况下程序一次提取 1000 行。

Joe*_*ish 6

递归 SQL 查询的锚点部分生成所有 7488 行。在我的机器上,查询的那部分在 100 毫秒内完成。SSMS 不会立即在网格结果中显示所有 7488 行。它也只对我显示 7480。我怀疑发生这种情况是因为结果以数据包形式发送,其余 8 行不足以填充数据包。

与第一部分相比,查询的后半部分非常昂贵。查看执行计划,您可以在嵌套循环的内侧看到六个聚集索引扫描。这些扫描将对锚点部分的每一行至少执行一次。SQL Server 在几分钟内总共进行了 44928 次扫描,最终从该部分生成 0 行。一旦该部分执行完成,SQL Server 将剩余的行数据包发送到客户端,您将在已完成查询的结果网格中看到所有 7488 行。找到最后8个八行需要三分钟的时间是不正确的。它几乎可以立即找到它们。发送它们只需要三分钟。

通过查看实际计划,您可以看到在查询的下半部分执行的大量工作。请注意箭头的粗细和扫描运算符的执行次数:

实际计划

这就是查询缓慢的原因。我没有任何建议让您加快速度,因为我不明白您要通过此查询完成什么。