在 SQL 中,我是否应该总是更喜欢 EXISTS 而不是 COUNT() > 0?

Should I always prefer EXISTS over COUNT() > 0 in SQL?

我经常遇到这样的建议,即在检查(子)查询中的任何行是否存在时,出于性能原因,应该使用 EXISTS 而不是 COUNT(*) > 0。具体来说,前者可以在找到单行后短路returnTRUE(或者NOT EXISTSFALSE),而COUNT需要实际上评估每一行以 return 一个数字,仅与零进行比较。

在简单的情况下,这一切对我来说都很有意义。但是,我最近 运行 遇到一个问题,我需要根据组中某一列中的所有值是否为 [=22] 来过滤 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
    )
)

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

现在,假设我想计算所有 没有任何 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
)

我的问题有两个:

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

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

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

第一个查询似乎是执行所需操作的正确方法。

这已经是聚合查询了,因为您要统计 Facebook 帐户。处理计算 google 个帐户的 having 子句的开销应该很小。

另一方面,第二种方法需要重新打开 table 并扫描它,这很可能更昂贵。

子查询 中,您应该更喜欢 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 table 并且真的不需要 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;

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