你以為count*很簡(jiǎn)單(上)?

最近在客戶(hù)現(xiàn)場(chǎng)討論數(shù)據(jù)對(duì)賬問(wèn)題,大家對(duì)24小時(shí)內(nèi)訂單數(shù)量統(tǒng)計(jì)方法進(jìn)行了深入的討論,特別是有同學(xué)提到,這還不簡(jiǎn)單,我們只需要通過(guò)一條select count(*) from order就能獲得訂單表的計(jì)數(shù),不需要那么麻煩的,比起來(lái)通過(guò)外部緩存redis來(lái)保存計(jì)數(shù)的值,并且實(shí)時(shí)刷新,需要的時(shí)候直接讀這種方式簡(jiǎn)單多了。那么問(wèn)題是,這兩種方式是否存在差異,或者說(shuō)那種更加適合(容易實(shí)現(xiàn),風(fēng)險(xiǎn)低)?

為了搞清楚這個(gè)事情啊,我周末花費(fèi)了一下時(shí)間,在本地進(jìn)行了測(cè)試,這篇文章就是基于測(cè)試的結(jié)果,來(lái)聊聊count(*)語(yǔ)句到底是如何執(zhí)行的,我們除了可以通過(guò)count(*)來(lái)統(tǒng)計(jì),那么使用count(order_id)有什么不同,如果在項(xiàng)目中頻繁的這種計(jì)數(shù)的業(yè)務(wù)需求,那么哪種方式會(huì)更好?

我之前提過(guò)MYSQL有兩種存儲(chǔ)引擎,在早期版本上,MYISAM為默認(rèn)的存儲(chǔ)引擎,最近的版本都是以InnoDB作為默認(rèn)的存儲(chǔ)引擎,也就是你不特別指定的話,大家現(xiàn)在創(chuàng)建表的時(shí)候,都是采用的InnoDB引擎,這里需要注意,因?yàn)槲覀兪窃趧?chuàng)建表的時(shí)候,或者alter表的時(shí)候指定特殊的存儲(chǔ)引擎需求,因此也就是說(shuō)一個(gè)庫(kù)下邊,每個(gè)表可以有不同的存儲(chǔ)引擎。

而MYSQL在執(zhí)行count(*)這條語(yǔ)句的時(shí)候,還真的在不同的存儲(chǔ)引擎上是不一樣的: 1, MYISAM引擎會(huì)把一個(gè)表的總行數(shù)保存在磁盤(pán)上,這樣當(dāng)我們執(zhí)行count的時(shí)候,直接從磁盤(pán)上讀?。?,而InnoDB引擎就沒(méi)有這種優(yōu)化,而是按照我們“預(yù)期”的方式,從磁盤(pán)上讀取每一條記錄,然后進(jìn)行計(jì)數(shù)統(tǒng)計(jì)。

說(shuō)到這里可能會(huì)有同學(xué)吐槽了,為啥InnoDB引擎不實(shí)現(xiàn)這個(gè)優(yōu)化,這不是顯而易見(jiàn)的有點(diǎn)嗎?我們是不是應(yīng)該直接用MYISAM?其實(shí)MYSQL引入這個(gè)插件式的外部存儲(chǔ)引擎比起自帶的MYISAM引擎,提供了事務(wù)支持,并發(fā)能力控制,以及數(shù)據(jù)安全等高級(jí)特性,但是就是count這個(gè)操作,InnoDB的執(zhí)行性能略差,并且隨著表的數(shù)據(jù)量越來(lái)越大,如果按照上邊這種執(zhí)行方式,會(huì)越來(lái)越慢。

