淺談MySQL 統計行數的 count

2021-12-24 碼農編程進階筆記

MySQL count() 函數我們並不陌生,用來統計每張表的行數。但如果你的表越來越大,且是 InnoDB 引擎的話,會發現計算的速度會越來越慢。在這篇文章裡,會先介紹 count() 實現的原理及原因,然後是 count 不同用法的性能分析,最後給出需要頻繁改變並需要統計表行數的解決方案。

InnoDB 和 MyISAM 是 MySQL 常用的數據引擎,由於兩者實現的不同,導致 count() 操作計算的效率也不同。

對於 MyISAM 來說,它把每個表的總行數都存在了磁碟上,因此使用 count(*) 計算時,效率很高直接返回結果。但如果加入了 where 條件,依然會進行搜索,所以效率是不高的。

對於 InnoDB 來說,在進行 count(*) 運算時,會把數據從引擎中一行行讀出來,然後累計計數,自然表大了之後,效率就變低了。

那麼,為什麼 InnoDB 不能像 MyISAM 在表中記錄呢?原因就在於 InnoDB 比 MyISAM 多了支持事務的特性,同時也需要一定的取捨。由於 MVCC 的控制,使得 MySQL 具有並發的能力,也就是說對於同一時刻,InnoDB 返回的表的行數是不一定的,事務看到的行數與開啟後的一致性視圖有關,換句話說,每個事務能看到的數據版本是不一樣的,只能一行行拿出來進行判斷。

像下面的事務,假設表 t 有 10000 條數據:

對於 Session A 來說,Session B 未提交不可見,Session C 提交了,但是在 Session A 啟動後提交的,也不可見。所以是 10000.

而對於 Session B 而言,Session C 在啟動之前提交,自己又插入了一條,所以結果是 10002.

其實 InnoDB 在進行 count(*) 操作時,還是做了優化的,在進行 count(*) 操作時,由於普通索引會保存主鍵的 id 值,所以會找到最小的那顆普通索引樹進行查找,而不是去遍歷主鍵索引樹。

在保證邏輯正確的前提下,減少掃描的數據量,是資料庫系統設計的通用法則。

另外在使用 show table status 時,也可以查詢出行數,而且速度很快,但需要注意的是,該命令是通過索引統計的值來採樣估算的。官方文檔說誤差可以有 40%-50%.

但如果我們真的需要實時的獲取的某個表的行數,應該怎麼辦呢?



用緩存系統來保存計數

對於進行更新的表,可能會想到用緩存系統來支持。比如 Redis 裡來保存某個表總行數。

每次插入資料庫時,Redis 計數加一,相反則減一,這樣看起來讀寫操作都很快,但會存在一些問題。

緩存系統會丟失更新:

對於 Redis 在內存中的數據,需要定期的同步到磁碟中,但對於 Redis 異常重啟,就沒有辦法了。比如在 Redis 中插入後,Redis 重啟,數據沒有持久化到硬碟。這時可以在重啟 Redis 後,從資料庫執行下 count(*) 操作,然後更新到 Redis 中。一次全表掃描還是可行的。

邏輯不精確:

假設一個頁面中,需要顯示一張表的行數,以及每一條數據。在實現時,可以先從 Redis 取數量,然後從資料庫裡取記錄。

但可能會出現這樣的情況:

資料庫查到 100 行結果裡有最新插入的記錄,而 Redis 計數裡少 1.

資料庫查到 100 行結果沒有最新的記錄,但 Redis 計數卻多了 1.

對於 Session B 來說,在 T2 時刻,會發現 Redis 的數量比資料庫少 1 條。

對於 Session B 來說,在 T2 時刻,會發現 Redis 的數量比資料庫多 1 條。

其實產生問題的原因就是因為 Redis 和資料庫查記錄沒有在同一個事務中。

用資料庫保存

由於 InnoDB 引擎的支持,MySQL 本身是支持事務的,所以將 Redis 的插入操作換成在資料庫的更新操作,就可以利用在RR級別下的事務特性,進而保證數據的精確性。

而且還有一點,由於 redo log 的支持,在 MySQL 發生異常時,是可以保證 crash-safe。



count() 本身是一個聚合函數,對於返回的結果集,一行行地判斷。如果參數不是 NULL 的話,會一直累加,最後返回結果。

