为什么 MERGE 不会将超过 277 条记录插入到配置了时态表和历史表上的非聚集索引的表中

Dan*_* C. 26 azure-sql-database columnstore merge

我再次发现 SQL Server 和 MERGE 语句存在问题,需要进行一些确认。

我可以在 Azure 数据库上不断重现我的问题(但不能在本地 SQL Server 2017/2019 上重现)。

请执行以下步骤(一步一步,而不是一次命令执行)!

1)架构脚本:

    CREATE TABLE [dbo].[ImpactValueHistory]
    (
        [Rn] BIGINT NOT NULL,

        [ImpactId] UNIQUEIDENTIFIER NOT NULL,
        [ImpactValueTypeId] INT NOT NULL,

        [Date] DATE NOT NULL,
        [Value] DECIMAL(38, 10) NOT NULL,

        [ValidFrom] DATETIME2 NOT NULL CONSTRAINT [DF_ImpactValueHistory_ValidFrom] DEFAULT CONVERT(DATETIME2, '0001-01-01'),
        [ValidTo] DATETIME2 NOT NULL CONSTRAINT [DF_ImpactValueHistory_ValidTo] DEFAULT CONVERT(DATETIME2, '9999-12-31 23:59:59.9999999'),

        [ImpactPeriodId] INT NOT NULL,

        [NormalizedValue] DECIMAL(38, 10) NOT NULL,
    )
    GO

    CREATE CLUSTERED COLUMNSTORE INDEX [COLIX_ImpactValueHistory]
        ON [dbo].[ImpactValueHistory];
    GO

    CREATE NONCLUSTERED INDEX [IX_ImpactValueHistory_ValidFrom_ValidTo_ImpactId_DimensionItemId]
        ON [dbo].[ImpactValueHistory] ([ValidFrom], [ValidTo], [ImpactId], [ImpactValueTypeId], [Date]);
    GO


    CREATE TABLE [dbo].[ImpactValue]
    (
        [Rn] BIGINT NOT NULL IDENTITY(1,1),

        [ImpactId] UNIQUEIDENTIFIER NOT NULL,
        [ImpactValueTypeId] INT NOT NULL,

        [Date] DATE NOT NULL,
        [Value] DECIMAL(38, 10) NOT NULL,

        [ValidFrom] DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL CONSTRAINT [DF_ImpactValue_ValidFrom] DEFAULT CONVERT(DATETIME2, '0001-01-01'),
        [ValidTo] DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL CONSTRAINT [DF_ImpactValue_ValidTo] DEFAULT CONVERT(DATETIME2, '9999-12-31 23:59:59.9999999'),

        [ImpactPeriodId] INT NOT NULL,

        [NormalizedValue] DECIMAL(38, 10) NOT NULL,

        PERIOD FOR SYSTEM_TIME ([ValidFrom], [ValidTo]),

        CONSTRAINT [PK_ImpactValue] PRIMARY KEY NONCLUSTERED ([ImpactId], [ImpactValueTypeId], [Date], [ImpactPeriodId])
    )
    WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [dbo].[ImpactValueHistory]))
    GO

    CREATE UNIQUE CLUSTERED INDEX [IX_ImpactValue_Id] ON [dbo].[ImpactValue]([Rn])
    GO

    CREATE COLUMNSTORE INDEX [CIX_ImpactValue] ON [dbo].[ImpactValue] ([ImpactId], [ImpactValueTypeId], [Date], [Value], [NormalizedValue])
    GO

Run Code Online (Sandbox Code Playgroud)

2)插入一些随机数据的脚本

DECLARE @inserted0 TABLE ([Date] DATE, [ImpactId] uniqueidentifier, [ImpactPeriodId] int, [ImpactValueTypeId] int);
MERGE [dbo].[ImpactValue] USING (
SELECT TOP 278 -- <-- this number is critical
        DATEADD(MONTH, ROW_NUMBER() OVER(ORDER BY [Name]) - 1, '2000-01-01') AS [Date],
        NEWID() AS [ImpactId], 
        1 AS [ImpactPeriodId], 
        1 AS [ImpactValueTypeId], 
        99 AS [Value], 
        99 AS [NormalizedValue]
    FROM [sys].[all_columns]
) AS i ([Date], [ImpactId], [ImpactPeriodId], [ImpactValueTypeId], [Value], [NormalizedValue]) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Date], [ImpactId], [ImpactPeriodId], [ImpactValueTypeId], [Value], [NormalizedValue])
VALUES (i.[Date], i.[ImpactId], i.[ImpactPeriodId], i.[ImpactValueTypeId], i.[Value], i.[NormalizedValue])
OUTPUT INSERTED.[Date], INSERTED.[ImpactId], INSERTED.[ImpactPeriodId], INSERTED.[ImpactValueTypeId]
INTO @inserted0;