雖然說(shuō)MYISAM引擎把數(shù)據(jù)表的總數(shù)單獨(dú)進(jìn)行了保存,但是你可靠考慮一下這個(gè)查詢(xún): select count(*) from order where createTime>'123456789'; 很不幸的是,這條語(yǔ)句在MYISAM上并不能直接返回保存的數(shù)量,因?yàn)槲覀兪褂昧瞬樵?xún)條線,因此這句SQL的執(zhí)行,和InnoDB的執(zhí)行就無(wú)二了,也是一條一條的掃描,然后通過(guò)where條件進(jìn)行對(duì)比,符合條件,計(jì)數(shù)就+1,直到最后一條記錄。

那么問(wèn)題就來(lái)了,為什么InnoDB不優(yōu)化這個(gè)計(jì)數(shù)的查詢(xún)呢?或者說(shuō)為啥InnoDB引擎不在數(shù)據(jù)庫(kù)里記錄比如說(shuō)訂單表的總數(shù)呢?其實(shí)這里的根本原因是因?yàn)镮nnoDB引入了對(duì)事務(wù)的支持,而MYSQL的InnoDB事務(wù)實(shí)現(xiàn)原理中,有一個(gè)叫MVCC的概念,也就是多版本并發(fā)控制,這個(gè)詞看起來(lái)很復(fù)雜,我試圖用幾句話把這個(gè)概念說(shuō)清楚。

工作年限長(zhǎng)的同學(xué)應(yīng)該有找工作的經(jīng)歷,特別是有過(guò)裸辭經(jīng)驗(yàn)的同學(xué),在找工作的時(shí)候,會(huì)和多個(gè)意向雇主在談,而你提供給每個(gè)意向雇主的個(gè)人履歷信息+現(xiàn)在找工作的狀態(tài)就可以稱(chēng)作是不同的版本,舉個(gè)例子:意向雇主A,意向雇主B,和小Q,小Q目前的進(jìn)度是雇主A已經(jīng)到了談offer階段,而雇主B正在發(fā)offer階段,對(duì)于小Q來(lái)說(shuō)(數(shù)據(jù)庫(kù)的一條記錄,找工作狀態(tài)其實(shí)有兩個(gè)版本),充分發(fā)揮自己的多版本控制能力,讓兩個(gè)事務(wù)雇主A和雇主B只能看到自己應(yīng)該看到的版本,而小Q在這里施加的魔力,就是多版本并發(fā)控制。

正是因?yàn)橛羞@個(gè)MVCC的存在,假設(shè)訂單庫(kù)有1000條記錄,如果我們?cè)谟唵螖?shù)據(jù)庫(kù)上執(zhí)行兩個(gè)事務(wù),事務(wù)A比事務(wù)B先開(kāi)始,并且啟動(dòng)后先查一次count,結(jié)果是1000;而事務(wù)B啟動(dòng)后,插入一條記錄,然后執(zhí)行一次count,結(jié)果是1001,符合預(yù)期,然后事務(wù)B提交,這個(gè)時(shí)候事務(wù)A在提交之前,再查詢(xún)一次count,你覺(jué)這個(gè)數(shù)字應(yīng)該是多少?其實(shí)如果你理解了剛才找工作的例子,很容易就能理解事務(wù)在第二次查詢(xún)count的時(shí)候,結(jié)果是1000。

為什么?如果這個(gè)結(jié)果不符合你的預(yù)期,或者你蒙的,想知道背后的原理,我們繼續(xù)來(lái)分析。搞數(shù)據(jù)庫(kù)的同學(xué),應(yīng)該聽(tīng)過(guò)說(shuō)事務(wù)的隔離級(jí)別,而MYSQL的默認(rèn)隔離級(jí)別(InnoDB引擎)是可重復(fù)讀,很多人對(duì)于四個(gè)隔離級(jí)別總是分布清楚,其實(shí)是你太復(fù)雜了,可重復(fù)讀的意思不言而喻,就是可以重復(fù)讀,那么換個(gè)可以重復(fù)讀具體是從哪里到哪里可以重復(fù)讀呢?

