select count(*) 底層到底幹了啥?



SELECT COUNT( * ) FROM TABLE 是個再常見不過的 SQL 需求了。sql

在 MySQL 的使用規範中,咱們通常使用事務引擎 InnoDB 做爲(通常業務)表的存儲引擎,在此前提下,COUNT( * )操做的時間複雜度爲 O(N),其中 N 爲表的行數。

而 MyISAM 表中能夠快速取到表的行數。這些實踐經驗的背後是怎樣的機制,以及爲何須要/能夠是這樣,就是此文想要探討的。
微信

先來看一下概況,MySQL COUNT( * ) 在 2 種存儲引擎中的部分問題:
數據結構

下面就帶着這些問題,以 InnoDB 存儲引擎爲主來進行討論。

1、InnoDB 全表 COUNT( * )
mvc

主要問題:

一、執行過程是怎樣的?

二、如何計算 count?影響 count 結果的因素有哪些?
三、count 值存在哪裏?涉及的數據結構是怎樣的?
四、爲何 InnoDB 只能經過掃表來實現 count( * )?(見本文最後的問題)
五、全表COUNT( * )做爲 table scan 類型操做的一個 case,有什麼風險?
六、COUNT(* )操做是否會像「SELECT * 」同樣可能讀取大字段涉及的溢出頁?
一、執行框架 – 循環: 讀取 + 計數?

1.一、基本結論:

  • 全表掃描,一個循環解決問題。app

  • 循環內: 先讀取一行,再決定該行是否計入 count。框架

  • 循環內是一行一行進行計數處理的。函數

1.二、說明:
簡單 SELELCT-SQL 的執行框架,類比 INSERT INTO … SELECT 是一樣的過程。
下面會逐步細化如何讀取與計數 ( count++ ) 。

二、執行過程?
優化

執行過程部分,分爲 4 個部分:
ui

(1)COUNT( * ) 前置流程: 從 Client 端發 SQL 語句,到 MySQL-Server 端執行 SELECT 以前,爲後面的一些闡述作一鋪墊。
(2)COUNT( * ) 流程: 簡要給出代碼層面的流程框架及 2 個核心步驟的重點調用棧部分。
(3)讀取一行: 可見性及 row_search_mvcc 函數,介紹可見性如何影響 COUNT( * ) 結果。
(4)計數一行: Evaluate_join_record 與列是否爲空,介紹計數過程如何影響 COUNT( * ) 結果。

若是讀者但願直接看如何進行 COUNT( * ),那麼也能夠忽略 (1),而直接跳到 (2) 開始看。
lua

2.一、COUNT( * ) 前置流程回憶 – 從 Client 端發 SQL 到 sub_select 函數

爲了使看到的調用過程不太突兀,咱們仍是先回憶一下如何執行到 sub_select 函數這來的:
(1)MySQL-Client 端發送 SQL 語句,根據 MySQL 通訊協議封包發送。
(2)Mysql-Server 端接收數據包,由協議解析出 command 類型 ( QUERY ) 及 SQL 語句 ( 字符串 ) 。
(3)SQL 語句通過解析器解析輸出爲 JOIN 類的對象,用於結構化地表達該 SQL 語句。
PS: 這裏的 JOIN 結構,不只僅是純語法結構,而是已經進行了語義處理,粗略地說,彙總了表的列表 ( table_list )、目標列的列表 ( target_list )、WHERE 條件、子查詢等語法結構。
在全表 COUNT( * )-case 中,table_list = [表「t」(別名也是「t」)],target_list = [目標列對象(列名爲「COUNT( * )」)],固然這裏沒有 WHERE 條件、子查詢等結構。
(4)JOIN 對象有 2 個重要的方法: JOIN::optimize(), JOIN::exec(),分別用於進行查詢語句的優化 和 查詢語句的執行。
  • join->optimize(),優化階段 (稍後 myisam 下全表 count( * ) 操做會涉及這裏的一點內容)。

  • join->exec(),執行階段 ( 重點 ),包含了 InnoDB 下全表count( * ) 操做的執行流程。

