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都需要对大范围数据进行整个遍历,无论如何性能都没办法再提升了。
当然,我肯定是做了结果缓存的,但有没有什么更好的办法从根本上提升查询速度呢?
另外还有个现象,就是使用异步进行并发查询的时候,尽管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)
Δ 表示自从上次统计后的增量
你先explain一下sql query的plan, 6300多万条应该也不会造成RDBMS卡顿.
MongoDB默认无index, 默认可能有性能问题. 且一般NoSQL读取性能较好 大过写性能.
我试过EXPLAIN,结果如下:
看来就是因为整表遍历来获得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也大概类似.
以上个人愚见,甚至帖子都没细看。
表不大 以前我们也是单表上亿
两种思路解决:
第一个实现简单,不过可能后期需求复杂了比较麻烦。
第二个可以覆盖所有统计需求
你这是OLAP应用,换吧
第一个办法:把磁盘换成高性能的,几千万全表扫描也不是事
第二个办法:统计分离出来,canal+databend或者clickhouse做好数据同步,PHP查clickhouse,count这种轻轻松松
最近看了点数据库相关知识,可能列式数据库更符合你的需求。文档型数据库不是干这的。
额……现在已经横向拆表了,难道还要纵向拆……
不用拆表啊,把数据同步到列数据库中,如ClickHouse。源库不动的,这是个分析库。
只能加个从机,复杂的消耗cpu的,只能这样操作了 我们之前云业务也是这样做的,后面有一部分转到mongodb中了
或许我没太玩明白MongoDB,不过以我自己的测试来看,MongoDB的速度在普通的SELECT时尚可,如果加上各种SUM/AVG/MIN/MAX计算,速度还不如MySQL…… =___=||
如果您的查询需要对大量数据进行计算,可以考虑定期或实时对数据进行汇总和预计算,并将结果存储在另一个表中。这样,您的查询可以直接从预计算的结果表中获取数据,而不需要每次都进行全表遍历和计算数据库优化:另外MySQL有许多配置选项和参数可以调整以优化性能。您可以查看MySQL的配置,并根据您的需求进行适当的调整,例如增加缓冲区大小、调整查询缓存等
你这表字段少 全都加了所以 遍历全表应该已经达到最大速了 就是各种聚合查太多了 要不说说你的业务 看看能不能减少电聚合查
朋友,如果这个业务处理真的只是一张表,你可以尝试引入clickhouse,算完结果写回MySQL给前台查询 。列式存储,就是不支持事务,导入和查询性能极好,就是不太适合更新和强事务的场景
设计优化下吧,需要用额外的表结构或者缓存维护COUNT/SUM/AVG/MAX/MIN的值,每次created、update、delete的时候去更新
有缓存结果,想看看有没有更优的方法……T___T
有缓存结果为啥还要去查db呀
话说,同样的数据,我也用MongoDB试过了,性能还不如MySQL……
换Tidb吧
试试dgraph
业务系统不要用max sum这些函数(性能消耗大)来做数据统计。你真的要做的是通过定时任务,按时间(小时,天,月)去计算汇总。定时任务里使用代码去计算数据。然后只要查汇总表就行了。
然后表设计也有问题,怎么会每个字段都需要索引。
可能要换一个思路了。聚合运算会开销很大的资源的,这个与后面什么数据库没有关系的。建议定时分段计算。
单靠sql当然难解决。
有没有试过把这条sql的每一个要获取的字段拆分成一条sql,然后用php生成你要的数据呢?
类似这种操作其实物化视图应该能比较好的支持,本质上就是冗余了一份统计数据,根据原表的操作进行增加更新,性能应该会好不少。不然的话要不就走OLAP的方式或者自己做一个统计表自己实现增量更新了(假设没有delete和update的操作,应该这几个都是容易实现的)