一直傻傻分不清count(*)count(id)count(1)這次終於整明白了

👉 這是一個或許對你有用的社群
🐱 一對一交流/面試小冊/簡歷最佳化/求職解惑,歡迎加入芋道快速開發平臺知識星球。下面是星球提供的部分資料:
👉這是一個或許對你有用的開源專案
國產 Star 破 10w+ 的開源專案,前端包括管理後臺 + 微信小程式,後端支援單體和微服務架構。
功能涵蓋 RBAC 許可權、SaaS 多租戶、資料許可權、商城、支付、工作流、大屏報表、微信公眾號、ERPCRMAI 大模型等等功能:
  • Boot 多模組架構:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 微服務架構:https://gitee.com/zhijiantianya/yudao-cloud
  • 影片教程:https://doc.iocoder.cn
【國內首批】支援 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 雙版本 
來源:江南一點雨

宣告:以下討論基於 InnoDB 儲存引擎,MyISAM 因為情況特殊我在文末會單獨說一下。
先說結論:這幾個效能差別不大。

1.實踐

我準備了一張有 100W 條資料的表,表結構如下:
CREATETABLE`user` (`id`int(11unsignedNOTNULL AUTO_INCREMENT,`username`varchar(255DEFAULTNULL,`address`varchar(255DEFAULTNULL,`password`varchar(255DEFAULTNULL,  PRIMARY KEY (`id`)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;
可以看到,有一個主鍵索引。
我們來用兩種方式統計一下表中的記錄數,如下:

可以看到,兩條 SQL 的執行效率其實差不多,都是 0.14s。
再來看另外兩個統計:

id 是主鍵,username 以及 address 則是普通欄位。
可以看出,用 id 來統計,也有一丟丟優勢。這裡因為測試資料樣板比較小,所以效果不明顯,小夥伴們可以加大測試資料量,那麼這種差異會更加明顯。
那麼到底是什麼原因造成的這種差異,接下來我們就來簡單分析一下。
基於 Spring Boot + MyBatis Plus + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
  • 專案地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 影片教程:https://doc.iocoder.cn/video/

2. explain 分析

我們先用 explain 來看下這幾個 SQL 不同的執行計劃:

可以看到,前三個統計方式的執行計劃是一樣的,後面兩個是一樣的。
我這裡和大家比較下 explain 中的不同項:
  • type:前三個的 type 值為 index,表示全索引掃描,就是把整個索引過一遍就行(注意是索引不是整個表);後兩個的 type 值為 all,表示全表掃描,即不會使用索引。
  • key:這個表示 MySQL 決定採用哪個索引來最佳化對該表的訪問,PRIMARY 表示利用主鍵索引,NULL 表示不用索引。
  • key_len:這個表示 MySQL 使用的鍵長度,因為我們的主鍵型別是 INT 且非空,所以值為 4。
  • Extra:這個中的 Using index 表示最佳化器只需要透過訪問索引就可以獲取到需要的資料(不需要回表)。
透過 explain 我們其實也能大概看出來前三種統計方式的執行效率是要高一些的(因為用到了索引),而後面兩種的統計效率相對來說要低一些的(沒用索引,需要全表掃描)。
僅有上面的分析還不夠,我們再來從原理角度來分析一下。
基於 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
  • 專案地址:https://github.com/YunaiV/yudao-cloud
  • 影片教程:https://doc.iocoder.cn/video/

3. 原理分析

3.1 主鍵索引與普通索引

在開始原理分析以前,我想先帶領大家看一下 B+ 樹,這對於我們理解接下來的內容有重要作用。
大家都知道,InnoDB 中索引的儲存結構都是 B+ 樹(至於什麼是 B+ 樹,和 B 樹有什麼區別,這個本文就不討論了,這兩個單獨都能整出來一篇文章),主鍵索引和普通索引的儲存又有所不同,如下圖表示主鍵索引:

可以看到,在主鍵索引中,葉子結點儲存了每一行的資料。
而在普通索引中,葉子結點儲存的是主鍵值,當我們使用普通索引去搜索資料的時候,先在葉子結點中找到主鍵,再拿著主鍵去主鍵索引中查詢資料,相當於做了兩次查詢,這也就是我們平常所說的回表操作。

3.2 原理分析

不知道小夥伴們有沒有注意過,我們學習 MySQL 的時候,count 函式是歸在聚合函式那一類的,就是 avg、sum 等,count 函式和這些歸在一起,說明它也是一個聚合函式。
既然是聚合函式,那麼就需要對返回的結果集進行一行行的判斷,這裡就涉及到一個問題,返回的結果是啥?我們分別來看:
對於 select count(1) from user; 這個查詢來說,InnoDB 引擎會去找到一個最小的索引樹去遍歷(不一定是主鍵索引),但是不會讀取資料,而是讀到一個葉子節點,就返回 1,最後將結果累加。
對於 select count(id) from user; 這個查詢來說,InnoDB 引擎會遍歷整個主鍵索引,然後讀取 id 並返回,不過因為 id 是主鍵,就在 B+ 樹的葉子節點上,所以這個過程不會涉及到隨機 IO(並不需要回表等操作去資料頁拿資料),效能也是 OK 的。
對於 select count(username) from user; 這個查詢來說,InnoDB 引擎會遍歷整張表做全表掃描,讀取每一行的 username 欄位並返回,如果 username 在定義時候設定了 not null,那麼直接統計 username 的個數;如果 username 在定義的時候沒有設定 not null,那麼就先判斷一下 username 是否為空,然後再統計。
最後再來說說 select count(*) from user; ,這個 SQL 的特殊之處在於它被 MySQL 最佳化過,當 MySQL 看到 count(*) 就知道你是想統計總記錄數,就會去找到一個最小的索引樹去遍歷,然後統計記錄數。
因為主鍵索引(聚集索引)的葉子節點是資料,而普通索引的葉子節點則是主鍵值,所以普通索引的索引樹要小一些。然而在上文的案例中,我們只有主鍵索引,所以最終使用的就是主鍵索引。
現在,如果我修改上面的表,為 username 欄位也新增索引,然後我們再來看 explain select count(*) from user; 的執行計劃:

可以看到,此時使用的索引就是 username 索引了,和我們前面的分析結果是一致的。
從上面的描述中我們就可以看出,第一個查詢效能最高,第二個次之(因為需要讀取 id 並返回),第三個最差(因為需要全表掃描),第四個的查詢效能則接近第一個。

4. MyISAM 呢?

可能有小夥伴知道,MyISAM 引擎中的 select count(*) from user; 操作執行起來是非常快的,那是因為 MyISAM 把表中的行數直接存在磁碟中了,需要的時候直接讀取出來就行了,所以非常快。
MyISAM 引擎之所以這樣做,主要是因為它是不支援事務的,所以它的統計實際上就非常容易,新增一行記錄一行就行了。
而我們常用的 InnoDB 卻不能這樣做!為啥?因為 InnoDB 支援事務!為了支援事務,InnoDB 引入了 MVCC 多版本併發控制,所以在資料讀取的時候可能會有髒讀、幻讀以及不可重複讀等問題。
所以,InnoDB 需要將每一行資料拿出來,判斷該行資料對當前會話是否可見,如果可見,就統計該行資料,否則不予統計。
當然,MySQL 中的 MVCC 實際上是一個非常宏大的話題,以後有空了再和大家詳細介紹 MVCC。

歡迎加入我的知識星球,全面提升技術能力。
👉 加入方式,長按”或“掃描”下方二維碼噢
星球的內容包括:專案實戰、面試招聘、原始碼解析、學習路線。

文章有幫助的話,在看,轉發吧。
謝謝支援喲 (*^__^*)

相關文章