【问题标题】:Optimal query to fetch a cumulative sum in MySQL在 MySQL 中获取累积和的最佳查询
【发布时间】:2011-03-09 20:31:54
【问题描述】:

什么是在 MySQL 中获取累积和的“正确”查询?

我有一个表格,我在其中保存有关文件的信息,一列列表包含文件的大小(以字节为单位)。 (实际文件保存在磁盘上的某个地方)

我想得到这样的累积文件大小:

+------------+---------+--------+----------------+
| fileInfoId | groupId | size   | cumulativeSize |
+------------+---------+--------+----------------+
|          1 |       1 | 522120 |         522120 |
|          2 |       2 | 316042 |         316042 |
|          4 |       2 | 711084 |        1027126 |
|          5 |       2 | 697002 |        1724128 |
|          6 |       2 | 663425 |        2387553 |
|          7 |       2 | 739553 |        3127106 |
|          8 |       2 | 700938 |        3828044 |
|          9 |       2 | 695614 |        4523658 |
|         10 |       2 | 744204 |        5267862 |
|         11 |       2 | 609022 |        5876884 |
|        ... |     ... |    ... |            ... |
+------------+---------+--------+----------------+
20000 rows in set (19.2161 sec.)

现在,我使用以下查询来获得上述结果

SELECT
  a.fileInfoId
, a.groupId
, a.size
, SUM(b.size) AS cumulativeSize
FROM fileInfo AS a
LEFT JOIN fileInfo AS b USING(groupId)
WHERE a.fileInfoId >= b.fileInfoId
GROUP BY a.fileInfoId
ORDER BY a.groupId, a.fileInfoId

但是,我的解决方案非常缓慢。 (大约 19 秒没有缓存)。

解释给出以下执行细节

+----+--------------+-------+-------+-------------------+-----------+---------+----------------+-------+-------------+
| id | select_type  | table | type  | possible_keys     | key       | key_len | ref            | rows  | Extra       |
+----+--------------+-------+-------+-------------------+-----------+---------+----------------+-------+-------------+
|  1 | SIMPLE       |     a | index | PRIMARY,foreignId | PRIMARY   |       4 | NULL           | 14905 |             |
|  1 | SIMPLE       |     b | ref   | PRIMARY,foreignId | foreignId |       4 | db.a.foreignId |    36 | Using where |
+----+--------------+-------+-------+-------------------+-----------+---------+----------------+-------+-------------+



我的问题是:

如何优化上述查询?



更新
我已经更新了问题,以提供表结构和用 20,000 条记录测试数据填充表的过程。

CREATE TABLE `fileInfo` (
  `fileInfoId` int(10) unsigned NOT NULL AUTO_INCREMENT
, `groupId` int(10) unsigned NOT NULL
, `name` varchar(128) NOT NULL
, `size` int(10) unsigned NOT NULL
, PRIMARY KEY (`fileInfoId`)
, KEY `groupId` (`groupId`)
) ENGINE=InnoDB;

delimiter $$
DROP PROCEDURE IF EXISTS autofill$$
CREATE PROCEDURE autofill()
BEGIN
    DECLARE i INT DEFAULT 0;
    DECLARE gid INT DEFAULT 0;
    DECLARE nam char(20);
    DECLARE siz INT DEFAULT 0;
    WHILE i < 20000 DO
        SET gid = FLOOR(RAND() * 250);
        SET nam = CONV(FLOOR(RAND() * 10000000000000), 20, 36);
        SET siz = FLOOR((RAND() * 1024 * 1024));
        INSERT INTO `fileInfo` (`groupId`, `name`, `size`) VALUES(gid, nam, siz);
        SET i = i + 1;
    END WHILE;
END;$$
delimiter ;

CALL autofill();

关于可能重复的问题
Forgotten Semicolon 链接的The question 不是同一个问题。我的问题有额外的专栏。由于这个额外的 groupId 列,那里接受的答案不适用于我的问题。 (也许它可以适应工作,但我不知道如何,因此我的问题)

