查询存储计划强制失败,NO_PLAN 取决于过滤器运算符在计划中的位置

SEa*_*986 6 sql-server hints execution-plan sql-server-2016 query-store

我有一个查询,我在查询存储中强制执行一个计划(该计划是为此查询编译的 SQL Server)如果我在强制执行该计划后立即运行该查询,NO_PLAN尽管数据库没有发生任何更改,但我会得到last_force_failure_reason_desc。我可以成功地对同一查询强制执行不同的计划

这个问题可以用下图来说明:

创建我们的测试数据库

USE [master]
CREATE DATABASE NO_PLAN
ALTER DATABASE [NO_PLAN] SET QUERY_STORE = ON
ALTER DATABASE [NO_PLAN] SET QUERY_STORE (OPERATION_MODE = READ_WRITE, QUERY_CAPTURE_MODE = ALL)
GO

USE NO_PLAN
GO
IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'MyTableA') DROP TABLE MyTableA
IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'MyTableB') DROP TABLE MyTableB

/* create  our tables */
CREATE TABLE [dbo].[MyTableA](
    [Column1] VARCHAR(50) NULL ,
    [Column2] VARCHAR(255) NULL ,
    [Column3] INT NULL ,
    [Column4] DATETIME NULL ,
    [Column5] INT NULL ,
    [Column6] VARCHAR(50) NULL ,
    [Column7] VARCHAR(255) NULL ,
    [Column8] INT NULL ,
    [Column9] DATETIME NULL ,
    [Column10] INT NULL ,
    [Column11] INT NULL ,
    [Column12] DATETIME NULL ,
    [Column13] VARCHAR(50) NULL ,
    [Column14] VARCHAR(50) NULL ,
    [Column15] DATETIME NULL ,
    [Column16] DATETIME NULL ,
    [Column17] VARCHAR(8) NULL ,
    [Column18] DATETIME NULL ,
    [Column19] INT NULL ,
    [Column20] INT NULL ,
    [Column21] VARCHAR(50) NULL ,
    [Column22] VARCHAR(255) NULL ,
    [Column23] VARCHAR(50) NULL ,
    [Column24] VARCHAR(255) NULL ,
    [Column25] VARCHAR(50) NULL ,
    [Column26] INT NULL ,
    [Column27] INT NULL ,
    [Column28] INT NULL ,
    [Column29] INT NULL ,
    [Column30] INT NULL ,
    [Column31] INT NULL ,
    [Column32] INT NULL ,
    [Column33] INT NULL ,
    [Column34] INT NULL ,
    [Column35] VARCHAR(50) NULL ,
    [Column36] VARCHAR(50) NULL ,
    [Column37] VARCHAR(50) NULL ,
    [Column38] VARCHAR(50) NULL ,
    [Column39] VARCHAR(255) NULL ,
    [Column40] INT NULL ,
    [Column41] VARCHAR(50) NULL ,
    [Column42] INT NULL ,
    [Column43] VARCHAR(255) NULL ,
    [Column44] INT NULL ,
    [Column45] VARCHAR(255) NULL ,
    [Column46] INT NULL ,
    [Column47] DATETIME NULL ,
    [Column48] DATETIME NULL ,
    [Column49] DATETIME NULL ,
    [Column50] INT NULL ,
    [Column51] VARCHAR(50) NULL ,
    [Column52] VARCHAR(255) NULL ,
    [Column53] VARCHAR(50) NULL ,
    [Column54] VARCHAR(255) NULL ,
    [Column55] VARCHAR(50) NULL ,
    [Column56] VARCHAR(255) NULL ,
    [Column57] VARCHAR(50) NULL ,
    [Column58] VARCHAR(50) NULL ,
    [Column59] CHAR NULL ,
    [Column60] CHAR NULL ,
    [Column61] CHAR NULL ,
    [Column62] CHAR NULL ,
    [Column63] CHAR NULL ,
    [Column64] CHAR NULL ,
    [Column65] CHAR NULL ,
    [Column66] CHAR NULL ,
    [Column67] CHAR NULL ,
    [Column68] CHAR NULL ,
    [Column69] CHAR NULL ,
    [Column70] CHAR NULL ,
    [Column71] CHAR NULL ,
    [Column72] CHAR NULL ,
    [Column73] CHAR NULL ,
    [Column74] CHAR NULL ,
    [Column75] CHAR NULL ,
    [Column76] DATETIME NULL ,
    [Column77] INT NULL ,
    [Column78] INT NULL ,
    [Column79] VARCHAR(50) NULL ,
    [Column80] VARCHAR(255) NULL ,
    [Column81] VARCHAR(50) NULL ,
    [Column82] VARCHAR(255) NULL ,
    [Column83] VARCHAR(50) NULL ,
    [Column84] VARCHAR(255) NULL ,
    [Column85] VARCHAR(50) NULL ,
    [Column86] VARCHAR(255) NULL ,
    [Column87] VARCHAR(50) NULL ,
    [Column88] VARCHAR(255) NULL ,
    [Column89] VARCHAR(50) NULL ,
    [Column90] VARCHAR(255) NULL ,
    [Column91] VARCHAR(50) NULL ,
    [Column92] VARCHAR(255) NULL ,
    [Column93] VARCHAR(50) NULL ,
    [Column94] VARCHAR(255) NULL ,
    [Column95] VARCHAR(50) NULL ,
    [Column96] VARCHAR(255) NULL ,
    [Column97] VARCHAR(50) NULL ,
    [Column98] VARCHAR(255) NULL ,
    [Column99] VARCHAR(50) NULL ,
    [Column100] VARCHAR(255) NULL ,
    [Column101] VARCHAR(50) NULL ,
    [Column102] VARCHAR(255) NULL ,
    [Column103] VARCHAR(50) NULL ,
    [Column104] VARCHAR(255) NULL ,
    [Column105] VARCHAR(50) NULL ,
    [Column106] VARCHAR(255) NULL ,
    [Column107] VARCHAR(50) NULL ,
    [Column108] VARCHAR(50) NULL ,
    [Column109] VARCHAR(50) NULL ,
    [Column110] VARCHAR(255) NULL ,
    [Column111] VARCHAR(50) NULL ,
    [Column112] VARCHAR(255) NULL ,
    [Column113] VARCHAR(50) NULL ,
    [Column114] VARCHAR(255) NULL ,
    [Column115] VARCHAR(50) NULL ,
    [Column116] VARCHAR(255) NULL ,
    [Column117] VARCHAR(50) NULL ,
    [Column118] VARCHAR(255) NULL ,
    [Column119] VARCHAR(50) NULL ,
    [Column120] VARCHAR(50) NULL ,
    [Column121] VARCHAR(255) NULL ,
    [Column122] VARCHAR(50) NULL ,
    [Column123] VARCHAR(255) NULL ,
    [Column124] VARCHAR(50) NULL ,
    [Column125] VARCHAR(255) NULL ,
    [Column126] VARCHAR(50) NULL ,
    [Column127] VARCHAR(255) NULL ,
    [Column128] VARCHAR(50) NULL ,
    [Column129] VARCHAR(255) NULL ,
    [Column130] VARCHAR(50) NULL ,
    [Column131] VARCHAR(255) NULL ,
    [Column132] DATETIME NULL ,
    [Column133] VARCHAR(50) NULL ,
    [Column134] VARCHAR(255) NULL ,
    [Column135] VARCHAR(50) NULL ,
    [Column136] INT NULL ,
    [Column137] VARCHAR(50) NULL ,
    [Column138] VARCHAR(255) NULL ,
    [Column139] VARCHAR(50) NULL ,
    [Column140] VARCHAR(255) NULL ,
    [Column141] VARCHAR(50) NULL ,
    [Column142] VARCHAR(255) NULL ,
    [Column143] VARCHAR(50) NULL ,
    [Column144] VARCHAR(255) NULL ,
    [Column145] VARCHAR(50) NULL ,
    [Column146] VARCHAR(255) NULL ,
    [Column147] VARCHAR(50) NULL ,
    [Column148] VARCHAR(255) NULL ,
    [Column149] VARCHAR(50) NULL ,
    [Column150] VARCHAR(255) NULL ,
    [Column151] VARCHAR(50) NULL ,
    [Column152] VARCHAR(255) NULL ,
    [Column153] VARCHAR(50) NULL ,
    [Column154] VARCHAR(255) NULL ,
    [Column155] VARCHAR(50) NULL ,
    [Column156] VARCHAR(255) NULL ,
    [Column157] VARCHAR(50) NULL ,
    [Column158] VARCHAR(255) NULL ,
    [Column159] INT NULL ,
    [Column160] INT NULL ,
    [Column161] VARCHAR(50) NULL ,
    [Column162] VARCHAR(50) NULL ,
    [Column163] VARCHAR(50) NULL ,
    [Column164] VARCHAR(50) NULL ,
    [Column165] VARCHAR(50) NULL ,
    [Column166] VARCHAR(50) NULL ,
    [Column167] VARCHAR(50) NULL ,
    [Column168] VARCHAR(50) NULL ,
    [Column169] VARCHAR(255) NULL ,
    [Column170] INT NULL ,
    [Column171] VARCHAR(50) NULL ,
    [Column172] INT NULL ,
    [Column173] VARCHAR(50) NULL ,
    [Column174] VARCHAR(50) NULL ,
    [Column175] VARCHAR(50) NULL ,
    [Column176] VARCHAR(255) NULL ,
    [Column177] VARCHAR(50) NULL ,
    [Column178] VARCHAR(255) NULL ,
    [Column179] VARCHAR(50) NULL ,
    [Column180] VARCHAR(50) NULL ,
    [Column181] VARCHAR(50) NULL ,
    [Column182] VARCHAR(255) NULL ,
    [Column183] VARCHAR(50) NULL ,
    [Column184] VARCHAR(255) NULL ,
    [Column185] VARCHAR(50) NULL ,
    [Column186] VARCHAR(255) NULL ,
    [Column187] VARCHAR(50) NULL ,
    [Column188] VARCHAR(255) NULL ,
    [Column189] VARCHAR(50) NULL ,
    [Column190] VARCHAR(50) NULL ,
    [Column191] VARCHAR(50) NULL ,
    [Column192] VARCHAR(255) NULL ,
    [Column193] VARCHAR(50) NULL ,
    [Column194] VARCHAR(255) NULL ,
    [Column195] VARCHAR(50) NULL ,
    [Column196] VARCHAR(50) NULL ,
    [Column197] VARCHAR(255) NULL ,
    [Column198] INT IDENTITY (1,1) ,
    [Column199] VARCHAR(500) NULL ,
    [Column200] VARCHAR(255) NULL ,
    [Column201] VARCHAR(50) NULL ,
    [Column202] VARCHAR(255) NULL ,
    [Column203] CHAR NULL ,
    [Column204] CHAR NULL ,
    [Column205] VARCHAR(50) NULL ,
    [Column206] VARCHAR(255) NULL ,
    [Column207] BIGINT NULL ,
    [Column208] VARCHAR(50) NULL ,
    [Column209] VARCHAR(50) NULL ,
    [Column210] VARCHAR(50) NULL ,
    [Column211] VARCHAR(255) NULL ,
    [Column212] VARCHAR(50) NULL ,
    [Column213] VARCHAR(255) NULL ,
    [Column214] VARCHAR(50) NULL ,
    [Column215] VARCHAR(50) NULL ,
    [Column216] VARCHAR(50) NULL ,
    [Column217] VARCHAR(50) NULL ,
    [Column218] VARCHAR(50) NULL ,
    [Column219] VARCHAR(50) NULL ,
    [Column220] VARCHAR(50) NULL ,
    [Column221] VARCHAR(50) NULL ,
    [Column222] DATETIME NULL ,
    [Column223] VARCHAR(50) NULL ,
    [Column224] VARCHAR(50) NULL ,
    [Column225] CHAR NULL ,
    [Column226] CHAR NULL ,
    [Column227] CHAR NULL ,
    [Column228] CHAR NULL ,
    [Column229] CHAR NULL ,
    [Column230] CHAR NULL ,
    [Column231] VARCHAR(50) NULL ,
    [Column232] VARCHAR(50) NULL ,
    [Column233] VARCHAR(50) NULL ,
    [Column234] VARCHAR(255) NULL ,
    [Column235] VARCHAR(50) NULL ,
    [Column236] VARCHAR(50) NULL ,
    [Column237] VARCHAR(255) NULL ,
    [Column238] VARCHAR(50) NULL ,
    [Column239] VARCHAR(255) NULL ,
    [Column240] VARCHAR(50) NULL ,
    [Column241] VARCHAR(255) NULL ,
    [Column242] CHAR NULL ,
    [Column243] CHAR NULL ,
    [Column244] DATE NULL ,
    [Column245] DATE NULL ,
    [Column246] DATE NULL ,
    [Column247] VARCHAR(50) NULL ,
    [Column248] VARCHAR(255) NULL ,
    [Column249] VARCHAR(50) NULL ,
    [Column250] VARCHAR(255) NULL ,
    [Column251] DATE NULL ,
    [Column252] DATE NULL ,
    CONSTRAINT [PKC_MyTableA] PRIMARY KEY CLUSTERED 
    (
        [Column198] ASC
    )
)
GO

