做者: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 存儲引擎能夠直接實現計數器的功能就行了!