使用 FOREIGN KEY 上的显式单个 KEY 值克服 MERGE JOIN(INDEX SCAN)

Ole*_*g I 10 performance foreign-key deadlock optimization t-sql query-performance

添加 7/11问题是由于 MERGE JOIN 期间的索引扫描而发生死锁。在这种情况下,一个事务试图在 FK 父表的整个索引上获得 S 锁,但之前另一个事务在索引的键值上放置了 X 锁。

让我从一个小例子开始(使用来自 70-461 课程的 TSQL2012 DB):

CREATE TABLE [Sales].[Orders](
[orderid] [int] IDENTITY(1,1) NOT NULL,
[custid] [int] NULL,
[empid] [int] NOT NULL,
[shipperid] [int] NOT NULL,
... )
Run Code Online (Sandbox Code Playgroud)

[custid], [empid], [shipperid]是相应的相关参数[Sales].[Customers], [HR].[Employees], [Sales].[Shippers]。在每种情况下,我们在父表中的引用列上都有一个聚集索引。

ALTER TABLE [Sales].[Orders]  WITH CHECK ADD  CONSTRAINT [FK_Orders_Customers] FOREIGN KEY([custid]) REFERENCES [Sales].[Customers] ([custid])
ALTER TABLE [Sales].[Orders]  WITH CHECK ADD  CONSTRAINT [FK_Orders_Employees] FOREIGN KEY([empid]) REFERENCES [HR].[Employees] ([empid])
ALTER TABLE [Sales].[Orders]  WITH CHECK ADD  CONSTRAINT [FK_Orders_Shippers] FOREIGN KEY([shipperid])REFERENCES [Sales].[Shippers] ([shipperid])
Run Code Online (Sandbox Code Playgroud)

我正在尝试INSERT [Sales].[Orders] SELECT ... FROM另一个名为的表[Sales].[OrdersCache],它与[Sales].[Orders]外键具有相同的结构。另一件事可能很重要,要提到表[Sales].[OrdersCache]是聚集索引。

CREATE CLUSTERED INDEX idx_c_OrdersCache ON Sales.OrdersCache ( custid, empid )
Run Code Online (Sandbox Code Playgroud)

正如预期的那样,当我尝试插入少量数据时 LOOP JOIN 工作正常,使外键上的索引查找。

对于大量数据,查询优化器使用 MERGE JOIN 作为维护查询中的外键的最有效方法。

除了在我们的情况下使用 OPTION (LOOP JOIN) 外键或在显式 JOIN 情况下使用 INNER LOOP JOIN 之外,与它无关。

下面是我试图在我的环境中运行的查询:

INSERT Sales.Orders (
        custid, empid, shipperid, ... )
SELECT  custid, empid, 2, ...
FROM Sales.OrdersCache
Run Code Online (Sandbox Code Playgroud)

查看计划,我们可以看到所有 3 个前键都通过 MERGE JOIN 验证。这对我来说不是一种合适的方式,因为它使用带有整个索引锁定的 INDEX SCAN。 外键验证期间的 MERGE JOIN

使用 OPTION (LOOP JOIN) 并不合适,因为它比 MERGE JOIN 的成本高出近 15%(我认为随着数据量的增长,回归会更大)。

在 SELECT 语句中,您可以看到shipperid整个插入集的属性的单个值。在我看来,至少对于不可变属性,必须有一种方法可以使插入集的验证阶段更快。就像是:

  • 如果我们有未定义的 JOIN 验证子集,则进行 LOOP JOIN、MERGE JOIN、HASH JOIN
  • 如果验证列只有一个显式值,我们只进行一次验证(INDEX SEEK)。

是否有任何通用模式可以使用代码结构、附加 DDL 对象等来克服上述情况?

添加 20/07。解决方案。查询优化器已经通过使用 MERGE JOIN 进行了“单键 - 外键”验证优化。并且只为 Sales.Shippers 表创建,同时为查询中的另一个连接留下 LOOP JOIN。由于我在父表中有几行,查询优化器使用排序合并连接算法,并且只将内部表中的每一行与父表比较一次。所以这就是我的问题的答案,如果有任何特定的机制可以在单键验证期间有效地处理集合中的单个值。这不是一个完美的决定,但这就是 SQL Server 优化案例的方式。

性能影响调查显示,在我的情况下,MERGE JOIN 和 LOOP JOIN 插入语句大约等于 750 个同时插入的行,具有以下 MERGE JOIN 的优势(在 CPU 时间资源中)。因此,使用 OPTION (LOOP JOIN) 是适合我的业务流程的解决方案。

Pau*_*ite 9

使用 OPTION (LOOP JOIN) 并不合适,因为它比 MERGE JOIN 的成本高出近 15%

showplan 输出中显示的成本百分比始终为优化器模型估计值,即使在执行后(实际)计划中也是如此。这些成本可能无法反映特定硬件上的实际运行时性能。唯一可以确定的方法是用您的工作负载测试替代方案,测量对您最重要的指标(已用时间、CPU 使用率等)。

根据我的经验,优化器从循环连接切换到合并连接以进行外键验证为时过早。除了最大的操作之外,我发现循环连接更可取。我所说的“大”至少是指数千万行,当然不是您在问题评论中指出的大约一千行。

在我看来,至少对于不可变属性,必须有一种方法可以使插入集的验证阶段更快。

这在原则上是有道理的,但如今在外键验证使用的计划构建逻辑中没有这样的逻辑。当前的逻辑故意非常通用并且与更广泛的查询正交;特定的优化使测试复杂化并增加了边缘情况错误的可能性。

是否有任何通用模式可以使用代码结构、附加 DDL 对象等来克服上述情况?

不是我所知道的。合并连接外键验证的死锁风险是众所周知的,最广泛使用的解决方法是OPTION (LOOP JOIN)提示。没有办法避免在外键验证期间使用共享锁,因为这些是正确性必需的,即使在行版本控制隔离级别下也是如此。

如果您希望保留多个并发进程以事务方式向父表和子表添加行的能力,则没有更好的通用答案(比循环连接提示)。但是,如果您愿意序列化数据修改(而不是读取),使用sp_getapplock是一种可靠且简单的技术。