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 的数据块。因此,具有原始索引的原始查询将如下所示:

  1. 扫描给定 user_id 的索引。这将是全部工作的一小部分。
  2. 对于索引中的每个条目,(随机)查找记录。这种情况发生了 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 上有更多讨论。

请注意,银行或混合方式的最新余额有点棘手:

  1. 获取昨晚的金额
  2. 添加当天发生的任何交易。

任何一种方法都比为用户扫描所有 773K 行快很多,但代码会更复杂。