所以 count(*), count(id), count(1) 表示都是返回滿足條件的結果集總行數。

而 count(欄位),則表示滿足條件的數據行裡,不為 NULL 的欄位。

對於 count(id) 來說,InnoDB 會遍歷整張表,把每行 id 取出來,給 server 層。Server 判斷 id 是否為空,然後累加。

對於 count(1) 來說,InnoDB 會遍歷整張表,但不取值。Server 層會自己放入 1,然後累加。

所以對於 count(1) 的執行會比 count(*) 要快,少了解析數據行以及拷貝欄位值的操作。

對於 count(欄位) 來說,如果欄位定義時是 not null, 會一行行讀出,並判斷不能為 null,然後累加。如果定義時可以為 null,執行時,需要將值去除,判斷不是 null 才累加。

count(*) 除外,專門做了優化,不取值,直接按行累加,並且會找到最小的索引樹進行計算。



MySQL count() 函數的執行效率和底層的數據引擎有關。MyISAM 不加 where 條件,查詢會很快,但不支持事務。InnoDB 支持事務,由於 MVCC 的實現,導致每次查詢都需要一行行的掃描,效率不高。

解決方法可以通過設計外部緩存如 Redis,保存記錄。但存在異常重啟和數據不準確的情況。可以通過在 InnoDB 中新建一張表,保存記錄這樣的解決方案。

最後,InnoDB 對 count(*) 做了獨立的優化,而其他的 count 操作,則需要額外的操作。

