爲何count(*)這麼慢?

前言

查詢數據條數詳解。
比如你維護着一張電商訂單表,業務的需求是查找所有訂單數,開發很快能寫出對應的 SQL :

select count(*) from order_01;

但你是否會發現,如果這張表很大後,這條 SQL 會非常耗時。

今天我們就一起重新認識下 count(),並想辦法去優化這類 SQL。

老規矩,先創建測試表並寫入數據。

use muke; /* 使用muke這個database */
drop table if exists t1; /* 如果表t1存在則刪除表t1 */

CREATE TABLE `t1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) DEFAULT NULL,
  `b` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_a` (`a`),
  KEY `idx_b` (`b`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;

drop procedure if exists insert_t1; /* 如果存在存儲過程insert_t1,則刪除 */
delimiter ;;
create procedure insert_t1() /* 創建存儲過程insert_t1 */
begin
declare i int; /* 聲明變量i */
set i=1; /* 設置i的初始值爲1 */
while(i<=10000)do /* 對滿足i<=10000的值進行while循環 */
insert into t1(a,b,c,d) values(i,i,i,i); /* 寫入表t1中a、b兩個字段,值都爲i當前的值 */
set i=i+1; /* 將i加1 */
end while;
end;;
delimiter ; /* 創建批量寫入10000條數據到表t1的存儲過程insert_t1 */
call insert_t1(); /* 運行存儲過程insert_t1 */

insert into t1(a,b,c,d) values (null,10001,10001,10001),(10002,10002,10002,10002);

drop table if exists t2; /* 如果表t2存在則刪除表t2 */
create table t2 like t1; /* 創建表t2,表結構與t1一致 */
alter table t2 engine =myisam; /* 把t2表改爲MyISAM存儲引擎 */
insert into t2 select * from t1;  /* 把t1表的數據轉到t2表 */

CREATE TABLE `t3` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) DEFAULT NULL,
  `b` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  CHARSET=utf8mb4;
insert into t3 select * from t1;  /* 把t1表的數據轉到t3表 */

重新認識 count()

count(a) 和 count(*) 的區別
當 count() 統計某一列時,比如 count(a),a 表示列名,是不統計 null 的。

比如測試表 t1,我們插入了字段 a 爲 null 的數據,我們來對 a 做一次 count():

select count(a) from t1;

在這裏插入圖片描述
實際在數據寫入時,寫入了 10002 行數據。因此,對 a 字段爲 null 的這一行不做統計。

而 count(*) 無論是否包含空值,都會統計。

我們對測試表 t1 執行一次 count(*):

select count(*) from t1;

在這裏插入圖片描述
顯然,統計的是所有的行。因此,如果希望知道結果集的行數,最好使用 count(*)。

MyISAM 引擎和 InnoDB 引擎 count(*) 的區別

對於 MyISAM 引擎,如果沒有 where 子句,也沒檢索其它列,那麼 count(*) 將會非常快。因爲 MyISAM 引擎會把表的總行數存在磁盤上。

首先我們看下對 t2 表(存儲引擎爲 MyISAM)不帶 where 子句做 count(*) 的執行計劃:

explain select count(*) from t2;

在這裏插入圖片描述
在 Extra 字段發現 “Select tables optimized away” 關鍵字,表示是從 MyISAM 引擎維護的準確行數上獲取到的統計值。

而 InnoDB 並不會保留表中的行數,因爲併發事務可能同時讀取到不同的行數。所以執行 count(*) 時都是臨時去計算的,會比 MyISAM 引擎慢很多。

我們看下對 t1 表(存儲引擎爲 InnoDB)執行 count(*) 的執行計劃:
在這裏插入圖片描述
發現使用的是 b 字段的索引 idx_b,並且掃描行數是10109,表示會遍歷 b 字段的索引樹去計算表的總量。

對比 MyISAM 引擎和 InnoDB 引擎 count(*) 的區別,可以知道:

1 MyISAM 會維護表的總行數,放在磁盤中,如果有 count(*) 的需求,直接返回這個數據
2 但是 InnoDB 就會去遍歷普通索引樹,計算表數據總量

在上面這個例子,InnoDB 表 t1 在執行 count(*) 時,爲什麼會走 b 字段的索引而不是走主鍵索引呢?下面我們分析下:

哪些方法可以加快 count()

用 Redis 做計數器
首先初始化時,執行一次精確計數:

select count(*) from t1;

在這裏插入圖片描述
表此時的總數是 10002,把這個值賦給 Redis 中一個 key,命令如下:

set   t1_count  10002

在這裏插入圖片描述
當表 t1 寫入一條數據時:

insert into t1(a,b,c,d) values (10003,10003,10003,10003);

把 Redis 中 t1_count 這個 key 的值加 1,命令如下:

INCR t1_count

在這裏插入圖片描述
當表 t1 刪除一條數據時:

delete from t1 where id=10003;

把 Redis 中 t1_count 這個 key 的值減 1,命令如下:

get t1_count

在這裏插入圖片描述

這裏對 Redis 的計數做一些補充:

INCR t1_count 表示爲鍵 t1_count 存儲的數字值加 1
DECR t1_count 表示爲鍵 t1_count存儲的數字值減 1

如果一次需要增加或者刪除多行,用法如下:

INCRBY t1_count 10 表示一次爲鍵 t1_count 存儲的數字值加 10。

DECRBY t1_count 10 表示一次爲鍵 t1_count 存儲的數字值減 10。

但是這種方法還是有缺點的,試想,在表 t1 寫入數據到 Redis ,再到把 t1_count 加 1,總會存在一個時間差,如果這中間另外一個 session 去讀取 Redis 中 t1_count 的值,此時 t1_count 的值沒增加,但是表的實際數據行已經增加了,所以就會不準確。

增加計數表

這一步操作我們用 MySQL 中一張 InnoDB 表來代替,而數據寫入操作和計數操作都放在一個事務中,就可以避免 出現計數不準確的情況。
在這裏插入圖片描述

因爲放在同一個事務裏,在圖中 1 這個位置點,因爲事務還沒提交,所以表 t1 寫入一條記錄本身就對其它 session 不可見,此時其它 session 去執行 select count(*) from t1 和查計數表 count_t1 的記錄都是一樣的,爲 101 。不會出現用 Redis 計數時,表實際總數與計數器的值不一致的情況。

總結

1 用 Redis 做計數器:能快速獲取結果,比 show table status 結果準確,但是併發場景計數可能不準確;
2 增加 InnoDB 計數表:能快速獲取結果,利用了事務特性確保了計數的準確,也是比較推薦的方法。

參考資料

該文爲本人學習的筆記,方便以後自己複習。參考
《高性能 MySQL》(第三版):6.7.1 優化 COUNT() 查詢

《MySQL 5.7 Reference Manual》:14.6.1.6 Limits on InnoDB Tables

《MySQL 5.7 Reference Manual》:12.20.1 Aggregate (GROUP BY) Function
Descriptions

《MySQL 5.7 Reference Manual》:13.7.5.36 SHOW TABLE STATUS Syntax
慕課網專欄:https://www.imooc.com/read/43

取其精華整合而成。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章