讨论关于千万行GB级MySQL表的查询性能问题,有牛人来帮忙解答一下吗?

头像
白玉虎
368阅读37评论

Update 20240216
最终使用了Clickhouse彻底一劳永逸解决统计问题,不过是用了一个比较笨的方法。RAW数据还是按行存到MySQL方便按id、datetime之类的方式按行查询。一些需要统计的数据塞进Clickhouse。浪费了磁盘空间(和一些Clickhouse的后台消耗),省了我的生命……

感谢大家给与的回复和建议!Case closed!

因为项目需要,用PHP/MySQL作了一个简易数据管理工具来存储和查询一些数据。开始的时候没觉得什么,结果当数据量变成6300多万条,6.8G的大表(MyISAM)后,事情就变得烦躁了起来……

大概来说,这个数据库存储了多个项目(project)的往来资料,表结构:

CREATE TABLE `records` (
  `serial_finance` bigint unsigned NOT NULL,
  `product_name` mediumint unsigned DEFAULT NULL,
  `trade_time` datetime DEFAULT NULL,
  `account` mediumint unsigned DEFAULT NULL,
  `amount_in` decimal(7,2) NOT NULL DEFAULT '0.00',
  `memo_project` smallint unsigned DEFAULT NULL,
  `memo_shopname` mediumint unsigned DEFAULT NULL,
  KEY `memo_shopname` (`memo_shopname`),
  KEY `product_name` (`product_name`),
  KEY `project_account_search` (`memo_project`,`account`) USING BTREE,
  KEY `trade_time` (`trade_time`),
  KEY `account` (`account`),
  KEY `serial_finance` (`serial_finance`),
  KEY `memo_project` (`memo_project`,`trade_time`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb3

因为所有数据都是从一个个CSV文件里导入的,这还是把所有TEXT类字段都放到别的表排重建立ID后再引用回来后的结果,所以相关字段都是INT类了,否则表应该已经超过20G了。之所以没有使用InnoDB也是考虑到没有频繁的INSERT/UPDATE操作,大多是SELECT。

如果根据索引来检索,在数据量没有很大的情况下尚可容忍,可是因为要经常进行整表计算,没有办法使用索引,又或者纯粹因为数据量太大,只能全表遍历,查询就变成了400多秒。比如:

SELECT memo_project, max(trade_time) as lasttime, sum(amount_in) as sum, avg(amount_in) as avg, max(amount_in) as max, min(amount_in) as min, count(*) AS num FROM records GROUP BY memo_project;

目前我尝试过PARTITIONING来分区,也试过把每个不同的memo_project使用MRG_MYISAM来分表,如下:

CREATE TABLE `records` (
  `serial_finance` bigint unsigned NOT NULL,
  `product_name` mediumint unsigned DEFAULT NULL,
  `trade_time` datetime DEFAULT NULL,
  `account` mediumint unsigned DEFAULT NULL,
  `amount_in` decimal(7,2) NOT NULL DEFAULT '0.00',
  `memo_project` smallint unsigned DEFAULT NULL,
  `memo_shopname` mediumint unsigned DEFAULT NULL,
  KEY `memo_shopname` (`memo_shopname`),
  KEY `product_name` (`product_name`),
  KEY `project_account_search` (`memo_project`,`account`) USING BTREE,
  KEY `trade_time` (`trade_time`),
  KEY `account` (`account`),
  KEY `serial_finance` (`serial_finance`),
  KEY `memo_project` (`memo_project`,`trade_time`)
) ENGINE=MRG_MyISAM DEFAULT CHARSET=utf8mb3 INSERT_METHOD=LAST UNION=(`record_p1`,`record_p2`,`record_p3`,`record_p4`,`record_p5`,`record_p6`,`record_p7`,`record_p8`,`record_p9`,`record_p10`,`record_p11`,`record_p12`,`record_p13`,`record_p14`,`record_p15`,`record_p16`,`record_p17`,`record_p18`,`record_p19`,`record_p20`,`record_p21`)

这样把每个项目分表后单独查询,然后通过PHP进行数据总和排序。

SELECT max(trade_time) as lasttime, sum(amount_in) as sum, avg(amount_in) as avg, max(amount_in) as max, min(amount_in) as min, count(*) AS num FROM record_p1;

分表后,甚至也尝试过用mysqli_poll做成并发处理,不过后来发现因为等待reap_async_query()的所有结果才能聚合数据,实际性能并没有提升,反而因为增加了CPU线程和磁盘IO,性能有所下降。

所以目前看来,无论分区、分表、异步,因为COUNT/SUM/AVG/MAX/MIN都需要对大范围数据进行整个遍历,无论如何性能都没办法再提升了。

当然,我肯定是做了结果缓存的,但有没有什么更好的办法从根本上提升查询速度呢?

讨论话题:
行业&时事
城市:
北京
收藏
举报
加载中…
精选评论
头像
等级1

另外还有个现象,就是使用异步进行并发查询的时候,尽管4个逻辑CPU都100%了,但是磁盘IO并没有任何读取,那么是不是数据和索引已经在内存里了?也就是说,磁盘IO并不是瓶颈,即便把分表或分区放到不同物理硬盘也没有区别?那么唯一的瓶颈就是CPU的速度了?还有别的办法吗?

show_ processlist下,数据应该是读进内存了,有可能是在运算。

是的,processlist中式有的,纯粹就是在运行,时间非常长,并不是卡死。所以我的问题是,即便把数据分块,似乎也没办法提升性能,因为毕竟需要读取的总数摆在那……如此看来,如果硬件条件一致,即便用cluster多几个read节点,性能也不会有所提升,毕竟磁盘IO和内存都不是瓶颈,瓶颈可能纯粹就是CPU不够快……

如果数据变动频率很小,比如一天才变动一次,看下能不能预先算好结果存起来,查询的时候直接用

我有做缓存机制。之前也想过每批插入的时候直接根据统计表的老数据更新一下统计,如果只是SUM/MAX/MIN还好,可是因为有AVG,所以每次还是得遍历整表……

看你语句里面也有 count(*),插入算avg应该也可以直接算的吧

确实……然而随着时间的推移,这个AVG的值应该会越来越不准确,毕竟只有decimal(7,2)。不过好歹也算个解决方法,谢谢!

这个好解决,字段中增加精度,返回使用的时候还是取2位,还有可以每天定时再跑下全量。

AVG这块不需要全表吧,currentAvg= (lastAvg*lastCount+ Δ Value)/(lastCount+ Δ Count)
Δ 表示自从上次统计后的增量

头像
等级1

你先explain一下sql query的plan, 6300多万条应该也不会造成RDBMS卡顿.
MongoDB默认无index, 默认可能有性能问题. 且一般NoSQL读取性能较好 大过写性能.

我试过EXPLAIN,结果如下:

+----+-------------+------------------+------------+------+---------------+------+---------+------+----------+----------+-------+
| id | select_type | table      | partitions | type | possible_keys | key  | key_len | ref  | rows     | filtered | Extra |
+----+-------------+------------------+------------+------+---------------+------+---------+------+----------+----------+-------+
|  1 | SIMPLE      | record_p13 | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 33377261 |   100.00 | NULL  |
+----+-------------+------------------+------------+------+---------------+------+---------+------+----------+----------+-------+

看来就是因为整表遍历来获得SUM/AVG的数据,所以也没办法用索引……

可能你需要根据你的sql query, 单独计算sum max avg.
看你的sql query内容可能有多个方式解决 可以利用TRIGGER或可以利用MATERIALZED VIEW来预先计算好值等.

使用Nosql的aggregate等sum max也需要计算

materialized view的机制以我的理解来看也是结果缓存,定期或通过trigger去出发更新。我是有自己手动作了数据缓存,可是效果还是觉得比较“笨”…… =__=||

直接
UPDATE ... SET total = total + NEW.value;
CREATE TRIGGER AFTER INSERT ON ... FOR EACH ROW ...
即可.
而avg也大概类似.

头像
等级1
  1. 数据量其实不大,可以说是挺小的,我们生产MySQL中有单表几亿的条数据也毫无压力。
  2. 看你很在乎存储陈本?7个字段的表,建了7个索引。。。老板见了直摇头。
  3. 如果只是因为上面说的6千万以及后续的数据增量,导致的查询性能问题,建议说一些上层业务需要的数据是什么样的,有时候表结构那样了,优化空间确实不大。
  4. 数据太多的话,引擎会直接全表扫描,即使你有索引,因为使用索引性价比太低了,还要多几次io。
  5. 针对仅有的一条sql查询语句的建议:可以换一个存储比如ES或者pg(如果觉得es太重的话,可以试试ZincSearch这种轻量级开源解决方案);或者换一个思路类似存算分离,导入数据时,根据数据变动异步计算查询结果即可,比如常见的canal方案。你实时的db里捞数据,上限太低。

以上个人愚见,甚至帖子都没细看。

表不大 以前我们也是单表上亿

头像
等级0

两种思路解决:

  1. 按照project维度汇总记录,每条新记录进来就更新这个汇总数据,包含总条数、最大、最小值、总量等,这样平均数也可以很快算出来,就不用每次统计源数据表。
  2. 使用clickhouse、starocks、greenplum等OLAP数据库。

第一个实现简单,不过可能后期需求复杂了比较麻烦。
第二个可以覆盖所有统计需求

头像
等级0

你这是OLAP应用,换吧

头像
等级0

第一个办法:把磁盘换成高性能的,几千万全表扫描也不是事
第二个办法:统计分离出来,canal+databend或者clickhouse做好数据同步,PHP查clickhouse,count这种轻轻松松

头像
等级1

最近看了点数据库相关知识,可能列式数据库更符合你的需求。文档型数据库不是干这的。

额……现在已经横向拆表了,难道还要纵向拆……

不用拆表啊,把数据同步到列数据库中,如ClickHouse。源库不动的,这是个分析库。

头像
等级0

只能加个从机,复杂的消耗cpu的,只能这样操作了 我们之前云业务也是这样做的,后面有一部分转到mongodb中了

或许我没太玩明白MongoDB,不过以我自己的测试来看,MongoDB的速度在普通的SELECT时尚可,如果加上各种SUM/AVG/MIN/MAX计算,速度还不如MySQL…… =___=||

头像
等级0

如果您的查询需要对大量数据进行计算,可以考虑定期或实时对数据进行汇总和预计算,并将结果存储在另一个表中。这样,您的查询可以直接从预计算的结果表中获取数据,而不需要每次都进行全表遍历和计算数据库优化:另外MySQL有许多配置选项和参数可以调整以优化性能。您可以查看MySQL的配置,并根据您的需求进行适当的调整,例如增加缓冲区大小、调整查询缓存等

头像
等级5

你这表字段少 全都加了所以 遍历全表应该已经达到最大速了 就是各种聚合查太多了 要不说说你的业务 看看能不能减少电聚合查

头像
等级0

朋友,如果这个业务处理真的只是一张表,你可以尝试引入clickhouse,算完结果写回MySQL给前台查询 。列式存储,就是不支持事务,导入和查询性能极好,就是不太适合更新和强事务的场景

头像
等级0

设计优化下吧,需要用额外的表结构或者缓存维护COUNT/SUM/AVG/MAX/MIN的值,每次created、update、delete的时候去更新

有缓存结果,想看看有没有更优的方法……T___T

有缓存结果为啥还要去查db呀

头像
等级1

话说,同样的数据,我也用MongoDB试过了,性能还不如MySQL……

头像
等级4

业务系统不要用max sum这些函数(性能消耗大)来做数据统计。你真的要做的是通过定时任务,按时间(小时,天,月)去计算汇总。定时任务里使用代码去计算数据。然后只要查汇总表就行了。
然后表设计也有问题,怎么会每个字段都需要索引。

头像
等级3

可能要换一个思路了。聚合运算会开销很大的资源的,这个与后面什么数据库没有关系的。建议定时分段计算。

头像
等级2

单靠sql当然难解决。

头像
等级1

有没有试过把这条sql的每一个要获取的字段拆分成一条sql,然后用php生成你要的数据呢?

头像
等级0
    SELECT memo_project, max(trade_time) as lasttime, sum(amount_in) as sum, avg(amount_in) as avg, max(amount_in) as max, min(amount_in) as min, count(*) AS num FROM records GROUP BY memo_project;

类似这种操作其实物化视图应该能比较好的支持,本质上就是冗余了一份统计数据,根据原表的操作进行增加更新,性能应该会好不少。不然的话要不就走OLAP的方式或者自己做一个统计表自己实现增量更新了(假设没有delete和update的操作,应该这几个都是容易实现的)