MySQL系列:count(*) 性能問題

2021-12-24 阿阿阿琦
1count( * )的實現方式

首先要明確的是,不同的引擎中count( * )有不同的實現方式。

MyISAM 引擎把一個表的總行數存在磁碟上,因此執行count( * )的時候會直接返回這個數,效率非常高;

InnoDB 引擎就會麻煩點,它執行count( * )的時候,需要把數據一行一行地從引擎裡面讀出來,然後再累積計數。

注意:這篇裡面討論的都是沒有過濾條件的count( * ), 如果加了where條件的話,MyISAM表也是不能返回得那麼快的。

InnoDB 在事務支持、並發能力、數據安全方面,都優於MyISAM。

如果表用的是InnoDB引擎,那麼當你的記錄數越多,計算一個表的總行數就會越來越慢。

為什麼 InnoDB 不跟 MyISAM 一樣把數字存起來???

這是因為即使是在同一個時刻的多個查詢,由於多版本並發控制(MVCC)的原因,InnoDB 表「應該返回多少行」也是不確定的。

假設表 t 中現有 10000 條記錄,設計了三個用戶並行的會話。

會話 B 啟動事務,插入一行後記錄後,查詢表的總行數;繪畫 C 先啟動一個單獨的語句,插入一行記錄後,查詢表的總行數。

假設從上到下是按照時間順序執行的,同一行語句是在同一時刻執行的。

會話 A會話 B會話 Cbegin;

select count(* )from t;



inset into t (插入一行);
begin;

inset into t (插入一行);
select count(* )from t;(返回 10000)select count(* )from t;(返回 10002)select count(* )from t;(返回 10001)

上面表格可以看到,在最後一個時刻,三個會話 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

T2begin; 表 C 中計數值加 1;
T3
begin; 讀表 C 計數值;查詢最近 100 條記錄;commit;T4插入一行數據 R,commit;

會話 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(* )。

給茄子注入靈魂

