在日常開發中,統計一張表中的記錄數是非常常見的需求,而 COUNT 函數就是我們最常用的工具。但 COUNT 的用法有多種,例如:
COUNT(*)COUNT(1)COUNT(字段)COUNT(主鍵字段)
平時我也會疑惑:這些寫法到底有什麼區別?哪種性能更好?是不是 COUNT(*) 最慢?
本文就結合 InnoDB 存儲引擎的執行原理,深入分析這些不同寫法的差異,並總結出最佳實踐。
COUNT() 的本質
COUNT() 是一個聚合函數,用來統計符合條件的記錄數。它的參數既可以是字段名,也可以是常量或表達式。關鍵點是:
COUNT(字段):統計該字段不為 NULL 的記錄數。COUNT(常量或表達式):因為常量不會為 NULL,所以統計的是所有記錄數。COUNT(*):和 COUNT(1)本質一樣,統計所有記錄數。
舉例:
假設有一張 user 表:
CREATE TABLE user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
age INT,
email VARCHAR(100)
);
插入一些數據:
INSERT INTO user (name, age, email) VALUES
('yunzhi', 23, 'yunzhi@163.com'),
('kxb', 24, 'kxb@163.com'),
('lq', 20, NULL);
此時:
SELECT COUNT(*) FROM user; -- 結果 3
SELECT COUNT(1) FROM user; -- 結果 3
SELECT COUNT(id) FROM user; -- 結果 3(主鍵一定不為 NULL)
SELECT COUNT(email) FROM user; -- 結果 2(有一條 email = NULL)
執行過程解析
要搞清楚性能問題,得看看 MySQL 是怎麼執行的。MySQL 分為兩層:
- Server 層:負責 SQL 解析、語法檢查、查詢優化、執行計劃生成、調用存儲引擎接口等。
- 存儲引擎層(如 InnoDB):負責實際的數據存儲、索引管理、事務控制、行級鎖等。
執行流程簡述:
客户端 SQL → 服務器層解析優化 → 存儲引擎訪問數據 → 返回結果
1. COUNT(主鍵字段)
在 MySQL 裏,當你用 COUNT(主鍵字段) 來數有多少條記錄時,Server 層會先準備一個“記數器”,名字就叫 count。它會一條一條地去問 InnoDB:“這一條算不算?”只要主鍵字段不是 NULL,就給記數器加 1。Server 層就這樣循環,一直數到表裏的記錄都遍歷完為止,然後把最終結果交給你。
至於 InnoDB,它用 B+ 樹來存數據。可以把它想象成一棵高高的樹,葉子節點裏放着記錄。聚簇索引的葉子節點裏放的是完整記錄,而二級索引的葉子節點只放主鍵值。因此,當你通過主鍵字段來統計時,InnoDB 訪問起來特別高效。
用下面為案例
explain select count(id) from user;
如果表裏只有主鍵索引,並且沒有二級索引時,那麼就會遍歷所有聚簇索引,讀到的數據返回 server 層,然後讀取記錄中id值,之後進行判斷id是否為null,如果不為null,講count變量中的值加1。
如果表只有主鍵索引,那遍歷的是 聚簇索引。但如果表存在二級索引,優化器會優先選擇 二級索引,接下來我們使用案例講解為什麼
用下面為案例
為 name 增加二級索引
CREATE INDEX idx_user_name ON user(name);
之後進行查詢
explain select count(id) from user;
這裏我們可以看到,即使我們使用主鍵進行查詢,這裏已經換成二級索引進行查詢(因為更小更快)。遍歷所有的二級索引,讀到的數據返回 server 層,然後讀取記錄中id值,之後進行判斷id是否為null,如果不為null,將count變量中的值加1。
2. COUNT(1)
如果只有主鍵索引,沒有二級索引時。
用下面為案例
explain select count(1) from user;
這裏我們會發現,InnoDB 循環遍歷了聚簇索引(主鍵索引),將讀取到的數據返回到 Server 層,但是他不會讀取到記錄中的任何字段,因為count() 函數的參數是 1,不是字段,所以不需要讀取記錄中的字段,因此 server 層讀取到一條記錄,就將count 變量加 1。
可以看到,使用count(1)相比count(主鍵字段)少了一個獲取記錄中的字段值,所以通常count(1)比count(*)的執行效率高一些。
如果表有二級索引時,InnoDB循環遍歷對象還是二級索引
explain select count(id) from user;
這裏我們可以看到,還是採用的二級索引(因為更小更快)。
3. COUNT(*)
- 很多人以為會取所有字段,其實不會!
- 在 COUNT() 中, 會被優化成 COUNT(0)。
- 執行過程與 COUNT(1) 完全一致。
MySQL 官方文檔明確寫到:
InnoDB handles SELECT COUNT(*) and SELECT COUNT(1) operations in the same way. There is no performance difference.
4. COUNT(字段)
count(字段)的執行效率是最差的
用下面為案例
select count(age) from user;
對於這個來説,會採用全表掃描的方式進行計數,所有他的執行效率是最差的
在 InnoDB 中,聚簇索引的葉子節點存儲的是完整的行數據,而主鍵值本身就是聚簇索引的鍵(key)。
當執行 COUNT(id)(id 為主鍵)時,MySQL Server 層會通過 read_set = {id} 告知存儲引擎:“我只需要主鍵字段”。
由於主鍵天然存在於索引結構中,InnoDB 無需解析整行數據,可直接從聚簇索引的鍵部分提取 id 值,數據訪問量達到最小。
相比之下,age 是一個普通字段且沒有任何索引,其值存儲在聚簇索引葉子節點的行數據體中(例如:[id][name][email][age])。
為了獲取 age 的值,InnoDB 必須完整讀取並解析整行記錄,才能定位並提取該字段。這不僅增加了 I/O 後續處理的 CPU 開銷,也導致內存訪問量顯著上升。