CREATE TABLE [dbo].[MyTableB]
(
    Column1 [INT] IDENTITY(1,1) NOT NULL,
    Column2 [INT] NULL,
    Column3 [VARCHAR](255) NOT NULL,
    Column4 [VARCHAR](255) NULL,
    Column5 [CHAR](1) NOT NULL,
    Column6 [VARCHAR](MAX) NULL,
    Column7 [VARCHAR](50) NULL,
    CONSTRAINT [PK_MyTableB] PRIMARY KEY CLUSTERED 
    (
        Column3 ASC
    )
)
GO
Run Code Online (Sandbox Code Playgroud)

插入一些虚拟数据:

DECLARE @valsSQL NVARCHAR(MAX) = 'SET IDENTITY_INSERT MyTableA ON; 

INSERT INTO [MyTableA] (' 

SELECT  @valsSQL += c.name + ','
FROM    sys.columns c
        JOIN sys.tables t
            ON c.object_id = t.object_id
WHERE   t.name = 'MyTableA'
ORDER BY column_id

SET @valsSQL = STUFF(@valsSQL,LEN(@valsSQL),1,')')

SET @valsSQL += ' VALUES ( '

SELECT  @valsSql +=
        CASE
            WHEN c.system_type_id = 167 OR --varchar 
                    c.system_type_id = 175 -- char
            THEN '''' +  REPLICATE('a',c.max_length) + ''''
            WHEN c.system_type_id = 61
            THEN '''' +  CONVERT(NVARCHAR,GETDATE(),120) + ''''
            WHEN c.system_type_id = 56 OR --int OR
                c.system_type_id = 47 OR -- bigint
                c.system_type_id = 127
            THEN CONVERT(NVARCHAR(10),CONVERT(INT,FLOOR(RAND()*2147483647)))
            WHEN c.system_type_id = 40
            THEN '''' +  '1900-01-01' + ''''
        END + ','
FROM    sys.columns c
        JOIN sys.types t
            ON c.system_type_id = t.system_type_id
WHERE   OBJECT_NAME(object_id) = 'MyTableA'
ORDER BY column_id

SET @valsSQL = STUFF(@valsSQL,LEN(@valsSQL),1,')')
SET @valsSQL += '; SET IDENTITY_INSERT MyTableA OFF;'

EXEC sp_executesql @stmt = @valsSQL 
GO 500
Run Code Online (Sandbox Code Playgroud)

现在数据库已设置完毕,运行查询:

USE NO_PLAN
SELECT  1
        -- my unique text to find this query in query store views
FROM    MyTableA 
        INNER JOIN MyTableB Alias  
            ON Alias.Column3 = 'value'
        LEFT JOIN MyTableB  
            ON MyTableB.Column3 =  'value'
WHERE   MyTableB.Column4 IS NULL
Run Code Online (Sandbox Code Playgroud)

注意 - 实际的执行计划在这里

使用查询存储 DMV 获取查询 id 和计划 id,以便我们可以强制执行计划:

SELECT  t.query_sql_text,
        q.query_id,
        p.plan_id,
        p.query_plan,
        p.is_forced_plan,
        p.last_force_failure_reason_desc,
        p.last_execution_time 
 FROM    sys.query_store_plan p
        JOIN sys.query_store_query q
            ON q.query_id = p.query_id
        JOIN sys.query_store_query_text t
            ON t.query_text_id = q.query_text_id 
 WHERE   t.query_sql_text LIKE '%-- my unique text to find this query in query store views%' AND
        t.query_sql_text NOT LIKE '%sys.query_store_plan%' /* exclude this query */
Run Code Online (Sandbox Code Playgroud)

我的输出如下:

在此输入图像描述

现在强制 SQL Server 在每次运行此查询时使用它刚刚编译的计划

EXEC sp_query_store_force_plan @query_id = 6, @plan_id = 6
Run Code Online (Sandbox Code Playgroud)

再次运行查询:

USE NO_PLAN
SELECT  1
        -- my unique text to find this query in query store views
FROM    MyTableA 
        INNER JOIN MyTableB Alias  
            ON Alias.Column3 = 'value'
        LEFT JOIN MyTableB  
            ON MyTableB.Column3 =  'value'
WHERE   MyTableB.Column4 IS NULL
Run Code Online (Sandbox Code Playgroud)

检查查询存储 DMV 以查看它是否使用了该计划:

SELECT  t.query_sql_text,
        q.query_id,
        p.plan_id,
        p.query_plan,
        p.is_forced_plan,
        p.last_force_failure_reason_desc,
        p.last_execution_time
FROM    sys.query_store_plan p
        JOIN sys.query_store_query q
            ON q.query_id = p.query_id
        JOIN sys.query_store_query_text t
            ON t.query_text_id = q.query_text_id
WHERE   t.query_sql_text LIKE '%-- my unique text to find this query in query store views%' AND
        t.query_sql_text NOT LIKE '%sys.query_store_plan%' /* exclude this query */
Run Code Online (Sandbox Code Playgroud)

我们可以看到NO_PLAN的失败原因:

在此输入图像描述

如果我通过截断表、清除查询存储然后仅向表中添加 20 行(或者删除数据库并运行上述所有设置但使用而GO 20不是GO 500)来重置内容:

USE NO_PLAN;
ALTER DATABASE NO_PLAN SET QUERY_STORE CLEAR;
TRUNCATE TABLE [MyTableA];

DECLARE @valsSQL NVARCHAR(MAX) = 'SET IDENTITY_INSERT MyTableA ON; 

INSERT INTO [MyTableA] (' 

SELECT  @valsSQL += c.name + ','
FROM    sys.columns c
        JOIN sys.tables t
            ON c.object_id = t.object_id
WHERE   t.name = 'MyTableA'
ORDER BY column_id

SET @valsSQL = STUFF(@valsSQL,LEN(@valsSQL),1,')')

SET @valsSQL += ' VALUES ( '

SELECT  @valsSql +=
        CASE
            WHEN c.system_type_id = 167 OR --varchar 
                    c.system_type_id = 175 -- char
            THEN '''' +  REPLICATE('a',c.max_length) + ''''
            WHEN c.system_type_id = 61
            THEN '''' +  CONVERT(NVARCHAR,GETDATE(),120) + ''''
            WHEN c.system_type_id = 56 OR --int OR
                c.system_type_id = 47 OR -- bigint
                c.system_type_id = 127
            THEN CONVERT(NVARCHAR(10),CONVERT(INT,FLOOR(RAND()*2147483647)))
            WHEN c.system_type_id = 40
            THEN '''' +  '1900-01-01' + ''''
        END + ','
FROM    sys.columns c
        JOIN sys.types t
            ON c.system_type_id = t.system_type_id
WHERE   OBJECT_NAME(object_id) = 'MyTableA'
ORDER BY column_id

SET @valsSQL = STUFF(@valsSQL,LEN(@valsSQL),1,')')
SET @valsSQL += '; SET IDENTITY_INSERT MyTableA OFF;'

EXEC sp_executesql @stmt = @valsSQL 
GO 20
Run Code Online (Sandbox Code Playgroud)

然后再次运行查询,我得到了不同的计划(注意过滤器运算符的位置已更改)

如果我随后重复获取 query_id 和 plan_id 的过程,强制执行计划并重新运行查询,这次它将强制执行计划:

在此输入图像描述

我可以确认 NO_PLAN 计划不能通过提示强制执行OPTION (RECOMPILE, USE PLAN N'<planxmlhere>'),我明白了

消息 8698,级别 16,状态 0,第 5 行 查询处理器无法生成查询计划,因为 USE PLAN 提示包含无法验证为查询合法的计划。删除或替换使用计划提示。为了获得计划强制成功的最佳可能性,请验证 USE PLAN 提示中提供的计划是否是 SQL Server 针对同一查询自动生成的计划。

许多文章表明 NO_PLAN 失败的原因是由于更改了索引,但是,从上面的示例可以看出,在强制执行查询和第二次运行查询之间没有任何变化。

A条

乙条

为什么 SQL Server 在没有任何改变的情况下不能被迫使用它刚刚生成的计划?第一个计划是什么导致强制失败,为什么这对第二个计划来说不是问题?

Pau*_*ite 8

并非 SQL Server 可以生成的每个计划都能够被强制,如错误消息所示(已添加强调):

消息 8698,级别 16,状态 0,行 xxx
查询处理器无法生成查询计划,因为 USE PLAN 提示包含无法验证为查询合法的计划。删除或替换使用计划提示。为了获得计划强制成功的最佳可能性,请验证 USE PLAN 提示中提供的计划是否是 SQL Server 针对同一查询自动生成的计划。

这是由所提供的 xml引导计划搜索方式的结果。SQL Server 使用该指南来选择可能产生所提供的运算符和属性的转换规则。当一切顺利时,将生成一个与所提供的 xml 表示具有相同主要功能的计划,尽管它可能在次要细节上有所不同,例如过滤器和计算标量放置:

此功能强制生成的执行计划将与强制执行的计划相同或相似。由于最终的计划可能与计划指南指定的计划不同,因此计划的性能可能会有所不同。在极少数情况下,性能差异可能显着且为负;在这种情况下,管理员必须删除强制计划。

SQL Server 仍然会经历与查找原始计划非常相似的过程。这是一个逐步将转换和替换应用于原始逻辑树表示的过程,同时考虑到暗示的计划形状。在这些步骤中,很多事情都可能出错,这意味着 SQL Server 最终不会接近预期的终点。

以下是引导式搜索的总体概述:

引导式搜索流程图

我提到这一切是因为人们通常不理解 xml 是内部可执行计划的表示,而不是计划本身。SQL Server 无法直接将 xml 转换为所有正确的内部结构。它必须使用 xml 作为粗略指南来经历搜索过程。

我想说的一点是,无论文档如何规定,计划强制并不是一个具有最小(且完整记录的)故障模式的精确过程。最初的计划指南并未得到广泛使用,失败很容易被解释(或挥手)。查询存储及其计划指导版本的日益普及,正在提高此功能的总体体验水平。

随着语句依赖于查询优化器转换的更复杂的交互,失败的可能性会增加。您的示例查询很好地说明了这一点,因为它在外连接/连接切换上采用了复杂的选择:

SelOJJoinSwitch - Sel((A JN B) OJ C) -> (Sel(A OJ C)) JN B
Run Code Online (Sandbox Code Playgroud)

这种复杂的重写可能(总是)不能很好地与引导搜索配合使用,因为它通常不会考虑已经指出的过滤器位置。

在只有 20 行的第二个示例中,由于预期计划成本较低,优化器的计划搜索在搜索 0 (事务处理)阶段后结束。搜索 0 不允许SelOJJoinSwitch规则。由于不运行该探索,过滤器(选择)在计划中的放置有所不同。

如果使用未记录的跟踪标志 8750 禁用搜索 0,您将获得与 20 行测试相同的不可强制计划。

原始 SQL 也是查询要求的一种奇怪的表达方式,交叉联接伪装成内部联接,并在其子句中对一个表进行选择ON


可以通过以下等效重写来强制提供的计划

-- Rewrite 1
SELECT 1 
FROM dbo.MyTableA, dbo.MyTableB AS Alias
LEFT JOIN dbo.MyTableB ON MyTableB.Column3 = 'value'
WHERE dbo.MyTableB.Column4 IS NULL

-- Rewrite 2
SELECT 1 
FROM dbo.MyTableA 
CROSS JOIN 
( 
    dbo.MyTableB AS Alias
    LEFT JOIN dbo.MyTableB
        ON MyTableB.Column3 = 'value'
)
WHERE dbo.MyTableB.Column4 IS NULL

-- Rewrite 3
SELECT 1
FROM MyTableA 
JOIN 
(
    MyTableB Alias              
    LEFT JOIN MyTableB ON MyTableB.Column3 =  'value'
) ON Alias.Column3 = 'value'
WHERE
    MyTableB.Column4 IS NULL
Run Code Online (Sandbox Code Playgroud)

强制计划示例

我使用了一个USE PLAN提示,但是任何这些重写都会生成相同的计划,可以使用查询存储强制执行。

由于绑定和优先规则,括号在CROSS JOIN和变体中很重要。INNER JOIN从语句文本派生的初始逻辑树需要从使最终计划可达的点开始。这三个是此类起点的示例,而您的原创则不是。

SQL Server 2022包含有限的优化器规则重播功能,因此计划指导的可靠性可能会提高。这可能会或可能不会优于基于计划的 xml 表示中发现的功能的指导。

最后,像这样的指导失败总是有可能是由于产品缺陷造成的。您需要联系 Microsoft 以获得明确的答案(尽管您仍然可能得不到)。如果是缺陷,很可能涉及SelOJJoinSwitch规则。

最小的例子

以下重现了该问题:

DECLARE @A table (c1 integer NULL);
DECLARE @B table (c1 integer NULL);
DECLARE @C table (c1 integer NULL);

SELECT (SELECT 1)
FROM @A AS A
CROSS JOIN @B AS B
LEFT JOIN @C AS C
    ON C.c1 = B.c1
WHERE C.c1 IS NULL;
Run Code Online (Sandbox Code Playgroud)

这将产生一个使用提示无法强制执行的计划USE PLAN。用常量 1替换子查询会生成在搜索 0(SELECT 1)阶段生成的强制计划。或者,禁用该规则也会产生一个强制计划。OPTION (QUERYRULEOFF SelOJJoinSwitch)

上面的重写也适用于最小的例子:

DECLARE @A table (c1 integer NULL);
DECLARE @B table (c1 integer NULL);
DECLARE @C table (c1 integer NULL);

SELECT (SELECT 1)
FROM @A AS A, @B AS B -- Changed cross join syntax
LEFT JOIN @C AS C
    ON C.c1 = B.c1
WHERE C.c1 IS NULL;
Run Code Online (Sandbox Code Playgroud)

这会产生一个具有相同结构的强制计划,而不涉及SelOJJoinSwitch

强制最小示例重写