选择查询中重复聚合函数的性能
Posted
技术标签:
【中文标题】选择查询中重复聚合函数的性能【英文标题】:Performance of a duplicate agregate function in a select query 【发布时间】:2014-05-05 15:25:32 【问题描述】:我有一个 SQL 查询(在 mysql 中),它选择一个总数和多少已完成任务的计数:
SELECT
count(*) as total, IF(SUM(NOT `completed`) IS NULL,0,SUM(NOT `completed`)) as incomplete
FROM
tasks
表格可以做成这样的:
CREATE TABLE `tasks` (
`clave` int(11) NOT NULL AUTO_INCREMENT,
`completed` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'If it is 0 isn\'t completed, otherwise is completed',
PRIMARY KEY (`clave`)
) ENGINE=InnoDB;
您可以观察到,在查询中我使用了两次SUM(NOT completed)
,一次检查它是否会返回 null(并返回 0),另一次返回计数(当它不为 null 时)。 SUM()
函数返回null
如果表(或查询)为空,即表根本没有行。
当您尝试求和两次时,我认为 MySQL 会计算两次总和。
我已经测试过是否可以为该列设置一个别名,然后在 IF 中使用它,这样 mysql 就不需要重新计算它了。
并且作为一项要求,不完整的列在任何情况下都不得为空(例如,如果表任务为空)。
我的问题是,这有效吗? MySQL 每次都需要重新计算总和还是记住它?
【问题讨论】:
您使用哪一列来检查“已完成”或未“完成”? 到目前为止,查询分析器告诉您有关您的查询的哪些信息?尝试使用EXPLAIN
(dev.mysql.com/doc/refman/5.0/en/explain.html)
@JosephB completed
是表 tasks
的一列布尔类型(小整数)。
@PhoneixS 感谢您的澄清!
@cfeduke 我不知道如何使用/解释它。
【参考方案1】:
这可能是一种更好的方法,因为您对性能
进行的计算更少使用 Richard 发布的架构 - 请参阅 FIDDLE
SELECT
total,
num_complete,
total - num_complete as num_incomplete
FROM(
SELECT
COUNT(*) as total,
SUM(IF(t.completed > 0, 1, 0)) as num_complete
FROM status_log t
) as t
根据 OP 请求检查空表
SELECT
total,
num_complete,
total - num_complete as num_incomplete
FROM(
SELECT
COUNT(*) as total,
IF(COUNT(*) > 0, SUM(IF(t.completed > 0, 1, 0)), 0) as num_complete
FROM status_log t
) as t
你应该用另一种编程语言检查空表或空表 ...一般来说,应该使用 SQL 从表中查询..而不是当它为空时...如果它完全是空的,那么您应该在以另一种编程语言运行此查询时检查空响应。这将大大提高它的性能。
【讨论】:
为什么要使用子查询而不是 CASE WHEN? 我写答案时不知道数据布局是什么 - 请参阅时间戳 - 所以我只是提供了一种方法来查询不完整的多次而不计算SUM
多次..如果我改变了我的答案,那只会抄袭别人:)
使用第二个查询,您仍然会遇到 SUM 返回 null 的问题。
@JohnRuddell 这是一个很好的捷径,基于您正在使用的数据集的假设。确实,诸如不完整/完整或活动/非活动或开/关之类的概念具有隐含的配对值。 @PhoneixS 实际上间接提到了第三个值:Null。为了使total - num_complete
语句起作用,我们需要知道Null
值是否有意义,或者它是否只是一个被忽略的值。
@RichardPascual 谢谢,您的回答帮助我了解了数据的布局方式!我不认为 NULL 会是一个问题,因为他提供的表模式将其实例化为not null default 0
【参考方案2】:
效率(或缺乏效率)将更多地与表上存在的索引以及存储引擎本身有关。同样,结果是否存储在缓存中将更多地与存储引擎有关,而不是与您正在编写的语句有关。
如果我在基于 INNODB 的存储引擎上编写此代码,我会执行以下操作:
SELECT
count(*) as total,
SUM(CASE WHEN completed = 0 OR completed IS NULL THEN 1 ELSE 0 END) AS incomplete
FROM tasks;
为了做到这一点,我会索引我的“已完成”列。
我将从“IF”更改为 case-when 的原因主要是为了实现代码可移植性。如果需要,CASE WHEN 将更容易迁移到其他数据库。
此外,完成时的索引将允许此查询简单地评估索引,而不是表值本身。使用 LRU,这应该会给你带来很多效率。
【讨论】:
Richard 的回答(上图)添加了不完整的倒数...所以 SUM(CASE WHEN 已完成 = 0 OR completed IS NULL THEN 1 ELSE 0 END) AS不完整且 SUM(CASE WHEN 已完成 = 1 THEN 1 ELSE 0 END) 作为完成。这是一个很好的接触。如果您也有任何理由跟踪它,我会添加它。null
问题不在completed
列中,而是在SUM()
函数中,好像任务为空SUM()
返回null
。
我会用 php 来处理。同样,我在这里要强调的最重要的一点是索引已完成。如果你不这样做,那张表就会变大,你手上就会有一个非常慢的查询。
这就是我在我的问题上与 cmets 一起去的地方......如果它是空的,则不应运行查询。如果它是空的并且运行查询,那么你应该检查另一个编程用于所有意图和目的的 null 值或 0 或数字 > 0 的语言(OP 一直在使用)
别忘了接受答案!我真的不在乎你是否不接受我的……但问一个问题然后不接受答案不是一个好习惯:)【参考方案3】:
这是使用CASE
语句的解决方案。您可以从单个 SQL 语句中获取这两个聚合值。第二个查询示例显示了与直接聚合查询语句相比,CASE
语句如何为您提供旋转输出:
[SQL Fiddle][1]
MySQL 5.5.32 架构设置:
CREATE TABLE status_log
(
id int auto_increment primary key,
type varchar(20),
status varchar(30)
);
INSERT INTO status_log
(type, status)
VALUES
('Alpha', 'COMPLETE'),
('Bravo', 'INCOMPLETE'),
('Charlie', 'INCOMPLETE'),
('Delta', 'COMPLETE'),
('Echo', 'COMPLETE'),
('Foxtrot', 'INCOMPLETE'),
('Golf', 'COMPLETE'),
('Hotel', 'COMPLETE')
查询 1:
SELECT count(1) as count_by_status, status
FROM status_log
GROUP BY status
Results:
| COUNT_BY_STATUS | STATUS |
|-----------------|------------|
| 5 | COMPLETE |
| 3 | INCOMPLETE |
查询 2:
SELECT count(*) as total_count,
sum(case when status = 'COMPLETE' then 1
else 0 end) as completed_count,
sum(case when status = 'INCOMPLETE' then 1
else 0 end) as incomplete_count
FROM status_log
Results:
| TOTAL_COUNT | COMPLETED_COUNT | INCOMPLETE_COUNT |
|-------------|-----------------|------------------|
| 8 | 5 | 3 |
【讨论】:
是的。我更喜欢 Richard 的解决方案(支持 Richard 添加 Completed_count。我也应该这样做)。我仍然会添加一个关于状态的索引。这将帮助您避免表扫描。 问题是您仍在使用 SUM() 如果表没有行,则返回 null。 另外请注意,我想知道 MySQL 是否需要重新计算重复表达式的列值。 Phoneix:MySQL 是否需要重新计算列值的答案实际上并不是这里最重要的问题。顺便说一句,这个答案是“也许”,这取决于我们在这里可以深入探讨的更多事情。您的查询是否会重新计算实际上并不是这里要问的最重要的问题。更便宜的计算并不像更便宜的计算那样有问题。在完成时获取索引,是否必须重新计算它并不重要。【参考方案4】:如果做成内联视图,可以从查询中选择多次,如下:
SELECT total, incomplete, incomplete
FROM
(
SELECT
count(*) as total, IF(SUM(NOT `completed`) IS NULL,0,SUM(NOT `completed`)) as incomplete
FROM
tasks
) incomplete_count;
如果您多次从子查询或内联视图中选择,则 MySQL 不必重新计算。此外,您可以在外部查询中执行任何其他操作/过滤器。
【讨论】:
我认为你没有正确阅读这个问题。这不是 OP 所要求的 @JohnRuddell 感谢您的反馈。我已经添加了一个简短的解释。 anytime :) 在您的子选择中,您正在执行SUM(NOT completed)
两次.. OP 询问这是否有效,或者是否可以不执行两次。看看我的回答。
@JosephB 是的,请参阅 John Ruddell 的回答。您仍在复制 SUM(...) 语句,因此您已将问题转换为子查询。
这个答案的编写方式和您的原始查询@PhoneixS mysql 将在您每次编写它时计算总和,并且它不会存储信息以供重用,除非您添加别名和查询离开那个别名【参考方案5】:
感谢 John Ruddell 和 PhoneixS 的澄清。这是使用 CASE WHEN 的版本:
select
count(*),
sum(
case completed
when 1 then 1
else 0
end) 'completed',
sum(
case when completed is null then 1
else 0
end) 'incomplete'
from tasks;
SQL Fiddle
【讨论】:
以上是关于选择查询中重复聚合函数的性能的主要内容,如果未能解决你的问题,请参考以下文章
查询没有重复和聚合函数或 GROUP BY 子句问题。 - 重复
如何在使用 SQLCLR 用户定义的聚合函数 (UDA) 时提高查询性能