“EXISTS (...) OR EXISTS (...)”中的子句顺序

j.r*_*j.r 11 sql-server

我有一类查询测试两件事中的一个的存在。它的形式

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM ...)
  OR EXISTS (SELECT 1 FROM ...)
THEN 1 ELSE 0 END;
Run Code Online (Sandbox Code Playgroud)

实际语句是用 C 生成的,并作为通过 ODBC 连接的即席查询执行。

最近发现,在大多数情况下,第二个 SELECT 可能比第一个 SELECT 更快,并且切换两个 EXISTS 子句的顺序导致至少在我们刚刚创建的一个滥用测试用例中急剧加速。

显而易见的事情就是继续并切换这两个子句,但我想看看更熟悉 SQL Server 的人是否愿意对此进行权衡。感觉就像我依靠巧合和“实施细节”。

(似乎如果 SQL Server 更智能,它将并行执行两个 EXISTS 子句,并让其中一个先完成另一个短路。)

有没有更好的方法让 SQL Server 持续改进此类查询的运行时间?

更新

感谢您抽出时间和对我的问题感兴趣。我没想到有关实际查询计划的问题,但我愿意分享它们。

这是用于支持 SQL Server 2008R2 及更高版本的软件组件。根据配置和使用的不同,数据的形状可能会有很大不同。我的同事考虑对查询进行此更改,因为(在示例中)dbf_1162761$z$rv$1257927703表中的行数总是大于或等于dbf_1162761$z$dd$1257927703表中的行数——有时要多得多(数量级)。

这是我提到的滥用案例。第一个查询很慢,大约需要 20 秒。第二个查询立即完成。

值得一提的是,最近还添加了“优化未知”位,因为参数嗅探会破坏某些情况。

原始查询:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)
Run Code Online (Sandbox Code Playgroud)

原计划:

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)
Run Code Online (Sandbox Code Playgroud)

固定查询:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)
Run Code Online (Sandbox Code Playgroud)

固定计划:

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
Run Code Online (Sandbox Code Playgroud)

Joe*_*ish 11

作为一般经验法则,SQL Server 将按CASE顺序执行语句的各个部分,但可以自由地对OR条件重新排序。对于某些查询,您可以通过更改语句中WHEN表达式的顺序来始终获得更好的性能CASE。有时,在更改OR语句中的条件顺序时也可以获得更好的性能,但这并不能保证行为。

最好用一个简单的例子来解释它。我正在针对 SQL Server 2016 进行测试,因此您可能不会在您的机器上获得完全相同的结果,但据我所知,适用相同的原则。首先,我将把一百万个从 1 到 1000000 的整数放在两个表中,一个有聚集索引,一个作为堆:

CREATE TABLE dbo.X_HEAP (ID INT NOT NULL, FLUFF VARCHAR(100));

INSERT INTO dbo.X_HEAP  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE TABLE dbo.X_CI (ID INT NOT NULL, FLUFF VARCHAR(100), PRIMARY KEY (ID));

INSERT INTO dbo.X_CI  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);
Run Code Online (Sandbox Code Playgroud)

考虑以下查询:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;
Run Code Online (Sandbox Code Playgroud)

我们知道对子查询进行评估X_CI将比对子查询更便宜X_HEAP,尤其是当没有匹配的行时。如果没有匹配的行,那么我们只需要对带有聚集索引的表进行一些逻辑读取。但是,我们需要扫描堆的所有行才能知道没有匹配的行。优化器也知道这一点。从广义上讲,与扫描表相比,使用聚集索引查找一行的成本非常低。

对于这个示例数据,我会像这样编写查询:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
ELSE 0 END;
Run Code Online (Sandbox Code Playgroud)

这有效地强制 SQL Server 首先针对具有聚集索引的表运行子查询。以下是结果SET STATISTICS IO, TIME ON

表'X_CI'。扫描计数 0,逻辑读取 3,物理读取 0

SQL Server 执行时间:CPU 时间 = 0 毫秒,已用时间 = 0 毫秒。

查看查询计划,如果标签 1 处的查找返回任何数据,而不是标签 2 处的扫描不需要并且不会发生:

很好的查询

以下查询的效率要低得多:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
ELSE 0 END
OPTION (MAXDOP 1);
Run Code Online (Sandbox Code Playgroud)

查看查询计划,我们看到标签 2 处的扫描总是发生。如果找到一行,则跳过标签 1 处的查找。这不是我们想要的顺序:

错误的查询计划

性能结果证明了这一点:

表'X_HEAP'。扫描计数 1,逻辑读取 7247

SQL Server 执行时间:CPU 时间 = 15 毫秒,已用时间 = 22 毫秒。

回到原始查询,对于这个查询,我看到按有利于性能的顺序评估搜索和扫描:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;
Run Code Online (Sandbox Code Playgroud)

在这个查询中,它们以相反的顺序进行评估:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
THEN 1 ELSE 0 END;
Run Code Online (Sandbox Code Playgroud)

但是,与前一对查询不同的是,没有什么可以强制 SQL Server 查询优化器先评估一个,然后再评估另一个。你不应该依赖这种行为来做任何重要的事情。

总之,如果您需要在另一个子查询之前评估一个子查询,请使用CASE语句或其他一些方法来强制排序。否则,您可以随意按OR您想要的条件对子查询进行排序,但要知道优化器不能保证按编写的顺序执行它们。

附录:

一个自然的后续问题是,如果您希望 SQL Server 决定哪个查询更便宜并首先执行该查询,您可以做什么?到目前为止,所有方法似乎都是由 SQL Server 按照编写查询的顺序实现的,即使对于其中一些方法不能保证其行为。

这是一个似乎适用于简单演示表的选项:

SELECT CASE
  WHEN EXISTS (
    SELECT 1
    FROM (
        SELECT TOP 2 1 t
        FROM 
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_HEAP 
            WHERE ID = 50000 
        ) h
        CROSS JOIN
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_CI
            WHERE ID = 50000
        ) ci
    ) cnt
    HAVING COUNT(*) = 2
)
THEN 1 ELSE 0 END;
Run Code Online (Sandbox Code Playgroud)

您可以在此处找到 db fiddle 演示。更改派生表的顺序不会更改查询计划。在这两个查询X_HEAP中都没有触及表。换句话说,查询优化器似乎首先执行成本较低的查询。我不建议在生产中使用这样的东西,所以这里主要是为了好奇心的价值。可能有更简单的方法来完成同样的事情。

  • 或者`CASE WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000 UNION ALL SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 ELSE 0 END`可能是一个替代方案,尽管这仍然依赖于手动决定哪个查询是更快,把那个放在第一位。我不确定是否有一种表达方式,以便 SQL Server 自动重新排序,以便首先自动评估便宜的。 (4认同)