更改跟踪 SQL Server 2016 中的错误计划

Jos*_*ama 5 performance sql-server sql-server-2016

是否有其他人在调用CHANGETABLESQL Server 2016时遇到过糟糕的计划?

我有一个使用更改跟踪来更新缓存的应用程序。应用服务器每秒获取多个表的更改。在CHANGETABLE如此频繁,通常仅返回几行调用。这在 2008 R2 和 2012 中效果很好。当我测试 2016 时,我发现 CPU 飙升,并发现该计划正在扫描更改跟踪内部表,它曾经在那里进行搜索。

如果您想在此处查看此行为,请执行复制它的步骤:

  1. 创建数据库并设置更改跟踪。
  2. 创建表:

    CREATE TABLE [dbo].[ChangeTable_Test](
    [Id] [bigint] IDENTITY(1,1) NOT NULL,
    [Name] [varchar](50) NOT NULL,
    CONSTRAINT [PK_Test_ChangeTable_Id] PRIMARY KEY CLUSTERED 
    (
         [Id] ASC
     )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
    ) ON [PRIMARY]
    GO
    ALTER TABLE [dbo].[ChangeTable_Test] ENABLE CHANGE_TRACKING 
    GO    
    INSERT INTO ChangeTable_Test (Name) VALUES ('Test')
    GO 1000
    
    Run Code Online (Sandbox Code Playgroud)
  3. 包括实际的执行计划并运行以下命令。

    SET STATISTICS IO ON;
    SET NOCOUNT OFF;
    ALTER DATABASE SCOPED CONFIGURATION SET QUERY_OPTIMIZER_HOTFIXES = OFF;
    GO
    ALTER DATABASE [Test] SET COMPATIBILITY_LEVEL = 110
    GO
    DECLARE @CurrentVersion INT;
    SELECT  @CurrentVersion=CHANGE_TRACKING_CURRENT_VERSION ()-1;
    SELECT * FROM CHANGETABLE(CHANGES ChangeTable_Test,@CurrentVersion) AS C
    GO
    ALTER DATABASE [Test] SET COMPATIBILITY_LEVEL = 130
    GO
    DECLARE @CurrentVersion INT;
    SELECT  @CurrentVersion=CHANGE_TRACKING_CURRENT_VERSION ()-1;
    SELECT * FROM CHANGETABLE(CHANGES ChangeTable_Test,@CurrentVersion) AS C
    
    Run Code Online (Sandbox Code Playgroud)

上面的演示在 SQL Server 2016 SP1-CU4 上对我有用。我上传了两个计划粘贴计划。第一个在 COMPAT 110 下,第二个在 130 下。

Microsoft 支持人员承认该问题,但不打算修复它。他们建议使用USE PLAN提示或计划指南的解决方法。但这会使该产品成为维护噩梦。

有趣的是,2014 年和 2017 年没有选择糟糕的计划,他们使用新的 Cardinality Estimator。

除了我提到的那些,你还有什么建议吗?

Joe*_*ish 5

根据您的问题,听起来您有一个高度并发的应用程序,它CHANGETABLE在许多不同的查询中调用各种表。强制每个查询计划具有带有USE PLAN提示或计划指南的特定形状将是一项艰巨的工作。由于其他原因,将活动数据库更改为兼容性级别为 110 而不进行查询修复可能是一个糟糕的选择。

这个答案中的建议可能很难实施,但感觉比上面列出的建议更好。这个想法是用将CHANGETABLE数据插入CHANGETABLE临时表的存储过程替换对的调用。需要更改数据的查询可以使用填充的临时表而不是CHANGETABLE直接使用。您的应用程序代码可能如下所示:

DECLARE @CurrentVersion INT = CHANGE_TRACKING_CURRENT_VERSION () - 1;

CREATE TABLE #changes (SYS_CHANGE_VERSION bigint NULL);

EXEC dbo.CHANGETABLE_API '#changes', 'TEST', 'dbo', 'ChangeTable_Test', @CurrentVersion;

-- run queries that need #changes temp table

DROP TABLE #changes;
Run Code Online (Sandbox Code Playgroud)

解决范围问题需要一些技巧。存储过程由三部分组成:

  1. 将静态列添加到临时表:SYS_CHANGE_CREATION_VERSIONSYS_CHANGE_OPERATIONSYS_CHANGE_COLUMNSSYS_CHANGE_CONTEXT
  2. 将动态主键列添加到临时表。您可以sys.columns为此使用其他 dmv。
  3. CHANGETABLE从具有 2012 兼容性级别的空数据库上下文以及获得良好性能所需的任何其他设置插入临时表。

下面是上述大部分内容的快速而肮脏的实现。它没有任何类型的错误检查或针对 SQL 注入攻击的保护。随意使用代码做任何你想做的事情:

CREATE OR ALTER PROCEDURE dbo.CHANGETABLE_API (
    @temp_table_name SYSNAME,
    @changes_database_name SYSNAME,
    @changes_schema_name SYSNAME,
    @changes_table_name SYSNAME,
    @last_sync_version BIGINT
)
AS
BEGIN
    DECLARE @sql_to_add_static_cols NVARCHAR(4000),
    @sql_to_add_table_cols NVARCHAR(4000),
    @sql_to_insert_rows NVARCHAR(4000);

    -- okay to hardcode these depending on how you call CHANGETABLE
    SET @sql_to_add_static_cols = N'ALTER TABLE '
    + QUOTENAME(@temp_table_name)
    + N' ADD '
    + N'SYS_CHANGE_CREATION_VERSION bigint NULL, '
    + N'SYS_CHANGE_OPERATION nchar(1) NULL, '
    + N'SYS_CHANGE_COLUMNS varbinary(4100) NULL, '
    + N'SYS_CHANGE_CONTEXT varbinary(128) NULL';

    EXEC (@sql_to_add_static_cols);

    -- this should be dynamic based on sys.columns and other dmvs
    SET @sql_to_add_table_cols = N'ALTER TABLE '
    + QUOTENAME(@temp_table_name)
    + N' ADD id BIGINT NOT NULL';

    EXEC (@sql_to_add_table_cols);

    -- key is to run the insert in a database with the settings that you need for a good query plan
    SET @sql_to_insert_rows = N'USE DB_2012_COMPAT; '
    + N'INSERT INTO '
    + QUOTENAME(@temp_table_name)
    + N' SELECT * FROM CHANGETABLE(CHANGES '
    + QUOTENAME(@changes_database_name)
    + N'.' + QUOTENAME(@changes_schema_name) 
    + N'.' + QUOTENAME(@changes_table_name)
    + N', ' + CAST(@last_sync_version AS NVARCHAR(30))
    + N') AS C';

    EXEC (@sql_to_insert_rows);
END;
Run Code Online (Sandbox Code Playgroud)

当我在兼容级别为 130 的数据库中调用存储过程时,我得到一个仅根据需要进行查找的查询计划:

好计划

在同一个会话中,如果我运行以下代码:

DECLARE @CurrentVersion INT = CHANGE_TRACKING_CURRENT_VERSION ()-1;
SELECT * FROM CHANGETABLE(CHANGES ChangeTable_Test,@CurrentVersion) AS C
OPTION (RECOMPILE);
Run Code Online (Sandbox Code Playgroud)

我收到了您想要避免的扫描:

糟糕的计划