我应该总是更喜欢 EXISTS 而不是 SQL 中的 COUNT() > 0 吗?

Posted

技术标签:

【中文标题】我应该总是更喜欢 EXISTS 而不是 SQL 中的 COUNT() > 0 吗?【英文标题】:Should I always prefer EXISTS over COUNT() > 0 in SQL? 【发布时间】:2020-12-03 15:39:20 【问题描述】:

我经常遇到这样的建议,即在检查(子)查询中是否存在任何行时,出于性能考虑,应该使用EXISTS 而不是COUNT(*) > 0。具体来说,前者可以在找到单行后短路并返回TRUE(或FALSE,在NOT EXISTS的情况下),而COUNT需要实际评估每一行才能返回一个数字,只有与零进行比较。

在简单的情况下,这一切对我来说都很有意义。但是,我最近遇到了一个问题,我需要根据组中某个列中的所有值是否都是NULL,来过滤GROUP BYHAVING 子句中的组。

为了清楚起见,让我们看一个例子。假设我有以下架构:

CREATE TABLE profile(
    id INTEGER PRIMARY KEY,
    user_id INTEGER NOT NULL,
    google_account_id INTEGER NULL,
    facebook_account_id INTEGER NULL,
    FOREIGN KEY (user_id) REFERENCES user(id),
    CHECK(
        (google_account_id IS NOT NULL) + (facebook_account_id IS NOT NULL) = 1
    )
)

即每个用户(为简洁起见未显示表格)具有 0 个或多个配置文件。每个个人资料都是 Google 或 Facebook 帐户。 (这是子类的转换或带有一些关联数据的总和类型——在我的真实模式中,帐户 ID 也是保存该关联数据的不同表的外键,但这与我的问题无关。)

现在,假设我想统计所有没有任何 Google 个人资料的用户的 Facebook 个人资料

起初,我使用COUNT() = 0编写了以下查询:

SELECT user_id, COUNT(facebook_account_id)
FROM profile
GROUP BY user_id
HAVING COUNT(google_account_id) = 0;

但后来我突然想到HAVING 子句中的条件实际上只是一个存在检查。于是我用子查询和NOT EXISTS重写了查询:

SELECT user_id, COUNT(facebook_account_id)
FROM profile AS p
GROUP BY user_id
HAVING NOT EXISTS (
    SELECT 1
    FROM profile AS q
    WHERE p.user_id = q.user_id
    AND q.google_id IS NOT NULL
)

我的问题有两个:

    我是否应该保留第二个重新制定的查询,并使用带有子查询的NOT EXISTS 而不是COUNT() = 0?这真的更有效率吗?我认为由于WHERE p.user_id = q.user_id 条件而导致的索引查找有一些额外的成本。 EXISTS 的短路行为是否会吸收这种额外成本也可能取决于组的平均基数,不是吗?

    或者,DBMS 是否可能足够聪明,能够识别分组键正在被比较的事实,并通过用当前组替换它来完全优化这个子查询(而不是实际为每个组执行索引查找) ?我严重怀疑 DBMS 是否可以优化掉这个子查询,而无法将 COUNT() = 0 优化为 NOT EXISTS

    抛开效率不谈,第二个查询似乎更复杂,对我来说不太明显正确,所以即使它碰巧更快,我也不愿意使用它。你怎么看,有没有更好的方法?我可以通过以更简单的方式使用NOT EXISTS(例如通过在 HAVING 子句中直接引用当前组)来吃蛋糕吗?

【问题讨论】:

【参考方案1】:

子查询中,您应该更喜欢EXISTS/NOT EXISTS 而不是COUNT()。所以而不是:

select t.*
from t
where (select count(*) from z where z.x = t.x) > 0

你应该改用:

select t.*
from t
where exists (select 1 from z where z.x = t.x)

这样做的原因是子查询可以在第一次匹配时停止处理。

这种推理不适用于聚合后的 HAVING 子句 - 无论如何都必须生成所有行,因此在第一次匹配时停止几乎没有价值。

但是,如果您有一个 users 表并且不需要 Facebook 计数,则可能不需要聚合。你可以使用:

select u.*
from users u
where not exists (select 1
                  from profiles p
                  where p.user_id = u.user_id and p.google_id is not null
                 );

此外,如果您在聚合之前过滤,聚合可能会更快:

SELECT user_id, COUNT(facebook_account_id)
FROM profile AS p
WHERE NOT EXISTS (
    SELECT 1
    FROM profile p2
    WHERE p2.user_id = p.user_id AND p2.google_id IS NOT NULL
)
GROUP BY user_id;

实际上是否更快取决于许多因素,包括实际过滤掉的行数。

【讨论】:

【参考方案2】:

第一个查询似乎是做你想做的事情的正确方法。

这已经是一个聚合查询,因为您想计算 Facebook 帐户。处理 having 子句(计算 Google 帐户)的开销应该很小。

另一方面,第二种方法需要重新打开表并对其进行扫描,这很可能更昂贵。

【讨论】:

以上是关于我应该总是更喜欢 EXISTS 而不是 SQL 中的 COUNT() > 0 吗?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我应该更喜欢单个'await Task.WhenAll'而不是多个等待?

你应该总是喜欢 xrange() 而不是 range() 吗?

为啥我应该更喜欢“显式类型的初始化程序”习语而不是显式给出类型

为啥我应该更喜欢 unsafe_unretained 限定符而不是为弱引用属性赋值? [复制]

在 C 宏中,是不是应该更喜欢 do ... while(0,0) 而不是 do ... while(0)?

SQL Server IIF 与 CASE