←←←←←←←←←←←← 快!點關注sql
「SELECT COUNT( ) FROM t」 是個再常見不過的 SQL 需求了。在 MySQL 的使用規範中,咱們通常使用事務引擎 InnoDB 做爲(通常業務)表的存儲引擎,在此前提下,COUNT( )操做的時間複雜度爲 O(N),其中 N 爲表的行數。數據結構
而 MyISAM 表中能夠快速取到表的行數。這些實踐經驗的背後是怎樣的機制,以及爲何須要/能夠是這樣,就是此文想要探討的。架構
先來看一下概況: MySQL COUNT( * ) 在 2 種存儲引擎中的部分問題:併發
下面就帶着這些問題,以 InnoDB 存儲引擎爲主來進行討論。mvc
簡單 SELELCT-SQL 的執行框架,類比 INSERT INTO … SELECT 是一樣的過程。框架
下面會逐步細化如何讀取與計數 ( count++ ) 。分佈式
引述: 執行過程部分,分爲 4 個部分:函數
若是讀者但願直接看如何進行 COUNT( * ),那麼也能夠忽略 (1),而直接跳到 (2) 開始看。高併發
爲了使看到的調用過程不太突兀,咱們仍是先回憶一下如何執行到 sub_select 函數這來的:oop
PS: 這裏的 JOIN 結構,不只僅是純語法結構,而是已經進行了語義處理,粗略地說,彙總了表的列表 ( table_list )、目標列的列表 ( target_list )、WHERE 條件、子查詢等語法結構。
在全表 COUNT( )-case 中,table_list = [表「t」(別名也是「t」)],target_list = [目標列對象(列名爲「COUNT( )」)],固然這裏沒有 WHERE 條件、子查詢等結構。
JOIN 對象有 2 個重要的方法: JOIN::optimize(), JOIN::exec(),分別用於進行查詢語句的優化 和 查詢語句的執行。
上層的流程與代碼是比較簡單的,集中在 sub_select 函數中,其中 2 類函數分別對應於前面」執行框架」部分所述的 2 個步驟 – 讀取、計數。先給出結論以下:
這裏會涉及行鎖的獲取、MVCC 及行可見性的問題。固然對 於 SELECT COUNT( * ) 這類快照讀而言,只會涉及 MVCC 及其可見性,而不涉及行鎖。詳情可跳至「可見性與 row_search_mvcc 函數」部分。
簡單來講,COUNT(arg) 自己爲 MySQL 的函數操做,對於一行來講,若括號內的參數 arg ( 某列或整行 ) 的值若不是 NULL,則 count++,不然對該行不予計數。詳情可跳至「 Evaluate_join_record 與列是否爲空」部分。
這兩個階段對 COUNT( * )結果的影響以下: (兩層過濾)
SQL 層流程框架相關代碼摘要以下:
1210 enum_nested_loop_state 1211 sub_select(JOIN *join, QEP_TAB *const qep_tab,bool end_of_records) 1212 { 1213 DBUG_ENTER("sub_select"); ... ... // 此處省略1000字 1265 while (rc == NESTED_LOOP_OK && join->return_tab >= qep_tab_idx) 1266 { 1267 int error; // 第一步,從存儲引擎中獲取一行; 1268 if (in_first_read) 1269 { 1270 in_first_read= false; // 第一步,首次讀取,掃描第一個知足條件的記錄; // 初始化cursor,從」頭」掃描到某個位置 // 相似: SELECT id FROM t LIMIT 1; 1271 error= (*qep_tab->read_first_record)(qep_tab); 1272 } 1273 else // 第一步,後續讀取,在前次掃描的位置上繼續遍歷,找到一個知足條件的記錄; // 相似: SELECT id FROM t WHERE id > $last_id LIMIT 1; 1274 error= info->read_record(info); ... ... // 此處省略1000字 // 第二步,處理剛剛取出的一行 1291 rc= evaluate_join_record(join, qep_tab); ... ... // 此處省略1000字 1303 DBUG_RETURN(rc); 1304 }
Q: 代碼層面,第一步驟(讀取一行)有 2 個分支,爲何?
A:從 InnoDB 接口層面考慮,分爲 「讀第一行」 和 「讀下一行」,是 2 個不一樣的執行過程,讀第一行須要找到一個 ( cursor ) 位置並作一些初始化工做讓後續的過程可遞歸。
正如咱們若是用腳本/程序來進行逐行的掃表操做,實現上就會涉及下面 2 個 SQL:
// SELECT id FROM t LIMIT 1; OR SELECT MIN(id)-1 FROM t; -> $last_id // SELECT id FROM t WHERE id > $last_id LIMIT 1;
具體涉及到此例的代碼,SQL 層到存儲引擎層的調用關係,讀取階段的調用棧以下:(供參考)
sub_select 函數中從 SQL 層到 InnoDB 層的函數調用關係:(同顏色、同縮進 表示同一層) Ø (*qep_tab->read_first_record) () | -- > join_read_first(tab) | -- > tab->read_record.read_record=join_read_next; | -- > table->file->ha_index_init() | -- > handler::ha_index_init(uint idx, bool sorted) | -- > ha_innobase::index_init() | -- > table->file->ha_index_first() | -- > handler::ha_index_first(uint idx, bool sorted) | -- > ha_innobase::index_first() | -- > ha_innobase::index_read() | -- > row_search_mvcc() 初始化cursor並將其放到一個有效的初始位置上; Ø info->read_record (info) | -- > join_read_next(info) | -- > info->table->file->ha_index_next(info->record)) | -- > handler::ha_index_next(uchar * buf) | -- > ha_innobase::index_next(uchar * buf) | -- > general_fetch(buf, ROW_SEL_NEXT, 0) | -- > row_search_mvcc() 「向前」移動一次cursor;
咱們能夠看到,不管是哪個分支的讀取,最終都異曲同工於 row_search_mvcc 函數。
以上是對 LOOP 中的代碼作一些簡要的說明,下面來看 row_search_mvcc 與 evaluate_join_record 如何輸出最終的 count 結果。
這裏咱們主要經過一組 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 這條記錄。
Q:某一行如何計入 count?
A:兩種狀況會將所讀的行計入 count:
若是 COUNT 函數中的參數是某列,則會判斷所讀行中該列定義是否 Nullable 以及該列的值是否爲 NULL;若二者均爲是,則不會計入 count,不然將計入 count。
若是 COUNT 中帶有 * ,則會判斷這部分的整行是否爲 NULL,若是判斷參數爲 NULL,則忽略該行,不然 count++。
Q: 特別地,對於 SELECT COUNT(id) FROM t,其中 id 字段是表 t 的主鍵,則如何?
A:效果上等價於 COUNT( )。由於不管是 COUNT( ),仍是 COUNT ( pk_col ) 都是由於有主鍵從而充分判定索取數據不爲 NULL,這類 COUNT 表達式能夠用於獲取當前可見的錶行數。
Q: 用戶層面對 InnoDB COUNT( * ) 的優化操做問題
A:這個問題是業界熟悉的一個問題,掃描非空惟一鍵可獲得錶行數,但所涉及的字節數可能會少不少(在表的行長與主鍵、惟一鍵的長度相差較多時),相對的 IO 代價小不少。
相關調用棧參考以下:
參考一: evaluate_join_record() | -- > rc= (*qep_tab->next_select)(join, qep_tab+1, 0); | -- > end_send_group(...) | -- > init_sum_functions(join->sum_funcs, join->sum_funcs_end[idx+1])) | -- > (*func_ptr)->reset_and_add() | -- > Item_sum::aggregator_clear() | -- > Item_sum::aggregator_add() | -- > update_sum_func(Item_sum **func_ptr) | -- > (*func_ptr)->add() | -- > Item_sum::aggregator_add() 參考二: (Item_sum::aggregator_add) ((Item_sum *) (*func_ptr))->aggregator_add() | -- > (Item_sum *)this->aggr->add() | -- > ((Aggregator_simple *) aggr)->item_sum->add() | -- > if (! aggr->arg_is_null(false)) | ------ > ((Item_sum_count *)aggr->item_sum)->count++;
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即爲所求。
因爲 MyISAM 引擎並不經常使用於實際業務中,僅作簡要描述以下:
Q:MyISAM 與 InnoDB 在 COUNT( * ) 操做的執行過程在哪裏開始分道揚鑣?
Q:InnoDB 中爲什麼沒法向 MyISAM 同樣維護住一個 row_count 變量?
A:從 MVCC 機制與行可見性問題中可獲得緣由,每一個事務所看到的行多是不同的,其 count( * ) 結果也多是不一樣的;反過來看,則是 MySQL-Server 端沒法在同一時刻對全部用戶線程提供一個統一的讀視圖,也就沒法提供一個統一的 count 值。
PS: 對於多個訪問 MySQL 的用戶線程 ( COUNT( * ) ) 而言,決定它們各自的結果的因素有幾個:
其中 一、2 對於 Server 而言都是全局或者說可控的,只有 3 是每一個用戶線程中事務所獨有的屬性,這是 Server 端不可控的因素,所以 Server 端也就對每一個 COUNT( * ) 結果不可控了。
Q:InnoDB-COUNT( * ) 屬 table scan 操做,是否會將現有 Buffer Pool 中其它用戶線程所需熱點頁從 LRU-list 中擠佔掉,從而其它用戶線程還需從磁盤 load 一次,忽然加劇 IO 消耗,可能對現有請求形成阻塞?
A:MySQL 有這樣的優化策略,將掃表操做所 load 的 page 放在 LRU-list 的 oung/old 的交界處 ( LRU 尾部約 3/8 處 )。這樣用戶線程所需的熱點頁仍然在 LRU-list-young 區域,而掃表操做不斷 load 的頁則會不斷沖刷 old 區域的頁,這部分的頁自己就是被認爲非熱點的頁,所以也相對符合邏輯。
PS: 我的認爲還有一種相似的優化思路,是限定掃描操做所使用的 Buffer Pool 的大小爲 O(1) 級別,但這樣作須要付出額外的內存管理成本。
Q:InnoDB-COUNT( ) 是否會像 SELECT FROM t 那樣讀取存儲大字段的溢出頁(若是存在)?
A:否。由於 InnoDB-COUNT( * ) 只須要數行數,而每一行的主鍵確定不是 NULL,所以只須要讀主鍵索引頁內的行數據,而無需讀取額外的溢出頁。
歡迎Java工程師朋友們加入Java高級架構進階: 963944895,羣內提供免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用本身每一分每一秒的時間來學習提高本身,不要再用"沒有時間「來掩飾本身思想上的懶惰!趁年輕,使勁拼,給將來的本身一個交代!