相關焦點

  • mysql中select count()的區別
    在 InnoDB引擎中count(*)、count(1)、count(主鍵)、count(欄位) 哪個性能最高?應該使用哪個?
  • mysql count(*)count(1)count(欄位)區別
    收錄於話題 #mysql 在mysql中查詢某個表多少條,一般是用count函數來查詢,但是count函數支持*,1,欄位等。
  • MySQL 優化案例 - select count-愛可生
    貌似也沒有什麼問題,走索引了呀!那麼是不是真的就沒問題呢?四、原理為了找到答案,通過 Google 查找 MySQL 下 select count(*)的原理,找到了答案。這邊省略過程,直接上結果。mysql> create index idx_rowguid on api_runtime_log(rowguid);Query OK, 0 rows affected (0.01 sec)Records: 0 Duplicates: 0 Warnings: 0mysql> select count(*) from api_runtime_log;++| count(
  • 技術分享 | MySQL:count(*)、count(欄位) 實現上區別
    實際上 count 很可能使用到覆蓋索引(Using index),本文主要討論它們實現的異同。count(*) 和 count(欄位) 在結果上可能並不一致。比如 count(欄位),但是某些行本欄位為 NULL 的話那麼將不會統計行數,下面將會說明這種 NULL 判斷的位置。本文還是使用簡單的全表掃描來進行對比實現上的區別。
  • mysql大表中count()的用法以及mysql中count()的優化
    本篇文章給大家帶來的內容是關於mysql大表中count()的用法以及mysql中count()的優化,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。一個單表中包含有6000w+的數據,然而你又不能拆分.需要分別統計表中有多少數據,A產品有多少,B產品有多少這幾個數據.
  • 故障分析 | MySQL 優化案例 - select count(*)
    ;我們先去運行一下這條 SQL,可以看到確實運行很慢,要 40 多秒左右,確實很不正常~mysql> select count(*) from api_runtime_log;++| count(*) |++|  5718952 |++1 row in set (42.95 sec)我們再去看下表結構,看上去貌似也挺正常的~存在主鍵
  • mysql中count(*)是最慢的嗎?
    count函數是什麼:這個問題,對於但凡接觸過資料庫的老鐵來說,實在太簡單了,但是對於筆者來說,學習任何一個事物,應該先對它的定義和概念一個清晰的認識,這樣才能更好的理解和學習它。首先 count函數是sql(Structured Query Language)中的一個聚合函數,用來統計滿足「條件」數據的行數。滿足的條件具體指什麼呢?
  • MySQL count(*),count(1),count(field)區別、性能差異及優化建議
    性能對比通過案例來測試一下count(*),count(1),count(field)的性能差異,MySQL版本為5.7.19,測試表是一張sysbench生成的表,表名sbtest1,總記錄數2411645,如下:測試SQL語句:select count
  • mysql中count()太慢,我該怎麼辦
    中,count()實現原理,以及count()的各種使用方式,和他們之間的差異,以及執行效率的比較,看完之後,想必你不會在說,count(*)執行效率是最慢的了吧,經過mysql優化後的count(*)的執行效率和count(1)已經不相上下了,但是當表中的數據量很大的時候,無論何種形式的count(),執行的效率都會很低,畢竟需要逐行統計計數。
  • MySQL 用 limit 為什麼會影響性能?
    灌入大量數據,共500萬:mysql> select count(*) from test;++| count(*) |++| 5242882 |++1 row in set (4.25 sec)我們知道,當limit offset rows中的offset很大時,會出現效率問題:
  • MySQL 用 limit 會影響性能?
    灌入大量數據,共500萬:mysql> select count(*) from test;++| count(*) |++| 5242882 |++1 row in set (4.25 sec)我們知道,當limit offset rows中的offset很大時,會出現效率問題:
  • MySQL的Limit 性能差?真的不能再用了?
    灌入大量數據,共500萬:mysql> select count(*) from test;++| count(*) |++| 5242882 |++1 row in set (4.25 sec)我們知道,當limit offset rows中的offset很大時,會出現效率問題:
  • 一文看懂MySQL開源審計的性能差異 | 附Python作圖
    為什麼要做性能測試?1. 從測試的角度看,軟體支持的特性和功能並不是唯一的問題,而性能測試的目標不僅僅是發現錯誤,還有消除性能的瓶頸。2. 從個人學習的角度看,筆者想通過性能測試更全面地了解一個關鍵參數的調整、一款軟體的選擇是否真的能夠提升性能。只有測試時考慮全面,上生產時才能更有信心,而不是道聽途說,人云亦云。
  • MySQL:SELECT COUNT 小結
    (1) 、 select count(*),和具體的select count(欄位)的不同寫法,本著分析的目的在會議室討論了起來,那這幾種寫法究竟孰優孰劣呢,我們一起來看一下。討論歸納先來看看MySQL官方對SELECT COUNT的定義:傳送門:https://dev.mysql.com/doc/refman/5.6/en/aggregate-functions.html#function_count
  • Mysql性能優化:為什麼你的count(*)這麼慢?
    count的用法有多種,分別是count(*)、count(欄位)、count(1)、count(主鍵id)。那麼多種用法,到底有什麼差別呢?當然,「前提是沒有where條件語句」。count(欄位):count(*):不會把全部欄位取出來,而是專門做了優化,不取值。count(*)肯定不是null,按行累加。
  • OLAP系統場景中,GaussDB(for MySQL)藉助PQ+NDP讓性能提升高達百倍
    並行查詢的性能提升倍數理論上與CPU的核數正相關,也就是說並行度越高能夠使用的CPU核數就越多,性能提升的倍數也就越高。比如一個表count(*)的執行過程,如下圖對於已經落地的數據而言,如果要分析數據,如何提高查詢效率呢,這又是一種比較棘手的問題。對於此種問題,GaussDB(for MySQL)提出了一種新的處理方式,就是NDP(Near Data Processing)。NDP主要針對是數據密集型查詢,將提取列,條件過濾,聚合運算等操作向下推送給GaussDB(for MySQL)的分布式存儲層的多個節點並行執行。
  • MySQL:為什麼用limit時,offset很大會影響性能
    灌入大量數據,共500萬:mysql> select count(*) from test;+| count(*) |+|  5242882 |+1 row in set (4.25 sec)我們知道,當limit offset rows中的offset很大時,會出現效率問題:
  • MySQL裡面的group by問題淺析
    mysql> select  backup_date ,count(*) piece_no  from redis_backup_result;+---++| backup_date | piece_no |+---++| 2018-08-14  |    40906 |+---++1 row in
  • MySQL 中 count(*) 比 count(1) 快?
    中 count(1) 比 count(*) 快,這能忍?3.2 原理分析不知道小夥伴們有沒有注意過,我們學習 MySQL 的時候,count 函數是歸在聚合函數那一類的,就是 avg、sum 等,count 函數和這些歸在一起,說明它也是一個聚合函數。既然是聚合函數,那麼就需要對返回的結果集進行一行行的判斷,這裡就涉及到一個問題,返回的結果是啥?
  • count(1)、count(*) 與 count(列名) 的執行區別
    從執行計劃來看,count(1)和count(*)的效果是一樣的。但是在表做過分析之後,count(1)會比count(*)的用時少些(1w以內數據量),不過差不了多少。 如果count(1)是聚索引,id,那肯定是count(1)快。但是差的很小的。 因為count(*),自動會優化指定到那一個欄位。