【问题讨论】:

  • 我没有时间,但有可能(使用 ROLLUP)[dev.mysql.com/doc/refman/5.1/en/group-by-modifiers.html] 将做你想做的事,而无需自我加入......
  • @OMG Ponies 如果你有时间回答,我会很想看看 With Rollup 解决方案。
  • 不能保证它会更快,但如果您可以使用脚本更新问题以创建表格并填充它 - 这会有所帮助。
  • WITH ROLLUP 非常快。它将进行汇总,在对每个组求和后添加一个总计行,但我不知道如何让它产生一个运行总计。如果可以做到,那将是一个很好的解决方案。

标签: sql mysql query-optimization


【解决方案1】:

您可以使用变量 - 它比任何连接都快:

SELECT
    id,
    size,
    @total := @total + size AS cumulativeSize,
FROM table, (SELECT @total:=0) AS t;

这是一个在运行 Debian 5.0 的 128MB RAM 奔腾 III 上的快速测试案例:

创建表:

DROP TABLE IF EXISTS `table1`;

CREATE TABLE `table1` (
    `id` int(11) NOT NULL auto_increment,
    `size` int(11) NOT NULL,
    PRIMARY KEY  (`id`)
) ENGINE=InnoDB;

用 20,000 个随机数填充:

DELIMITER //
DROP PROCEDURE IF EXISTS autofill//
CREATE PROCEDURE autofill()
BEGIN
    DECLARE i INT DEFAULT 0;
    WHILE i < 20000 DO
        INSERT INTO table1 (size) VALUES (FLOOR((RAND() * 1000)));
        SET i = i + 1;
    END WHILE;
END;
//
DELIMITER ;

CALL autofill();

检查行数:

SELECT COUNT(*) FROM table1;

+----------+
| COUNT(*) |
+----------+
|    20000 |
+----------+

运行累计总数查询:

SELECT
    id,
    size,
    @total := @total + size AS cumulativeSize
FROM table1, (SELECT @total:=0) AS t;

+-------+------+----------------+
|    id | size | cumulativeSize |
+-------+------+----------------+
|     1 |  226 |            226 |
|     2 |  869 |           1095 |
|     3 |  668 |           1763 |
|     4 |  733 |           2496 |
...
| 19997 |  966 |       10004741 |
| 19998 |  522 |       10005263 |
| 19999 |  713 |       10005976 |
| 20000 |    0 |       10005976 |
+-------+------+----------------+
20000 rows in set (0.07 sec)

更新

我错过了原始问题中按 groupId 进行的分组,这确实使事情变得有些棘手。然后我写了一个使用临时表的解决方案,但我不喜欢它——它很乱而且过于复杂。我离开并进行了更多研究,并提出了更简单、更快的方法。

我不能为此声明所有功劳——事实上,我几乎不能声明任何功劳,因为它只是 Emulate row number 的修改版本 Common MySQL Queries

它非常简单、优雅且非常快速:

SELECT fileInfoId, groupId, name, size, cumulativeSize
FROM (
    SELECT
        fileInfoId,
        groupId,
        name,
        size,
        @cs := IF(@prev_groupId = groupId, @cs+size, size) AS cumulativeSize,
        @prev_groupId := groupId AS prev_groupId
    FROM fileInfo, (SELECT @prev_groupId:=0, @cs:=0) AS vars
    ORDER BY groupId
) AS tmp;

如果您不介意返回 prev_groupID 列,可以删除外部的 SELECT ... AS tmp。我发现没有它,它的运行速度会稍微快一些。

这是一个简单的测试用例:

