T-SQL != 运算符对 IN 运算符的性能

T-SQL Performance of != operator against IN operator

我已经编写了一个 SQL Server 2008 R2 存储过程来执行对帐,并且我有一个对帐状态标志 (TINYINT),其值可以为 0(新)、1(已对帐)或 2(异常)。

在此过程中,我使用 != 运算符选择所有未成功协调到临时 table 中的记录:

SELECT FIELDS
INTO #TEMP_TABLE
FROM PERMANENT_TABLE
WHERE RECONCILIATION_STATUS != 1

在工作中与 DBA 交谈,他认为将其重新编码为:

SELECT FIELDS
INTO #TEMP_TABLE
FROM PERMANENT_TABLE
WHERE RECONCILIATION_STATUS in (0, 2)

会更高效,因为我们知道 RECONCILIATION_STATUS 字段的所有可能值。我找不到任何文献支持这一点,想知道他是否真的正确?

Alex K 在评论中提到,使用 in 子句需要对每个值进行两次比较,而使用 != 只是一次。所以从表面上看,这将使单一价值解决方案更具吸引力。

我会将其与过滤 WHERE Reconcilition_Status != 1Reconciliation_Status 列上的过滤索引相结合。从长远来看,这可能最终会带来更多的性能提升。

另一件需要考虑的事情是代码的可维护性。如果将来有可能在此列中允许更多值,那么如果查询未更新,使用 in 解决方案可能会立即使结果无效(因为如果您添加 3 作为新值,in (0,2) 过滤器将排除具有 3 的行,而 != 1 仍将 return 可能是预期的结果。

显而易见的解决方案是同时测试两者。

首先设置示例模式:

IF OBJECT_ID(N'dbo.T', 'U') IS NOT NULL DROP TABLE dbo.T;
CREATE TABLE dbo.T 
(
    ID INT IDENTITY(1, 1) NOT NULL PRIMARY KEY,
    RECONCILIATION_STATUS TINYINT NOT NULL CHECK (RECONCILIATION_STATUS IN (0, 1, 2)),
    Filler CHAR(100) NULL
);

INSERT dbo.T (RECONCILIATION_STATUS)
SELECT  TOP (100000) FLOOR(RAND(CHECKSUM(NEWID())) * 3)
FROM    sys.all_objects a, sys.all_objects b;

然后在没有索引的情况下进行测试

SELECT  COUNT(Filler)
FROM    dbo.T
WHERE   RECONCILIATION_STATUS != 1;

SELECT  COUNT(Filler)
FROM    dbo.T
WHERE   RECONCILIATION_STATUS IN (0, 2);

每个计划是:

如您所见,这里的差异可以忽略不计,因为没有索引,所以两个查询都需要进行聚簇索引扫描。

由于可能的值如此之少,非聚集索引不太可能有任何用处,除非您将所有经常需要的列作为非键列包含在内,或者没有太多数据。在 100,000 个示例行上构建标准非聚集索引,如下所示:

CREATE NONCLUSTERED INDEX IX_T__RECONCILIATION_STATUS
    ON dbo.T (RECONCILIATION_STATUS);

执行计划与聚簇索引扫描相同。

将其他列作为非键索引包括在内:

CREATE NONCLUSTERED INDEX IX_T__RECONCILIATION_STATUS
    ON dbo.T (RECONCILIATION_STATUS) INCLUDE (Filler);

!= 1的计划变得相当复杂,虽然我不会太强调它的重要性,但估计的成本是一样的:

但是,IO 统计数据显示所需的实际读取几乎没有任何不同:

Table 'T'. Scan count 2, logical reads 935, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Table 'T'. Scan count 2, logical reads 934, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

到目前为止,差别不大,但这实际上取决于您的数据分布,以及您拥有的索引和约束。

有趣的是,如果您为测试创建一个临时 table 并在其上定义检查约束:

IF OBJECT_ID(N'tempdb..#T', 'U') IS NOT NULL DROP TABLE #T;
CREATE TABLE #T 
(
    ID INT IDENTITY(1, 1) NOT NULL PRIMARY KEY,
    RECONCILIATION_STATUS TINYINT NOT NULL CHECK (RECONCILIATION_STATUS IN (0, 1, 2)),
    Filler CHAR(100) NULL
);

INSERT #T (RECONCILIATION_STATUS)
SELECT  TOP (100000) FLOOR(RAND(CHECKSUM(NEWID())) * 3)
FROM    sys.all_objects a, sys.all_objects b;

优化器实际上会重写这个查询:

SELECT  COUNT(Filler)
FROM    #T
WHERE   RECONCILIATION_STATUS != 1;

作为

SELECT  COUNT(Filler)
FROM    #T
WHERE   RECONCILIATION_STATUS = 0
OR      RECONCILIATION_STATUS = 2;

如本执行计划所示:

虽然我无法在永久 table 上复制此行为。尽管如此,这让我相信最好的选择是

WHERE   RECONCILIATION_STATUS IN (0, 2);

不仅在性能方面,尽管在大多数情况下它看起来微不足道或根本没有,而且在可读性和未来证明附加值方面肯定如此。

然而,没有比 运行 自己对自己的数据进行此类测试更好的方法了。这将使您更好地了解什么比我从小样本数据集中得出的任何假设更好。