MySQL查询过程
MySQL 的架构共分为两层:Server 层和存储引擎层
- Server 层负责建立连接、分析和执行 SQL。主要包括连接器,查询缓存、解析器、预处理器、优化器、执行器等。另外,所有的内置函数(如日期、时间、数学和加密函数等)和所有跨存储引擎的功能(如存储过程、触发器、视图等。)都在 Server 层实现
- 存储引擎层负责数据的存储和提取。支持 InnoDB、MyISAM、Memory 等多个存储引擎,不同的存储引擎共用一个 Server 层。从 MySQL 5.5 版本开始, InnoDB 成为了 MySQL 的默认存储引擎。索引数据结构就是由存储引擎层实现,不同的存储引擎支持的索引类型也不相同,比如 InnoDB 支持索引类型是 B+树 ,且是默认使用,在数据表中创建的主键索引和二级索引默认使用的是 B+ 树索引
第一步:连接器
启动Mysql服务,连接 MySQL 服务
# -h 指定 MySQL 服务得 IP 地址,如果是连接本地的 MySQL服务,可以不用这个参数; # -u 指定用户名,管理员角色名为 root; # -p 指定密码,如果命令行中不填写密码(为了密码安全,建议不要在命令行写密码),就需要在交互对话里面输入密码 mysql -h$ip -u$user -p查看 MySQL 服务被多少客户端连接
执行
show processlist命令进行查看MySQL 定义空闲连接的最大空闲时长,由
wait_timeout参数控制的,如果空闲连接超过了这个时间,连接器就会自动将它断开mysql> show variables like 'wait_timeout'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | wait_timeout | 28800 | +---------------+-------+ 1 row in set (0.00 sec)处于空闲状态的连接被服务端主动断开后,这个客户端并不会马上知道;发起请求获知
MySQL 服务支持的最大连接数由 max_connections 参数控制
mysql> show variables like 'max_connections'; +-----------------+-------+ | Variable_name | Value | +-----------------+-------+ | max_connections | 151 | +-----------------+-------+长连接占用内存
- 定期断开长连接。既然断开连接后就会释放连接占用的内存资源,那么可以定期断开长连接
- 客户端主动重置连接。MySQL 5.7 版本实现了
mysql_reset_connection()函数的接口
连接器总结:
- 与客户端进行 TCP 三次握手建立连接;
- 校验客户端的用户名和密码,如果用户名或密码不对,则会报错;
- 如果用户名和密码都对了,会读取该用户的权限,然后后面的权限逻辑判断都基于此时读取到的权限
第二步:查询缓存
- SQL 是查询语句(select 语句),MySQL 就会先去查询缓存( Query Cache )里查找缓存数据,看看之前有没有执行过这一条命令,这个查询缓存是以 key-value 形式保存在内存中的,key 为 SQL 查询语句,value 为 SQL 语句查询的结果
- 查询缓存还挺有用,但是其实查询缓存挺鸡肋的。对于更新比较频繁的表,查询缓存的命中率很低的
- MySQL 8.0 版本直接将查询缓存删掉了,也就是说 MySQL 8.0 开始,执行一条 SQL 查询语句,不会再走到查询缓存这个阶段了
第三步:解析SQL
正式执行 SQL 查询语句之前, MySQL 会先对 SQL 语句做解析
- 第一件事情:词法分析
- 根据你输入的字符串识别关键字,例如:SQL语句 select username from userinfo,分析之后,会得到4个Token,其中2个Keyword,为select和from。
- 第二件事情:语法分析
- 根据词法分析结果,语法解析器会根据语法规则,如果没问题就会构建出 SQL 语法树,这样方便后面模块获取 SQL 类型、表名、字段名、 where 条件等等
- 第一件事情:词法分析
第四步:执行SQL
每条
SELECT查询语句流程主要可以分为下面这三个阶段:- prepare 阶段,也就是预处理阶段;
- optimize 阶段,也就是优化阶段;
- execute 阶段,也就是执行阶段;
预处理阶段
- 检查 SQL 查询语句中的表或者字段是否存在;
- 将
select *中的*符号,扩展为表上的所有列
优化阶段
- 优化器主要负责将 SQL 查询语句的执行方案确定下来,比如在表里面有多个索引的时候,优化器会基于查询成本的考虑,来决定选择使用哪个索引
执行阶段
- 经历完优化器后,就确定了执行方案,接下来 MySQL 就真正开始执行语句
- 主键索引查询
- 查询条件用到了主键索引,而且是等值查询,同时主键 id 是唯一,不会有 id 相同的记录
- 全表扫描
- 这条查询语句的查询条件没有用到索引,所以优化器决定选用访问类型为 ALL 进行查询
- 索引下推
- 索引下推能够减少二级索引在查询时的回表操作,提高查询的效率
总结:
执行一条 SQL 查询语句,发生了什么?
- 连接器:建立连接,管理连接、校验用户身份;
- 查询缓存:如果查询语句命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块;
- 解析 SQL,通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型;
- 执行 SQL:执行 SQL 共有三个阶段:
- 预处理阶段:检查表或字段是否存在;将
select *中的*符号扩展为表上的所有列。 - 优化阶段:基于查询成本的考虑, 选择查询成本最小的执行计划;
- 执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端
- 预处理阶段:检查表或字段是否存在;将
更新语句也是同样会走一遍:
- 更新一条记录
UPDATE t_user SET name = 'xiaolin' WHERE id = 1;的流程如下:- 客户端先通过连接器建立连接,连接器自会判断用户身份;
- 因为这是一条 update 语句,所以不需要经过查询缓存,但是表上有更新语句,是会把整个表的查询缓存清空的,所以说查询缓存很鸡肋,在 MySQL 8.0 就被移除这个功能了;
- 解析器会通过词法分析识别出关键字 update,表名等等,构建出语法树,接着还会做语法分析,判断输入的语句是否符合 MySQL 语法;
- 预处理器会判断表和字段是否存在;
- 优化器确定执行计划,因为 where 条件中的 id 是主键索引,所以决定要使用 id 这个索引;
- 执行器负责具体执行,会调用存储引擎的接口,通过主键索引树搜索获取 id = 1 这一行记录:
- 如果 id=1 这一行所在的数据页本来就在 buffer pool 中,就直接返回给执行器更新;
- 如果记录不在 buffer pool,将数据页从磁盘读入到 buffer pool,返回记录给执行器。
- 执行器得到聚簇索引记录后,会看一下更新前的记录和更新后的记录是否一样:
- 如果一样的话就不进行后续更新流程;
- 如果不一样的话就把更新前的记录和更新后的记录都当作参数传给 InnoDB 层,让 InnoDB 真正的执行更新记录的操作;
- 开启事务, InnoDB 层更新记录前,首先要记录相应的 undo log,因为这是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面,不过在内存修改该 Undo 页面后,需要记录对应的 redo log。
- InnoDB 层开始更新记录,会先更新内存(同时标记为脏页),然后将记录写到 redo log 里面,这个时候更新就算完成了。为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。
- 至此,一条记录更新完了
- 更新一条记录
索引
索引:帮助存储引擎快速获取数据的一种数据结构,形象的说就是索引是数据的目录
存储引擎:就是如何存储数据、如何为存储的数据建立索引和如何更新、查询数据等技术的实现方法
索引分类
按数据结构分类:常见有 B+Tree 索引、HASH 索引、Full-Text 索引
- InnoDB 是在 MySQL 5.5 之后成为默认的 MySQL 存储引擎,B+Tree 索引类型也是 MySQL 存储引擎采用最多的索引类型
- InnoDB 存储引擎根据不同的场景选择不同的列作为索引:
- 如果有主键,默认会使用主键作为聚簇索引的索引键(key);
- 如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键(key);
- 在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id 列作为聚簇索引的索引键(key);
- 其它索引都属于辅助索引(Secondary Index),也被称为二级索引或非聚簇索引。创建的主键索引和二级索引默认使用的是 B+Tree 索引
- 存储在 B+Tree 索引时是长什么样子
- B+Tree 是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,而且每个节点里的数据是按主键顺序存放
- 每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息
- 每一个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表
- 数据库的索引和数据都是存储在硬盘的,我们可以把读取一个节点当作一次磁盘 I/O 操作
- B+Tree 存储千万级的数据只需要 3-4 层高度就可以满足,B+Tree 相比于 B 树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I/O 依然维持在 3-4次
- 主键索引的 B+Tree 和二级索引的 B+Tree 区别:
- 主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里;
- 二级索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据
- 找到对应的叶子节点,然后获取主键值,再通过主键索引中的 B+Tree 树查询到对应的叶子节点,然后获取整行数据。这个过程叫「回表」,也就是说要查两个 B+Tree 才能查到数据
- 当查询的数据是能在二级索引的 B+Tree 的叶子节点里查询到,这时就不用再查主键索引查;在二级索引的 B+Tree 就能查询到结果的过程就叫作「覆盖索引」,也就是只需要查一个 B+Tree 就能找到数据
- 联合索引是二级索引的一种形式(除非它是主键),如果表显式定义了主键索引(聚簇索引),则所有其他索引(无论是单列、联合、唯一索引等)均属于二级索引
B+Tree 相比于 B 树、二叉树或 Hash 索引结构的优势
1、B+Tree vs B Tree
B+Tree 只在叶子节点存储数据,而 B 树 的非叶子节点也要存储数据,B+Tree 的单个节点的数据量更小,在相同的磁盘 I/O 次数下,就能查询更多的节点。
B+Tree 叶子节点采用的是双链表连接,适合 MySQL 中常见的基于范围的顺序查找,而 B 树无法做到这一点。
2、B+Tree vs 二叉树
对有 N 个叶子节点的 B+Tree,其搜索复杂度为
O(logdN),其中 d 表示节点允许的最大子节点个数为 d 个在实际的应用当中, d 值是大于100的,这样就保证了,即使数据达到千万级别时,B+Tree 的高度依然维持在 3~4 层左右
二叉树的每个父节点的儿子节点个数只能是 2 个,意味着其搜索复杂度为
O(logN),因此二叉树检索到目标数据所经历的磁盘 I/O 次数要更多3、B+Tree vs Hash
Hash 在做等值查询的时候效率贼快,搜索复杂度为 O(1)
但是 Hash 表不适合做范围查询,它更适合做等值的查询,这也是 B+Tree 索引要比 Hash 表索引有着更广泛的适用场景的原因
物理存储分类
- 物理存储的角度:索引分为聚簇索引(主键索引)、二级索引(辅助索引)
- 两个区别:
- 主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里;
- 二级索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据
字段特性分类
主键索引
建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。
创建表时,创建主键索引的方式如下:
CREATE TABLE table_name ( .... PRIMARY KEY (index_column_1) USING BTREE );
唯一索引
建立在 UNIQUE 字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值
在创建表时,创建唯一索引的方式如下:
CREATE TABLE table_name ( .... UNIQUE KEY(index_column_1,index_column_2,...) );建表后,如果要创建唯一索引,可以使用这面这条命令:
CREATE UNIQUE INDEX index_name ON table_name(index_column_1,index_column_2,...);
普通索引
是建立在普通字段上的索引,既不要求字段为主键,也不要求字段为 UNIQUE
在创建表时,创建普通索引的方式如下:
CREATE TABLE table_name ( .... INDEX(index_column_1,index_column_2,...) );建表后,如果要创建普通索引,可以使用这面这条命令:
CREATE INDEX index_name ON table_name(index_column_1,index_column_2,...);
前缀索引
对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引,前缀索引可以建立在字段类型为 char、 varchar、binary、varbinary 的列上
目的:减少索引占用的存储空间,提升查询效率
创建表时,创建前缀索引的方式如下:
CREATE TABLE table_name( column_list, INDEX(column_name(length)) );建表后,如果要创建前缀索引,可以使用这面这条命令:
CREATE INDEX index_name ON table_name(column_name(length));
按字段个数分类
从字段个数的角度来看,索引分为单列索引、联合索引(复合索引)
- 建立在单列上的索引称为单列索引,比如主键索引
- 建立在多列上的索引称为联合索引
联合索引
使用联合索引时,存在最左匹配原则,就是按照最左优先的方式进行索引的匹配。在使用联合索引进行查询的时候,如果不遵循「最左匹配原则」,联合索引会失效,这样就无法利用到索引快速查询的特性了
- 查询条件之所以会失效,是因为
(a, b, c)联合索引,- 先按 a 排序,在 a 相同的情况再按 b 排序,在 b 相同的情况再按 c 排序。
- b 和 c 是全局无序,局部相对有序的,这样在没有遵循最左匹配原则的情况下,是无法利用到索引的
- 查询条件之所以会失效,是因为
联合索引范围
联合索引的最左匹配原则,在遇到范围查询(如 >、<)的时候,就会停止匹配,也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。注意,对于 >=、<=、BETWEEN、like 前缀匹配的范围查询,并不会停止匹配,前面我也用了四个例子说明了
索引下推
可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数
当你的查询语句的执行计划里,出现了 Extra 为
Using index condition,那么说明使用了索引下推的优化索引区分度
建立联合索引时的字段顺序,对索引效率也有很大影响。越靠前的字段被用于索引过滤的概率越高,实际开发工作中建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的 SQL 使用到
实际使用
select * from order where status = 1 order by create_time asc给 status 和 create_time 列建立一个联合索引,因为这样可以避免 MySQL 数据库发生文件排序
要利用索引的有序性,在 status 和 create_time 列建立联合索引,这样根据 status 筛选后的数据就是按照 create_time 排好序的,避免在文件排序,提高了查询效率
优化索引
前缀索引优化
- 使用某个字段中字符串的前几个字符建立索引,可以增加一个索引页中存储的索引值,有效提高索引的查询速度
- 前缀索引有一定的局限性,例如:
- order by 就无法使用前缀索引;
- 无法把前缀索引用作覆盖索引;
覆盖索引优化
- SQL 中 query 所有字段,在索引 B+Tree 的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表的操作
主键索引自增
- 如果我们使用自增主键,那么每次插入的新数据就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,当页面写满,就会自动开辟一个新页面。因为每次插入一条新记录,都是追加操作,不需要重新移动数据,因此这种插入数据的方法效率非常高
- 如果我们使用非自增主键,由于每次插入主键的索引值都是随机的,因此每次插入新的数据时,就可能会插入到现有数据页中间的某个位置,这将不得不移动其它数据来满足新数据的插入,甚至需要从一个页面复制数据到另外一个页面,我们通常将这种情况称为页分裂。页分裂还有可能会造成大量的内存碎片,导致索引结构不紧凑,从而影响查询效率
索引设置为NOT NULL
原因:索引列存在 NULL 就会导致优化器在做索引选择的时候更加复杂,更加难以优化
原因:NULL 值是一个没意义的值,但是它会占用物理空间,所以会带来的存储空间的问题
防止索引失效
- 说一下,发生索引失效情况:
- 使用左或者左右模糊匹配的时候,也就是
like %xx或者like %xx%这两种方式都会造成索引失效; - 在查询条件中对索引列做了计算、函数、类型转换操作,这些情况都会造成索引失效;
- 能正确使用需要遵循最左匹配原则,否则就会导致索引失效。
- 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效
- 使用左或者左右模糊匹配的时候,也就是
- 说一下,发生索引失效情况:
Explain使用
在sql语句前添加explain,作用是查看mysql对这条sql的执行计划信息
- 对于执行计划,参数有:
- possible_keys 字段表示可能用到的索引;
- key 字段表示实际用的索引,如果这一项为 NULL,说明没有使用索引;
- key_len 表示索引的长度;
- rows 表示扫描的数据行数。
- type 表示数据扫描类型,我们需要重点看这个。
- type 字段就是描述了找到所需数据时使用的扫描方式是什么,常见扫描类型的执行效率从低到高的顺序为:
- All(全表扫描);
- index(全索引扫描);
- range(索引范围扫描);
- ref(非唯一索引扫描);
- eq_ref(唯一索引扫描);
- const(结果只有一条的主键或唯一索引扫描)
- 也要关注 extra 显示的结果,几个重要的参考指标:
- Using filesort :当查询语句中包含 group by 操作,而且无法利用索引完成排序操作的时候, 这时不得不选择相应的排序算法进行,甚至可能会通过文件排序,效率是很低的,所以要避免这种问题的出现。
- Using temporary:使了用临时表保存中间结果,MySQL 在对查询结果排序时使用临时表,常见于排序 order by 和分组查询 group by。效率低,要避免这种问题的出现。
- Using index:所需数据只需在索引即可全部获得,不须要再到表中取数据,也就是使用了覆盖索引,避免了回表操作,效率不错
- 对于执行计划,参数有:
需要与不需要索引
- 索引最大好处:提高查询速度
- 索引缺点:
- 需要占用物理空间,数量越大,占用空间越大
- 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增大
- 会降低表的增删改的效率,因为每次增删改索引,B+ 树为了维护索引有序性,都需要进行动态维护
- 需要索引
- 字段有唯一性限制的,比如商品编码;
- 经常用于
WHERE查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。 - 经常用于
GROUP BY和ORDER BY的字段,这样在查询的时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的
- 不需要索引
WHERE条件,GROUP BY,ORDER BY里用不到的字段,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的- 字段中存在大量重复数据,不需要创建索引,比如性别字段,只有男女,如果数据库表中,男女的记录分布均匀,无论搜索哪个值都可能得到一半的数据
- 表数据太少的时候,不需要创建索引;
- 经常更新的字段不用创建索引,比如不要对电商项目的用户余额建立索引,因为索引字段频繁修改,由于要维护 B+Tree的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能的