【问题标题】:sum() vs. count()sum() 与 count()
【发布时间】:2013-02-06 12:55:55
【问题描述】:

考虑一个在 PostgreSQL 中实现的投票系统,每个用户都可以对“foo”进行投票。有一个foo 表存储所有“foo 信息”,还有一个votes 表存储user_id、foo_id 和vote,其中vote 是+1 或-1。

要获得每个 foo 的投票数,可以使用以下查询:

SELECT sum(vote) FROM votes WHERE foo.foo_id = votes.foo_id;

但是,以下方法也可以:

(SELECT count(vote) FROM votes 
 WHERE foo.foo_id = votes.foo_id 
 AND votes.vote = 1)
- (SELECT count(vote) FROM votes 
   WHERE foo.foo_id = votes.foo_id 
   AND votes.vote = (-1))

我目前在votes.foo_id 上有一个索引。

哪种方法更有效? (换句话说,哪个会跑得更快?) 我对 PostgreSQL 特定答案和一般 SQL 答案都感兴趣。

编辑

很多答案都考虑了vote 为空的情况。我忘了提到在投票列上有一个NOT NULL 约束。

此外,许多人一直指出第一个更容易阅读。是的,这绝对是真的,如果一位同事写了第二个,除非有表演的必要性,否则我会愤怒地爆炸。无论如何,问题仍然在于两人的表现。 (从技术上讲,如果第一个查询方式慢,那么编写第二个查询就不是犯罪了。)

【问题讨论】:

  • EXPLAIN ANALYZE ...?!
  • 我想不出第二个可以更快的任何方式 - 您仍然需要阅读每一行的 vote 列。
  • @deceze 是对的。但是,我建议查看一个重要方面:使用 sum() 的第一个方面是可读的,并且一眼就能理解。第二个立即在我的大脑中抛出一个“WTFException”,然后在能够破译它之后,whoDidThis() 立即被一个不可屏蔽的中断运行。这种方法的副作用可能包括用大鳟鱼打某人...
  • 您想同时获得一个特定foo的分数,还是所有foo的分数?
  • 你在实现 StackOverflow!?

标签: sql postgresql aggregate-functions


【解决方案1】:

我希望第一个查询能够更快地工作,因为这是一个单一的查询并且它更具可读性(方便您在一段时间后必须回到这个问题)。

第二个查询由两个查询组成。您只会得到一个结果,就好像它是一个查询一样。

也就是说,为了绝对确定其中哪一个更适合您,我将使用大量虚拟数据填充两个表并检查查询执行时间。

【讨论】:

  • 我不确定。计算行数很可能比必须使用行的内容来计算数字更有效。但出于可读性目的,我仍然会选择 Sum over Count。
  • @Flater:我也不确定。正如我所说,第一个选项大大提高了可读性,而只有基准测试才能判断哪种方法更有效。
  • 除非这个查询将在一个大得令人作呕的数据库上运行,否则我会重视可读性而不是轻微的性能提升 :)
  • 我也是,但问题是哪一个更有效。我仍然希望第一个查询不仅更具可读性,而且性能更好。
【解决方案2】:

当然,第一个例子更快、更简单、更容易阅读。甚至在获得slapped with aquatic creatures 之前就应该很明显。虽然sum() 比count() 稍微贵一点,但更重要的是第二个示例需要两次扫描。

但也有一个实际差异:sum() 可以返回 NULL,而 count() 不能。我引用manual on aggregate functions:

需要注意的是,除了count,这些函数都返回一个 未选择任何行时为空值。特别是,没有行的总和 返回 null,而不是预期的零,

由于您似乎有一个性能优化的弱点,这里有一个您可能喜欢的细节:count(*) 比count(vote) 稍快。仅当投票为 NOT NULL 时才等效。使用EXPLAIN ANALYZE 测试性能。

仔细观察

这两个查询都是句法上的废话,单独存在。仅当您从较大查询的 SELECT 列表中复制它们时才有意义,例如:

SELECT *, (SELECT sum(vote) FROM votes WHERE votes.foo_id = foo.foo_id)
FROM   foo;

这里的重点是相关子查询——如果您在查询中只读取votes 的一小部分,这可能没问题。我们会看到额外的WHERE 条件,并且您应该有匹配的索引。

在 Postgres 9.3 或更高版本中,替代的、更简洁、100% 等效的解决方案是LEFT JOIN LATERAL ... ON true:

SELECT *
FROM   foo f
LEFT   JOIN LATERAL (
   SELECT sum(vote) FROM votes WHERE foo_id = f.foo_id
   ) v ON true;

通常类似的性能。详情:

但是,在从表 votes 中读取大部分或全部时,这会(快得多):

SELECT f.*, v.score
FROM   foo f
JOIN   (
   SELECT foo_id, sum(vote) AS score
   FROM   votes
   GROUP  BY 1
   ) v USING (foo_id);

先聚合子查询中的值,然后加入结果。
关于USING:

【讨论】:

  • 我对“count(*) 比count(vote) 稍快”这件事感到非常惊讶(看起来确实如此)。您能否分享一些见解,为什么会这样?我本来希望两者都必须做同样的工作
  • @a_horse_with_no_name: count(*) 不必检查列本身的值,行的存在就足够了。
  • 但是对于定义为not null的列,也不需要检查该值。
  • @a_horse_with_no_name: 使用count(*) Postgres 甚至不需要检查列的定义。另外:在连接表时(例如使用LEFT JOIN),即使是定义为NOT NULL 的列也可能最终包含NULL 值。
  • 但是not null的检查在计划阶段只进行一次,所以不应该增加执行时间。对于普通的选择(无连接),不会发生空值。似乎优化器可以在那里改进;)
【解决方案3】:

第一个会更快。您可以通过简单的方式尝试一下。

生成一些数据:

CREATE TABLE votes(foo_id integer, vote integer);
-- Insert 1000000 rows into 100 foos (1 to 100)
INSERT INTO votes SELECT round(random()*99)+1, CASE round(random()) WHEN 0 THEN -1 ELSE 1 END FROM generate_series(1, 1000000);
CREATE INDEX idx_votes_id ON votes (foo_id);

同时检查

EXPLAIN ANALYZE SELECT SUM(vote) FROM votes WHERE foo_id = 5;
EXPLAIN ANALYZE SELECT (SELECT COUNT(*) AS count FROM votes WHERE foo_id=5 AND vote=1) - (SELECT COUNT(*)*-1 AS count FROM votes WHERE foo_id=5 AND vote=-1);

但事实是它们并不等同,为了确保第一个可以作为第二个,您需要处理 null 案例:

SELECT COALESCE(SUM(vote), 0) FROM votes WHERE foo_id = 5;

还有一件事。如果您使用的是 PostgreSQL 9.2,您可以创建包含两列的索引,这样您就有机会使用仅索引扫描:

CREATE INDEX idx_votes_id ON votes (foo_id, vote);

但是!在某些情况下,此索引可能最差,因此您应该尝试同时使用这两个索引并运行 EXPLAIN ANALYZE 以查看哪个是最好的,或者甚至创建两者并检查哪个 PostgreSQL 使用最多(并排除另一个)。

【讨论】:

  • 谢谢。就null 的情况而言,该列具有not null 约束。
  • @orryowr,没关系,如果没有foo_id = 5 的注册(例如没有投票),例如,SUM 也将返回null。顺便说一句,如果有null,SUM 将忽略那些(如count)而不返回null。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-07
  • 2012-11-02
  • 1970-01-01
  • 2021-03-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多