MySQL 統計行數的 count

MySQL count() 函數咱們並不陌生,用來統計每張表的函數。但若是你的表愈來愈大,而且是 InnoDB 引擎的話,會發現計算的速度會愈來愈慢。在這篇文章裏,會先介紹 count() 實現的原理及緣由,而後是 count 不一樣用法的性能分析,最後給出須要頻繁改變並須要統計表行數的解決方案。html

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
select count(*) from t;
insert into t ();
begin;
insert into t();
select count(*) from t; select count(*) from t; select count(*) from t;
10000; 結果是 10002 結果是 10001

對於 Session A 來講,Session B 未提交不可見,Session C 提交了,可是在 Session A 啓動後提交的,也不可見。因此是 10000.優化

而對於 Session B 而言,Session C 在啓動以前提交,本身又插入了一條,因此結果是 10002.設計

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

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

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

但若是咱們真的須要實時的獲取的某個表的行數,應該怎麼辦呢?

手動保存表的數量

用緩存系統來保存計數

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

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

緩存系統會丟失更新:

對於 Redis 在內存中的數據,須要按期的同步到磁盤中,但對於 Redis 異常重啓,就沒有辦法了。好比在 Redis 中插入後,Redis 重啓,數據沒有持久化到硬盤。這時能夠在重啓 Redis 後,從數據庫執行下 count(*) 操做,而後更新到 Redis 中。一次全表掃描仍是可行的。

邏輯不精確:

假設一個頁面中,須要顯示一張表的行數,以及每一條數據。在實現時,能夠先從 Redis 取數量,而後從數據庫裏取記錄。

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

  1. 數據庫查到 100 行結果裏有最新插入的記錄,而 Redis 計數裏少 1.
  2. 數據庫查到 100 行結果沒有最新的記錄,但 Redis 計數卻多了 1.
Session A Session B
插入一條數據; T1
讀 Redis 計數; T2
從數據庫中查記錄;
Redis 計數加 1; T3

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

Session A Session B
Redis 計數加 1; T1
讀 Redis 計數; T2
從數據庫中查記錄;
插入一條數據; T3

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

其實產生問題的緣由就是由於 Redis 和數據庫查記錄沒有在同一個事務中。

用數據庫保存

因爲 InnoDB 引擎的支持,MySQL 自己是支持事務的,因此將 Redis 的插入操做換成在數據庫的更新操做,就能夠利用在RR級別下的事務特性,進而保證數據的精確性。

並且還有一點,因爲 redo log 的支持,在 MySQL 發生異常時,是能夠保證 crash-safe。

不一樣 count 用法的執行效率

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 操做,則須要額外的操做。

相關文章
相關標籤/搜索