在可重復(fù)讀的隔離級(jí)別下,事務(wù)在開(kāi)始的時(shí)候,會(huì)生成一個(gè)一致性視圖(consistent view),這個(gè)略技術(shù)性的概念還有另外一個(gè)名字,叫read view,其實(shí)read view這個(gè)名字不好,我覺(jué)得應(yīng)該叫read-only view,也就是事務(wù)啟動(dòng)的時(shí)候給數(shù)據(jù)庫(kù)拍個(gè)照,那么快照在整個(gè)事務(wù)期間就不會(huì)動(dòng),即便同時(shí)有多個(gè)事務(wù)在進(jìn)行CUD操作,但是對(duì)于事務(wù)A來(lái)說(shuō),看到的是一個(gè)一致性的,或者只讀的視圖。

這個(gè)就厲害了,但是你可能回頭一想,不對(duì)啊,我的數(shù)據(jù)庫(kù)有2個(gè)T,你是如何能給它高效的拍個(gè)照,作為用戶(hù)還感受不到?因?yàn)橐粋€(gè)數(shù)據(jù)庫(kù)上并發(fā)訪問(wèn)的量會(huì)非??捎^,如果有100個(gè)這樣的事務(wù),豈不是要拍100次,不合理啊,無(wú)法理解。

在繼續(xù)介紹如何拍照之前,我覺(jué)得事務(wù)這個(gè)概念有必要吐槽一下,事務(wù)在英文單詞中叫Transaction,而這個(gè)詞其實(shí)是和交易相關(guān),而交易其實(shí)更容易體現(xiàn)出ACID的屬性,因此我一直認(rèn)為事務(wù)描述的ACID比較抽象,大家要從因?yàn)閱卧~的角度來(lái)理解,會(huì)更加容易一些。

我們繼續(xù)快照這個(gè)事情,當(dāng)然MYSQL不能每次啟動(dòng)一個(gè)事務(wù),都拷貝一次數(shù)據(jù)。其實(shí)啊,MYSQL在快照的時(shí)候,結(jié)合前邊描述的MVCC的概念,只要大喊一聲“我要開(kāi)始執(zhí)行了,我只能看到事務(wù)ID4之前所有移交提交事務(wù),之后的版本(有更大的事務(wù)ID,因?yàn)槭聞?wù)ID是單調(diào)遞增),以及之后提交的版本我都看不到”,其實(shí)這里的大喊一聲,對(duì)應(yīng)到數(shù)據(jù)庫(kù)中,就是記錄一下事務(wù)ID而已。

這也是為什么我一直建議給訂單表增加版本號(hào),也是為了讓訂單數(shù)據(jù)能夠保持這種單調(diào)遞增, 特別是在并發(fā)的情況下,當(dāng)然通過(guò)更新時(shí)間也可以做到,但是時(shí)間本身在分布式系統(tǒng)中存在扭曲的可能性,因此對(duì)數(shù)據(jù)一致性要求比較高大規(guī)模分布式系統(tǒng),一般不會(huì)用時(shí)間來(lái)保持單調(diào)遞增,因?yàn)闀r(shí)間物理上是單調(diào)遞增,但是多個(gè)單調(diào)遞增的時(shí)間源,就可能出現(xiàn)時(shí)光逆轉(zhuǎn),造成數(shù)據(jù)一致性的問(wèn)題。

總結(jié)一下,上邊羅里吧嗦的一堆就是想說(shuō)明,MYSQL不單獨(dú)保存這個(gè)計(jì)數(shù)值是有原因的,并不是懶惰,而正是因?yàn)檫@個(gè)原因,count這個(gè)看起來(lái)貌似很天真無(wú)邪,童叟無(wú)欺的操作,給很多人造成了困擾。

我必須澄清一下,雖然說(shuō)MYSQL的InnoDB引擎執(zhí)行count看起來(lái)智商不在線,但是也盡了努力做了優(yōu)化。在MYSQL中,數(shù)據(jù)既索引,索引既數(shù)據(jù)這10個(gè)大字要牢記啊,因此我們查詢(xún)數(shù)據(jù)其實(shí)都是在索引這棵B+數(shù)上做搜索。

