简体   繁体   English

MySQL SUM查询非常慢

[英]MySQL SUM Query is extremely slow

There is a table called transactions with ~6 million rows. 有一个表称为具有约600万行的transactions Below query counts the current user balance. 下面的查询计算当前用户余额。 Here is the log after I enabled slow_query_log = 'ON' : 这是我启用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;

As you can see it took ~38 seconds ! 如您所见,它花费了~38 seconds

Here is transactions table EXPLAIN : 这是transactions表EXPLAIN:

在此处输入图片说明

This query sometimes run fast (about ~1 second) and sometimes really slow! 该查询有时运行很快(大约1秒左右),有时却非常慢!

Any help would be great appreciated. 任何帮助将不胜感激。

PS: PS:

It's InnoDB and transactions table has frequent INSERT and SELECT operations. 它的InnoDB和transactions表具有频繁的INSERT和SELECT操作。

I tried running the query with SQL_NO_CACHE , but it is still sometimes fast, sometimes slow. 我尝试使用SQL_NO_CACHE运行查询,但有时还是很快,有时还是很慢。

transactions Table Schema : transactions表架构:

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 is running on a VPS with 12GB RAM and 9 Logical CPU cores. MySQL在具有12GB RAM和9个逻辑CPU内核的VPS上运行。

Here is a part of my.cnf : 这是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

(Sorry to step on all the good comments. I hope I have added enough to justify claiming an "Answer".) (对不起,我要发表所有好的评论。我希望我已经添加足够的内容来证明要求“答案”。)

Are there 6M rows in the table? 表格中是否有600万行? But 773K rows with that user_id ? 但是,具有该user_id 773K行呢?

9GB buffer_pool? 9GB buffer_pool? The table is about 4GB of data? 该表大约有4GB的数据? So it fits in the buffer_pool if there is not much else to bump it out. 因此,如果没有其他需要解决的问题,它就适合buffer_pool。 ( SHOW TABLE STATUS and check "Data_length".) SHOW TABLE STATUS并检查“ Data_length”。)

The existing INDEX(user_id) might be 20MB, easily cachable. 现有的INDEX(user_id)可能为20MB,很容易实现。

If the user_ids are sufficiently scattered around the table, the query probably needs to fetch virtually every 16KB block of the data. 如果user_ids充分分散在表周围,则查询可能实际上需要获取数据的每个16KB块。 So, the original query with original index will go something like: 因此,具有原始索引的原始查询将如下所示:

  1. scan the index for the given user_id . 扫描给定user_id的索引。 This will be a minor part of the total effort. 这将是总努力的一小部分。
  2. For each entry in the index, look up (randomly) the record. 对于索引中的每个条目,(随机)查找记录。 This happens 1.5M times. 这发生了150万次。 With a "cold" cache, this would easily take 38 seconds or more. 使用“冷”缓存,这很容易花费38秒或更长时间。 Where the "slow" times soon after a restart? 重新启动后不久的“慢速”时间在哪里? Or something else that blows out the cache? 还是其他东西耗尽了缓存? With a "warm" cache, it is all CPU (no I/O), hence 1 second is reasonable. 使用“热”缓存时,它全部是CPU(无I / O),因此1秒钟是合理的。

If you change to the optimal, "covering", INDEX(user_id, confirmed, amount) , things change some... 如果更改为最佳“覆盖” INDEX(user_id, confirmed, amount) ,则情况会有所变化...

  • "Covering" means that the entire query would be performed in the index. “覆盖”是指整个查询将在索引中执行。 (This composite index might be more like 40MB, but this is still very small compared to the data.) (此复合索引可能更像40MB,但与数据相比仍然很小。)
  • In a "cold" cache, only 40MB would need fetching -- expect much better than 38s. 在“冷”缓存中,仅需要40MB的内容即可获取-比38s好得多。
  • In a "warm" cache (this time only the 40MB), it might run in half a second. 在“热”缓存(这次只有40MB)中,它可能会在半秒钟内运行。

If there were also a date range in the WHERE clause, I would push for building and maintaining a "Summary table". 如果WHERE子句中也有日期范围,我将推动构建和维护“摘要表”。 This might speed up similar queries by a factor of 10. 这样可以将类似的查询速度提高10倍。

