日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

SELECT COUNT(*) 底层究竟干了啥么?

發布時間:2025/3/21 编程问答 16 豆豆
生活随笔 收集整理的這篇文章主要介紹了 SELECT COUNT(*) 底层究竟干了啥么? 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

“SELECT COUNT( * ) FROM TABLE” 是個再常見不過的 SQL 需求了。在 MySQL 的使用規范中,我們一般使用事務引擎 InnoDB 作為(一般業務)表的存儲引擎,在此前提下,COUNT( * )操作的時間復雜度為 O(N),其中 N 為表的行數。

而 MyISAM 表中可以快速取到表的行數。這些實踐經驗的背后是怎樣的機制,以及為什么需要/可以是這樣,就是此文想要探討的。

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

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

一、InnoDB 全表 COUNT(*)

主要問題:

1、執行過程是怎樣的?

2、如何計算 count?影響 count 結果的因素有哪些?

3、count 值存在哪里?涉及的數據結構是怎樣的?

4、為什么 InnoDB 只能通過掃表來實現 count( * )?(見本文最后的問題)

5、全表COUNT( * )作為 table scan 類型操作的一個 case,有什么風險?

6、COUNT(* )操作是否會像“SELECT * ”一樣可能讀取大字段涉及的溢出頁?

1、執行框架 – 循環: 讀取 + 計數?

1.1、基本結論:

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

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

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

1.2、說明:

簡單 SELELCT-SQL 的執行框架,類比 INSERT INTO … SELECT 是同樣的過程。

下面會逐步細化如何讀取與計數 ( count++ ) 。

2、執行過程?

執行過程部分,分為 4 個部分:

(1)COUNT( * ) 前置流程: 從 Client 端發 SQL 語句,到 MySQL-Server 端執行 SELECT 之前,為后面的一些闡述做一鋪墊。

(2)COUNT( * ) 流程: 簡要給出代碼層面的流程框架及 2 個核心步驟的重點調用棧部分。

(3)讀取一行: 可見性及 row_search_mvcc 函數,介紹可見性如何影響 COUNT( * ) 結果。

(4)計數一行: Evaluate_join_record 與列是否為空,介紹計數過程如何影響 COUNT( * ) 結果。

如果讀者希望直接看如何進行 COUNT( * ),那么也可以忽略 (1),而直接跳到 (2) 開始看。

2.1、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.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.3、行可見性及 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.4、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 代價小很多。

相關調用棧參考如下:

二、數據結構

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 全表 COUNT(*)

由于 MyISAM 引擎并不常用于實際業務中,僅做簡要描述如下:

1、MyISAM-COUNT( * ) 操作是 O(1) 時間復雜度的操作。

2、每張 MyISAM 表中存放了一個 meta 信息-count 值,在內存中與文件中各有一份,內存中的 count 變量值通過讀取文件中的 count 值來進行初始化。

3、SELECT COUNT( * ) FROM t 會直接讀取內存中的表 t 對應的 count 變量值。

4、內存中的 count 值與文件中的 count 值由寫操作來進行更新,其一致性由表級鎖來保證。

5、表級鎖保證的寫入串行化使得,同一時刻所有用戶線程的讀操作要么被鎖,要么只會看到一種數據狀態。

四、幾個問題

Q:MyISAM 與 InnoDB 在 COUNT( * ) 操作的執行過程在哪里開始分道揚鑣?

  • 共性:共性存在于 SQL 層,即 SQL 解析之后的數據結構是一致的,count 變量都是存在于作為結果列的 Item_sum_count 類型對象中;返回給客戶端的過程也類似 – 對該 count 變量進行賦值并經由 MySQL 通信協議返回給客戶端。

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

Q:InnoDB 中為何無法向 MyISAM 一樣維護住一個 row_count 變量?

**A:**從 MVCC 機制與行可見性問題中可得到原因,每個事務所看到的行可能是不一樣的,其 count( * ) 結果也可能是不同的;反過來看,則是 MySQL-Server 端無法在同一時刻對所有用戶線程提供一個統一的讀視圖,也就無法提供一個統一的 count 值。

