如何在没有提示的情况下强制合并连接

Ale*_*kiy 2 join sql-server optimization t-sql execution-plan

前段时间我使用了查询提示,但后来意识到大多数时候 SQL Server 比我更聪明。 构建额外的索引/重新组织数据或查询并获得比强制服务器使用计划要好得多的结果这通常效率低下,但对于某些数据子集来说速度足够快。但现在我处于一种我不知道如何更好地组织数据的情况。

我有两张桌子。第一个表 T1 是 (Id, CustomerId),第二个表 T2 具有相同的列。我想在 CustomerId 上加入 T1 到 T2。并获得前 N 行。在这种情况下,优化器看到我只需要 N 个 top 并说,“嘿,我将使用循环并很快找到 N 个匹配项,尤其是当我使用索引查找时。” 但它不起作用,因为没有满足条件的数据。因此它使用 aloop join来连接一个 25m 的表和一个非常慢的 100k 表。

在此处输入图片说明

当我强制 SQL Server 使用合并联接时,我得到以下计划,该计划在一秒钟内执行:

在此处输入图片说明

我不想强迫它有两个原因:

  • 首先,正如我之前所说,SQL Server 足够智能。
  • 其次,我使用的是 ORM,并且很难在生成的查询中注入提示,所以我想避免它。

在这种情况下我该怎么办?

Joe*_*ish 6

要在没有提示的情况下从嵌套循环连接切换到合并连接,您需要具有合并连接的计划的估计成本低于具有嵌套循环的计划。从技术上讲,您还需要查询优化器在计划探索期间找到成本较低的计划,但对此您无能为力。

首先,关于合并连接成本的说明。根据我的经验,SQL Server 对合并连接成本计算非常悲观。当您考虑更改少量数据时合并连接的 IO 要求会发生多大变化时,这似乎是可以理解的。考虑一个简单的例子,其中一个表包含 1 - 10000 的整数,另一个表包含 100001 到 1100000 的整数:

DROP TABLE IF EXISTS X_SMALL_TABLE;

CREATE TABLE X_SMALL_TABLE (ID INT NOT NULL PRIMARY KEY (ID));

INSERT INTO X_SMALL_TABLE WITH (TABLOCK) (ID)
SELECT N
FROM dbo.GetNums(10000);

UPDATE STATISTICS X_SMALL_TABLE WITH FULLSCAN;

DROP TABLE IF EXISTS X_LARGE_TABLE;

CREATE TABLE X_LARGE_TABLE (ID INT NOT NULL PRIMARY KEY (ID));

INSERT INTO X_LARGE_TABLE WITH (TABLOCK) (ID)
SELECT N + 100000
FROM dbo.GetNums(1000000);

UPDATE STATISTICS X_LARGE_TABLE WITH FULLSCAN;
Run Code Online (Sandbox Code Playgroud)

如果我将表连接在一起,我将返回 0 行。如果我强制执行,MERGE JOIN那么 SQL Server 将扫描小表中的所有行,而只扫描大表中的行之一。默认情况下,SQL Server 将根据连接键按升序遍历表。但是,查询优化器估计将从大表中扫描所有 1000000 行,并随后为该运算符分配相对较大的成本:

计划一

正如我们从实际计划中看到的那样,只扫描了一行。如果我们只在小表中插入一个新行会发生什么?

INSERT INTO X_SMALL_TABLE
SELECT 1200000;

UPDATE STATISTICS X_SMALL_TABLE WITH FULLSCAN;
Run Code Online (Sandbox Code Playgroud)

由于合并连接算法在小表中ID达到ID10000时按顺序运行,因此我们应该遍历大表中的所有行,直到达到ID1200000。

计划二

实际计划确认所有行都从两个表中读取。因此,数据的微小变化可能会导致合并连接性能的巨大差异。基于此,SQL Server 对合并连接成本持悲观态度似乎是合理的,如果您试图让合并连接出现在您的查询中,这将对您不利。在您的第二个查询中,您扫描CustomerDiscountCards. 与整个嵌套循环连接计划相比,单独该操作的估计成本可能更高。

当使用提示强制MERGE JOIN注释时,MERGE JOIN 连接提示也会FORCE ORDER向查询添加提示。根据问题中的信息,您的第二个查询不会因为MERGE JOIN. 由于新的连接顺序,它运行得很快。优化器将两个表连接在一起CustomerDiscountCardsDiscountCards使用完整扫描并找到 0 个匹配行。任何针对 0 行结果集的内部联接都会很快,因为查询执行可以在该点停止(忽略在此之前发生的阻塞运算符)。因此,与其专注于强制合并连接(这在没有提示的情况下可能会很困难),我会专注于使连接顺序正确。

强制连接顺序的一种简单方法是将您想要的第一个结果集具体化到临时表中。您可以在初始查询中加入CustomerDiscountCardsDiscountCards并将匹配的相关行放入临时表中。对于您的测试用例,这应该在一秒钟内运行,因为您实际上不会将任何行插入到临时表中。如果您的第二个查询连接到空表,它应该几乎立即完成。您将需要测试其他参数值的查询性能,因为您可能会将相当数量的行和列放入 tempdb。

您可以重写 SQL 以有效地强制连接顺序,而无需使用临时表。当然,使用FORCE ORDER提示是最简单的方法,但您说您不想使用提示,而该提示可能很难使用。它适用于整个查询,因此如果您的数据发生变化,您最终可能会出现性能不佳的情况。对于如何在没有提示的情况下强制连接顺序的想法,看起来像将要首先连接到派生表中的表配对并添加多余的TOP运算符似乎将 SQL Server 推向正确的方向:

INNER JOIN 
(
    SELECT TOP 9223372036854775807 ...
    FROM 
    CustomerDiscountCards
    INNER JOIN DiscountCards ON ...
) t ON ...
Run Code Online (Sandbox Code Playgroud)

但是,我无法断定 SQL Server 是否真的被迫与查询的其余部分分开评估该连接。和以前一样,您需要进行测试。

为了给您一个最终的选择,因为最初的问题似乎与行目标有关,您可以对优化器隐藏行目标。实现这一点的一种方法是使用OPTIMIZE FOR查询提示并为您的TOP语句使用变量。考虑下面的查询片段:

DECLARE @TOP BIGINT = 10;

SELECT TOP (@top)
...
OPTION (OPTIMIZE FOR (@TOP = 9223372036854775807))
Run Code Online (Sandbox Code Playgroud)

该查询将只返回它在结果集中找到的前 10 行。但是,计划将被创建,就好像它返回前 9223372036854775807 行一样。这可能不利于您遇到问题的嵌套循环连接计划,但很难说它的总体性能如何。

最终,根据我们在问题中掌握的信息,很难说更多。据推测,您的查询在某些情况下可以返回行(没有太多理由继续运行始终不返回行的查询),因此您需要针对所有不同情况测试您的选项。