选择查询中重复聚合函数的性能

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 谢谢,您的回答帮助我了解了数据的布局方式!我不认为 N​​ULL 会是一个问题,因为他提供的表模式将其实例化为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 子句问题。 - 重复

MYSQL查询--聚合函数查询

如何在使用 SQLCLR 用户定义的聚合函数 (UDA) 时提高查询性能

MySQL聚合函数

列的原因在选择列表中无效,因为它不包含在聚合函数或 GROUP BY 子句中[重复]

使用聚合函数而不选择聚合列且不使用子查询