INSERT INTO `fileInfo` VALUES
( 1, 3, 'name0', '10'),
( 5, 3, 'name1', '10'),
( 7, 3, 'name2', '10'),
( 8, 1, 'name3', '10'),
( 9, 1, 'name4', '10'),
(10, 2, 'name5', '10'),
(12, 4, 'name6', '10'),
(20, 4, 'name7', '10'),
(21, 4, 'name8', '10'),
(25, 5, 'name9', '10');

SELECT fileInfoId, groupId, name, size, cumulativeSize
FROM (
    SELECT
        fileInfoId,
        groupId,
        name,
        size,
        @cs := IF(@prev_groupId = groupId, @cs+size, size) AS cumulativeSize,
        @prev_groupId := groupId AS prev_groupId
    FROM fileInfo, (SELECT @prev_groupId := 0, @cs := 0) AS vars
    ORDER BY groupId
) AS tmp;

+------------+---------+-------+------+----------------+
| fileInfoId | groupId | name  | size | cumulativeSize |
+------------+---------+-------+------+----------------+
|          8 |       1 | name3 |   10 |             10 |
|          9 |       1 | name4 |   10 |             20 |
|         10 |       2 | name5 |   10 |             10 |
|          1 |       3 | name0 |   10 |             10 |
|          5 |       3 | name1 |   10 |             20 |
|          7 |       3 | name2 |   10 |             30 |
|         12 |       4 | name6 |   10 |             10 |
|         20 |       4 | name7 |   10 |             20 |
|         21 |       4 | name8 |   10 |             30 |
|         25 |       5 | name9 |   10 |             10 |
+------------+---------+-------+------+----------------+

以下是 20,000 行表中最后几行的示例:

|      19481 |     248 | 8CSLJX22RCO | 1037469 |       51270389 |
|      19486 |     248 | 1IYGJ1UVCQE |  937150 |       52207539 |
|      19817 |     248 | 3FBU3EUSE1G |  616614 |       52824153 |
|      19871 |     248 | 4N19QB7PYT  |  153031 |       52977184 |
|        132 |     249 | 3NP9UGMTRTD |  828073 |         828073 |
|        275 |     249 | 86RJM39K72K |  860323 |        1688396 |
|        802 |     249 | 16Z9XADLBFI |  623030 |        2311426 |
...
|      19661 |     249 | ADZXKQUI0O3 |  837213 |       39856277 |
|      19870 |     249 | 9AVRTI3QK6I |  331342 |       40187619 |
|      19972 |     249 | 1MTAEE3LLEM | 1027714 |       41215333 |
+------------+---------+-------------+---------+----------------+
20000 rows in set (0.31 sec)

【讨论】:

  • +1:在副本中看到了这个,我只使用变量来进行排名/窗口功能......
  • 看起来很有希望,会尝试一下:)
  • 抱歉,我没有发现重复的帖子 - 我在睡觉前匆匆回答了这个问题。我有点喜欢 SQL 查询中的变量,并尽可能将它们硬塞进去;-)
  • 不幸的是,您的答案会计算所有文件的累积大小,而不是按 foreignId (groupId) 分组。 (另见更新的问题)
  • @Jacco:啊,是的,这确实有点像扳手。你能创建一个临时表吗?如果是这样,我也许能够想出一个快速的解决方案。
【解决方案2】:

我认为 MySQL 只使用表中的一个索引。在这种情况下,它选择了 foreignId 上的索引。

添加一个包含 primaryId 和 foreignId 的覆盖复合索引。

【讨论】:

  • 当主键已经是聚集索引时,这将是多余的。
  • @OMG,我认为问题在于 MySQL 仅使用表上的索引之一。如果我错了,请告诉我。
  • 你是对的 - MySQL 每个语句只使用一个索引(解释中的行)。取决于性能,但鉴于 pk 已经是一个聚集索引 - 我会先为 fk 尝试一个单独的索引。
猜你喜欢
  • 2015-10-26
  • 1970-01-01
  • 2020-07-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-12-31
  • 1970-01-01
  • 2011-11-29
相关资源
最近更新 更多