SELECT * FROM @inserted0

Run Code Online (Sandbox Code Playgroud)

此步骤应返回所有插入的行!

3) 删除步骤2)中的数据 这一步是填充配置的历史表

DELETE [dbo].[ImpactValue]
Run Code Online (Sandbox Code Playgroud)

4)再次插入一些随机数据 您可以使用步骤2)中的脚本

我必须注意,步骤 1) - 4) 应单独执行,而不是在GO.

同样,此步骤应返回所有插入的行!但事实并非如此! 在我这边,我总是得到一个空的结果。这可以在我们的三个生产数据库上重现:(

MERGE 语句由 EF Core 生成,目前我通过设置最大批量大小来解决此问题。但这不可能是最终的解决方案。

它必须与临时表有关,并且在临时表上配置了非聚集索引。

也可以看看:

过去我已经偶然发现了这个问题:

但我当前的问题只能在 Azure SQL 数据库上重现,并且不会引发任何错误。

有趣的旁注:

  1. 如果我暂时禁用临时表 -> 它就可以工作
  2. 如果我删除非聚集索引 [IX_ImpactValueHistory_ValidFrom_ValidTo_ImpactId_DimensionItemId] -> 它正在工作
  3. 如果我在步骤 2) 中使用 SELECT TOP (@BatchSize) --> 它正在工作
  4. 如果我只使用 OUTPUT 而不是 OUTPUT INTO @inserted0 --> 它正在工作

如果历史表上没有 COLUMNSTORE 索引,它就可以工作。通过仅删除主表上的 COLUMNSTORE 索引,我看到了同样的问题。

(a) 重现问题和TOP 278(b) 不重现问题的案例的实际执行计划TOP (@BatchSize)可在https://1drv.ms/u/s!AsOa6e9ukBWQlIRg9_9eySDFp5hvEA?e=KBQBsP上找到。我还添加了批量大小为 277 的实际执行计划。两者都使用这个大小!

Pau*_*ite 27

Azure SQL 数据库有时会为合并插入构建无效的执行计划。

当它决定使用单个运算符(狭窄的计划)维护列存储历史表时,一切都很好。这简单地包括历史表没有二级索引的情况。

当它决定对基表和二级索引使用单独的运算符来维护历史表时(一个广泛的计划),使用该OUTPUT INTO选项时会出现问题。计划的选择对基数估计很敏感。

例如,计划OUTPUT仅(不写入表变量)包含表假脱机。假脱机保存过滤器之前的行,该过滤器从流中删除ValidTo为空的任何行。然后假脱机重播(未过滤的)行以返回给客户端:

带线轴的计划

使用时OUTPUT INTO,同一流用于维护历史表上的二级索引并为输出表提供行。这会产生一个问题,因为纯插入不会导致任何行添加到历史记录中,因此所有行都会被过滤掉。

无线轴计划

历史表上不需要列存储索引来体现此问题。

这是一个产品缺陷,您应该通过在 Azure 门户中创建支持请求来直接向 Microsoft 支持报告。

旁注:到达历史表索引插入的行实际上并未插入,因为操作列告诉它不要这样做。不幸的是,这些细节没有在展示计划中公开。一个可能的修复方法是测试操作以及过滤器中的ValidTo 。


额外的筛选器不会出现在 SQL Server 2019 CU16-GDR 上:

2019年计划

这感觉就像是对隐含的可空性问题的错误修复,在盒装产品之前已应用于 Azure SQL 数据库。如果是这样,那么它对 QO 兼容性级别提示没有反应就有点令人惊讶了。


丹尼尔·C .:

微软确认这是一个错误,并针对我们受影响的数据库之一推出了快速修复程序。我可以确认此修复正在解决我的问题。