Оптимальный запрос для получения накопительной суммы в MySQL
Что такое "правильный" запрос для получения кумулятивной суммы в 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 |
+----+--------------+-------+-------+-------------------+-----------+---------+----------------+-------+-------------+
Мой вопрос:
Как я могу оптимизировать вышеуказанный запрос?
Обновить
Я обновил вопрос, чтобы предоставить структуру таблицы и процедуру для заполнения таблицы 20000 тестовыми данными записей.
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, это не тот же вопрос. У моего вопроса есть дополнительный столбец. из-за этого дополнительного столбца groupId принятый ответ там не работает для моей проблемы. (возможно, его можно адаптировать к работе, но я не знаю как, отсюда и мой вопрос)
2 ответа
Вы можете использовать переменную - это намного быстрее, чем любое соединение:
SELECT
id,
size,
@total := @total + size AS cumulativeSize,
FROM table, (SELECT @total:=0) AS t;
Вот быстрый тестовый пример на Pentium III с 128 МБ ОЗУ под управлением Debian 5.0:
Создайте таблицу:
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;
Заполните 20000 случайных чисел:
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, и это определенно усложнило ситуацию. Затем я написал решение, в котором использовалась временная таблица, но она мне не понравилась - она была грязной и чрезмерно сложной. Я ушел и сделал еще несколько исследований, и придумал что-то гораздо проще и быстрее.
Я не могу взять на себя всю ответственность за это - на самом деле, я почти не могу претендовать на это вообще, так как это всего лишь модифицированная версия номера строки эмуляции из 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;
Вы можете удалить внешний SELECT ... AS tmp
если ты не против prev_groupID
столбец возвращается. Я обнаружил, что без него он работает немного быстрее.
Вот простой тестовый пример:
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 |
+------------+---------+-------+------+----------------+
Вот пример нескольких последних строк из таблицы строк 20000:
| 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)
Я думаю, что MySQL использует только один из индексов в таблице. В данном случае это выбор индекса по Fore ignId.
Добавьте составной индекс покрытия, который включает в себя primaryId и foreignId.