实际返回的行数超出请求的行数怎么解决_count(*)这么慢,我该怎么办?
在開發(fā)系統(tǒng)的時(shí)候,你可能經(jīng)常需要計(jì)算一個(gè)表的行數(shù),比如一個(gè)交易系統(tǒng)的所有變更記錄總數(shù)。這時(shí)候你可能會(huì)想,一條select count(*) from t 語(yǔ)句不就解決了嗎?
但是,你會(huì)發(fā)現(xiàn)隨著系統(tǒng)中記錄數(shù)越來(lái)越多,這條語(yǔ)句執(zhí)行得也會(huì)越來(lái)越慢。然后你可能就想了,MySQL怎么這么笨啊,記個(gè)總數(shù),每次要查的時(shí)候直接讀出來(lái),不就好了嗎。
那么今天,我們就來(lái)聊聊count(*)語(yǔ)句到底是怎樣實(shí)現(xiàn)的,以及MySQL為什么會(huì)這么實(shí)現(xiàn)。然后,我會(huì)再和你說(shuō)說(shuō),如果應(yīng)用中有這種頻繁變更并需要統(tǒng)計(jì)表行數(shù)的需求,業(yè)務(wù)設(shè)計(jì)上可以怎么做。
count(*)的實(shí)現(xiàn)方式
你首先要明確的是,在不同的MySQL引擎中,count(*)有不同的實(shí)現(xiàn)方式。
MyISAM引擎把一個(gè)表的總行數(shù)存在了磁盤上,因此執(zhí)行count(*)的時(shí)候會(huì)直接返回這個(gè)數(shù),效率很高;
而InnoDB引擎就麻煩了,它執(zhí)行count(*)的時(shí)候,需要把數(shù)據(jù)一行一行地從引擎里面讀出來(lái),然后累積計(jì)數(shù)。
這里需要注意的是,我們?cè)谶@篇文章里討論的是沒(méi)有過(guò)濾條件的count(*),如果加了where 條件的話,MyISAM表也是不能返回得這么快的。
在前面的文章中,我們一起分析了為什么要使用InnoDB,因?yàn)椴徽撌窃谑聞?wù)支持、并發(fā)能力還是在數(shù)據(jù)安全方面,InnoDB都優(yōu)于MyISAM。我猜你的表也一定是用了InnoDB引擎。這就是當(dāng)你的記錄數(shù)越來(lái)越多的時(shí)候,計(jì)算一個(gè)表的總行數(shù)會(huì)越來(lái)越慢的原因。
那為什么InnoDB不跟MyISAM一樣,也把數(shù)字存起來(lái)呢?
這是因?yàn)榧词故窃谕粋€(gè)時(shí)刻的多個(gè)查詢,由于多版本并發(fā)控制(MVCC)的原因,InnoDB表“應(yīng)該返回多少行”也是不確定的。這里,我用一個(gè)算count(*)的例子來(lái)為你解釋一下。
假設(shè)表t中現(xiàn)在有10000條記錄,我們?cè)O(shè)計(jì)了三個(gè)用戶并行的會(huì)話。
會(huì)話A先啟動(dòng)事務(wù)并查詢一次表的總行數(shù);
會(huì)話B啟動(dòng)事務(wù),插入一行后記錄后,查詢表的總行數(shù);
會(huì)話C先啟動(dòng)一個(gè)單獨(dú)的語(yǔ)句,插入一行記錄后,查詢表的總行數(shù)。
我們假設(shè)從上到下是按照時(shí)間順序執(zhí)行的,同一行語(yǔ)句是在同一時(shí)刻執(zhí)行的。
圖1 會(huì)話A、B、C的執(zhí)行流程你會(huì)看到,在最后一個(gè)時(shí)刻,三個(gè)會(huì)話A、B、C會(huì)同時(shí)查詢表t的總行數(shù),但拿到的結(jié)果卻不同。
這和InnoDB的事務(wù)設(shè)計(jì)有關(guān)系,可重復(fù)讀是它默認(rèn)的隔離級(jí)別,在代碼上就是通過(guò)多版本并發(fā)控制,也就是MVCC來(lái)實(shí)現(xiàn)的。每一行記錄都要判斷自己是否對(duì)這個(gè)會(huì)話可見,因此對(duì)于count(*)請(qǐng)求來(lái)說(shuō),InnoDB只好把數(shù)據(jù)一行一行地讀出依次判斷,可見的行才能夠用于計(jì)算“基于這個(gè)查詢”的表的總行數(shù)。
備注:如果你對(duì)MVCC記憶模糊了,可以再回顧下第3篇文章《事務(wù)隔離:為什么你改了我還看不見?》和第8篇文章《事務(wù)到底是隔離的還是不隔離的?》中的相關(guān)內(nèi)容。
當(dāng)然,現(xiàn)在這個(gè)看上去笨笨的MySQL,在執(zhí)行count(*)操作的時(shí)候還是做了優(yōu)化的。
你知道的,InnoDB是索引組織表,主鍵索引樹的葉子節(jié)點(diǎn)是數(shù)據(jù),而普通索引樹的葉子節(jié)點(diǎn)是主鍵值。所以,普通索引樹比主鍵索引樹小很多。對(duì)于count(*)這樣的操作,遍歷哪個(gè)索引樹得到的結(jié)果邏輯上都是一樣的。因此,MySQL優(yōu)化器會(huì)找到最小的那棵樹來(lái)遍歷。在保證邏輯正確的前提下,盡量減少掃描的數(shù)據(jù)量,是數(shù)據(jù)庫(kù)系統(tǒng)設(shè)計(jì)的通用法則之一。
如果你用過(guò)show table status 命令的話,就會(huì)發(fā)現(xiàn)這個(gè)命令的輸出結(jié)果里面也有一個(gè)TABLE_ROWS用于顯示這個(gè)表當(dāng)前有多少行,這個(gè)命令執(zhí)行挺快的,那這個(gè)TABLE_ROWS能代替count(*)嗎?
你可能還記得在第10篇文章《 MySQL為什么有時(shí)候會(huì)選錯(cuò)索引?》中我提到過(guò),索引統(tǒng)計(jì)的值是通過(guò)采樣來(lái)估算的。實(shí)際上,TABLE_ROWS就是從這個(gè)采樣估算得來(lái)的,因此它也很不準(zhǔn)。有多不準(zhǔn)呢,官方文檔說(shuō)誤差可能達(dá)到40%到50%。所以,show table status命令顯示的行數(shù)也不能直接使用。
到這里我們小結(jié)一下:
MyISAM表雖然count(*)很快,但是不支持事務(wù);
show table status命令雖然返回很快,但是不準(zhǔn)確;
InnoDB表直接count(*)會(huì)遍歷全表,雖然結(jié)果準(zhǔn)確,但會(huì)導(dǎo)致性能問(wèn)題。
那么,回到文章開頭的問(wèn)題,如果你現(xiàn)在有一個(gè)頁(yè)面經(jīng)常要顯示交易系統(tǒng)的操作記錄總數(shù),到底應(yīng)該怎么辦呢?答案是,我們只能自己計(jì)數(shù)。
接下來(lái),我們討論一下,看看自己計(jì)數(shù)有哪些方法,以及每種方法的優(yōu)缺點(diǎn)有哪些。
這里,我先和你說(shuō)一下這些方法的基本思路:你需要自己找一個(gè)地方,把操作記錄表的行數(shù)存起來(lái)。
用緩存系統(tǒng)保存計(jì)數(shù)
對(duì)于更新很頻繁的庫(kù)來(lái)說(shuō),你可能會(huì)第一時(shí)間想到,用緩存系統(tǒng)來(lái)支持。
你可以用一個(gè)Redis服務(wù)來(lái)保存這個(gè)表的總行數(shù)。這個(gè)表每被插入一行Redis計(jì)數(shù)就加1,每被刪除一行Redis計(jì)數(shù)就減1。這種方式下,讀和更新操作都很快,但你再想一下這種方式存在什么問(wèn)題嗎?
沒(méi)錯(cuò),緩存系統(tǒng)可能會(huì)丟失更新。
Redis的數(shù)據(jù)不能永久地留在內(nèi)存里,所以你會(huì)找一個(gè)地方把這個(gè)值定期地持久化存儲(chǔ)起來(lái)。但即使這樣,仍然可能丟失更新。試想如果剛剛在數(shù)據(jù)表中插入了一行,Redis中保存的值也加了1,然后Redis異常重啟了,重啟后你要從存儲(chǔ)redis數(shù)據(jù)的地方把這個(gè)值讀回來(lái),而剛剛加1的這個(gè)計(jì)數(shù)操作卻丟失了。
當(dāng)然了,這還是有解的。比如,Redis異常重啟以后,到數(shù)據(jù)庫(kù)里面單獨(dú)執(zhí)行一次count(*)獲取真實(shí)的行數(shù),再把這個(gè)值寫回到Redis里就可以了。異常重啟畢竟不是經(jīng)常出現(xiàn)的情況,這一次全表掃描的成本,還是可以接受的。
但實(shí)際上,將計(jì)數(shù)保存在緩存系統(tǒng)中的方式,還不只是丟失更新的問(wèn)題。即使Redis正常工作,這個(gè)值還是邏輯上不精確的。
你可以設(shè)想一下有這么一個(gè)頁(yè)面,要顯示操作記錄的總數(shù),同時(shí)還要顯示最近操作的100條記錄。那么,這個(gè)頁(yè)面的邏輯就需要先到Redis里面取出計(jì)數(shù),再到數(shù)據(jù)表里面取數(shù)據(jù)記錄。
我們是這么定義不精確的:
一種是,查到的100行結(jié)果里面有最新插入記錄,而Redis的計(jì)數(shù)里還沒(méi)加1;
另一種是,查到的100行結(jié)果里沒(méi)有最新插入的記錄,而Redis的計(jì)數(shù)里已經(jīng)加了1。
這兩種情況,都是邏輯不一致的。
我們一起來(lái)看看這個(gè)時(shí)序圖。
圖2 會(huì)話A、B執(zhí)行時(shí)序圖圖2中,會(huì)話A是一個(gè)插入交易記錄的邏輯,往數(shù)據(jù)表里插入一行R,然后Redis計(jì)數(shù)加1;會(huì)話B就是查詢頁(yè)面顯示時(shí)需要的數(shù)據(jù)。
在圖2的這個(gè)時(shí)序里,在T3時(shí)刻會(huì)話B來(lái)查詢的時(shí)候,會(huì)顯示出新插入的R這個(gè)記錄,但是Redis的計(jì)數(shù)還沒(méi)加1。這時(shí)候,就會(huì)出現(xiàn)我們說(shuō)的數(shù)據(jù)不一致。
你一定會(huì)說(shuō),這是因?yàn)槲覀儓?zhí)行新增記錄邏輯時(shí)候,是先寫數(shù)據(jù)表,再改Redis計(jì)數(shù)。而讀的時(shí)候是先讀Redis,再讀數(shù)據(jù)表,這個(gè)順序是相反的。那么,如果保持順序一樣的話,是不是就沒(méi)問(wèn)題了?我們現(xiàn)在把會(huì)話A的更新順序換一下,再看看執(zhí)行結(jié)果。
圖3 調(diào)整順序后,會(huì)話A、B的執(zhí)行時(shí)序圖你會(huì)發(fā)現(xiàn),這時(shí)候反過(guò)來(lái)了,會(huì)話B在T3時(shí)刻查詢的時(shí)候,Redis計(jì)數(shù)加了1了,但還查不到新插入的R這一行,也是數(shù)據(jù)不一致的情況。
在并發(fā)系統(tǒng)里面,我們是無(wú)法精確控制不同線程的執(zhí)行時(shí)刻的,因?yàn)榇嬖趫D中的這種操作序列,所以,我們說(shuō)即使Redis正常工作,這個(gè)計(jì)數(shù)值還是邏輯上不精確的。
在數(shù)據(jù)庫(kù)保存計(jì)數(shù)
根據(jù)上面的分析,用緩存系統(tǒng)保存計(jì)數(shù)有丟失數(shù)據(jù)和計(jì)數(shù)不精確的問(wèn)題。那么,如果我們把這個(gè)計(jì)數(shù)直接放到數(shù)據(jù)庫(kù)里單獨(dú)的一張計(jì)數(shù)表C中,又會(huì)怎么樣呢?
首先,這解決了崩潰丟失的問(wèn)題,InnoDB是支持崩潰恢復(fù)不丟數(shù)據(jù)的。
備注:關(guān)于InnoDB的崩潰恢復(fù),你可以再回顧一下第2篇文章《日志系統(tǒng):一條SQL更新語(yǔ)句是如何執(zhí)行的?》中的相關(guān)內(nèi)容。
然后,我們?cè)倏纯茨懿荒芙鉀Q計(jì)數(shù)不精確的問(wèn)題。
你會(huì)說(shuō),這不一樣嗎?無(wú)非就是把圖3中對(duì)Redis的操作,改成了對(duì)計(jì)數(shù)表C的操作。只要出現(xiàn)圖3的這種執(zhí)行序列,這個(gè)問(wèn)題還是無(wú)解的吧?
這個(gè)問(wèn)題還真不是無(wú)解的。
我們這篇文章要解決的問(wèn)題,都是由于InnoDB要支持事務(wù),從而導(dǎo)致InnoDB表不能把count(*)直接存起來(lái),然后查詢的時(shí)候直接返回形成的。
所謂以子之矛攻子之盾,現(xiàn)在我們就利用“事務(wù)”這個(gè)特性,把問(wèn)題解決掉。
圖4 會(huì)話A、B的執(zhí)行時(shí)序圖我們來(lái)看下現(xiàn)在的執(zhí)行結(jié)果。雖然會(huì)話B的讀操作仍然是在T3執(zhí)行的,但是因?yàn)檫@時(shí)候更新事務(wù)還沒(méi)有提交,所以計(jì)數(shù)值加1這個(gè)操作對(duì)會(huì)話B還不可見。
因此,會(huì)話B看到的結(jié)果里, 查計(jì)數(shù)值和“最近100條記錄”看到的結(jié)果,邏輯上就是一致的。
不同的count用法
在前面文章的評(píng)論區(qū),有同學(xué)留言問(wèn)到:在select count(?) from t這樣的查詢語(yǔ)句里面,count(*)、count(主鍵id)、count(字段)和count(1)等不同用法的性能,有哪些差別。今天談到了count(*)的性能問(wèn)題,我就借此機(jī)會(huì)和你詳細(xì)說(shuō)明一下這幾種用法的性能差別。
需要注意的是,下面的討論還是基于InnoDB引擎的。
這里,首先你要弄清楚count()的語(yǔ)義。count()是一個(gè)聚合函數(shù),對(duì)于返回的結(jié)果集,一行行地判斷,如果count函數(shù)的參數(shù)不是NULL,累計(jì)值就加1,否則不加。最后返回累計(jì)值。
所以,count(*)、count(主鍵id)和count(1) 都表示返回滿足條件的結(jié)果集的總行數(shù);而count(字段),則表示返回滿足條件的數(shù)據(jù)行里面,參數(shù)“字段”不為NULL的總個(gè)數(shù)。
至于分析性能差別的時(shí)候,你可以記住這么幾個(gè)原則:
server層要什么就給什么;
InnoDB只給必要的值;
現(xiàn)在的優(yōu)化器只優(yōu)化了count(*)的語(yǔ)義為“取行數(shù)”,其他“顯而易見”的優(yōu)化并沒(méi)有做。
這是什么意思呢?接下來(lái),我們就一個(gè)個(gè)地來(lá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è)用法的差別的話,你能對(duì)比出來(lái),count(1)執(zhí)行得要比count(主鍵id)快。因?yàn)閺囊娣祷豬d會(huì)涉及到解析數(shù)據(jù)行,以及拷貝字段值的操作。
對(duì)于count(字段)來(lái)說(shuō):
如果這個(gè)“字段”是定義為not null的話,一行行地從記錄里面讀出這個(gè)字段,判斷不能為null,按行累加;
如果這個(gè)“字段”定義允許為null,那么執(zhí)行的時(shí)候,判斷到有可能是null,還要把值取出來(lái)再判斷一下,不是null才累加。
也就是前面的第一條原則,server層要什么字段,InnoDB就返回什么字段。
但是count(*)是例外,并不會(huì)把全部字段取出來(lái),而是專門做了優(yōu)化,不取值。count(*)肯定不是null,按行累加。
看到這里,你一定會(huì)說(shuō),優(yōu)化器就不能自己判斷一下嗎,主鍵id肯定非空啊,為什么不能按照count(*)來(lái)處理,多么簡(jiǎn)單的優(yōu)化啊。
當(dāng)然,MySQL專門針對(duì)這個(gè)語(yǔ)句進(jìn)行優(yōu)化,也不是不可以。但是這種需要專門優(yōu)化的情況太多了,而且MySQL已經(jīng)優(yōu)化過(guò)count(*)了,你直接使用這種用法就可以了。
所以結(jié)論是:按照效率排序的話,count(字段)
小結(jié)
今天,我和你聊了聊MySQL中獲得表行數(shù)的兩種方法。我們提到了在不同引擎中count(*)的實(shí)現(xiàn)方式是不一樣的,也分析了用緩存系統(tǒng)來(lái)存儲(chǔ)計(jì)數(shù)值存在的問(wèn)題。
其實(shí),把計(jì)數(shù)放在Redis里面,不能夠保證計(jì)數(shù)和MySQL表里的數(shù)據(jù)精確一致的原因,是這兩個(gè)不同的存儲(chǔ)構(gòu)成的系統(tǒng),不支持分布式事務(wù),無(wú)法拿到精確一致的視圖。而把計(jì)數(shù)值也放在MySQL中,就解決了一致性視圖的問(wèn)題。
InnoDB引擎支持事務(wù),我們利用好事務(wù)的原子性和隔離性,就可以簡(jiǎn)化在業(yè)務(wù)開發(fā)時(shí)的邏輯。這也是InnoDB引擎?zhèn)涫芮嗖A的原因之一。
最后,又到了今天的思考題時(shí)間了。
在剛剛討論的方案中,我們用了事務(wù)來(lái)確保計(jì)數(shù)準(zhǔn)確。由于事務(wù)可以保證中間結(jié)果不被別的事務(wù)讀到,因此修改計(jì)數(shù)值和插入新記錄的順序是不影響邏輯結(jié)果的。但是,從并發(fā)系統(tǒng)性能的角度考慮,你覺(jué)得在這個(gè)事務(wù)序列里,應(yīng)該先插入操作記錄,還是應(yīng)該先更新計(jì)數(shù)表呢?
你可以把你的思考和觀點(diǎn)寫在留言區(qū)里,我會(huì)在下一篇文章的末尾給出我的參考答案。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一起閱讀。
上期問(wèn)題時(shí)間
上期我給你留的問(wèn)題是,什么時(shí)候使用alter table t engine=InnoDB會(huì)讓一個(gè)表占用的空間反而變大。
在這篇文章的評(píng)論區(qū)里面,大家都提到了一個(gè)點(diǎn),就是這個(gè)表,本身就已經(jīng)沒(méi)有空洞的了,比如說(shuō)剛剛做過(guò)一次重建表操作。
在DDL期間,如果剛好有外部的DML在執(zhí)行,這期間可能會(huì)引入一些新的空洞。
@飛翔 提到了一個(gè)更深刻的機(jī)制,是我們?cè)谖恼轮袥](méi)說(shuō)的。在重建表的時(shí)候,InnoDB不會(huì)把整張表占滿,每個(gè)頁(yè)留了1/16給后續(xù)的更新用。也就是說(shuō),其實(shí)重建表之后不是“最”緊湊的。
假如是這么一個(gè)過(guò)程:
將表t重建一次;
插入一部分?jǐn)?shù)據(jù),但是插入的這些數(shù)據(jù),用掉了一部分的預(yù)留空間;
這種情況下,再重建一次表t,就可能會(huì)出現(xiàn)問(wèn)題中的現(xiàn)象。
https://www.cnblogs.com/a-phper/p/10313903.html
總結(jié)
以上是生活随笔為你收集整理的实际返回的行数超出请求的行数怎么解决_count(*)这么慢,我该怎么办?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 电脑音频服务未运行怎么解决_电脑提示音频
- 下一篇: 信奥中的数学:斯特林数、卡特兰数