MySQL統計總數就用count(*),別花裡胡哨的《死磕MySQL系列 十》

發表於2021-11-15

有一個問題是這樣的統計資料總數用count(*)、count(主鍵ID)、count(欄位)、count(1)那個效率高。

先說結論,不用那麼花裡胡哨遇到統計總數全部使用count(*).

但是有很多小夥伴就會問為什麼呢?本期文章就解決大家的為什麼。

系列文章

五分鐘,讓你明白MySQL是怎麼選擇索引《死磕MySQL系列 六》

字串可以這樣加索引,你知嗎?《死磕MySQL系列 七》

無法復現的“慢”SQL《死磕MySQL系列 八》

什麼?還在用delete刪除資料《死磕MySQL系列 九》

一、不同儲存引擎的做法

你需要知道的是在不同的儲存引擎下,MySQL對於使用count(*)返回結果的流程是不一樣的。

在Myisam中,每張表的總行數都會儲存在磁碟上,因此執行count(*)時,是直接從磁碟拿到這個值返回,效率是非常高的。但你也要知道如果加了條件的統計總數返回也不會那麼快的。

在Innodb引擎中,執行count(*),需要把資料一行一行的讀出來,然後再統計總數返回。

問題:為什麼Innodb不跟Myisam一樣把表總數存起來呢?

這個問題就需要追溯的我們之前的MVCC文章,就是因為要實現多版本併發控制,才會導致Innodb不能直接儲存表總數。

因為每個事務獲取到的一致性檢視都是不一樣的,所以返回的資料總數也是不一致的。

如果你無法理解,再回到MVCC文章好好看看,意思就跟不同事務看到的資料不一致一回事。

實戰案例

假設這三個使用者是並行的,你會看到三個使用者看到最終的資料總數都不一致。

每個使用者會根據read view儲存的資料來判斷那些資料是自己可以看見的,那些是看不見的。

read view

當執行SQL語句查詢時會產生一致性檢視,也就是read-view,它是由查詢的那一時間所有未提交事務ID組成的陣列,和已經建立的最大事務ID組成的。

在這個陣列中最小的事務ID被稱之為min_id,最大事務ID被稱之為max_id,查詢的資料結果要根據read-view做對比從而得到快照結果。

於是就產生了以下的對比規則,這個規則就是使用當前的記錄的trx_id跟read-view進行對比,對比規則如下。

如果落在trx_id<min_id,表示此版本是已經提交的事務生成的,由於事務已經提交所以資料是可見的

如果落在trx_id>max_id,表示此版本是由將來啟動的事務生成的,是肯定不可見的

若在min_id<=trx_id<=max_id時

  • 如果row的trx_id在陣列中,表示此版本是由還沒提交的事務生成的,不可見,但是當前自己的事務是可見的
  • 如果row的trx_id不在陣列中,表明是提交的事務生成了該版本,可見

二、MySQL對count(*)做了什麼優化

先來看兩個索引結構,一個是主鍵索引、另一個是普通索引。

主鍵索引
主鍵索引
普通索引
普通索引

現在你應該知道了,主鍵索引的葉子節點儲存的是整行資料,而普通索引葉子節點儲存的是主鍵值。

得出結論就是普通索引的比主鍵索引會小很多。

所以,MySQL對於count(*)這樣的操作,不管遍歷那個索引樹得到的結果在邏輯上都一樣。

因此,優化器會找到最小的那棵樹來遍歷,在保證正確的邏輯前提下,儘量減少掃描資料量,是資料庫系統設計的通用法則之一。

問題:為什麼儲存的有資料怎麼不用?

這個圖的資料怎麼得到的,我想你應該知道了,沒錯,就是執行show table status \G;得來的。

那為什麼innodb儲存引擎不直接使用Rows這個值呢?

還記不記得在第六期文章中,五分鐘,讓你明白MySQL是怎麼選擇索引《死磕MySQL系列 六》

先不要返回去看這篇文章,看下上文圖中最後查到的資料總條數是多少。

你會發現這兩個統計的資料是不一致的,因此這個值肯定是不可以用的。

具體原因

因為Rows這個值跟索引基數Cardinality一樣,都是通過取樣統計的。

取樣規則

首先,會選出N個資料頁,然後統計每個資料頁上不同的值,最後得到一個平均值。再用這個平均值乘索引的資料頁總數得到的就是索引基數。

並且這個索引基數也不是一成不變的,會隨著資料持續增刪改,當變更的資料超過1/M時才會觸發,M值是根據MySQL引數innodb_stats_persistent得到的,設定為on是10,off是16。

在MySQL8.0這個預設值為on,也就是說當這張表的資料變更超過總資料的1/10就會重新觸發取樣統計。

三、不同count的用法

以下所有的結論都基於MySQL的Innodb儲存引擎。

count(主鍵ID)

innodb引擎會遍歷整張表,把每一行的ID值都那出來,然後返回給server層,server層拿到ID後,判斷不可能為空,進行累加。

count(1)

同樣遍歷整張表,但不取值,server層對返回的每一行,放一個數字1進去,判斷是不可能為空的,按行累加。

count(欄位)

分為兩種情況,欄位定義為not null和null

  • 為not null時:逐行從記錄裡面讀出這個欄位,判斷不能為null,累加
  • 為 null時:執行時,判斷到有可能是null,還要把值取出來再判斷一下,不是null才累加。

count(*)

這個哥們就厲害了,不是帶了*就把所有值取出來,而是MySQL做了專門的優化,count ( * )肯定不是null,按行累加。

結論

按照效率的話,欄位 < 主鍵ID < 1 ~ ,最好都使用count(),別花裡胡哨的。

五、總結

本期文章就一句話,統計總數就用count(*),別花裡胡哨的

堅持學習、堅持寫作、堅持分享是咔咔從業以來所秉持的信念。願文章在偌大的網際網路上能給你帶來一點幫助,我是咔咔,下期見。

相關文章