CTE 与 UNION ALL 未按预期工作

Slo*_*gic 0 sql-server cte union sql-server-2017

下面的查询看似简单明了,但却产生了意想不到的结果。


CREATE TABLE #NUMBERS
(
    N BIGINT
);

INSERT INTO #NUMBERS VALUES
(1),
(2),
(3),
(4),
(5),
(6),
(7),
(8),
(9)
;



WITH
A AS
(   
    -- CHOOSE A ROW AT RANDOM
    SELECT   TOP 1 *
    FROM     #NUMBERS            
    ORDER BY NewID()           
),
B AS
(
    SELECT A.N AS QUANTITY, 'METERS' AS UNIT FROM A
    
    UNION ALL

    SELECT A.N*100 AS QUANTITY, 'CENTIMETERS' AS UNIT FROM A
    
    UNION ALL

    SELECT A.N*1000 AS QUANTITY, 'MILLIMETERS' AS UNIT FROM A
    
    UNION ALL

    SELECT A.N*1000000 AS QUANTITY, 'MICRONS' AS UNIT FROM A

    UNION ALL

    SELECT A.N*1000000000 AS QUANTITY, 'NANOMETERS' AS UNIT FROM A
)
SELECT   *
FROM     B
ORDER BY B.QUANTITY
;
Run Code Online (Sandbox Code Playgroud)

我希望它执行 CTE A 一次,然后将这些结果带入 CTE B 以产生如下结果:

数量 单元
4
400 厘米
4000 毫米
4000000 微米
4000000000 纳米

但是,它会产生如下结果:

数量 单元
8
700 厘米
1000 毫米
6000000 微米
3000000000 纳米

这意味着它会返回并执行 CTE A 五次,每次在 CTE B 中提及 A 一次。这不仅是不需要的且不直观的,而且看起来效率也不必要地低。

这是怎么回事?CTE 天才将如何重写它以产生所需的结果?


顺便说一句,关于 CTE 的 Microsoft 文档页面包含以下可能相关或可能不相关的神秘声明:

如果定义了多个 CTE_query_definition,则查询定义必须由以下集合运算符之一连接:UNION ALL、UNION、EXCEPT 或 INTERSECT。


最后,重写查询以消除 CTE B 没有帮助:

WITH
A AS
(   
    -- CHOOSE A ROW AT RANDOM
    SELECT   TOP 1 *
    FROM     #NUMBERS            
    ORDER BY NewID()           
)
SELECT   *
FROM     (
          SELECT A.N AS QUANTITY, 'METERS' AS UNIT FROM A
    
          UNION ALL

          SELECT A.N*100 AS QUANTITY, 'CENTIMETERS' AS UNIT FROM A
    
          UNION ALL

          SELECT A.N*1000 AS QUANTITY, 'MILLIMETERS' AS UNIT FROM A
    
          UNION ALL

          SELECT A.N*1000000 AS QUANTITY, 'MICRONS' AS UNIT FROM A

          UNION ALL

          SELECT A.N*1000000000 AS QUANTITY, 'NANOMETERS' AS UNIT FROM A

         ) AS B
ORDER BY B.QUANTITY
;
Run Code Online (Sandbox Code Playgroud)

Eri*_*ing 7

将公用表表达式想象成更像表达式而不像(永久)表是有帮助的。每次引用公共表表达式时,它都必须重新表达自身。

这是一个简单的例子:

DECLARE
    @t table(id int);

INSERT 
    @t
(
    id
)
SELECT
    id = 1

SET STATISTICS XML ON;

WITH
    t AS
(
    SELECT
        t.id
    FROM @t AS t
)
SELECT
    t.*
FROM t 
JOIN t AS t1
  ON t1.id = t.id
JOIN t AS t2
  ON t2.id = t.id;
Run Code Online (Sandbox Code Playgroud)

查询计划将如下所示,对于公共表表达式之间的每个连接,都连接到基表变量:

坚果

同样,UNION (ALL) 每次也会生成一个引用:

WITH
    t AS
(
    SELECT
        t.id
    FROM @t AS t
)
SELECT
    t.*
FROM t 
UNION ALL
SELECT
    t.*
FROM @t AS t
UNION ALL
SELECT
    t.*
FROM @t AS t;
Run Code Online (Sandbox Code Playgroud)

坚果

如果需要稳定结果,则需要使用:

  • #temp桌子
  • @table多变的
  • 常驻表


And*_*y M 7

其他答案已经解释了问题发生的原因:基本上,CTE 只是一个表达式,其计算次数与引用次数相同,因此导致A每次计算时返回不同的值。

我想在回答中解决的是问题的这一部分:

CTE 天才将如何重写它以产生期望的结果?

在一些 CTE 天才聚集讨论 CTE 相关业务的地方闲逛可能教会了我一些我想分享的技巧。

我认为对于解决当前的问题非常有用的是两件事:

  • 运营商CROSS APPLY;
  • 行构造函数VALUES

使用这两个,我将专门重写BCTE,如下所示:

B AS
(
    SELECT   X.*
    FROM     A
    CROSS APPLY
    (
        VALUES
        (A.N, 'METERS'),
        (A.N*100, 'CENTIMETERS'),
        (A.N*1000, 'MILLIMETERS'),
        (A.N*1000000, 'MICRONS'),
        (A.N*1000000000, 'NANOMETERS')
    ) AS X (QUANTITY, UNIT)
)
Run Code Online (Sandbox Code Playgroud)

保留查询的其余部分不变。

该方式B在上面定义,A仅被引用(和评估)一次。它仍然生成一个行集而不是单个行,因为它用行集替换(在 的帮助下CROSS APPLY)返回的行,并且行集(由 构造)本质上将其作为参数,生成所需的值集。AVALUESA.N

您可以在dbfiddle.uk测试完整查询。