为了故事的顺利发展,我们引入一个表:

CREATE TABLE t (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
key1 INT,
common_field VARCHAR(100),
PRIMARY KEY (id),
KEY idx_key1 (key1)
) Engine=InnoDB CHARSET=utf8;

复制代码

这个表就包含2个索引(也就是2棵B+树):

  • id列为主键对应的聚簇索引。
  • key1列建立的二级索引idx_key1

我们向表中插入一些记录:

INSERT INTO t VALUES
(1, 30, ‘b’),
(2, 80, ‘b’),
(3, 23, ‘b’),
(4, NULL, ‘b’),
(5, 11, ‘b’),
(6, 53, ‘b’),
(7, 63, ‘b’),
(8, NULL, ‘b’),
(9, 99, ‘b’),
(10, 12, ‘b’),
(11, 66, ‘b’),
(12, NULL, ‘b’),
(13, 66, ‘b’),
(14, 30, ‘b’),
(15, 11, ‘b’),
(16, 90, ‘b’);

复制代码

所以现在t表的聚簇索引示意图就是这样:

mysql 语句 一起执行 mysql语句怎么执行_mysql 语句 一起执行

t表的二级索引示意图就是这样:

mysql 语句 一起执行 mysql语句怎么执行_mysql 语句 一起执行_02

小贴士:

原谅我们画了一个极度简化版的B+树:我们省略了B+树中的页面节点,省略了每层页面节点之间的双向链表,省略了页面中记录的单向链表,省略了页面中的页目录结构,还有省略了好多东西。但是我们保留B+树最为核心的一个特点:记录是按照键值大小进行排序的。即对于聚簇索引来说,记录是按照id列进行排序的;对于二级索引idx_key1来说,记录是按照key1列进行排序的,在key1列相同时再按照id列进行排序。

从上边聚簇索引和二级索引的结构中大家可以发现:每一条聚簇索引记录都可以在二级索引中找到唯一的一条二级索引记录与其相对应。

前置知识2——server层和存储引擎的交互


以下边这个查询为例:

SELECT * FROM t WHERE key1 > 70 AND common_field != ‘a’;

复制代码

假设优化器认为通过扫描二级索引idx_key1中key1值在(70, +∞)这个区间中的二级索引记录的成本更小,那么查询将以下述方式执行:

  • server层先让InnoDB去查在key1值在(70, +无穷)区间中的第一条记录。
  • InnoDB通过二级索引idx_key1对应的B+树,从B+树根页面一层一层向下定位,快速找到(70, +无穷)区间的第一条二级索引记录,然后根据该二级索引记录进行回表操作,找到完整的聚簇索引记录,然后返回给server层。
  • server层判断InnoDB返回的记录符不符合搜索条件key1 > 70 AND common_field != 'a',如果不符合的话就跳过该记录,否则将其发送到客户端。

小贴士:

此处将记录发送给客户端其实是发送到本地的网络缓冲区,缓冲区大小由net_buffer_length控制,默认是16KB大小。等缓冲区满了才真正发送网络包到客户端。

  • 然后server层向InnoDB要下一条记录。
  • InnoDB根据上一次找到的二级索引记录的next_record属性,获取到下一条二级索引记录,回表后将完整的聚簇索引记录返回给server层。
  • server继续判断,不符合搜索条件即跳过该记录,否则发送到客户端。
  • … 一直循环上述过程,直到InnoDB找不到下一条记录,则向server层报告查询完毕。
  • server层收到InnoDB报告的查询完毕请求,停止查询。

可见,一般情况下server层和存储引擎层是以记录为单位进行交互的。

我们看一下源码中读取一条记录的函数调用栈:

mysql 语句 一起执行 mysql语句怎么执行_java_03

其中的handler::ha_index_next便是server层向存储引擎要下一条记录的接口。

其中的row_search_mvcc是读取一条记录最重要的函数,这个函数长的吓人,有一千多行:

mysql 语句 一起执行 mysql语句怎么执行_mysql 语句 一起执行_04

每读取一条记录,都要做非常多的工作,诸如进行多版本的可见性判断,要不要对记录进行加锁的判断,要是加锁的话加什么锁的选择,完成记录从InnoDB的存储格式到server层存储格式的转换等等等等十分繁杂的工作。

小贴士:

不知道你们公司有没有写这么长函数的同学,如果有的话你想不想打他。

前置知识3——COUNT是个啥


COUNT是一个汇总函数(聚集函数),它接收1个表达式作为参数:

COUNT(expr)

复制代码

COUNT函数用于统计在符合搜索条件的记录中,指定的表达式expr不为NULL的行数有多少。这里需要特别注意的是,expr不仅仅可以是列名,其他任意表达式都是可以的。

比方说:

SELECT COUNT(key1) FROM t;

复制代码

这个语句是用于统计在single_table表的所有记录中,key1列不为NULL的行数是多少。

再看这个:

SELECT COUNT(‘abc’) FROM t;

复制代码

这个语句是用于统计在single_table表的所有记录中,'abc’这个表达式不为NULL的行数是多少。很显然,'abc’这个表达式永远不是NULL,所以上述语句其实就是统计single_table表里有多少条记录。

再看这个:

SELECT COUNT(*) FROM t;

复制代码

【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】

浏览器打开:qq.cn.hn/FTf 免费领取

这个语句就是直接统计single_table表有多少条记录。

总结+注意:COUNT函数的参数可以是任意表达式,该函数用于统计在符合搜索条件的记录中,指定的表达式不为NULL的行数有多少

MySQL中COUNT是怎样执行的


做了那么多铺垫,终于到了MySQL中COUNT是怎样执行的了。