这个常量扫描和左外连接在一个简单的 SELECT 查询计划中来自哪里?

sha*_*oth 23 sql-server execution-plan azure-sql-database

我有这张桌子:

CREATE TABLE [dbo].[Accounts] (
    [AccountId] UNIQUEIDENTIFIER UNIQUE NOT NULL DEFAULT NEWID(),
    -- WHATEVER other columns
);
GO
CREATE UNIQUE CLUSTERED INDEX [AccountsIndex]
    ON [dbo].[Accounts]([AccountId] ASC);
GO
Run Code Online (Sandbox Code Playgroud)

这个查询:

DECLARE @result UNIQUEIDENTIFIER
SELECT @result = AccountId FROM Accounts WHERE AccountId='guid-here'
Run Code Online (Sandbox Code Playgroud)

使用由单个索引查找组成的查询计划执行 - 正如预期的那样:

SELECT <---- Clustered Index Seek
Run Code Online (Sandbox Code Playgroud)

此查询执行相同的操作:

DECLARE @result UNIQUEIDENTIFIER
SET @result = (SELECT AccountId FROM Accounts WHERE AccountId='guid-here')
Run Code Online (Sandbox Code Playgroud)

但它是按照一个计划执行的,其中索引查找的结果与一些常量扫描的结果左外连接,然后输入计算标量:

SELECT <--- Compute Scalar <--- Left Outer Join <--- Constant Scan
                                      ^
                                      |------Clustered Index Seek
Run Code Online (Sandbox Code Playgroud)

什么是额外的魔法?常量扫描后跟左外连接有什么作用?

Pau*_*ite 29

这两个语句的语义是不同的:

  • 如果没有找到行,第一个不会设置变量的值。
  • 第二个总是设置变量,如果没有找到行,则包括为 null。

常量扫描产生一个空行(没有列!),这将导致变量被更新,以防基表中没有匹配项。左连接确保空行在连接后仍然存在。可以认为变量赋值发生在执行计划的根节点。

使用 SELECT @result

-- Set initial value
DECLARE @result uniqueidentifier = {guid 'FE2CA909-1162-4C6C-A7AC-33B257E28539'};

-- @result does not change
SELECT @result = AccountId 
FROM Accounts 
WHERE AccountId={guid '7AD4D33C-1ED7-4183-B7F3-48C33D666525'};

SELECT @result;
Run Code Online (Sandbox Code Playgroud)

结果 1

使用 SET @result

-- Set initial value
DECLARE @result uniqueidentifier = {guid 'FE2CA909-1162-4C6C-A7AC-33B257E28539'};

-- @result set to null
SET @result = 
(
    SELECT AccountId 
    FROM Accounts 
    WHERE AccountId={guid '7AD4D33C-1ED7-4183-B7F3-48C33D666525'}
);

SELECT @result;
Run Code Online (Sandbox Code Playgroud)

结果 2

执行计划

选择分配没有行到达根节点,因此没有分配发生。

SET 赋值一行总是到达根节点,因此会发生变量赋值。


额外的常量扫描和嵌套循环左外连接无需担心。特别是连接很便宜,因为它保证在其外部输入上遇到一行,而在内部输入上最多遇到一行(在您的示例中)。

还有其他方法可以确保从子查询生成一行以确保发生变量分配。一种是使用冗余标量聚合(没有 group by 子句):

-- Set initial value
DECLARE @result uniqueidentifier = {guid 'FE2CA909-1162-4C6C-A7AC-33B257E28539'};

-- @result set to null
SET @result = 
    (
        SELECT MAX(AccountId)
        FROM Accounts 
        WHERE AccountId={guid '7AD4D33C-1ED7-4183-B7F3-48C33D666525'} 
    );
SELECT @result;
Run Code Online (Sandbox Code Playgroud)

结果 3

标量聚合执行计划

请注意,即使没有收到输入,标量聚合也会生成一行。

文档:

如果 SELECT 语句不返回任何行,则变量保留其当前值。如果表达式是不返回值的标量子查询,则该变量设置为 NULL。

对于分配变量,我们建议您使用 SET @local_variable 而不是 SELECT @local_variable。

进一步阅读: