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:count(*)、count(字段) 實現上區別

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


分享到:


相關文章: