MySQL中count(*)深度解析與性能優(yōu)化實(shí)踐案例
在日常MySQL開發(fā)中,count()函數(shù)是統(tǒng)計數(shù)據(jù)行數(shù)的常用工具,但很多開發(fā)者對count(*)、count(字段)、count(1)的區(qū)別一知半解,也常困惑于不同存儲引擎下count(*)的性能差異。本文將結(jié)合實(shí)際測試案例,從原理到實(shí)踐,帶你徹底搞懂count(*),并分享3種高效優(yōu)化count()性能的方案。
一、測試環(huán)境搭建
為了讓所有結(jié)論有數(shù)據(jù)支撐,我們先搭建統(tǒng)一的測試環(huán)境——創(chuàng)建3張不同配置的表(InnoDB帶索引、MyISAM、InnoDB無二級索引),并插入測試數(shù)據(jù)。
1.1 建表語句與存儲過程
-- 切換數(shù)據(jù)庫(需提前創(chuàng)建martin庫:create database martin;) use martin; -- 1. 創(chuàng)建InnoDB引擎表t1(含主鍵+二級索引) drop table if exists t1; CREATE TABLE `t1` ( `id` int NOT NULL AUTO_INCREMENT, `a` int DEFAULT NULL, -- 允許為null,用于測試count(字段) `b` int NOT NULL, `c` int DEFAULT NULL, `d` int DEFAULT NULL, PRIMARY KEY (`id`), -- 聚簇索引 KEY `idx_a` (`a`), -- 二級索引 KEY `idx_b` (`b`) -- 二級索引 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 2. 創(chuàng)建批量插入10000條數(shù)據(jù)的存儲過程 drop procedure if exists insert_t1; delimiter ;; -- 臨時修改語句結(jié)束符,避免與存儲過程中的;沖突 create procedure insert_t1() begin declare i int; set i=1; while(i<=10000)do insert into t1(a,b,c,d) values(i,i,i,i); -- 初始數(shù)據(jù)a無null set i=i+1; end while; end;; delimiter ; -- 恢復(fù)語句結(jié)束符 -- 3. 執(zhí)行存儲過程+補(bǔ)充1條a為null的數(shù)據(jù) call insert_t1(); insert into t1(a,b,c,d) values (null,10001,10001,10001),(10002,10002,10002,10002); -- 此時t1共10002行數(shù)據(jù),其中1行a為null -- 4. 創(chuàng)建MyISAM引擎表t2(結(jié)構(gòu)與t1一致,用于對比引擎差異) drop table if exists t2; create table t2 like t1; alter table t2 engine = MyISAM; -- 修改引擎 insert into t2 select * from t1; -- 同步t1數(shù)據(jù) -- 5. 創(chuàng)建無二級索引的InnoDB表t3(用于測試索引對count(*)的影響) drop table if exists t3; CREATE TABLE `t3` ( `id` int NOT NULL AUTO_INCREMENT, `a` int DEFAULT NULL, `b` int NOT NULL, `c` int DEFAULT NULL, `d` int DEFAULT NULL, PRIMARY KEY (`id`) -- 僅聚簇索引 ) ENGINE=InnoDB CHARSET=utf8mb4; insert into t3 select * from t1;
二、重新認(rèn)識count(*):4個核心疑問解答
2.1 count(a)與count(*)的區(qū)別:是否統(tǒng)計null?
很多人誤以為count(字段)和count(*)功能一致,實(shí)則關(guān)鍵差異在是否統(tǒng)計字段為null的行:
count(a):僅統(tǒng)計a字段不為null的行(若字段有null值,會過濾掉);count(*):統(tǒng)計表中所有行(無論字段是否為null,包括全null的行)。
測試驗(yàn)證(基于t1表,10002行,1行a為null):
-- 結(jié)果為10001(排除a為null的1行) select count(a) from t1; -- 結(jié)果為10002(統(tǒng)計所有行) select count(*) from t1;

2.2 MyISAM與InnoDB:count(*)性能天差地別?
兩種主流引擎對count(*)的處理邏輯完全不同,導(dǎo)致性能差異顯著:
- MyISAM:會將表的總行數(shù)存儲在磁盤(僅針對無where子句、無其他列檢索的場景),查詢時直接讀取該值,速度極快;
- InnoDB:需臨時掃描表/索引計算行數(shù)(因InnoDB支持事務(wù),行數(shù)據(jù)可能被鎖定或版本不同,無法緩存固定行數(shù)),速度較慢。
執(zhí)行計劃對比:
-- 1. MyISAM表t2的count(*):Extra為Select tables optimized away,核心含義是:MySQL 通過優(yōu)化邏輯,直接從索引中獲取了所需的全部數(shù)據(jù),完全無需訪問實(shí)際的表,因此 “跳過了表的訪問步驟” explain select count(*) from t2; -- 2. InnoDB表t1的count(*):type為index,表示 “全索引掃描”,而非 “全表掃描”,Extra為Using index,表示查詢所需的所有信息都能從索引中直接獲取,完全不需要回表讀取行數(shù)據(jù) explain select count(*) from t1;
從執(zhí)行計劃可見,MyISAM直接復(fù)用預(yù)存的行數(shù),而InnoDB需掃描索引計算。

2.3 MySQL 5.7.18+:count(*)為何優(yōu)先選二級索引?
在MySQL 5.7.18之前,InnoDB的count(*)默認(rèn)掃描聚簇索引(主鍵索引);而5.7.18之后,優(yōu)化器會優(yōu)先選擇最小的二級索引,原因是:
- 聚簇索引的葉子節(jié)點(diǎn)存儲整行數(shù)據(jù),體積較大;
- 二級索引的葉子節(jié)點(diǎn)僅存儲主鍵值,體積遠(yuǎn)小于聚簇索引,掃描成本更低。
若表無二級索引(如t3表),則仍會掃描聚簇索引。

2.4 count(1)比count(*)快?謠言!
很多開發(fā)者認(rèn)為count(1)性能優(yōu)于count(*),實(shí)則兩者結(jié)果一致、性能無差異:
count(1):將“1”視為恒真表達(dá)式,統(tǒng)計所有行(與count(*)邏輯一致);count(*):MySQL對其有專門優(yōu)化,不會展開為所有字段,而是直接統(tǒng)計行數(shù)。
執(zhí)行計劃驗(yàn)證:
-- 兩條語句的執(zhí)行計劃完全一致(均掃描二級索引,rows=10002) explain select count(1) from t1; explain select count(*) from t1;
結(jié)論:無需糾結(jié)count(1)和count(*),優(yōu)先用count(*)更符合語義。

三、3種方法加快count():從“慢統(tǒng)計”到“快查詢”
當(dāng)表數(shù)據(jù)量達(dá)百萬/千萬級時,InnoDB的count(*)會明顯變慢,以下3種方案可根據(jù)場景選擇:
3.1 場景1:僅需“大概數(shù)據(jù)量”→ show table status
若業(yè)務(wù)無需精確行數(shù)(如后臺數(shù)據(jù)概覽),可使用show table status,它直接讀取MySQL的表元數(shù)據(jù),無需掃描表:
-- 結(jié)果中Rows字段即為表的大概行數(shù)(t1表約10002行) show table status like 't1';
優(yōu)缺點(diǎn):速度極快,但數(shù)據(jù)可能有誤差(誤差通常在10%以內(nèi))。

3.2 場景2:需高性能+可接受少量延遲→ Redis計數(shù)器
利用Redis的原子操作(INCR/DECR)維護(hù)表行數(shù),查詢時直接讀Redis,避免掃描MySQL表:
步驟1:初始化計數(shù)器
-- 1. 先查詢MySQL表的初始行數(shù) select count(*) from t1; -- 結(jié)果10002 -- 2. 將初始值寫入Redis(key為t1_count,值為10002) set t1_count 10002;
步驟2:增刪數(shù)據(jù)時同步更新計數(shù)器
-- 插入數(shù)據(jù)時,Redis計數(shù)器+1 insert into t1(a,b,c,d) values (10003,10003,10003,10003); INCR t1_count; -- Redis命令 -- 刪除數(shù)據(jù)時,Redis計數(shù)器-1 delete from t1 where id=10003; DECR t1_count; -- Redis命令
步驟3:查詢行數(shù)時讀Redis
-- 直接獲取Redis中的值,耗時微秒級 get t1_count;
優(yōu)缺點(diǎn):性能極高,但存在“Redis與MySQL數(shù)據(jù)不一致”風(fēng)險(如插入MySQL成功但Redis更新失?。?,適合對一致性要求不嚴(yán)格的場景。
3.3 場景3:需強(qiáng)一致性→ 計數(shù)表(InnoDB)
用一張InnoDB表專門存儲行數(shù),通過事務(wù)保證“數(shù)據(jù)操作”與“計數(shù)更新”的原子性,徹底解決一致性問題:
步驟1:創(chuàng)建計數(shù)表
-- 創(chuàng)建count_t1表,僅存儲t1的行數(shù)
create table count_t1 (
table_name varchar(50) not null primary key, -- 表名(可擴(kuò)展到多表)
count int not null default 0 -- 行數(shù)
);
-- 初始化t1的計數(shù)
insert into count_t1(table_name, count) values ('t1', (select count(*) from t1));步驟2:事務(wù)中同步增刪與計數(shù)
-- 插入數(shù)據(jù)時,在同一事務(wù)中更新計數(shù) begin; -- 開啟事務(wù) insert into t1(a,b,c,d) values (10003,10003,10003,10003); update count_t1 set count=count+1 where table_name='t1'; commit; -- 提交事務(wù)(要么都成功,要么都失?。? -- 刪除數(shù)據(jù)時同理 begin; delete from t1 where id=10003; update count_t1 set count=count-1 where table_name='t1'; commit;
步驟3:查詢行數(shù)時讀計數(shù)表
-- 直接查詢計數(shù)表,僅掃描1行,速度極快 select count from count_t1 where table_name='t1';
優(yōu)缺點(diǎn):強(qiáng)一致性、性能好,但需額外維護(hù)計數(shù)表,適合對數(shù)據(jù)一致性要求高的核心業(yè)務(wù)(如訂單數(shù)統(tǒng)計)。
四、總結(jié):count(*)使用與優(yōu)化指南
- 基礎(chǔ)選擇:統(tǒng)計所有行用
count(*),統(tǒng)計非null字段用count(字段),無需用count(1); - 引擎差異:MyISAM適合靜態(tài)表(行數(shù)不變),InnoDB需通過索引優(yōu)化
count(*); - 優(yōu)化方案:
- 概覽數(shù)據(jù):
show table status; - 高性能低一致性:Redis計數(shù)器;
- 強(qiáng)一致性:InnoDB計數(shù)表。
- 概覽數(shù)據(jù):
掌握以上知識,可避免在MySQL計數(shù)場景中踩坑,讓統(tǒng)計邏輯既高效又可靠。
到此這篇關(guān)于MySQL中count(*)深度解析與性能優(yōu)化實(shí)踐案例的文章就介紹到這了,更多相關(guān)mysql count(*)性能優(yōu)化內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
mysql實(shí)現(xiàn)游標(biāo)分頁的方法詳解
這篇文章主要為大家詳細(xì)介紹了mysql實(shí)現(xiàn)游標(biāo)分頁的相關(guān)方法,文中的示例代碼講解詳細(xì),具有一定的借鑒價值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-10-10