而一般的表都會(huì)有主鍵索引和非主鍵索引,主鍵索引也叫聚簇索引,這個(gè)索引上放的就是以主鍵為序的所有數(shù)據(jù),舉個(gè)例子,如果我們的訂單表上有1000個(gè)訂單數(shù)據(jù),那么這些數(shù)據(jù)存在于主鍵索引的B+樹(shù)的葉子節(jié)點(diǎn)上,并且從左到右保持有序存儲(chǔ)。

除了主鍵索引,我們還有非主鍵索引,而非主鍵索引又叫做二級(jí)索引,二級(jí)索引其實(shí)也很容易理解,就是二級(jí)的,次級(jí)的,而這里“二”的主要意思是:不好存數(shù)據(jù),只保存主鍵值;那么就可以看到二級(jí)索引上B+樹(shù)的葉子節(jié)點(diǎn)上,除了索引構(gòu)建的列的數(shù)據(jù)之外,還有主鍵值。

除了這兩個(gè)概念,大家還聽(tīng)說(shuō)過(guò)唯一索引,覆蓋索引,聯(lián)合索引概念。我特別說(shuō)一下覆蓋索引,覆蓋索引是一個(gè)二級(jí)索引,純粹是為了讓查詢(xún)的時(shí)候,盡量不用去回表(拿著主鍵ID去主鍵索引的B+樹(shù)上找其他行的數(shù)據(jù))而創(chuàng)建的,本質(zhì)就是空間換時(shí)間的把戲,當(dāng)然所有除了主鍵索引的二級(jí)索引,都是空間換時(shí)間的把戲。

回到count操作,其實(shí)MYSQL的優(yōu)化就是,因?yàn)槊總€(gè)索引其實(shí)葉子節(jié)點(diǎn)是相等的(你可以仔細(xì)考慮一下,因?yàn)槊恳粭l記錄都會(huì)對(duì)應(yīng)一個(gè)索引節(jié)點(diǎn),或者說(shuō)任意B+樹(shù)的葉子節(jié)點(diǎn),因此你的數(shù)據(jù)庫(kù)有1000條記錄,那么任意一個(gè)索引都會(huì)有1000個(gè)葉子節(jié)點(diǎn)),因此MYSQL會(huì)選擇高度更低的B+數(shù)來(lái)遍歷,而B(niǎo)+數(shù)的高低,主要是由索引保存的數(shù)據(jù)量決定的,這么看起來(lái)的話,如果在訂單表上,我們除了主鍵索引,還有基于用戶(hù)年齡age的二級(jí)索引,那么這個(gè)count操作大概率會(huì)在age這個(gè)索引的B+樹(shù)上執(zhí)行,因?yàn)锽+樹(shù)越低,一個(gè)數(shù)據(jù)也的記錄越多,一個(gè)頁(yè)面的數(shù)據(jù)記錄越多,這棵樹(shù)總數(shù)據(jù)頁(yè)越少,而總數(shù)據(jù)頁(yè)越少,掃描的額外開(kāi)銷(xiāo)越低。

有點(diǎn)復(fù)雜了,總之大部分情況下,我們執(zhí)行count操作其實(shí)問(wèn)題不大,因此很不幸的是,我們?cè)贛YSQL的InnoDB引擎上,count操作就是這么執(zhí)行的,很明顯我們需要有一些優(yōu)化的措施,要不然在高并發(fā)的情況下,每次都這么進(jìn)行一次全表掃描,也受不了啊。

關(guān)于我們有哪些可選的方案來(lái)提升count操作的性能,下文繼續(xù)介紹吧。其實(shí)也沒(méi)有啥好辦法,在MYSQL這種現(xiàn)狀下,只能自己計(jì)數(shù)了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容