If you do add a composite index starting with user_id , you should (not must ) DROP the index on just user_id as being redundant. 如果您添加开头的综合指数user_id ,你应该 (不应该DROP上只是user_id说明为冗余的索引。 (If you don't drop it, it will mostly waste disk space.) (如果不删除它,通常会浪费磁盘空间。)

As for doing it in production... 至于在生产中...

  • If you have a new enough version of MySQL, ALTER TABLE ... ALGORITHM=INPLACE ... , which is feasible for adding/dropping indexes with minimal impact. 如果您有足够新的MySQL版本,请使用ALTER TABLE ... ALGORITHM=INPLACE ... ,这对于添加/删除索引的影响最小。
  • For older versions, see pt-online-schema-change . 对于旧版本,请参阅pt-online-schema-change It requires that there be no other Triggers, and does take a very short downtime. 它要求没有其他触发器,并且停机时间非常短。 The Trigger takes care of the 200 writes/minute 'transparently'. 触发器“透明”地处理了200次写入/分钟。

ALGORITHM=INPLACE was added in MySQL 5.6 and MariaDB 10.0. 在MySQL 5.6和MariaDB 10.0中添加了ALGORITHM=INPLACE

(Yes, I am adding another answer. Justification: It addresses the underlying problem a different way.) (是的,我要添加另一个答案。理由:它以不同的方式解决了潜在的问题。)

The underlying problem seems to be that there is an ever-growing "transaction" table from which is derived various statistics, such as SUM(amount) . 潜在的问题似乎是,有一个不断增长的“交易”表,可以从中获得各种统计信息,例如SUM(amount) The performance of this will only get worse and worse as the table(s) grow. 随着表的增长,这种性能只会越来越差。

The basis for this Answer will be to look at the data in two ways: "History" and "Current". 此答案的基础将以两种方式查看数据:“历史”和“当前”。 Transactions is the History. Transactions就是历史。 A new table would be the Current totals for each User. 一个新表将是每个用户的“ Current总计”。 But I see multiple ways to do that. 但是我看到了多种方法。 Each involves some form of subtotal(s) so as to avoid adding 773K rows to get the answer. 每项都涉及某种形式的小计,以避免添加773K行以获得答案。

  • The traditional banking way... Each night tally up the day's Transactions and add them to Current . 传统的银行业务方式...每天晚上汇总一天的Transactions并将其添加到Current
  • The Materialized View way... Each time a row is added to Transactions , increment Current . 物化视图的方式...每次向Transactions添加一行,都会增加Current
  • Hybrid: Keep daily subtotals in a "Summary Table". 混合:将每日小计保存在“汇总表”中。 Sum those subtotals to get the SUM through last night. 总结这些分类汇总得到SUM经过昨晚。

More discussion in my blog on Summary Tables . 在我的摘要表博客中有更多讨论。

Note that the up-to-the-second balance for the banking or hybrid way is a little tricky: 请注意,银行或混合方式的最新余额有些棘手:

  1. Get last night's amount 获取昨晚的金额
  2. Add any Transactions that occurred during the day. 添加当天发生的所有交易。

Any of the approaches will be a lot faster than scanning all 773K rows for the user, but it will be more complex code. 任何方法会比扫描所有行773K为用户快得多 ,但它会更复杂的代码。

One thing you might try is adding an composite index to see if it speeds up the select part of the query: 您可能要尝试的一件事是添加一个复合索引,以查看它是否可以加快查询的选择部分:

ALTER TABLE `transactions` ADD INDEX `user_confirmed` (`user_id`, `confirmed`);

Also, as @wajeeh pointed out in a comment, the LIMIT clause is unnecessary here since you're already calling an aggregate function. 另外,正如@wajeeh在评论中指出的,在这里LIMIT子句是不必要的,因为您已经在调用聚合函数。

It would be helpful if you can post the table schema in your question as well. 如果您也可以在问题中发布表架构,将很有帮助。

Take a look at this answer Any way to select without causing locking in MySQL? 看看这个答案有没有选择方法而不会导致锁定MySQL?

And this article: Consistent Nonlocking Reads 以及本文: 一致的非锁定读取

In your case as I think and as @billynoah mention, the table have to do many write operations as it is as Log table, So this may help you. 就我而言,就像@billynoah提到的那样,该表必须像Log表一样执行许多写操作,因此这可能对您有所帮助。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM