MySQL:count(*)、count(字段) 實現上區別

文章末尾有他著作的《深入理解 MySQL 主從原理 32 講》,深入透徹理解 MySQL 主從,GTID 相關技術知識。


繼上一篇文章:


我們繼續來討論一下 count(*)、count(字段)實現上的區別。注意我們這裡都使用 Innodb 做為存儲引擎,不討論其他引擎。因為了有了前面的討論,更容易看出它們的區別,這裡我們有如下注意點:

我們需要做到執行計劃一樣,這裡以全表掃描為例。實際上 count 很可能使用到覆蓋索引(Using index),本文主要討論它們實現的異同。count(*) 和 count(字段) 在結果上可能並不一致。比如 count(字段),但是某些行本字段為 NULL 的話那麼將不會統計行數,下面將會說明這種 NULL 判斷的位置。本文還是使用簡單的全表掃描來進行對比實現上的區別。首先我們要明確的是 count 使用的是一個 COUNT 計數器。


一、使用示例

在示例中我們也可以看到兩個語句的結果實際上並不一致。

<code>mysql> show create table baguai_f \\G*************************** 1. row *************************** Table: baguai_fCreate Table: CREATE TABLE `baguai_f` ( `id` int(11) DEFAULT NULL, `a` varchar(20) DEFAULT NULL, `b` varchar(20) DEFAULT NULL, `c` varchar(20) DEFAULT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf81 row in set (0.00 sec)mysql> select * from baguai_f ;+------+------+------+------+| id | a | b | c |+------+------+------+------+| 1 | g | g | NULL || 1 | g1 | g1 | g1 || 3 | g2 | g2 | g2 || 4 | g | g | NULL || 5 | g | g | NULL || 6 | g3 | g3 | g3 |+------+------+------+------+6 rows in set (0.00 sec)mysql> desc select count(*) from baguai_f where b='g';+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------------+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------------+| 1 | SIMPLE | baguai_f | NULL | ALL | NULL | NULL | NULL | NULL | 6 | 16.67 | Using where |+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------------+1 row in set, 1 warning (0.00 sec)mysql> desc select count(c) from baguai_f where b='g';+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------------+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------------+| 1 | SIMPLE | baguai_f | NULL | ALL | NULL | NULL | NULL | NULL | 6 | 16.67 | Using where |+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------------+1 row in set, 1 warning (0.00 sec)mysql> select count(*) from baguai_f where b='g';+----------+| count(*) |+----------+| 3 |+----------+1 row in set (0.00 sec)mysql> select count(c) from baguai_f where b='g';+----------+| count(c) |+----------+| 0 |+----------+1 row in set (0.00 sec)/<code>

這種不一致來自於 b='g' 的 c 列中都是 NULL 值,因此 count(c) 返回為 0。


二、示例中 count(*) 獲取數據流程簡析

注意在 一文中我們已經詳細的描述了部分流程,這裡不再贅述,如果需要更加詳細的瞭解,自行參考。

1. MySQL 層 構建 read_set

這裡構建的 read_set 實際上只會包含列b,即一個字段。

2. Innodb 層 構建模板

同理根據 read_set 構建的字段模板中只會包含列b。

LOOP:這裡開始循環返回每一條數據

3. Innodb 層 根據模板返回數據

這裡我們可以看看模板的數量和模板對應的具體列名

模板的數量

<code>斷點:row_sel_store_mysql_rec查看模板數量:(gdb) p prebuilt->n_template$1 = 1/<code>查看模板對應的字段

<code>斷點:row_sel_field_store_in_mysql_format_func查看模板對應的字段:(gdb) p field->name$3 = {m_name = 0x7ffe7c99cf85 "b"}/<code>

顯然這裡只是將 b 列的值返回給了 MySQL層,這裡也很好理解,因為 b 列在 MySQL 層需要繼續做過濾操作。

4. MySQL 層 過濾條件 b='g'

好了,當前返回給 MySQL 層的數據中只有 b 列的數據,然後施加 b='g' 這個條件進行過濾。

5. MySQL 層 過濾後做一個 COUNT 計數操作

對於普通的 select 語句過濾後的數據就可以返回了,但是對於 count 這種操作,這裡做的是一個計數操作,其中行會對 count 字段的 NULL 值進行判斷,當然這裡是 count(*) 也就不存在 NULL 值判斷了,下面是這段代碼:

<code>bool Item_sum_count::add(){ if (aggr->arg_is_null(false)) return 0; count++; return 0;}/<code>

END LOOP

最終我們只需要返回這個計數就可以了。下面是發送的數據,斷點可以設置在 Query_result_send::send_data 中。

<code>$22 = Item::SUM_FUNC_ITEM(gdb) p ((Item*)(items)->first->info)->field_type()$23 = MYSQL_TYPE_LONGLONG(gdb) p ((Item*)(items)->first->info)->val_int()$24 = 3(gdb) p (items)->first->info$26 = (void *) 0x7ffe7c006580(gdb) p ((Item_sum_count*)$26)->count$28 = 3/<code>

我們可以發送的數據實際就是這個計數器,最終值為 3。


三、示例中 count(c) 獲取數據流程的不同

實際上整個流程基本一致,但是區別在於:

構建的 read_set 不同,模板個數自然不同,因為需要 2 個字段,即 b、c 兩個字段,其中 b 列用於 where 條件過濾,而 b 列用於統計是否有 NULL 值,因此模板數量為 2,如下:

<code>(gdb) p prebuilt->n_template$29 = 2/<code>做 COUNT 計數器的時候會根據 c 列的 NULL 值做實際的過濾,操作只要是 NULL 則 count 計數不會增加 1,這個還是參考這段代碼:

<code>bool Item_sum_count::add(){ if (aggr->arg_is_null(false)) //過濾NULL值 return 0; count++; return 0;}/<code>

最終會調入函數 Field::is_null 進行 NULL 值判斷,斷點可以設置在這裡。


四、不同點總結

示例中的語句 count(c) 返回為 0。現在我們很清楚了,這些數據什麼時候過濾掉的,總結如下:

Innodb 層返回了全部的行數據。MySQL 層通過 where 條件過濾,剩下了 b='g' 的行。MySQL 層通過 NULL 判斷,將剩下的 count(c) 中為 NULL 的行也排除在計數之外。

而 count(*) 則沒有第 3 步,這是一個不同。

然後的不同點就是在返回的字段上:

count(c) 很明顯除了 where 條件以外,還需要返回 c 列給 MySQL 層count(*) 則不需要返回額外的字段給 MySQL 層,只需要 MySQL 層過濾需要的b列即可。

通過上面的分析,實際上效率沒有太大的差別,我覺得同樣執行計劃,同樣返回數據結果的前提下,可能 count(*) 的效率要略微高一點。


五、備用棧幀(下圖需點擊放大查看)

NULL 值計數過濾棧幀


最後推薦高鵬的專欄《深入理解 MySQL 主從原理 32 講》,想要透徹瞭解學習 MySQL 主從原理的朋友不容錯過。