“存在(…)或存在(…)”中子句的顺序


11

我有一类查询,用于测试两件事之一的存在。它的形式

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM ...)
  OR EXISTS (SELECT 1 FROM ...)
THEN 1 ELSE 0 END;

实际语句是用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秒。第二个查询立即完成。

对于它的价值,最近还添加了“ OPTIMIZE FOR UNKNOWN”位,因为参数嗅探会浪费某些情况。

原始查询:

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)

原始计划:

|--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)

固定查询:

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)

固定方案:

|--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)

Answers:


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);

考虑以下查询:

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;

我们知道,针对评估子查询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;

这有效地迫使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);

查看查询计划,我们看到总是在标签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;

在此查询中,它们的评估顺序相反:

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;

但是,与前面的查询对不同,没有什么可以强迫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;

您可以在此处找到db小提琴演示。更改派生表的顺序不会更改查询计划。在两个查询中,X_HEAP都未触及该表。换句话说,查询优化器似乎首先执行便宜的查询。我不建议在生产中使用这样的东西,因此它主要是出于好奇的价值。完成同一件事可能有一种更简单的方法。


4
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将自动重新排序,以便首先自动评估廉价的。
马丁·史密斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.