MySQL SUM 查询速度极慢
MySQL SUM Query is extremely slow
有一个名为 transactions
的 table,大约有 600 万行。下面的查询计算当前用户余额。这是我启用 slow_query_log = 'ON'
后的日志:
# Time: 170406 9:51:48
# User@Host: root[root] @ [xx.xx.xx.xx]
# Thread_id: 13 Schema: main_db QC_hit: No
# Query_time: 38.924823 Lock_time: 0.000034 Rows_sent: 1 Rows_examined: 773550
# Rows_affected: 0
SET timestamp=1491456108;
SELECT SUM(`Transaction`.`amount`) as total
FROM `main_db`.`transactions` AS `Transaction`
WHERE `Transaction`.`user_id` = 1008
AND `Transaction`.`confirmed` = 1
LIMIT 1;
如您所见,它花了 ~38 seconds
!
这里是 transactions
table 解释:
这个查询有时 运行 快(大约 1 秒),有时真的很慢!
如有任何帮助,我们将不胜感激。
P.S:
是 InnoDB 并且 transactions
table 有频繁的 INSERT 和 SELECT 操作。
我尝试 运行 使用 SQL_NO_CACHE
查询,但它仍然时快时慢。
transactions
Table 架构:
CREATE TABLE `transactions` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`ref_id` varchar(40) COLLATE utf8_persian_ci NOT NULL,
`payment_id` tinyint(3) unsigned NOT NULL,
`amount` decimal(10,1) NOT NULL,
`created` datetime NOT NULL,
`private_note` varchar(6000) COLLATE utf8_persian_ci NOT NULL,
`public_note` varchar(200) COLLATE utf8_persian_ci NOT NULL,
`confirmed` tinyint(3) NOT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=13133663 DEFAULT CHARSET=utf8 COLLATE=utf8_persian_ci
MySQL 运行 在具有 12GB RAM 和 9 个逻辑 CPU 内核的 VPS 上运行。
这是 my.cnf
的一部分:
# * InnoDB
#
# InnoDB is enabled by default with a 10MB datafile in /var/lib/mysql/.
# Read the manual for more InnoDB related options. There are many!
default_storage_engine = InnoDB
# you can't just change log file size, requires special procedure
innodb_buffer_pool_size = 9G
innodb_log_buffer_size = 8M
innodb_file_per_table = 1
innodb_open_files = 400
innodb_io_capacity = 400
innodb_flush_method = O_DIRECT
innodb_thread_concurrency = 0
innodb_read_io_threads = 64
innodb_write_io_threads = 64
# Instead of skip-networking the default is now to listen only on
# localhost which is more compatible and is not less secure.
#bind-address = 127.0.0.1
#
# * Fine Tuning
#
max_connections = 500
connect_timeout = 5
wait_timeout = 600
max_allowed_packet = 16M
thread_cache_size = 128
sort_buffer_size = 4M
bulk_insert_buffer_size = 16M
tmp_table_size = 32M
max_heap_table_size = 32M
您可以尝试的一件事是添加一个复合索引,看看它是否加速了查询的 select 部分:
ALTER TABLE `transactions` ADD INDEX `user_confirmed` (`user_id`, `confirmed`);
此外,正如@wajeeh 在评论中指出的那样,LIMIT
子句在这里是不必要的,因为您已经在调用聚合函数。
如果您也可以 post 您问题中的 table 架构,那将会很有帮助。
(很抱歉踩到所有好的评论。我希望我已经添加了足够的内容来证明获得 "Answer" 的理由。)
table中是否有 6M 行?但是 user_id
?
有 773K 行
9GB buffer_pool? table 大约是 4GB 的数据?所以它适合 buffer_pool 如果没有其他东西可以解决它。 (SHOW TABLE STATUS
并勾选 "Data_length"。)
现有的 INDEX(user_id)
可能是 20MB,易于缓存。
如果 user_ids 充分分散在 table 周围,查询可能需要获取几乎每 16KB 的数据块。因此,具有原始索引的原始查询将如下所示:
- 扫描给定
user_id
的索引。这将是全部工作的一小部分。
- 对于索引中的每个条目,(随机)查找记录。这种情况发生了 150 万次。使用 "cold" 缓存,这很容易需要 38 秒或更长时间。哪里的"slow"次重启后不久?或者其他什么东西会耗尽缓存?使用 "warm" 缓存,全部是 CPU(没有 I/O),因此 1 秒是合理的。
如果您更改为最优,"covering"、INDEX(user_id, confirmed, amount)
,情况会有所改变...
- "Covering" 表示整个查询将在索引中执行。 (这个复合索引可能更像是 40MB,但与数据相比仍然很小。)
- 在 "cold" 缓存中,只有 40MB 需要提取——预计比 38s 好得多。
- 在"warm"缓存中(这次只有40MB),半秒后可能运行。
如果 WHERE
条款中也有日期范围,我会推动建立和维护 "Summary table"。这可能会将类似查询的速度提高 10 倍。
如果您添加以 user_id
开头的复合索引,您应该(不是必须)DROP
user_id 上的索引是多余的。 (不掉的话,多半是浪费磁盘space。)
至于在生产中做...
- 如果您有足够新的 MySQL 版本,
ALTER TABLE ... ALGORITHM=INPLACE ...
,这对 adding/dropping 索引是可行的,影响最小。
- 对于旧版本,请参阅
pt-online-schema-change
。它要求没有其他触发器,并且停机时间非常短。触发器负责 200 writes/minute 'transparently'.
ALGORITHM=INPLACE
已添加到 MySQL 5.6 和 MariaDB 10.0。
(是的,我正在添加 另一个 答案。理由:它以不同的方式解决了根本问题。)
潜在的问题似乎是有一个不断增长的 "transaction" table 从中导出各种统计数据,例如 SUM(amount)
。随着 table(s) 的增长,这种性能只会越来越差。
本答案的基础是通过两种方式查看数据:"History" 和 "Current"。 Transactions
是历史。新的 table 将是每个用户的 Current
总数。但我看到有多种方法可以做到这一点。每个都涉及某种形式的小计,以避免添加 773K 行来获得答案。
- 传统的银行业务方式...每晚统计一天的
Transactions
并将它们添加到 Current
。
- 实体化视图方式...每次向
Transactions
添加一行,递增Current
。
- 混合:将每日小计保存在 "Summary Table" 中。将这些小计加起来得到昨晚的
SUM
。
我的博客 Summary Tables 上有更多讨论。
请注意,银行或混合方式的最新余额有点棘手:
- 获取昨晚的金额
- 添加当天发生的任何交易。
任何一种方法都比为用户扫描所有 773K 行快很多,但代码会更复杂。
有一个名为 transactions
的 table,大约有 600 万行。下面的查询计算当前用户余额。这是我启用 slow_query_log = 'ON'
后的日志:
# Time: 170406 9:51:48
# User@Host: root[root] @ [xx.xx.xx.xx]
# Thread_id: 13 Schema: main_db QC_hit: No
# Query_time: 38.924823 Lock_time: 0.000034 Rows_sent: 1 Rows_examined: 773550
# Rows_affected: 0
SET timestamp=1491456108;
SELECT SUM(`Transaction`.`amount`) as total
FROM `main_db`.`transactions` AS `Transaction`
WHERE `Transaction`.`user_id` = 1008
AND `Transaction`.`confirmed` = 1
LIMIT 1;
如您所见,它花了 ~38 seconds
!
这里是 transactions
table 解释:
这个查询有时 运行 快(大约 1 秒),有时真的很慢!
如有任何帮助,我们将不胜感激。
P.S:
是 InnoDB 并且 transactions
table 有频繁的 INSERT 和 SELECT 操作。
我尝试 运行 使用 SQL_NO_CACHE
查询,但它仍然时快时慢。
transactions
Table 架构:
CREATE TABLE `transactions` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`ref_id` varchar(40) COLLATE utf8_persian_ci NOT NULL,
`payment_id` tinyint(3) unsigned NOT NULL,
`amount` decimal(10,1) NOT NULL,
`created` datetime NOT NULL,
`private_note` varchar(6000) COLLATE utf8_persian_ci NOT NULL,
`public_note` varchar(200) COLLATE utf8_persian_ci NOT NULL,
`confirmed` tinyint(3) NOT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=13133663 DEFAULT CHARSET=utf8 COLLATE=utf8_persian_ci
MySQL 运行 在具有 12GB RAM 和 9 个逻辑 CPU 内核的 VPS 上运行。
这是 my.cnf
的一部分:
# * InnoDB
#
# InnoDB is enabled by default with a 10MB datafile in /var/lib/mysql/.
# Read the manual for more InnoDB related options. There are many!
default_storage_engine = InnoDB
# you can't just change log file size, requires special procedure
innodb_buffer_pool_size = 9G
innodb_log_buffer_size = 8M
innodb_file_per_table = 1
innodb_open_files = 400
innodb_io_capacity = 400
innodb_flush_method = O_DIRECT
innodb_thread_concurrency = 0
innodb_read_io_threads = 64
innodb_write_io_threads = 64
# Instead of skip-networking the default is now to listen only on
# localhost which is more compatible and is not less secure.
#bind-address = 127.0.0.1
#
# * Fine Tuning
#
max_connections = 500
connect_timeout = 5
wait_timeout = 600
max_allowed_packet = 16M
thread_cache_size = 128
sort_buffer_size = 4M
bulk_insert_buffer_size = 16M
tmp_table_size = 32M
max_heap_table_size = 32M
您可以尝试的一件事是添加一个复合索引,看看它是否加速了查询的 select 部分:
ALTER TABLE `transactions` ADD INDEX `user_confirmed` (`user_id`, `confirmed`);
此外,正如@wajeeh 在评论中指出的那样,LIMIT
子句在这里是不必要的,因为您已经在调用聚合函数。
如果您也可以 post 您问题中的 table 架构,那将会很有帮助。
(很抱歉踩到所有好的评论。我希望我已经添加了足够的内容来证明获得 "Answer" 的理由。)
table中是否有 6M 行?但是 user_id
?
9GB buffer_pool? table 大约是 4GB 的数据?所以它适合 buffer_pool 如果没有其他东西可以解决它。 (SHOW TABLE STATUS
并勾选 "Data_length"。)
现有的 INDEX(user_id)
可能是 20MB,易于缓存。
如果 user_ids 充分分散在 table 周围,查询可能需要获取几乎每 16KB 的数据块。因此,具有原始索引的原始查询将如下所示:
- 扫描给定
user_id
的索引。这将是全部工作的一小部分。 - 对于索引中的每个条目,(随机)查找记录。这种情况发生了 150 万次。使用 "cold" 缓存,这很容易需要 38 秒或更长时间。哪里的"slow"次重启后不久?或者其他什么东西会耗尽缓存?使用 "warm" 缓存,全部是 CPU(没有 I/O),因此 1 秒是合理的。
如果您更改为最优,"covering"、INDEX(user_id, confirmed, amount)
,情况会有所改变...
- "Covering" 表示整个查询将在索引中执行。 (这个复合索引可能更像是 40MB,但与数据相比仍然很小。)
- 在 "cold" 缓存中,只有 40MB 需要提取——预计比 38s 好得多。
- 在"warm"缓存中(这次只有40MB),半秒后可能运行。
如果 WHERE
条款中也有日期范围,我会推动建立和维护 "Summary table"。这可能会将类似查询的速度提高 10 倍。
如果您添加以 user_id
开头的复合索引,您应该(不是必须)DROP
user_id 上的索引是多余的。 (不掉的话,多半是浪费磁盘space。)
至于在生产中做...
- 如果您有足够新的 MySQL 版本,
ALTER TABLE ... ALGORITHM=INPLACE ...
,这对 adding/dropping 索引是可行的,影响最小。 - 对于旧版本,请参阅
pt-online-schema-change
。它要求没有其他触发器,并且停机时间非常短。触发器负责 200 writes/minute 'transparently'.
ALGORITHM=INPLACE
已添加到 MySQL 5.6 和 MariaDB 10.0。
(是的,我正在添加 另一个 答案。理由:它以不同的方式解决了根本问题。)
潜在的问题似乎是有一个不断增长的 "transaction" table 从中导出各种统计数据,例如 SUM(amount)
。随着 table(s) 的增长,这种性能只会越来越差。
本答案的基础是通过两种方式查看数据:"History" 和 "Current"。 Transactions
是历史。新的 table 将是每个用户的 Current
总数。但我看到有多种方法可以做到这一点。每个都涉及某种形式的小计,以避免添加 773K 行来获得答案。
- 传统的银行业务方式...每晚统计一天的
Transactions
并将它们添加到Current
。 - 实体化视图方式...每次向
Transactions
添加一行,递增Current
。 - 混合:将每日小计保存在 "Summary Table" 中。将这些小计加起来得到昨晚的
SUM
。
我的博客 Summary Tables 上有更多讨论。
请注意,银行或混合方式的最新余额有点棘手:
- 获取昨晚的金额
- 添加当天发生的任何交易。
任何一种方法都比为用户扫描所有 773K 行快很多,但代码会更复杂。