PS: 對于多個訪問 MySQL 的用戶線程 ( COUNT( * ) ) 而言,決定它們各自的結果的因素有幾個:

(1)一組事務執行前的數據狀態(初始數據狀態)。

(2)有時間重疊的事務們的執行序列 (操作時序,事務理論表明 并發事務操作的可串行化是正確性的必要條件)。

(3)事務們各自的隔離級別(每個操作的輸入)。

其中 1、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,因此只需要讀主鍵索引頁內的行數據,而無需讀取額外的溢出頁。

總結

以上是生活随笔為你收集整理的SELECT COUNT(*) 底层究竟干了啥么?的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

主站蜘蛛池模板: 欧美国产精品一二三 | 天天干夜夜爽 | 美女张开双腿让男人捅 | 18成人免费观看视频 | 精品国产一区二区三区久久久 | 在线播放第一页 | 欧美99久久精品乱码影视 | 成人自拍av | 求av网址| 香蕉精品在线 | 亚洲精品久久久狠狠狠爱 | 小日子的在线观看免费第8集 | 古装做爰无遮挡三级视频 | 打屁股疼的撕心裂肺的视频 | 日韩精品电影一区 | 亚洲国产成人精品无码区99 | 久久97人妻无码一区二区三区 | 成年人免费网站 | 欧美最猛黑人xxxx黑人猛交 | 国产剧情一区在线 | 大肉大捧一进一出好爽 | 国产欧美自拍 | 国产亚洲精久久久久久无码77777 | 青草热视频 | 色综合久久精品亚洲国产 | 久久草精品 | 一级黄色毛毛片 | 国产精品久久久久久影视 | 国产精品一区电影 | 国产精品第5页 | 国产高清精品在线观看 | 亚洲综合图片网 | 四虎精品在线观看 | 国产极品免费 | 日韩亚洲欧美一区二区三区 | 国产18毛片| 幸福,触手可及 | 精品无码av一区二区三区四区 | 国产午夜手机精彩视频 | 秘密基地免费观看完整版中文 | 国产成人免费看 | 国产精品婷婷 | 114国产精品久久免费观看 | 国产丝袜一区二区三区 | 在线观看91视频 | 欧美日韩精品国产 | 国产精品综合久久久久久 | 琪琪伦伦影院理论片 | 字幕网在线观看 | 日本一区二区三区四区视频 | 丝袜 亚洲 另类 国产 制服 | 免费在线观看一区 | 欧美老熟妇又粗又大 | 久久亚洲免费视频 | 亚洲第一区视频 | 精品自拍偷拍 | 日本在线二区 | 天天干夜夜添 | www.一起操 | 欧美性色a| 国产1区2区3区中文字幕 | 一区二区影院 | 在线视频欧美日韩 | 最近中文字幕免费视频 | 999国产精品亚洲77777 | 国产精品福利影院 | 久久久99精品国产一区二区三区 | 久久精品色欲国产AV一区二区 | 福利电影在线播放 | 久久无码精品丰满人妻 | 伊人婷婷在线 | 麻豆av一区二区三区在线观看 | 污漫在线观看 | 乱妇乱女熟妇熟女网站 | 超碰免费91 | 成人a级免费视频 | 香蕉视频成人在线观看 | 欧美一级网址 | 色综合久| av一区二区在线播放 | 国产伦精品一区二区三区视频网站 | 色综合五月 | 欧美成人看片黄a免费看 | 免费的黄色大片 | 综合五月激情 | 用舌头去添高潮无码视频 | 婷婷色影院 | 骚av在线| xxx老太太 | 国产精品一区无码 | 亚洲欧美第一页 | 男女男精品网站 | 向着小小的花蕾绽放 | 理论片91 | 日韩精品高清视频 | 在线尤物 | 日韩欧美第一页 | 色综合狠狠操 | 国产影视一区二区 |