0.背景
自從大家對於MySQL數據庫的穩定性有了更高的追求后,經常有小伙伴有這樣的疑問,對於count(*)這樣的操作,有沒有正確的姿勢,或者有沒有可以優化的地方?
但答案比較殘酷,如果已經使用了正確的索引,那么基本上沒有可以優化的地方。一旦出現慢查詢了,它就是慢查詢了,要改,只能自己計數或者通過其他搜索平台來做。
今天,就一起來看看為什么會這樣,並對大家日常會遇到的一些的困惑進行解答。
1.count(*)的實現方式
據說,MyISAM 引擎把一個表的總行數存在了磁盤上,因此執行 count(*) 的時候會直接返回這個數,效率很高。
而我們的mysql一般都是用Innodb的引擎,Innodb是怎么實現count操作的呢?
InnoDB 引擎就比較麻煩來,它執行 count(*) 的時候,需要把數據一行一行地從引擎里面讀出來,然后累積計數。
所以,當我們的表里面的記錄越來越多的時候,count(*)就會越來越慢。
當然,我們這里說的都是不帶where條件的,如果帶上where條件的話,MyISAM也是很慢的。
2.正確的打開方式
嗯,首先還是說,mysql上不太推薦用count(*)來做統計相關業務,尤其是表非常大的情況下。
那如果業務比較小,需要快速上馬,那么,至少應該保證count(*)帶上了科學的where條件,然后,這個表也已經建立了科學的索引。
- 如果count(*)帶上的where條件,而且能夠走覆蓋索引,那還是可以偶爾走一走的。
- 如果count(*)帶上的where條件,能夠走索引,但是需要回表,那么這種就會比較危險,尤其是隨着表規模的擴大,終究是一顆雷。
- 如果純粹count(*),或者where條件沒有任何索引,萬萬萬不推薦!
那對於統計類的業務,推薦的幾種做法:
- 帶自增id的,可以用最大id來近似獲取
- 自己計數
- 其他數據分析平台進行聚合
3.能否用表統計信息代替count(*)
有同學在日常使用過程中,問能否使用 系統表的統計信息 來代替count。
答案是不行。這里的tableRows只是一個參考值。
這里的表統計信息,實際上是使用show table status獲取的。這個值是如何得到的呢?我們需要了解下mysql的采樣統計方法。
為什么要采樣統計呢?因為把整張表取出來一行行統計,雖然可以得到精確的結果,但是代價太高了,所以只能選擇“采樣統計”。(所以其實mysql自己也沒有count(*)的好方法)
采樣統計的時候,InnoDB 默認會選擇 N 個數據頁,統計這些頁面上的不同值,得到一個平均值,然后乘以這個索引的頁面數,就得到了這個索引的基數。
而數據表是會持續更新的,索引統計信息也不會固定不變。所以,當變更的數據行數超過 1/M 的時候,會自動觸發重新做一次索引統計。
因此,這個采樣估算得來的值,是很不准的。有多不准呢,官方文檔說誤差可能達到 40% 到 50%。
4.關於那些奇奇怪怪的count(?)
在看一些老代碼查詢的時候,我們經常會看到count(1),count(id),count(字段)等方式,那它們糾結孰優孰劣,到底有沒有性能上的差異呢?
這里,我們先要弄清楚 count() 的語義。
count() 是一個聚合函數,對於返回的結果集,一行行地判斷,如果 count 函數的參數不是 NULL,累計值就加 1,否則不加。最后返回累計值。
- count(主鍵id)
InnoDB 引擎會遍歷整張表,把每一行的 id 值都取出來,返回給 server 層。server 層拿到 id 后,判斷是不可能為空的,就按行累加。
- count(1)
InnoDB 引擎遍歷整張表,但不取值。server 層對於返回的每一行,放一個數字“1”進去,判斷是不可能為空的,按行累加。
- count(字段)
如果這個“字段”是定義為 not null 的話,一行行地從記錄里面讀出這個字段,判斷不能為 null,按行累加;
如果這個“字段”定義允許為 null,那么執行的時候,判斷到有可能是 null,還要把值取出來再判斷一下,不是 null 才累加。
- count(*)
並不會把全部字段取出來,而是專門做了優化,不取值。count(*) 肯定不是 null,按行累加。
所以結論是:按照效率排序的話,count(字段)<count(主鍵 id)<count(1)≈count(*),所以我建議,盡量使用 count(*)。
都看到最后了,原創不易,點個關注,點個贊吧~
知識碎片重新梳理,構建Java知識圖譜: https://github.com/saigu/JavaKnowledgeGraph (歷史文章查閱非常方便)
掃碼關注我的公眾號“阿丸筆記”,第一時間獲取最新更新。同時能免費獲取海量Java技術棧電子書、各個大廠面試題哦。