首先要明確的是,不同的引擎中count( * )有不同的實現方式。
MyISAM 引擎把一個表的總行數存在磁碟上,因此執行count( * )的時候會直接返回這個數,效率非常高;
InnoDB 引擎就會麻煩點,它執行count( * )的時候,需要把數據一行一行地從引擎裡面讀出來,然後再累積計數。
注意:這篇裡面討論的都是沒有過濾條件的count( * ), 如果加了where條件的話,MyISAM表也是不能返回得那麼快的。
InnoDB 在事務支持、並發能力、數據安全方面,都優於MyISAM。
如果表用的是InnoDB引擎,那麼當你的記錄數越多,計算一個表的總行數就會越來越慢。
為什麼 InnoDB 不跟 MyISAM 一樣把數字存起來???
這是因為即使是在同一個時刻的多個查詢,由於多版本並發控制(MVCC)的原因,InnoDB 表「應該返回多少行」也是不確定的。
假設表 t 中現有 10000 條記錄,設計了三個用戶並行的會話。
會話 B 啟動事務,插入一行後記錄後,查詢表的總行數;繪畫 C 先啟動一個單獨的語句,插入一行記錄後,查詢表的總行數。假設從上到下是按照時間順序執行的,同一行語句是在同一時刻執行的。
會話 A會話 B會話 Cbegin;上面表格可以看到,在最後一個時刻,三個會話 A、B、C 會同時查詢表 t 的總行數,但拿到的結果卻不同。
這和InnoDB的事務設計有關係,可重複讀是它默認的隔離級別,在代碼上就是通過 MVCC 來實現的。
每一行記錄都要判斷自己是否對這個會話可見,因此對於count( * )請求來說,innodb 只好把數據一行一行地讀出依次判斷,可見的行才能夠用於計算「基於這個查詢」的表的總行數。
忘了MVCC可以回顧 MySQL系列:事務隔離 和 MySQL系列:再談事務隔離
但,MySQL 在執行 count( * )操作的時候還是做了優化的。
innodb 是索引組織表,主鍵索引樹的葉子節點是數據,而普通索引樹的葉子節點是主鍵值。 所以,普通索引樹比主鍵索引樹小很多。
對於count( * )這樣的操作,遍歷哪個索引樹得到的結果邏輯上都是一樣的。因此,mysql 優化器會找到最小的那顆樹來遍歷。
在保證邏輯正確的前提下,儘量減少掃描的數據量,是資料庫系統設計的通用法則 之一。
B+樹只有葉子節點上有數據,全部遍歷其實就是對葉子節點的鍊表進行遍歷。此時如果遍歷主鍵索引樹,由於其葉子節點上存放的是完整的行信息,對於一個數據頁而言其行密度會比較小,最終導致要掃描的數據頁較多,進而 IO 開銷也比較大。
如果遍歷第二索引樹,其葉子節點只存放主鍵信息,其數據頁行密度比較大,最終掃描的數據頁較少,節省了 IO 開銷。
MySQL show table status 命令輸出結果裡面也有一個 TABLE_ROWS用於顯示這個表當前有多少行,執行挺快的。能代替 count( * ) ???
MySQL系列:優化器選錯索引來龍去脈 中提到過,索引統計的值是通過採樣來估算的。實際上,table_rows 就是從這個採樣估算得來的,因此它很不準。官方文檔說誤差可能達到 40% 到 50%。所以不能直接用show table status。
小結
MyISAM 表雖然 count( * )很快,但是不支持事務;show table status 命令雖然返回很快,但是不準確;innodb 表直接count( * )會遍歷全表,雖然結果準確,但會導致性能問題。2用緩存系統保存計數如果更新比較頻繁的庫話,可以用一個 redis 服務來保存這個表的總行數。這個表每被插入一行 redis 計數就加 1,每被刪除一行 redis 計數就減1。這種方式下,讀和更新操作都很快,但仔細一想,會存在個問題 -- 緩存系統可能會丟失更新。
但實際上,將計數保存在緩存系統中的方式,還不只是丟失更新的問題。即使 Redis 正常工作,這個值還是邏輯上不精確的。
這裡主要原因是因為「MySQL 插入一行數據」 跟 「redis 計數加 1」 這兩個操作是分開的,不是原子性,這就很可能在中間過程因為某些並發出現問題。
更抽象一點:mysql 和 redis 是兩個不同的載體,將關聯數據記錄到不同的載體,而不同的載體要實現原子性很難,由於不是原子性很容易引起並發問題。
如果能將數據統一在同個載體 即 MySQL,並由其保證操作的原子性,即將插入一行數據和計數加 1 作為一個完整的事務,同個事務的隔離此時外界看到的就是要麼全部執行完畢,要麼全部都沒執行,進而保持邏輯一致。
3在資料庫保存計數用緩存系統保存計數有丟失數據和計數不精確的問題。如果我們把這個計數直接放到資料庫裡單獨的一張計數表 C 中,會怎樣??
首先,這解決了崩潰丟失的問題,InnoDB 是支持崩潰恢復不丟數據的。
MySQL系列:更新語句是如何執行的中有相關內容。
時刻會話 A會話 BT1會話 B 的讀操作是在 T3 執行的,但是因為這個時候更新事務還沒有提交,所以計數值加 1 這個操作對會話 B 還不可見。
因此,會話 B 看到的結果裡,查計數值和「最近 100 條記錄」看到的結果,邏輯上就是一致的。
4不同的 count 用法下面基於 InnoDB 引擎。
首先,count()是一個聚合函數,對於返回結果集,一行行的判斷,如果 count 函數的參數不是 null,累計值就加 1,否則不加。最後返回累計值。
執行過程分為 2 步:
count(* )、count(主鍵 id)、count(1) 都表示返回滿足條件的結果集的總行數;
count(欄位) 表示返回滿足條件的數據行裡面,參數「欄位」不為 null 的總個數。
分析性能差別,下面幾個原則:
現在的優化器只優化了 count(* ) 的語義為「取行數」。對於count(主鍵 id)來說, innodb 引擎會遍歷整張表,把每一行的 id 值都取出來,返回給 server 層。server 層拿到 id 後,判斷是不可能為空的,就按行累加。
對於(1)來說, innodb 引擎遍歷整張表,但不取值。server 層對於返回的每一行,放一個數字「1」進去,判斷是不可能為空的,按行累加。
但看這兩個用法的差別的話,能看出,count(1) 執行比 count(主鍵 id) 快。因為從引擎返回 id 會涉及到解析數據行,以及拷貝欄位值的操作。
對於 count(欄位)來說:
如果這個「欄位」是定義為 not null 的話,一行行地從記錄裡面讀出這個欄位,判斷不能為 null,按行累加;如果這個「欄位」 定義允許為 null,那麼執行的時候,判斷到有可能是 null,還要把值取出來再判斷一下,不是 null 才累加。也就是前面的第一條原則,server 層要什麼欄位,innodb 就返回什麼欄位。
但 count( * )是個例外, 並不會把全部欄位取出來,而是專門做了優化,不取值。count(* ) 肯定不是 null,按行累加。
結論: 按照效率排序的話,count(欄位)<count(主鍵 id)<count(1)≈count(* ),所以我建議你,儘量使用 count(* )。
給茄子注入靈魂