相關焦點

  • mysql中select count()的區別
    在 InnoDB引擎中count(*)、count(1)、count(主鍵)、count(欄位) 哪個性能最高?應該使用哪個?
  • mysql中count(*)是最慢的嗎?
    count如何實現的:在mysql中,當數據表數據比較少的時候,使用count可以很快進行返回需要統計的數據行數,當數量比較大的時候,count的返回結果的速度就變慢的很多,出現這種情況時,使用的mysql存儲引擎大概率是InnoDB。因為在MyISAM引擎中,對每個表的總行數都會進行記錄,並存在磁碟上。
  • mysql中count()太慢,我該怎麼辦
    中,count()實現原理,以及count()的各種使用方式,和他們之間的差異,以及執行效率的比較,看完之後,想必你不會在說,count(*)執行效率是最慢的了吧,經過mysql優化後的count(*)的執行效率和count(1)已經不相上下了,但是當表中的數據量很大的時候,無論何種形式的count(),執行的效率都會很低,畢竟需要逐行統計計數。
  • MySQL 優化案例 - select count-愛可生
    二、原 SQL 分析Server version: 5.7.24-log MySQL Community Server (GPL)SQL 如下,僅僅就是統計 api_runtime_log這張表的行數,一條簡單的不能再簡單的 SQL:select count(*) from api_runtime_log;我們先去運行一下這條 SQL
  • MySQL:SELECT COUNT 小結
    討論歸納先來看看MySQL官方對SELECT COUNT的定義:傳送門:https://dev.mysql.com/doc/refman/5.6/en/aggregate-functions.html#function_count
  • 故障分析 | MySQL 優化案例 - select count(*)
    Server version: 5.7.24-log MySQL Community Server (GPL)SQL 如下,僅僅就是統計 api_runtime_log 這張表的行數,一條簡單的不能再簡單的 SQL:select count(*) from api_runtime_log
  • 技術分享 | MySQL:count(*)、count(欄位) 實現上區別
    實際上 count 很可能使用到覆蓋索引(Using index),本文主要討論它們實現的異同。count(*) 和 count(欄位) 在結果上可能並不一致。比如 count(欄位),但是某些行本欄位為 NULL 的話那麼將不會統計行數,下面將會說明這種 NULL 判斷的位置。本文還是使用簡單的全表掃描來進行對比實現上的區別。
  • count(1)、count(*) 與 count(列名) 的執行區別
    count(欄位) 會統計該欄位在表中出現的次數,忽略欄位為null 的情況。即不統計欄位為null 的記錄。 轉自:http://www.cnblogs.com/Dhouse/p/6734837.htmlcount(*) 和 count(1)和count(列名)區別執行效果上:  count(*)包括了所有的列,相當於行數,在統計結果的時候,不會忽略列值為NULL
  • mysql大表中count()的用法以及mysql中count()的優化
    本篇文章給大家帶來的內容是關於mysql大表中count()的用法以及mysql中count()的優化,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。一個單表中包含有6000w+的數據,然而你又不能拆分.需要分別統計表中有多少數據,A產品有多少,B產品有多少這幾個數據.
  • MySQL的COUNT是怎麼執行的
    前置知識3——COUNT是個啥COUNT是一個匯總函數(聚集函數),它接收1個表達式作為參數:COUNT函數用於統計在符合搜索條件的記錄中,指定的表達式expr不為NULL的行數有多少。這裡需要特別注意的是,expr不僅僅可以是列名,其他任意表達式都是可以的。
  • MySQL的COUNT語句,竟然都能被面試官虐的這麼慘!?
    2、如果查詢結果沒有命中任何記錄,則返回03、但是,值得注意的是,COUNT(*) 的統計結果中,會包含值為NULL的行數。即以下表記錄create table insert insert insert insert insert insert insert 使用語句count(*),count(id),count(id2)查詢結果如下:select count(*),count
  • 資料庫 select count(1) ,count(*),count(欄位) 有什麼區別?
    討論歸納先來看看MySQL官方對SELECT COUNT的定義:傳送門:https://dev.mysql.com/doc/refman/5.6/en/aggregate-functions.html#function_count
  • mysql count(*)count(1)count(欄位)區別
    收錄於話題 #mysqlcount() 是一個聚合函數,對於返回的結果集,一行行地判斷,如果 count 函數的參數不是 NULL,累計值就加 1,否則不加。最後返回累計值。所以,count(*)、count(主鍵 id) 和 count(1) 都表示返回滿足條件的結果集的總行數;而 count(欄位),則表示返回滿足條件的數據行裡面,參數「欄位」不為 NULL 的總個數。
  • MySQL 表如何計算統計信息-愛可生
    比如對比指定表在系統表 mysql.innodb_index_stats 的數據跟 distinct 查詢的結果,如果相差太大,可以考慮增加這個值。 當 analyze table 變的非常慢時,可能是這個值設置的太大了,此時要考慮減小這個值。
  • MySQL 中 count(*) 比 count(1) 快?
    中 count(1) 比 count(*) 快,這能忍?3.2 原理分析不知道小夥伴們有沒有注意過,我們學習 MySQL 的時候,count 函數是歸在聚合函數那一類的,就是 avg、sum 等,count 函數和這些歸在一起,說明它也是一個聚合函數。既然是聚合函數,那麼就需要對返回的結果集進行一行行的判斷,這裡就涉及到一個問題,返回的結果是啥?
  • MySQL count(*),count(1),count(field)區別、性能差異及優化建議
    MySQL count函數是用來獲取表中滿足一定條件的記錄數,常見用法有三種,count(*),count(1),count(field),這三種有什麼區別?在性能上有何差異?本文將通過測試案例詳細介紹和分析。
  • 小王:MySQL count(1) 真的比 count(*) 快麼?
    有 Where 條件的 count,會根據掃碼結果count 一下所有的行數,其性能更依賴於你的 Where 條件,所以文章我們僅針對沒有 Where 的情況進行說明。MyISAM 引擎會把一個表的總行數記錄了下來,所以在執行 count(*) 的時候會直接返回數量,執行效率很高。
  • MySQL 8.0 新特性之統計直方圖
    體系架構、主從複製,熟悉問題定位與解決出品   沃趣科技原文連結:https://mysqlserverteam.com/histogram-statistics-in-mysql/|  概覽MySQL8.0實現了統計直方圖。
  • MySQL最常用分組聚合函數
    mysql> select count(*) from salary_tab where salary='1000';++| count(*) |++| 2 |++mysql> select count(*) from salary_tab;  #沒有條件,默認統計表數據行數++| count
  • 卓象程式設計師:MySQL中count的使用
    select count(*)應該是一個比較常用的語句,用來統計記錄行數。但是,慢慢地你會發現,這個語句越來越慢了,為什麼呢?MySQL 中,不同的存儲引擎,count(*)的實現方式是不同的。因為它不支持事務啊,不用擔心數據不一致的問題,因此可以快速返回count(*)。