(5)join->exec() 通過若干調用,將調用到 sub_select 函數來執行簡單 SQL,包括 COUNT( * ) 。
(6)END of sub_select 。

2.二、COUNT( * ) 流程 ( 於 sub_select 函數中 )

上層的流程與代碼是比較簡單的,集中在 sub_select 函數中,其中 2 類函數分別對應於前面」執行框架」部分所述的 2 個步驟 – 讀取、計數。先給出結論以下:

(1)讀取一行:從相對頂層的 sub_select 函數通過一番調用,最終全部分支將調用到 row_search_mvcc 函數中,該函數就是用於從 InnoDB 存儲引擎所存儲的 B+-tree 結構中讀取一行到內存中的一個 buf (uchar * ) 中,待後續處理使用。

這裏會涉及行鎖的獲取、MVCC 及行可見性的問題。固然對 於 SELECT COUNT( * ) 這類快照讀而言,只會涉及 MVCC 及其可見性,而不涉及行鎖。詳情可跳至「可見性與 row_search_mvcc 函數」部分。

(2)計數一行: 代碼層面,將會在 evaluate_join_record 函數中對所讀取的行進行評估,看其是否應當計入 count 中 ( 便是否要 count++ )。

簡單來講,COUNT(arg) 自己爲 MySQL 的函數操做,對於一行來講,若括號內的參數 arg ( 某列或整行 ) 的值若不是 NULL,則 count++,不然對該行不予計數。詳情可跳至「 Evaluate_join_record 與列是否爲空」部分。

這兩個階段對 COUNT( * )結果的影響以下: (兩層過濾)

SQL 層流程框架相關代碼摘要以下:

Q: 代碼層面,第一步驟(讀取一行)有 2 個分支,爲何?
A:從 InnoDB 接口層面考慮,分爲 「讀第一行」 和 「讀下一行」,是 2 個不一樣的執行過程,讀第一行須要找到一個 ( cursor ) 位置並作一些初始化工做讓後續的過程可遞歸。
正如咱們若是用腳本/程序來進行逐行的掃表操做,實現上就會涉及下面 2 個 SQL:
具體涉及到此例的代碼,SQL 層到存儲引擎層的調用關係,讀取階段的調用棧以下:(供參考)
咱們能夠看到,不管是哪個分支的讀取,最終都異曲同工於 row_search_mvcc 函數。
以上是對 LOOP 中的代碼作一些簡要的說明,下面來看 row_search_mvcc 與 evaluate_join_record 如何輸出最終的 count 結果。

2.三、行可見性及 row_search_mvcc 函數

這裏咱們主要經過一組 case 和幾個問題來看行可見性對 COUNT( * ) 的影響。
Q: 對於「SELECT COUNT( * ) FROM t」或者「SELECT MIN(id) FROM t」操做,第一次的讀行操做讀到的是表 t 中 ( B+ 樹最左葉節點 page 內 ) 的最小記錄嗎?( ha_index_first 爲什麼也調用 row_search_mvcc 來獲取最小 key 值?)
A:不必定。即便是 MIN ( id ) 也不必定就讀取的是 id 最小的那一行,由於也一樣有行可見性的問題,實際上 index_read 取到的是 當前事務內語句可見的最小 index 記錄。這也反映了前面提到的 join_read_first 與 join_read_next 「異曲同工」到 row_search_mvcc 是理所應當的。
Q: 針對圖中最後一問,若是事務 X 是 RU ( Read-Uncommitted ) 隔離級別,且 C-Insert ( 100 ) 的完成是在 X-count( * ) 執行過程當中 ( 僅掃描到 5 或 10 這條記錄 ) 完成的,那麼 X-count( * ) 在事務 C-Insert ( 100 ) 完成後,可否在以後的讀取過程當中看到 100 這條記錄呢?
A:MySQL 採起」讀到什麼就是什麼」的策略,即 X-count( * ) 在後面能夠讀到 100 這條記錄。

2.四、evaluate_join_record 與列是否爲空

