前言
查詢數據條數詳解。
比如你維護着一張電商訂單表,業務的需求是查找所有訂單數,開發很快能寫出對應的 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取其精華整合而成。