count(*) 的實(shí)現(xiàn)方式
在沒(méi)有過(guò)濾條件的 count()下核偿,在不同的 MySQL 引擎中贤牛,count() 有不同的實(shí)現(xiàn)方式尘吗。
- MyISAM 引擎把一個(gè)表的總行數(shù)存在了磁盤(pán)上鹉梨,因此執(zhí)行 count(*) 的時(shí)候會(huì)直接返回這個(gè)數(shù)婴程,效率很高;
- 而 InnoDB 引擎就麻煩了渠脉,即使是在同一個(gè)時(shí)刻的多個(gè)查詢(xún),由于多版本并發(fā)控制(MVCC)的原因瓶佳,InnoDB 表“應(yīng)該返回多少行”也是不確定的芋膘。它執(zhí)行 count(*) 的時(shí)候,需要把數(shù)據(jù)一行一行地從引擎里面讀出來(lái)霸饲,然后累積計(jì)數(shù)为朋。
MyISAM 表雖然 count() 很快,但是不支持事務(wù)厚脉;
show table status 命令雖然返回很快习寸,但是不準(zhǔn)確;
InnoDB 表直接 count() 會(huì)遍歷全表傻工,雖然結(jié)果準(zhǔn)確霞溪,但會(huì)導(dǎo)致性能問(wèn)題。
InnDB對(duì)count(*)的優(yōu)化
InnoDB 是索引組織表中捆,主鍵索引樹(shù)的葉子節(jié)點(diǎn)是數(shù)據(jù)鸯匹,而普通索引樹(shù)的葉子節(jié)點(diǎn)是主鍵值。
所以泄伪,普通索引樹(shù)比主鍵索引樹(shù)小很多殴蓬。對(duì)于 count(*) 這樣的操作,遍歷哪個(gè)索引樹(shù)得到的結(jié)果邏輯上都是一樣的蟋滴。
因此染厅,MySQL 優(yōu)化器會(huì)找到最小的那棵樹(shù)來(lái)遍歷痘绎。
在保證邏輯正確的前提下,盡量減少掃描的數(shù)據(jù)量肖粮,是數(shù)據(jù)庫(kù)系統(tǒng)設(shè)計(jì)的通用法則之一孤页。
如果你現(xiàn)在有一個(gè)頁(yè)面經(jīng)常要顯示交易系統(tǒng)的操作記錄總數(shù),到底應(yīng)該怎么辦呢尿赚?
答案是散庶,我們只能自己計(jì)數(shù)。
用緩存系統(tǒng)保存計(jì)數(shù)
例如使用Redis string類(lèi)型進(jìn)行存儲(chǔ)表的行數(shù)凌净。
問(wèn)題是會(huì)有丟失數(shù)據(jù)和計(jì)數(shù)不精確的問(wèn)題悲龟。
seesionA插入一條數(shù)據(jù)后,sessionB馬上讀取Redis中該表行數(shù)冰寻,實(shí)際是舊數(shù)據(jù)须教。
只有sessionA把Redis更新了才生效。
在數(shù)據(jù)庫(kù)保存計(jì)數(shù)
可以利用“事務(wù)”這個(gè)特性斩芭,把問(wèn)題解決掉轻腺。
雖然會(huì)話(huà) B 的讀操作仍然是在 T3 執(zhí)行的,但是因?yàn)檫@時(shí)候更新事務(wù)還沒(méi)有提交划乖,所以計(jì)數(shù)值加 1 這個(gè)操作對(duì)會(huì)話(huà) B 還不可見(jiàn)贬养。
因此,會(huì)話(huà) B 看到的結(jié)果里琴庵, 查計(jì)數(shù)值和“最近 100 條記錄”看到的結(jié)果误算,邏輯上就是一致的。
不同的 count 用法
count含義:count() 是一個(gè)聚合函數(shù)迷殿,對(duì)于返回的結(jié)果集儿礼,一行行地判斷,如果 count 函數(shù)的參數(shù)不是 NULL庆寺,累計(jì)值就加 1蚊夫,否則不加。最后返回累計(jì)值懦尝。
所以知纷,count(*)、count(主鍵 id) 和 count(1) 都表示返回滿(mǎn)足條件的結(jié)果集的總行數(shù)导披;而 count(字段)屈扎,則表示返回滿(mǎn)足條件的數(shù)據(jù)行里面,參數(shù)“字段”不為 NULL 的總個(gè)數(shù)撩匕。
至于分析性能差別的時(shí)候鹰晨,可以記住這么幾個(gè)原則:
- server 層要什么就給什么;
- InnoDB 只給必要的值;
- 現(xiàn)在的優(yōu)化器只優(yōu)化了 count(*) 的語(yǔ)義為“取行數(shù)”其他“顯而易見(jiàn)”的優(yōu)化并沒(méi)有做模蜡。
對(duì)于 count(主鍵 id) 來(lái)說(shuō)漠趁,InnoDB 引擎會(huì)遍歷整張表,把每一行的 id 值都取出來(lái)忍疾,返回給 server 層闯传。server 層拿到 id 后,判斷是不可能為空的卤妒,就按行累加甥绿。
對(duì)于 count(1) 來(lái)說(shuō),InnoDB 引擎遍歷整張表则披,但不取值共缕。server 層對(duì)于返回的每一行,放一個(gè)數(shù)字“1”進(jìn)去士复,判斷是不可能為空的图谷,按行累加。
單看這兩個(gè)用法的差別的話(huà)阱洪,你能對(duì)比出來(lái)便贵,count(1) 執(zhí)行得要比 count(主鍵 id) 快。因?yàn)閺囊娣祷?id 會(huì)涉及到解析數(shù)據(jù)行冗荸,以及拷貝字段值的操作承璃。
對(duì)于 count(字段) 來(lái)說(shuō):
- 如果這個(gè)“字段”是定義為 not null 的話(huà),一行行地從記錄里面讀出這個(gè)字段蚌本,判斷不能為 null绸硕,按行累加;
- 如果這個(gè)“字段”定義允許為 null魂毁,那么執(zhí)行的時(shí)候,判斷到有可能是 null出嘹,還要把值取出來(lái)再判斷一下席楚,不是 null 才累加。也就是前面的第一條原則税稼,server 層要什么字段烦秩,InnoDB 就返回什么字段。
但是 count() 是例外郎仆,并不會(huì)把全部字段取出來(lái)只祠,而是專(zhuān)門(mén)做了優(yōu)化,不取值扰肌。count() 肯定不是 null抛寝,按行累加。
看到這里,你一定會(huì)說(shuō)盗舰,優(yōu)化器就不能自己判斷一下嗎晶府,主鍵 id 肯定非空啊,為什么不能按照 count() 來(lái)處理钻趋,多么簡(jiǎn)單的優(yōu)化啊川陆。
當(dāng)然,MySQL 專(zhuān)門(mén)針對(duì)這個(gè)語(yǔ)句進(jìn)行優(yōu)化蛮位,也不是不可以较沪。但是這種需要專(zhuān)門(mén)優(yōu)化的情況太多了,而且 MySQL 已經(jīng)優(yōu)化過(guò) count() 了失仁,你直接使用這種用法就可以了尸曼。
結(jié)論是,按照效率排序:count(字段) < count(主鍵) < count(1) = count()陶因。 盡量使用count()骡苞。
總結(jié)
在不同引擎中 count(*) 的實(shí)現(xiàn)方式是不一樣的,用緩存系統(tǒng)來(lái)存儲(chǔ)計(jì)數(shù)值是存在的問(wèn)題楷扬。
把計(jì)數(shù)放在 Redis 里面解幽,不能夠保證計(jì)數(shù)和 MySQL 表里的數(shù)據(jù)精確一致的原因,是這兩個(gè)不同的存儲(chǔ)構(gòu)成的系統(tǒng)烘苹,不支持分布式事務(wù)躲株,無(wú)法拿到精確一致的視圖。
而把計(jì)數(shù)值也放在 MySQL 中镣衡,就解決了一致性視圖的問(wèn)題霜定。