Q: 某一行如何計入 count?
A:兩種狀況會將所讀的行計入 count:
(1)若是 COUNT 函數中的參數是某列,則會判斷所讀行中該列定義是否 Nullable 以及該列的值是否爲 NULL;若二者均爲是,則不會計入 count,不然將計入 count。
  • e.g. SELECT COUNT(col_name) FROM t

  • col_name 能夠是主鍵、惟一鍵、非惟一鍵、非索引字段

(2)若是 COUNT 中帶有 * ,則會判斷這部分的整行是否爲 NULL,若是判斷參數爲 NULL,則忽略該行,不然 count++。
  • e.g-1. SELECT COUNT(*) FROM t

  • e.g-2. SELECT COUNT(B.*) FROM A LEFT JOIN B ON A.id = B.id

Q: 特別地,對於 SELECT COUNT(id) FROM t,其中 id 字段是表 t 的主鍵,則如何?
A:效果上等價於 COUNT( * )。由於不管是 COUNT( * ),仍是 COUNT ( pk_col ) 都是由於有主鍵從而充分判定索取數據不爲 NULL,這類 COUNT 表達式能夠用於獲取當前可見的錶行數。
Q: 用戶層面對 InnoDB COUNT( * ) 的優化操做問題
A:這個問題是業界熟悉的一個問題,掃描非空惟一鍵可獲得錶行數,但所涉及的字節數可能會少不少(在表的行長與主鍵、惟一鍵的長度相差較多時),相對的 IO 代價小不少。
相關調用棧參考以下:
2、數據結構
Q: count 值存儲在哪一個內存變量裏?
A:SQL 解析後,存儲於表達 COUNT( * ) 這一項中,((Item_sum_count*)item_sum)->count
以下圖所示回顧咱們以前「COUNT( * )前置流程」部分提到的 JOIN 結構。
即 SQL 解析器爲每一個 SQL 語句進行結構化,將其放在一個 JOIN 對象 ( join ) 中來表達。在該對象中建立並填充了一個列表 result_field_list 用於存放結果列,列表中每一個元素則是一個結果列的 ( Item_result_field* ) 對象 ( 指針 ) 。
在 COUNT( * )-case 中,結果列列表只包含一個元素,( Item_sum_count: public Item_result_field ) 類型對象 ( name = 「COUNT( * )」),其中該類所特有的成員變量 count即爲所求。

3、MyISAM 全表 COUNT( * )

因爲 MyISAM 引擎並不經常使用於實際業務中,僅作簡要描述以下:
一、MyISAM-COUNT( * ) 操做是 O(1) 時間複雜度的操做。
二、每張 MyISAM 表中存放了一個 meta 信息-count 值,在內存中與文件中各有一份,內存中的 count 變量值經過讀取文件中的 count 值來進行初始化。
三、SELECT COUNT( * ) FROM t 會直接讀取內存中的表 t 對應的 count 變量值。
四、內存中的 count 值與文件中的 count 值由寫操做來進行更新,其一致性由表級鎖來保證。
五、表級鎖保證的寫入串行化使得,同一時刻全部用戶線程的讀操做要麼被鎖,要麼只會看到一種數據狀態。
4、幾個問題
Q: MyISAM 與 InnoDB 在 COUNT( * ) 操做的執行過程在哪裏開始分道揚鑣?
  • 共性:共性存在於 SQL 層,即 SQL 解析以後的數據結構是一致的,count 變量都是存在於做爲結果列的 Item_sum_count 類型對象中;返回給客戶端的過程也相似 – 對該 count 變量進行賦值並經由 MySQL 通訊協議返回給客戶端。

  • 區別:InnoDB 的 count 值計算是在 SQL 執行階段進行的;而 MyISAM 表自己在內存中有一份包含了表 row_count 值的 meta 信息,在 SQL 優化階段經過存儲引擎的標記給優化器一個 hint,代表該表所用的存儲引擎保存了精確行數,能夠直接獲取到,無需再進入執行器。

BLOG地址www.liangsonghua.com

關注微信公衆號:松花皮蛋的黑板報,獲取更多精彩!

公衆號介紹:分享在京東工做的技術感悟,還有JAVA技術和業內最佳實踐,大部分都是務實的、能看懂的、可復現的

相關文章
相關標籤/搜索