在開發系統的時候,可能常常須要計算一個表的行數,好比一個交易系統的全部變動記錄總數。這時候你可能會想,一條select count(*) from t 語句不就解決了嗎?redis
可是,會發現隨着系統中記錄數愈來愈多,這條語句執行得也會愈來愈慢。而後你可能就想了,MySQL怎麼這麼笨啊,記個總數,每次要查的時候直接讀出來,不就行了嗎。數據庫
在不一樣的MySQL引擎中,count(*)有不一樣的實現方式。緩存
1)MyISAM引擎把一個表的總行數存在了磁盤上,所以執行count(*)的時候會直接返回這個數,效率很高;安全
2)而InnoDB引擎就麻煩了,它執行count(*)的時候,須要把數據一行一行地從引擎裏面讀出來,而後累積計數。併發
這裏須要注意的是,咱們在這篇文章裏討論的是沒有過濾條件的count(*),若是加了where 條件的話,MyISAM表也是不能返回得這麼快的。分佈式
爲何要使用InnoDB,由於不管是在事務支持、併發能力仍是在數據安全方面,InnoDB都優於MyISAM。我猜你的表也必定是用了InnoDB引擎。這就是當你的記錄數愈來愈多的時候,計算一個表的總行數會愈來愈慢的緣由。函數
爲何innodb不和myisam同樣呢,也把數字存起來。性能
這是由於即便是在同一個時刻的多個查詢,因爲多版本併發控制(MVCC)的緣由,InnoDB表「應該返回多少行」也是不肯定的。優化
假設表t中如今有10000條記錄,咱們設計了三個用戶並行的會話。線程
會話A先啓動事務並查詢一次表的總行數;
會話B啓動事務,插入一行後記錄後,查詢表的總行數;
會話C先啓動一個單獨的語句,插入一行記錄後,查詢表的總行數。
在最後一個時刻,三個會話A、B、C會同時查詢表t的總行數,但拿到的結果卻不一樣。
這和InnoDB的事務設計有關係,可重複讀是它默認的隔離級別,在代碼上就是經過多版本併發控制,也就是MVCC來實現的。每一行記錄都要判斷本身是否對這個會話可見,所以對於count(*)請求來講,InnoDB只好把數據一行一行地讀出依次判斷,可見的行纔可以用於計算「基於這個查詢」的表的總行數。
InnoDB是索引組織表,主鍵索引樹的葉子節點是數據,而普通索引樹的葉子節點是主鍵值。因此,普通索引樹比主鍵索引樹小不少。對於count(*)這樣的操做,遍歷哪一個索引樹獲得的結果邏輯上都是同樣的。所以,MySQL優化器會找到最小的那棵樹來遍歷。在保證邏輯正確的前提下,儘可能減小掃描的數據量,是數據庫系統設計的通用法則之一。
若是你用過show table status 命令的話,就會發現這個命令的輸出結果裏面也有一個TABLE_ROWS用於顯示這個表當前有多少行,這個命令執行挺快的,那這個TABLE_ROWS能代替count(*)嗎?
實際上,TABLE_ROWS就是從這個採樣估算得來的,所以它也很不許。有多不許呢,官方文檔說偏差可能達到40%到50%。因此,show table status命令顯示的行數也不能直接使用。
這裏進行總結一下:
MyISAM表雖然count(*)很快,可是不支持事務;
show table status命令雖然返回很快,可是不許確;
InnoDB表直接count(*)會遍歷全表,雖然結果準確,但會致使性能問題。
對於更新很頻繁的庫來講,你可能會第一時間想到,用緩存系統來支持。
你能夠用一個Redis服務來保存這個表的總行數。這個表每被插入一行Redis計數就加1,每被刪除一行Redis計數就減1。這種方式下,讀和更新操做都很快,但你再想一下這種方式存在什麼問題嗎?
沒錯,緩存系統可能會丟失更新。
Redis的數據不能永久地留在內存裏,因此你會找一個地方把這個值按期地持久化存儲起來。但即便這樣,仍然可能丟失更新。試想若是剛剛在數據表中插入了一行,Redis中保存的值也加了1,而後Redis異常重啓了,重啓後你要從存儲redis數據的地方把這個值讀回來,而剛剛加1的這個計數操做卻丟失了。
固然了,這仍是有解的。好比,Redis異常重啓之後,到數據庫裏面單獨執行一次count(*)獲取真實的行數,再把這個值寫回到Redis裏就能夠了。異常重啓畢竟不是常常出現的狀況,這一次全表掃描的成本,仍是能夠接受的。
但實際上,將計數保存在緩存系統中的方式,還不僅是丟失更新的問題。即便Redis正常工做,這個值仍是邏輯上不精確的。
你能夠設想一下有這麼一個頁面,要顯示操做記錄的總數,同時還要顯示最近操做的100條記錄。那麼,這個頁面的邏輯就須要先到Redis裏面取出計數,再到數據表裏面取數據記錄。
咱們是這麼定義不精確的:
這兩種狀況,都是邏輯不一致的。
咱們一塊兒來看看這個時序圖。
圖2中,會話A是一個插入交易記錄的邏輯,往數據表裏插入一行R,而後Redis計數加1;會話B就是查詢頁面顯示時須要的數據。
在圖2的這個時序裏,在T3時刻會話B來查詢的時候,會顯示出新插入的R這個記錄,可是Redis的計數還沒加1。這時候,就會出現咱們說的數據不一致。
你必定會說,這是由於咱們執行新增記錄邏輯時候,是先寫數據表,再改Redis計數。而讀的時候是先讀Redis,再讀數據表,這個順序是相反的。那麼,若是保持順序同樣的話,是否是就沒問題了?咱們如今把會話A的更新順序換一下,再看看執行結果。
你會發現,這時候反過來了,會話B在T3時刻查詢的時候,Redis計數加了1了,但還查不到新插入的R這一行,也是數據不一致的狀況。
在併發系統裏面,咱們是沒法精確控制不一樣線程的執行時刻的,由於存在圖中的這種操做序列,因此,咱們說即便Redis正常工做,這個計數值仍是邏輯上不精確的。
根據上面的分析,用緩存系統保存計數有丟失數據和計數不精確的問題。那麼,若是咱們把這個計數直接放到數據庫裏單獨的一張計數表C中,又會怎麼樣呢?
首先,這解決了崩潰丟失的問題,InnoDB是支持崩潰恢復不丟數據的。
count()是一個聚合函數,對於返回的結果集,一行行地判斷,若是count函數的參數不是NULL,累計值就加1,不然不加。最後返回累計值。
因此,count(*)、count(主鍵id)和count(1) 都表示返回知足條件的結果集的總行數;而count(字段),則表示返回知足條件的數據行裏面,參數「字段」不爲NULL的總個數。
至於分析性能差異的時候,你能夠記住這麼幾個原則:
server層要什麼就給什麼;
InnoDB只給必要的值;
如今的優化器只優化了count(*)的語義爲「取行數」,其餘「顯而易見」的優化並無作。
這是什麼意思呢?接下來,咱們就一個個地來看看。
對於count(主鍵id)來講,InnoDB引擎會遍歷整張表,把每一行的id值都取出來,返回給server層。server層拿到id後,判斷是不可能爲空的,就按行累加。
對於count(1)來講,InnoDB引擎遍歷整張表,但不取值。server層對於返回的每一行,放一個數字「1」進去,判斷是不可能爲空的,按行累加。
單看這兩個用法的差異的話,你能對比出來,count(1)執行得要比count(主鍵id)快。由於從引擎返回id會涉及到解析數據行,以及拷貝字段值的操做。
可是count(*)是例外,並不會把所有字段取出來,而是專門作了優化,不取值。count(*)確定不是null,按行累加。
看到這裏,你必定會說,優化器就不能本身判斷一下嗎,主鍵id確定非空啊,爲何不能按照count(*)來處理,多麼簡單的優化啊。
固然,MySQL專門針對這個語句進行優化,也不是不能夠。可是這種須要專門優化的狀況太多了,並且MySQL已經優化過count(*)了,你直接使用這種用法就能夠了。
因此結論是:按照效率排序的話,count(字段)<count(主鍵id)<count(1)≈count(*),因此建議,儘可能使用count(*)。
提到了在不一樣引擎中count(*)的實現方式是不同的,也分析了用緩存系統來存儲計數值存在的問題。
其實,把計數放在Redis裏面,不可以保證計數和MySQL表裏的數據精確一致的緣由,是這兩個不一樣的存儲構成的系統,不支持分佈式事務,沒法拿到精確一致的視圖。而把計數值也放在MySQL中,就解決了一致性視圖的問題。
InnoDB引擎支持事務,咱們利用好事務的原子性和隔離性,就能夠簡化在業務開發時的邏輯。這也是InnoDB引擎備受青睞的緣由之一。