SQL Server 中的合并行大小溢出 - “无法创建大小的行..”

Yos*_*ari 8 sql-server sql-server-2012 merge

我试图将数据合并到的目标表有 ~660 列。合并代码:

MERGE TBL_BM_HSD_SUBJECT_AN_1 AS targetTable
USING
        (                
SELECT * 
FROM TBL_BM_HSD_SUBJECT_AN_1_STAGING
WHERE [ibi_bulk_id] in (20150520141627106) and  id in(101659113)
    ) AS sourceTable 
ON (...)
WHEN MATCHED AND ((targetTable.[sampletime] <= sourceTable.[sampletime]))
        THEN UPDATE SET ...
WHEN NOT MATCHED 
        THEN INSERT (...)
    VALUES (...)
Run Code Online (Sandbox Code Playgroud)

我第一次运行这个(即当表为空时)它导致成功,并插入了一行。

我第二次使用相同的数据集运行此程序时,返回错误:
无法创建大小为 8410 的行,该行大于允许的最大行大小 8060。

为什么我第二次尝试合并已经插入的同一行会导致错误。如果该行超过了最大行大小,则预计它不可能首先插入它。

所以我尝试了两件事,(成功了!):

  • 从合并语句中删除“WHEN NOT MATCHED”部分
  • 使用我尝试合并的同一行运行更新语句

为什么使用合并更新不成功,而插入可以,直接更新也可以?

更新:

设法找到实际的行大小 - 4978。我创建了一个只有这一行的新表,并通过这种方式找到行大小: 在此处输入图片说明

而且我仍然没有看到超出允许限制的东西。

更新(2):

完全重现

努力使这种复制不需要任何额外的辅助对象,并且数据将(有点)混淆。

在 2012 版和 2008 版的几台服务器上尝试了这一点,并且能够在所有服务器中完全重现。

Pau*_*ite 10

为什么我第二次尝试合并已经插入的同一行会导致错误。如果该行超过了最大行大小,则预计它不可能首先插入它。

首先,感谢您提供复制脚本。

问题不在于 SQL Server 无法插入或更新特定用户可见 行。正如您所指出的,已经插入到表中的行从根本上来说肯定不会太大,SQL Server 无法处理。

出现此问题是因为 SQL ServerMERGE实现在执行计划的中间步骤期间添加了计算信息(作为额外的列)。出于技术原因需要此额外信息,以跟踪每行是否应导致插入、更新或删除;并且还与 SQL Server 在更改索引期间一般避免瞬时键冲突的方式有关。

SQL Server 存储引擎要求索引在任何时候都是唯一的(在内部,包括任何隐藏的唯一符)——因为每一行都被处理——而不是在整个事务的开始和结束时。在更复杂的MERGE场景中,这需要拆分(将更新转换为单独的删除和插入)、排序和可选的折叠(将同一键上的相邻插入和更新转换为更新)。更多信息

顺便说一句,请注意,如果目标表是堆(删除聚集索引以查看这一点),则不会发生此问题。我不建议将此作为修复,只是提及它以突出始终保持索引唯一性(在当前情况下为集群)与拆分-排序-折叠之间的联系。

简单的 MERGE查询中,使用合适的唯一索引,以及源行和目标行之间的直接关系(通常使用ON具有所有关键列的子句进行匹配),查询优化器可以简化大部分通用逻辑,从而产生相对简单的计划不需要 Split-Sort-Collapse 或 Segment-Sequence Project 来检查目标行是否只触及一次。

在具有更多不透明逻辑的复杂 MERGE查询中,优化器通常无法应用这些简化,从而暴露出正确处理所需的更多基本复杂逻辑(尽管存在产品错误,并且已经有很多错误)。

您的查询肯定是复杂的。该ON子句与索引键不匹配(我明白为什么),并且“源表”是一个涉及排名窗口函数的自联接(同样,有原因):

MERGE MERGE_REPRO_TARGET AS targetTable
USING
(
    SELECT * FROM 
    (
        SELECT 
            *, 
            ROW_NUMBER() OVER (
                PARTITION BY ww,id, tenant 
                ORDER BY 
                (
                    SELECT COUNT(1) 
                    FROM MERGE_REPRO_SOURCE AS targetTable
                    WHERE 
                        targetTable.[ibi_bulk_id] = sourceTable.[ibi_bulk_id] 
                        AND targetTable.[ibi_row_id] <> sourceTable.[ibi_row_id] 
                        AND 
                        (
                            (targetTable.[ww] = sourceTable.[ww]) 
                            AND (targetTable.[id] = sourceTable.[id]) 
                            AND (targetTable.[tenant] = sourceTable.[tenant])
                        ) 
                        AND NOT ((targetTable.[sampletime] <= sourceTable.[sampletime]))
                ),
                sourceTable.ibi_row_id DESC
            ) AS idx
        FROM MERGE_REPRO_SOURCE sourceTable 
        WHERE [ibi_bulk_id] in (20150803110418887)
    ) AS bulkData
    where idx = 1
) AS sourceTable 
ON 
    (targetTable.[ww] = sourceTable.[ww]) 
    AND (targetTable.[id] = sourceTable.[id]) 
    AND (targetTable.[tenant] = sourceTable.[tenant])
...
Run Code Online (Sandbox Code Playgroud)

这会导致许多额外的计算列,主要与拆分和更新转换为插入/更新对时所需的数据相关联。这些额外的列导致中间行超过了早期排序中允许的 8060 字节 - 过滤器之后的行:

问题排序

请注意,过滤器在其输出列表中有1,319 列(表达式和基本列)。附加调试器会显示引发致命异常时的调用堆栈:

堆栈跟踪

顺便指出的问题是不是在后台-除了有被转换成一个关于警告潜在的行太大。

为什么使用合并更新不成功,而插入可以,直接更新也可以?

直接更新的内部复杂性与MERGE. 这是一个从根本上更简单的操作,往往会更好地简化和优化。删除NOT MATCHED子句还可以消除足够的复杂性,从而在某些情况下不会产生错误。然而,repro 不会发生这种情况。

最终,我的建议是避免MERGE执行更大或更复杂的任务。我的经验是单独的插入/更新/删除语句趋于优化更好,更简单易懂,而且还经常进行更好的整体,相比MERGE