做者:xuty
本文来源:原创投稿
*爱可生开源社区出品,原创内容未经受权不得随意使用,转载请联系小编并注明来源。
本文关键字:count、SQL、二级索引
项目组联系我说是有一张 500w 左右的表作select count(*)速度特别慢。mysql
Server version: 5.7.24-log MySQL Community Server (GPL)
SQL 以下,仅仅就是统计 api_runtime_log 这张表的行数,一条简单的不能再简单的 SQL:sql
select count(*) from api_runtime_log;
咱们先去运行一下这条 SQL,能够看到确实运行很慢,要 40 多秒左右,确实很不正常~api
mysql> select count(*) from api_runtime_log; +----------+ | count(*) | +----------+ | 5718952 | +----------+ 1 row in set (42.95 sec)
咱们再去看下表结构,看上去貌似也挺正常的~存在主键,表引擎也是 InnoDB,字符集也没问题。缓存
CREATE TABLE `api_runtime_log_copy` ( `BelongXiaQuCode` varchar(50) DEFAULT NULL, `OperateUserName` varchar(50) DEFAULT NULL, `OperateDate` datetime DEFAULT NULL, `Row_ID` int(11) DEFAULT NULL, `YearFlag` varchar(4) DEFAULT NULL, `RowGuid` varchar(50) NOT NULL, ...... `apiid` varchar(50) DEFAULT NULL, `apiname` varchar(50) DEFAULT NULL, `apiguid` varchar(50) DEFAULT NULL, PRIMARY KEY (`RowGuid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
经过执行计划,咱们看下是否能够找到什么问题点。函数
mysql> explain select count(*) from api_runtime_log \G; *************************** 1. row *************************** id: 1 select_type: SIMPLE table: api_runtime_log partitions: NULL type: index possible_keys: NULL key: PRIMARY key_len: 152 ref: NULL rows: 5718952 filtered: 100.00 Extra: Using index
能够看到,查询走的是 PRIMARY,也就是主键索引。貌似也没有什么问题,走索引了呀!那么是否是真的就没问题呢?性能
为了找到答案,经过 Google 查找 MySQL 下select count(*)的原理,找到了答案。这边省略过程,直接上结果。测试
简单介绍下原理:优化
在 InnoDB 存储引擎中,count(*)函数是先从内存中读取数据到内存缓冲区,而后进行扫描得到行记录数。这里 InnoDB 会优先走二级索引;若是同时存在多个二级索引,会选择key_len 最小的二级索引;若是不存在二级索引,那么会走主键索引;若是连主键都不存在,那么就走全表扫描!这里咱们因为走的是主键索引,因此 MySQL 须要先把整个主键索引读取到内存缓冲区,这是个从磁盘读写到内存的过程,并且主键索引基本等于整个表数据量(10GB+),因此很是耗时!ui
答案就是:建二级索引。code
由于二级索引只包含对应的索引列及主键列,因此体积很是小。在select count(*)的查询过程当中,只须要将二级索引读取到内存缓冲区,只有几十 MB 的数据量,因此速度会很是快。举个形象的比喻,咱们想知道一本书的页数:
建立二级索引后,再次执行 SQL 及查看执行计划。
mysql> create index idx_rowguid on api_runtime_log(rowguid); Query OK, 0 rows affected (0.01 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> select count(*) from api_runtime_log; +----------+ | count(*) | +----------+ | 5718952 | +----------+ 1 row in set (0.89 sec) mysql> explain select count(*) from api_runtime_log \G; *************************** 1. row *************************** id: 1 select_type: SIMPLE table: api_runtime_log partitions: NULL type: index possible_keys: NULL key: idx_rowguid key_len: 152 ref: NULL rows: 5718952 filtered: 100.00 Extra: Using index 1 row in set, 1 warning (0.00 sec)
能够看到添加二级索引后,确实速度明显变快,并且执行计划也变成了走二级索引。至此这个问题其实已经解决了,就是因为表上缺乏二级索引致使。
为了进一步验证上述的推论,因此就作了以下的测试。
1. 聚簇索引
查询当前内存缓冲区状态,结果为空证实不缓存测试表数据。
mysql> select * from sys.innodb_buffer_stats_by_table where object_schema = 'test'; Empty set (1.92 sec) mysql> select count(*) from test.sbtest1; +----------+ | count(*) | +----------+ | 5188434 | +----------+ 1 row in set (5.52 sec)
再次查看内存缓冲区,发现缓存了 sbtest1 表上 1G 多的数据,基本等于整个表数据量。
mysql> select * from sys.innodb_buffer_stats_by_table where object_schema = 'test' \G; *************************** 1. row *************************** object_schema: test object_name: sbtest1 allocated: 1.08 GiB data: 1.01 GiB pages: 71081 pages_hashed: 0 pages_old: 28119 rows_cached: 5189798
最后咱们再来看下执行计划,确实走的是主键索引,放在最后执行是为了不影响缓冲区。
mysql> explain select count(*) from test.sbtest1 \G; *************************** 1. row *************************** id: 1 select_type: SIMPLE table: sbtest1 partitions: NULL type: index possible_keys: NULL key: PRIMARY key_len: 4 ref: NULL rows: 5117616 filtered: 100.00 Extra: Using index
2. 二级索引
建立二级索引 idx_id,查看 sbtest1 表上主键索引与二级索引的数据量。
mysql> create index idx_id on sbtest1(id); Query OK, 0 rows affected (12.97 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> SELECT sum(stat_value) pages ,index_name , (round((sum(stat_value) * @@innodb_page_size)/1024/1024)) as MB FROM mysql.innodb_index_stats WHERE table_name = 'sbtest1' AND database_name = 'test' AND stat_description = 'Number of pages in the index' GROUP BY index_name; +-------+------------+------+ | pages | index_name | MB | +-------+------------+------+ | 72000 | PRIMARY | 1125 | | 3492 | idx_id | 55 | +-------+------------+------+
重启 MySQL,再次查看缓冲区一样为空,证实没有缓存测试表上的数据。
mysql> select * from sys.innodb_buffer_stats_by_table where object_schema = 'test'; Empty set (1.49 sec) mysql> select count(*) from test.sbtest1; +----------+ | count(*) | +----------+ | 5188434 | +----------+ 1 row in set (2.92 sec)
再次查看内存缓冲区,发现仅仅缓存了 sbtest1 表上的 50M 数据,约等于二级索引的数据量。
mysql> select * from sys.innodb_buffer_stats_by_table where object_schema = 'test' \G; *************************** 1. row *************************** object_schema: test object_name: sbtest1 allocated: 49.48 MiB data: 46.41 MiB pages: 3167 pages_hashed: 0 pages_old: 1575 rows_cached: 2599872
最后确认下执行计划,确实走的是二级索引。
mysql> explain select count(*) from test.sbtest1 \G; *************************** 1. row *************************** id: 1 select_type: SIMPLE table: sbtest1 partitions: NULL type: index possible_keys: NULL key: idx_id key_len: 4 ref: NULL rows: 5117616 filtered: 100.00 Extra: Using index
从上述这个测试结果能够看出,和以前的推论基本吻合。若是select count(*)走的是主键索引,那么会缓存整个表数据,大量查询时间会花费在读取表数据到缓冲区。
若是存在二级索引,那么只须要读取索引页到缓冲区便可,速度天然快。
另:项目上因为磁盘性能层次不齐,因此当赶上这种状况时,性能较差的磁盘更会放大这个问题;一张超级大表,统计行数时若是走了主键索引,后果可想而知~
这次测试过程当中咱们仅仅模拟是百万数据量,此时咱们经过二级索引统计表行数,只须要读取几十 M 的数据量,就能够获得结果。
那么当咱们的表数据量是上千万,甚至上亿时呢。此时即使是最小的二级索引也是 几百 M、过 G 的数据量,若是继续经过二级索引来统计行数,那么速度就不会如此迅速了。
这个时候能够经过避免直接select count(*) from table来解决,方法较多,例如:
固然,何时 InnoDB 存储引擎能够直接实现计数器的功能就行了!