MySQL-进阶篇-索引

文章目录

  • [1. 准备工作](#1. 准备工作)
  • [2. 索引概述](#2. 索引概述)
    • [2.1 什么是索引](#2.1 什么是索引)
    • [2.2 索引的优缺点](#2.2 索引的优缺点)
  • [3. 索引的结构](#3. 索引的结构)
    • [3.1 索引结构介绍](#3.1 索引结构介绍)
    • [3.2 二叉树](#3.2 二叉树)
    • [3.3 BTree](#3.3 BTree)
    • [3.4 B+Tree](#3.4 B+Tree)
    • [3.5 MySQL 中的 B+Tree](#3.5 MySQL 中的 B+Tree)
    • [3.6 Hash](#3.6 Hash)
    • [3.7 思考题:为什么 InnoDB 存储引擎选择使用 B+Tree 索引结构](#3.7 思考题:为什么 InnoDB 存储引擎选择使用 B+Tree 索引结构)
  • [4. 索引的分类](#4. 索引的分类)
  • [5. 索引的语法](#5. 索引的语法)
    • [5.1 创建索引](#5.1 创建索引)
    • [5.2 查看索引](#5.2 查看索引)
    • [5.3 删除索引](#5.3 删除索引)
    • [5.4 练习-准备工作](#5.4 练习-准备工作)
    • [5.5 练习-实现需求](#5.5 练习-实现需求)
      • [5.5.1 需求一](#5.5.1 需求一)
      • [5.5.2 需求二](#5.5.2 需求二)
      • [5.5.3 需求三](#5.5.3 需求三)
      • [5.5.4 需求四](#5.5.4 需求四)
      • [5.5.5 需求五](#5.5.5 需求五)
  • [6. 索引-性能分析工具](#6. 索引-性能分析工具)
    • [6.1 查看 SQL 语句的执行频率](#6.1 查看 SQL 语句的执行频率)
    • [6.2 慢查询日志](#6.2 慢查询日志)
    • [6.3 show profiles](#6.3 show profiles)
      • [6.3.1 查看每一条指令的基本耗时情况](#6.3.1 查看每一条指令的基本耗时情况)
      • [6.3.2 查看指定 query_id 的 SQL 语句各个阶段的耗时情况](#6.3.2 查看指定 query_id 的 SQL 语句各个阶段的耗时情况)
      • [6.3.3 查看指定 query_id 的 SQL 语句耗费 CPU 资源的情况](#6.3.3 查看指定 query_id 的 SQL 语句耗费 CPU 资源的情况)
    • [6.4 explain](#6.4 explain)
    • [6.5 explain 执行计划中各字段的含义](#6.5 explain 执行计划中各字段的含义)
      • [6.5.1 id](#6.5.1 id)
        • [6.5.1.1 含义](#6.5.1.1 含义)
        • [6.5.1.2 准备工作](#6.5.1.2 准备工作)
        • [6.5.1.3 需求一](#6.5.1.3 需求一)
        • [6.5.1.4 需求二](#6.5.1.4 需求二)
      • [6.5.2 select_type](#6.5.2 select_type)
      • [6.5.3 type(重点关注)](#6.5.3 type(重点关注))
      • [6.5.4 possible_key(重点关注)](#6.5.4 possible_key(重点关注))
      • [6.5.5 key(重点关注)](#6.5.5 key(重点关注))
      • [6.5.6 key_len](#6.5.6 key_len)
      • [6.5.7 rows](#6.5.7 rows)
      • [6.5.8 filtered](#6.5.8 filtered)
      • [6.5.9 Extra](#6.5.9 Extra)
  • [7. 索引的使用规则](#7. 索引的使用规则)
    • [7.1 验证索引的效率](#7.1 验证索引的效率)
    • [7.2 最左前缀法则](#7.2 最左前缀法则)
    • [7.3 范围查询](#7.3 范围查询)
    • [7.4 索引失效的情况](#7.4 索引失效的情况)
      • [7.4.1 对索引列进行函数运算](#7.4.1 对索引列进行函数运算)
      • [7.4.2 字符串不加单引号](#7.4.2 字符串不加单引号)
      • [7.4.3 头部模糊匹配](#7.4.3 头部模糊匹配)
      • [7.4.4 用 or 连接条件](#7.4.4 用 or 连接条件)
      • [7.4.5 数据分布影响](#7.4.5 数据分布影响)
    • [7.5 SQL 提示](#7.5 SQL 提示)
    • [7.6 覆盖索引与回表查询](#7.6 覆盖索引与回表查询)
    • [7.7 前缀索引](#7.7 前缀索引)
    • [7.8 单列索引与联合索引](#7.8 单列索引与联合索引)
  • [8. 索引的设计原则](#8. 索引的设计原则)

1. 准备工作

在学习 MySQL 的索引前,需要先在 Linux 系统上安装 MySQL 8.x(因为无论是开发环境、测试环境还是生产环境,绝大部分使用的都是 Linux 环境) ,具体的安装教程可以参考我的另一篇博文:在 Linux 系统中安装MySQL 8.x(Ubuntu和CentOS)

索引是 MySQL 进阶篇中最重要的一部分内容,我们在讲解 SQL 优化的时候很大程度上都是围绕着索引去优化的,索引部分内容需要大家重点关注

2. 索引概述

2.1 什么是索引

索引(Index)是帮助 MySQL 高效获取数据的有序数据结构

除了数据以外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引


一提到数据结构可能大家就懵圈了,脑子里就会浮现出二叉树、红黑树、BTree、B+Tree 等,那这些数据结构之间有什么区别,以及 MySQL 采用的是哪种数据结构呢?先别着急,后面会为大家详细讲解


我们先来看一下,假如一张表没有索引,在查询数据时会是什么样的情况

假设目前有一张 user 表,这张表是没有索引的,接下来我要执行一条 SQL 语句 select * from user where age = 45 ,也就是根据 age 字段进行查询

此时是怎么查找的呢,会从表的第一条数据开始查找,判断 age 字段是否等于 45 ,如果 age 字段不等于 45 ,就找下一条,一条一条地找,直到找到某条数据的 age 字段等于 45

当这条 SQL 语句找到第一条 age 字段等于 45 的数据时,SQL 语句还会往下执行、继续匹配数据吗?答案是会,因为无法保证表中的记录只有一条数据的 age 字段是等于 45

这就是表没有索引的情况,相当于把整张表的记录都扫描了一遍(全表扫描),效率极低


接下来我们来看一下表有索引的情况,还是以上述的 user 表为例,根据 age 字段查找数据

在有索引的情况下,我们需要为 age 字段维护一定的数据结构,至于是什么数据结构,我们暂时不关心,先用大家比较熟悉的二叉树来为大家演示一下有索引的情况

在有索引的情况下,只需要 3 次比对就找到了目标数据,搜索效率是十分高的

注意:上述的二叉树索引结构只是一个示意图,只是为了让大家理解什么是索引,并不是真实的索引结构

2.2 索引的优缺点

优点 缺点
提高数据检索的效率,降低数据库的 IO 成本 索引也需要占用一定的磁盘空间
通过索引列对数据进行排序,降低数据排序的成本,降低 CPU 的消耗 索引大大提高了查询效率,同时却也降低更新表的速度,例如 INSERT、UPDATE、DELETE 语句时效率会降低

一般在考虑为表添加索引的时候,索引的缺点基本上可以忽略,主要有以下两点原因:

  1. 磁盘现在很便宜
  2. 对于一个正常的业务系统来说,增删改的比例很小,主要是查询

3. 索引的结构

我们先回想一下,在 MySQL 的体系结构中,索引是在哪一层实现的?没错,就是在第三层------ 存储引擎层 实现的,这意味着存储引擎不同,索引的结构也会不同


3.1 索引结构介绍

MySQL 的索引结构主要有以下四种(重点是 B+ Tree 索引):

  1. B+Tree 索引:最常见的索引类型,大部分存储引擎都支持 B+Tree 索引
  2. Hash 索引:底层数据结构是用哈希表实现的,只有精确匹配索引列的查询才有效,不支持范围查询
  3. R-tree(空间索引):空间索引是 MyISAM 引擎的一个特殊索引类型,主要用于地理空间数据类型,通常使用较少
  4. Full-text(全文索引):一种通过建立倒排索引来快速匹配文档的方式,类似于 Lucene、Solr、ElasticSearch ,通常使用较少

不同的索引结构在不同存储引擎中的支持情况:

注意:我们平常所说的索引,如果没有特别指明,都是指由 B+Tree 数据结构组织的索引

提到 B+Tree ,很多同学并不是很清楚这是一个怎么样的数据结构,我们先不去研究 B+Tree ,我们先来看一下大家比较熟悉的二叉树

3.2 二叉树

二叉树,顾名思义,一个节点下面最多包含两个子节点

在上述二叉树中,36 是二叉树的根节点,左侧子树中的所有节点都比 36 小,左侧子树中的所有节点都比 36 大

当我们要去查找一个数据时,比如说 17 ,首先会将 17 和根节点 36 对比,17 比 36 小,从根节点的左侧子树开始查找 17 ,以此类推,只需要查找 4 次就能够找到 17


我们再来看另一颗二叉树,如果我们在维护二叉树的时候,数据是顺序插入的,比如插入顺序为 36、34、33、23、22、20、19、17 时

最终形成的二叉树相当于一个单向链表,此时我再去寻找 17 这条数据,是不是相当于把整个列表都遍历了一遍

这就是二叉树的弊端:

  1. 顺序插入时会形成一个链表,导致查询性能大大降低
  2. 二叉树的一个节点下最多包含两个子节点,在数据量很大的情况下,节点的层级会比较深,数据的检索速度也会变得十分缓慢

我们要想办法规避二叉树的弊端,对于第一个弊端,熟悉数据结构的同学可能已经想到了红黑树,红黑树是一个自平衡的二叉树, 以下红黑树便是由 36、34、33、23、22、20、19、17 这组数据构建而成

在查询 17 这条数据时,只需要 4 次查找。第一个问题基本解决了,通过红黑树解决树的平衡问题

但是红黑树本质上也是一个二叉树,所以也无法避免的二叉树的第二个弊端------ 在数据量很大的情况下,节点的层级会比较深,数据的检索速度也会变得十分缓慢

要想解决 在数据量很大的情况下,节点的层级会比较深 这个问题,我们该怎么办呢,我们能不能构建一棵树,让这棵树的下面能有多个节点,不只是 2 个,有可能是 10 个,甚至 20 个,可不可以呢?答案是可以,此时需要为大家介绍一种新的数据结构------BTree

3.3 BTree

BTree:Balance Tree,多路平衡查找树,多路指的就是一个节点下面可以包含多个子节点

知识小贴士:树的度数指的是一个节点的子节点个数

我们以一颗最大度数为 5 的 BTree 为例(每个节点最多存储 4 个 key,5 个指针)为例

以根节点为例,一个节点最多存储四个 key (20、30、62、89),四个 key 对应五个指针,这五个指针分别指向五个子节点


以下是 BTree 数据查找的逻辑(以根节点为例):

  • 如果数据小于 20 ,走第一个指针
  • 如果数据大于等于 20 且小于 30 ,走第二个指针
  • 如果数据大于等于 30 且小于 62 ,走第三个指针
  • 如果数据大于等于 62 且小于 89 ,走第四个指针

接下来为大家演示一下 BTree 的动态构建过程,以及 BTree 是如何分裂的

我们先打开一个数据结构可视化网站(数据结构可视化),找到 BTree ,选择最大度数为 5 ,以 100 65 169 368 900 556 780 35 215 1200 234 888 158 90 1000 88 120 268 250 这组数据为例,观察数据插入过程中 BTree 的变化

最终构建出来的 BTree 如下

3.4 B+Tree

B+Tree 实际上是 BTree 的变种,我们以一颗最大度数为 4 的 B+Tree 为例来分析一下 B+Tree

通过观察上述结构图,可以发现 B+Tree 与 BTree 是比较像的,但是和 BTree 也有比较大的区别

区别在哪呢,大家可以看到,在 B+Tree 中:

所有元素都会出现在叶子结点,非叶子节点只起到索引的作用,叶子节点才是存放数据的

所有的叶子节点形成了一个单向链表,每一个节点都会通过一个指针指向下一个节点


接下来我们以图形化的方式来演示一下 B+Tree 的演变过程(数据结构可视化网站:数据结构可视化),还是以 100 65 169 368 900 556 780 35 215 1200 234 888 158 90 1000 88 120 268 250 这组数据为例,选择 5 阶的 B+Tree

最终构建出来的 B+Tree 如下

3.5 MySQL 中的 B+Tree

MySQL 对经典的 B+Tree 进行了优化,在 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,叶子节点之间形成了一个双向链表,提高了区间访问(范围搜索、排序)的性能

可以看到,每一个节点都是存储在数据块(也被称为页)中的

大家可以回想一下,我们在讲解 InnoDB 存储引擎的时候,InnoDB 的逻辑结构(表空间、段、区、页、行)

在 InnoDB 存储引擎中,一个页的默认大小为 16 K

3.6 Hash

哈希索引就是采用一定 Hash 算法,将键值换算成新的 Hash 值,映射到对应的槽位上,然后存储在 Hash 表中

假如现在有一张 user 表,包含 id、name、age 字段(其中 id 字段为主键),我们为 name 字段创建一个 Hash 索引的数据结构,具体做法如下:

  • 算出表中每一行数据的 Hash 值
  • 拿到 name 字段的所有值,针对 name 字段的所有值,通过内部的 Hash 函数计算每一个 name 值该落在哪一个哈希表的槽位上

如果两个(或多个)键值,映射到一个相同的槽位上,他们就会产生 Hash 冲突(也称为 Hash 碰撞),可以通过链表来解决


Hash 索引的特点:

  1. 只能用于对等比较( =,in),不支持范围查询( between,>,<)
  2. 无法利用索引完成排序操作
  3. 查询效率高,通常只需要一次检索(效率通常要高于 B+tree 索引)

在 MySQL 中,支持 Hash 索引的是 Memory 引擎,其它存储引擎暂不支持,但 InnoDB 存储引擎中有一个自适应 Hash 功能,MySQL 会根据我们的查询条件,在指定的情况下自动地将 B+Tree 索引构建为 Hash 索引

3.7 思考题:为什么 InnoDB 存储引擎选择使用 B+Tree 索引结构

  • 相对于二叉树来说,B+Tree 的层级更少,搜索效率更高
  • 相对于 BTree 来说,BTree 无论是叶子节点还是非叶子节点,都会保存数据,这样会导致一页中存储的键值减少,指针也会跟着减少,要保存同样的大量数据,只能增加树的高度,导致搜索性能降低
  • 相对 Hash 索引来说,B+Tree 索引结构支持范围匹配和排序操作

4. 索引的分类

索引主要有以下四类:

分类 含义 特点 关键字
主键索引 针对于表中主键字段创建的索引 默认自动创建,只能有一个 PRIMARY
唯一索引 避免同一个表中某个字段的值重复 可以有多个 UNIQUE
常规索引 快速定位特定数据 可以有多个
全文索引 全文索引查找的是文本中的关键词,而不是比较索引中的值 可以有多个 FULLTEXT

在 InnoDB 存储引擎中,根据索引的存储形式,又可以分为以下两种:

分类 含义 特点
聚集索引(Clustered Index) 将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据 必须有,而且只能有一个
二级索引(Secondary Index) 将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键 可以存在多个

聚集索引选取规则:

  1. 如果存在主键,主键索引就是聚集索引
  2. 如果不存在主键,将使用第一个唯一(UNIQUE)索引作为聚集索引
  3. 如果表没有主键,也没有合适的唯一索引,则 InnoDB 会自动生成一个 row_id 作为隐藏的聚集索引

大家可能还是不知道聚集索引和二级索引的具体结构,接下来为大家演示一下

还是以 user 表为例

我们再来看一下,如果我们要查询数据,具体流程是怎么样的

假设现在我们执行以下 SQL 语句(假设 name 字段是根据字典序排列)

sql 复制代码
select * from user where name = 'Arm'

因为我们是根据 name 字段查询数据,不能够直接走 id 字段对应的聚集索引,要先走 name 字段对应的二级索引

根据 name 字段找到数据后,发现索引中只包含了 name 字段的信息,但是 SQL 语句要求包含的是全部字段的信息,所以只能根据二级索引中关联的主键值 10 去聚集索引中再次查找目标数据

以上查找过程对应着一个专业术语------回表查询,回表索引指的是先走二级索引,找到对应的主键值,再根据主键值到聚集索引中拿到对应某一行的数据


思考题:以下两个 SQL 语句,哪一个 SQL 语句的执行效率更高,为什么?(备注:id 字段为主键,针对 name 字段创建了索引)

sql 复制代码
select * from user where id= 10;
sql 复制代码
select * from user where name ='Arm';

5. 索引的语法

5.1 创建索引

备注:中括号 [] 表示可选选项

一个索引可以关联多个字段

  1. 如果一个索引只关联了一个字段,我们就称这种索引为单列索引
  2. 如果一个索引关联了多个字段,我们就称这种索引为联合索引(也叫组合索引)

5.2 查看索引

5.3 删除索引

5.4 练习-准备工作

我们先创建一个名为 blog 的数据库,再准备一张名为 tb_user 的表,建表语句如下

sql 复制代码
/*
 Navicat Premium Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 80034 (8.0.34)
 Source Host           : localhost:3306
 Source Schema         : 

 Target Server Type    : MySQL
 Target Server Version : 80034 (8.0.34)
 File Encoding         : 65001

 Date: 12/08/2024 
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for tb_user
-- ----------------------------
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user`  (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
  `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '手机号',
  `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱',
  `profession` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '专业',
  `age` tinyint UNSIGNED NULL DEFAULT NULL COMMENT '年龄',
  `gender` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '性别 , 1: 男, 2: 女',
  `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '状态',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 25 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统用户表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of tb_user
-- ----------------------------
INSERT INTO `tb_user` VALUES (1, '吕布', '17799990000', 'lvbu666@163.com', '软件工程', 23, '1', '6', '2001-02-02 00:00:00');
INSERT INTO `tb_user` VALUES (2, '曹操', '17799990001', 'caocao666@qq.com', '通讯工程', 33, '1', '0', '2001-03-05 00:00:00');
INSERT INTO `tb_user` VALUES (3, '赵云', '17799990002', '17799990@139.com', '英语', 34, '1', '2', '2002-03-02 00:00:00');
INSERT INTO `tb_user` VALUES (4, '孙悟空', '17799990003', '17799990@sina.com', '工程造价', 54, '1', '0', '2001-07-02 00:00:00');
INSERT INTO `tb_user` VALUES (5, '花木兰', '17799990004', '19980729@sina.com', '软件工程', 23, '2', '1', '2001-04-22 00:00:00');
INSERT INTO `tb_user` VALUES (6, '大乔', '17799990005', 'daqiao666@sina.com', '舞蹈', 22, '2', '0', '2001-02-07 00:00:00');
INSERT INTO `tb_user` VALUES (7, '露娜', '17799990006', 'luna_love@sina.com', '应用数学', 24, '2', '0', '2001-02-08 00:00:00');
INSERT INTO `tb_user` VALUES (8, '程咬金', '17799990007', 'chengyaojin@163.com', '化工', 38, '1', '5', '2001-05-23 00:00:00');
INSERT INTO `tb_user` VALUES (9, '项羽', '17799990008', 'xiaoyu666@qq.com', '金属材料', 43, '1', '0', '2001-09-18 00:00:00');
INSERT INTO `tb_user` VALUES (10, '白起', '17799990009', 'baiqi666@sina.com', '机械工程及其自动化', 27, '1', '2', '2001-08-16 00:00:00');
INSERT INTO `tb_user` VALUES (11, '韩信', '17799990010', 'hanxin520@163.com', '无机非金属材料工程', 27, '1', '0', '2001-06-12 00:00:00');
INSERT INTO `tb_user` VALUES (12, '荆轲', '17799990011', 'jingke123@163.com', '会计', 29, '1', '0', '2001-05-11 00:00:00');
INSERT INTO `tb_user` VALUES (13, '兰陵王', '17799990012', 'lanlinwang666@126.com', '工程造价', 44, '1', '1', '2001-04-09 00:00:00');
INSERT INTO `tb_user` VALUES (14, '狂铁', '17799990013', 'kuangtie@sina.com', '应用数学', 43, '1', '2', '2001-04-10 00:00:00');
INSERT INTO `tb_user` VALUES (15, '貂蝉', '17799990014', '84958948374@qq.com', '软件工程', 40, '2', '3', '2001-02-12 00:00:00');
INSERT INTO `tb_user` VALUES (16, '妲己', '17799990015', '2783238293@qq.com', '软件工程', 31, '2', '0', '2001-01-30 00:00:00');
INSERT INTO `tb_user` VALUES (17, '芈月', '17799990016', 'xiaomin2001@sina.com', '工业经济', 35, '2', '0', '2000-05-03 00:00:00');
INSERT INTO `tb_user` VALUES (18, '嬴政', '17799990017', '8839434342@qq.com', '化工', 38, '1', '1', '2001-08-08 00:00:00');
INSERT INTO `tb_user` VALUES (19, '狄仁杰', '17799990018', 'jujiamlm8166@163.com', '国际贸易', 30, '1', '0', '2007-03-12 00:00:00');
INSERT INTO `tb_user` VALUES (20, '安琪拉', '17799990019', 'jdodm1h@126.com', '城市规划', 51, '2', '0', '2001-08-15 00:00:00');
INSERT INTO `tb_user` VALUES (21, '典韦', '17799990020', 'ycaunanjian@163.com', '城市规划', 52, '1', '2', '2000-04-12 00:00:00');
INSERT INTO `tb_user` VALUES (22, '廉颇', '17799990021', 'lianpo321@126.com', '土木工程', 19, '1', '3', '2002-07-18 00:00:00');
INSERT INTO `tb_user` VALUES (23, '后羿', '17799990022', 'altycj2000@139.com', '城市园林', 20, '1', '0', '2002-03-10 00:00:00');
INSERT INTO `tb_user` VALUES (24, '姜子牙', '17799990023', '37483844@qq.com', '工程造价', 29, '1', '4', '2003-05-26 00:00:00');

SET FOREIGN_KEY_CHECKS = 1;

先查看一下 tb_user 表的结构

5.5 练习-实现需求

在实现需求前,我们先查看 tb_user 表当前有哪些索引

sql 复制代码
show index from tb_user;

可以看到,tb_user 表目前只有一个主键索引(id 字段是主键),索引的数据结构类型为 BTree (虽然表面上显示的是 Btree ,但实际上是 B+Tree ,因为 B+Tree 是 BTree 的变种)


索引名称的规范:index_表名_字段名

5.5.1 需求一

name 字段为姓名字段,该字段的值可能会重复,为该字段创建索引

sql 复制代码
create index index_user_name on tb_user (name);

创建成功后再次查看 tb_user 表的索引

可以看到,即使我们没有指定索引要使用哪种数据结构,新创建的 index_user_name 索引的数据结构仍为 BTree(B+Tree),因为当前 MySQL 使用的是 InnoDB 存储引擎,InnoDB 引擎默认使用的索引数据结构为 BTree(B+Tree)

5.5.2 需求二

phone 手机号字段的值,是非空且唯一的,为该字段创建唯一索引

sql 复制代码
create unique index index_user_phone on tb_user (phone);

创建成功后再次查看 tb_user 表的索引

5.5.3 需求三

为 profession、age、status 三个字段创建联合索引

sql 复制代码
create index index_user_profession_age_status on tb_user (profession, age, status);

创建成功后再次查看 tb_user 表的索引

我们可以看到,为 profession、age、status 三个字段创建联合索引后,表的记录一下添加了三条,并且表中的 Seq_in_index 属性记录了联合索引中字段的顺序

需要注意的是,创建联合索引的时候,字段的顺序是有讲究的,具体可参考本文的最左前缀法则章节

5.5.4 需求四

为 email 字段建立合适的索引,提升查询效率

sql 复制代码
create index index_user_email on tb_user (email);

创建成功后再次查看 tb_user 表的索引

5.5.5 需求五

删除为 email 字段创建的索引

sql 复制代码
drop index index_user_email on tb_user;

删除成功后再次查看 tb_user 表的索引

6. 索引-性能分析工具

  • 在优化 SQL 语句时,主要优化的是 select 语句(查询语句)
  • 在优化 select 语句的时候,索引的优化占据了主导地位

6.1 查看 SQL 语句的执行频率

通过以下 SQL 语句可以查看增删查改语句在某个数据库中所占的比例

sql 复制代码
show global status like 'Com_______';

如果某个数据库的 SQL 语句以 select 语句为主,那我们就需要针对 SQL 语句进行优化


下面以 blog 数据库为例,为大家演示如何查看数据库中 SQL 的执行频率

我们再次执行两次查询语句

sql 复制代码
select * from user;

可以看到 select 语句的查询次数增加了两次

6.2 慢查询日志

慢查询日志记录了所有执行时间超过了指定参数(long_query_time,单位:秒,默认10秒)的所有 SQL 语句的日志

可以运行以下指令查看 MySQL 是否开启了慢查询日志

sql 复制代码
show variables like 'slow_query_log';

MySQL 默认没有开启慢查询日志,需要在 MySQL 的配置文件 /etc/my.cnf 中配置如下信息

shell 复制代码
sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf

按下 G 键(大写)后到达配置文件末尾,再按下 i 进入编辑模式,将下述内容复制到文件中后保存并退出

properties 复制代码
# 开启MySQL慢日志查询开关
slow_query_log=1
# 设置慢日志的时间为2秒,SQL语句执行时间超过2秒,就会视为慢查询,记录慢查询日志
long_query_time=2

当然,还有另一种配置方式,就是直接在 MySQL 的控制台输入以下信息

mysql 复制代码
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;

配置完毕之后,重新启动 MySQL 服务器

shell 复制代码
sudo systemctl restart mysql

重启 MySQL 之后,可以发现慢查询日志已经开启了


慢查询日志一般存放在 /var/lib/mysql 目录下,但该目录普通用户是没有权限访问的,先运行以下指令开放 /var/lib/mysql 目录的部分权限

shell 复制代码
sudo chmod +rx -R /var/lib/mysql

接着进入 /var/lib/mysql 目录

shell 复制代码
cd /var/lib/mysql

找到慢查询日志文件

shell 复制代码
ls -l | grep slow

查看慢查询日志文件,可以发现文件仅包含了一些简单信息(版本、端口号等)

6.3 show profiles

慢查询日志文件记录的是执行时间超过指定时间的 SQL 语句,比如我们指定的时间为 2 秒,那么只有执行时间超过 2 秒的 SQL 语句才会被记录在日志文件中

假如有些 SQL 的耗时为 1.7 秒、1.9 秒、1.95 秒等,那这些 SQL 语句是不会记录在慢查询日志文件中的

在系统中,如果有一些 SQL 语句的业务逻辑很简单,但耗时却接近两秒,那这些 SQL 的性能相对来说也是比较低的,我们也需要对这些 SQL 语句进行优化,那我们怎么定位到这些 SQL 呢,慢查询日志满足不了我们的要求,此时我们需要借助另一条指令------show profiles


在做 SQL优化时,show profiles 指令能够帮助我们了解 SQL 语句的时间都耗费到哪里去了

通过 have_profiling 参数,能够看到当前 MySQL 是否支持 profile 操作

sql 复制代码
SELECT @@have_profiling;

如果 @@have_profiling 属性为 NO ,可以通过以下语句将 profiling 属性设置为 YES

sql 复制代码
set profiling=1;

将 profiling 属性设置为 YES 时会有一个警告

警告的大概意思就是:profiling 属性已经过时,在将来的某个版本中, MySQL 会移除这个属性


接下来,我们将执行一系列的业务 SQL 的操作,然后通过如下指令查看 SQL 语句的耗时情况

sql 复制代码
select * from tb_user;
select * from tb_user where id = 1;
select * from tb_user where name = '白起';

6.3.1 查看每一条指令的基本耗时情况

sql 复制代码
show profiles;

6.3.2 查看指定 query_id 的 SQL 语句各个阶段的耗时情况

sql 复制代码
show profile for query 4;

但我们开发人员很少关注这些信息,了解即可

6.3.3 查看指定 query_id 的 SQL 语句耗费 CPU 资源的情况

查看 SQL 语句的每一个阶段耗费 CPU 资源的情况大概是怎样的(了解即可)

sql 复制代码
show profile cpu for query 4;

6.4 explain

通过执行时间的长短来判定 SQL 语句的性能是粗略的评判方式,并不能真正地评判 SQL 语句的性能,我们要想真正地查看 SQL 语句的性能,需要借助另一个指令------ explain

explain 指令在 SQL 语句的优化中占据着非常重要的地位,我们经常通过它来判定 SQL 语句的执行性能,通过 explain 指令可以查看以下内容:

  1. SQL 语句的执行计划
  2. 执行过程中是否用到了索引
  3. 表的连接情况、表的连接顺序

explain 指令的语法:直接在 select 语句前面加上 explain 关键字即可

sql 复制代码
explain select * from tb_user where id = 1;

6.5 explain 执行计划中各字段的含义

6.5.1 id

6.5.1.1 含义

id 字段指的是 select 查询的序列号,表示本次查询中执行 select 子句或者是操作表的顺序

  • 若 id 相同,执行顺序为从上到下
  • 若 id 不同,id 值越大,对应的 SQL 语句越先执行
6.5.1.2 准备工作

为了方便大家更好地理解 id 字段的含义,准备了三张表(course、student、student_course),建表的 SQL 语句如下

course.sql

sql 复制代码
/*
 Navicat Premium Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 80034 (8.0.34)
 Source Host           : localhost:3306
 Source Schema         : itheima

 Target Server Type    : MySQL
 Target Server Version : 80034 (8.0.34)
 File Encoding         : 65001

 Date: 12/08/2024 22:31:10
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for course
-- ----------------------------
DROP TABLE IF EXISTS `course`;
CREATE TABLE `course`  (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `name` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '课程名称',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '课程表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of course
-- ----------------------------
INSERT INTO `course` VALUES (1, 'Java');
INSERT INTO `course` VALUES (2, 'PHP');
INSERT INTO `course` VALUES (3, 'MySQL');
INSERT INTO `course` VALUES (4, 'Hadoop');

SET FOREIGN_KEY_CHECKS = 1;

student.sql

sql 复制代码
/*
 Navicat Premium Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 80034 (8.0.34)
 Source Host           : localhost:3306
 Source Schema         : itheima

 Target Server Type    : MySQL
 Target Server Version : 80034 (8.0.34)
 File Encoding         : 65001

 Date: 12/08/2024 22:31:17
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for student
-- ----------------------------
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student`  (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `name` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '姓名',
  `no` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '学号',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '学生表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES (1, '黛绮丝', '2000100101');
INSERT INTO `student` VALUES (2, '谢逊', '2000100102');
INSERT INTO `student` VALUES (3, '殷天正', '2000100103');
INSERT INTO `student` VALUES (4, '韦一笑', '2000100104');

SET FOREIGN_KEY_CHECKS = 1;

student_course.sql

sql 复制代码
/*
 Navicat Premium Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 80034 (8.0.34)
 Source Host           : localhost:3306
 Source Schema         : itheima

 Target Server Type    : MySQL
 Target Server Version : 80034 (8.0.34)
 File Encoding         : 65001

 Date: 12/08/2024 22:31:24
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for student_course
-- ----------------------------
DROP TABLE IF EXISTS `student_course`;
CREATE TABLE `student_course`  (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
  `student_id` int NOT NULL COMMENT '学生ID',
  `course_id` int NOT NULL COMMENT '课程ID',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `fk_courseId`(`course_id` ASC) USING BTREE,
  INDEX `fk_studentId`(`student_id` ASC) USING BTREE,
  CONSTRAINT `fk_courseId` FOREIGN KEY (`course_id`) REFERENCES `course` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT `fk_studentId` FOREIGN KEY (`student_id`) REFERENCES `student` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '学生课程中间表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of student_course
-- ----------------------------
INSERT INTO `student_course` VALUES (1, 1, 1);
INSERT INTO `student_course` VALUES (2, 1, 2);
INSERT INTO `student_course` VALUES (3, 1, 3);
INSERT INTO `student_course` VALUES (4, 2, 2);
INSERT INTO `student_course` VALUES (5, 2, 3);
INSERT INTO `student_course` VALUES (6, 3, 4);

SET FOREIGN_KEY_CHECKS = 1;

三张表的结构如下图

一个学生可以选修多个课程,一个课程可以被多个学生选修,两张表(student 表和 course 表)是多对多的关系,所以需要用一张中间表(student_course 表)来维护两张表(student 表和 course 表)的关系

6.5.1.3 需求一

需求一:查询出所有学生的选修课程情况

sql 复制代码
select student.*, course.*
from student,
     course,
     student_course
where student.id = student_course.student_id
  and course.id = student_course.course_id;

我们再用 explain 关键字查看 SQL 语句的执行情况

sql 复制代码
explain
select student.*, course.*
from student,
     course,
     student_course
where student.id = student_course.student_id
  and course.id = student_course.course_id;

有三条记录,我们重点关注 id 字段,可以看到三条记录的 id 字段都是 1 ,并不是自增的,这种情况表的执行顺序是从上往下,也就意味着 student 表会优先执行,接着是 student_course 表,最后是 course 表

为什么会这样执行呢,因为 student 表和 course 表并没有直接的关联,两张表之间产生关系需要通过 student_course 这张中间表

6.5.1.4 需求二

需求二:查询出哪些学生的选修了 MySQL 课程(要求使用子查询

sql 复制代码
select student.*
from student
where student.id in
      (select student_id
       from student_course
       where course_id = (select id from course where name = 'MySQL'));

我们再用 explain 关键字查看 SQL 语句的执行情况

sql 复制代码
explain
select student.*
from student
where student.id in
      (select student_id
       from student_course
       where course_id = (select id from course where name = 'MySQL'));

可以看到,优先执行的是最内层的 SQL 语句,也就是 course 表,接着执行的是 student_course 表 ,最后执行的是 student 表

6.5.2 select_type

表示 SELECT 的类型,常见的取值有(参考意义不大,了解即可):

  • SIMPLE:简单表,即不使用表连接或者子查询
  • PRIMARY:主查询,即外层的查询
  • UNION:UNION中的第二个或者后面的查询语句
  • SUBQUERY:SELECT 或 WHERE 之后包含子查询等

6.5.3 type(重点关注)

type 代表的是访问类型(连接类型),是一个相对来说比较重要的指标

各个连接类型按性能由好到差排列如下:

  1. NULL:不需要访问任何表
  2. system:访问系统表时访问类型一般为 system
  3. const:根据主键索引或唯一索引查找数据时,访问类型一般为 const
  4. eq_ref
  5. ref:使用非唯一性索引进行查询时,访问类型一般为 ref
  6. range
  7. index:用了索引,但也会对索引进行扫描,遍历整个索引树,性能很低
  8. ALL:当访问类型为 ALL 时,一般是进行了全表扫描,性能最低,需要避免

我们优化 SQL 的时候,尽量往前面几个访问类型上靠


注意:业务系统中 SQL 语句的访问类型一般达不到 NULL ,因为访问类型为 NULL 的 SQL 语句不需要访问任何表


示例

sql 复制代码
explain select * from tb_user where id = 1;
sql 复制代码
explain select * from tb_user where phone = '17799990021';
sql 复制代码
explain select * from tb_user where name = '典韦';

6.5.4 possible_key(重点关注)

possible_key 代表的是可能应用在这张表上的索引(一个或多个)

6.5.5 key(重点关注)

实际使用的索引,如果该字段为 NULL ,代表没有使用索引

6.5.6 key_len

表示索引中使用的字节数,该值为索引字段的最大可能长度,并非实际使用长度,在不损失精确性的前提下,长度越短越好

6.5.7 rows

MySQL 认为必须要执行查询的行数,在使用了 InnoDB 存储引擎的表中,该字段是一个估计值,可能并不总是准确的

6.5.8 filtered

表示返回结果的行数占需读取行数的百分比,filtered 的值越大越好

6.5.9 Extra

Extra 的具体含义参考本文的 覆盖索引与回表查询 章节

7. 索引的使用规则

7.1 验证索引的效率

验证索引效率需要准备的东西比较多(一张具有 1000 万数据的表,该表数据完全导出后的 SQL 文件大小为 4.23 G,想要具体 SQL 文件的可以私聊我),就不演示了,直接通过视频查看索引的效率:验证索引效率


需要注意的是,当一张表的数据量很大时,为表的某一个字段构建索引需要耗费较长时间

创建索引前查询语句的耗时(20.79 秒)

创建索引耗时(11.20 秒)

创建索引后查询语句的耗时(0.01 秒)

通过对比可以发现,索引确实提升了查询语句的效率,而且提升的效率不是一点点(创建索引前后查询语句的耗时差了几个数量级)

7.2 最左前缀法则

最左前缀法则主要针对的是联合索引,如果某个索引使用了表中的多个字段(多个列),要遵守最左前缀法则

  • 最左前缀法则指的是查询需要从索引的最左边的列开始,并且不能跳过索引中间的列
  • 如果跳过了某一列,索引将部分失效(后面的字段索引将会失效)

我们先来查看一下当前 tb_user 表当前有哪些索引

sql 复制代码
show index from tb_user;

从中可以看出 profession 是联合索引中的第一个字段,age 是联合索引的第二个字段,status 是联合索引的第三个字段


接下来通过一组案例来为大家演示一下最左前缀法则,重点观察 explain 执行计划中 type 字段 和 key_len 字段,判断 SQL 语句是否使用了索引,以及使用了索引的哪些字段

第一个案例(使用了 profession、age、status 字段,字段顺序为 profession、age、status )

sql 复制代码
explain
select *
from tb_user
where profession = '软件工程'
  and age = 31
  and status = '0';

第二个案例(使用了 profession、age 字段,字段顺序为 profession、age )

sql 复制代码
explain
select *
from tb_user
where profession = '软件工程'
  and age = 31;

第三个案例(使用了 profession字段,字段顺序为 profession )

sql 复制代码
explain
select *
from tb_user
where profession = '软件工程';

通过前三个案例可以看出:索引中 profession 字段的长度为 47 ,age 字段的长度为 2,status 字段的长度为 5


第四个案例(使用了 age、status 字段,字段顺序为 age、status )

sql 复制代码
explain
select *
from tb_user
where age = 31
  and status = '0';

第五个案例(使用了 status 字段,字段顺序为 status )

sql 复制代码
explain
select *
from tb_user
where status = '0';

第六个案例(使用了 profession、status 字段,字段顺序为 profession、status )

sql 复制代码
explain
select *
from tb_user
where profession = '软件工程'
  and status = '0';

接下来我们再看一个特殊的案例:(使用了 profession、age、status 字段,字段顺序为 age、status、profession )

sql 复制代码
explain
select *
from tb_user
where age = 31
  and profession = '软件工程'
  and status = '0';

可以看到使用了索引,而且索引的长度为 54 ,说明索引的三个字段都用上了,说明最左前缀法则的规则是索引中前面的字段必须存在,但与字段的出现顺序无关

7.3 范围查询

在联合索引中,如果出现开区间的范围查询( >< ),范围查询右侧的列索引将会失效

我们用 explain 语句查看以下两个 SQL 语句的执行计划

第一个 SQL 语句(age 字段使用的是大于号)

sql 复制代码
explain
select *
from tb_user
where profession = '软件工程'
  and age > 31
  and status = '0';

第二个 SQL 语句(age 字段使用的是大于等于号)

sql 复制代码
explain
select *
from tb_user
where profession = '软件工程'
  and age >= 31
  and status = '0';

对比可以发现,第一个 SQL 语句的执行计划中 key_len 的长度为 49 ,第二个 SQL 语句的执行计划中 key_len 的长度为 49,说明使用大于号的范围查询后面的列索引失效了,但使用大于等于号的范围查询后面的列索引没有失效

所以说,在业务运行的情况下,范围查询尽量使用大于等于号(或者是小于等于号)

7.4 索引失效的情况

除了最左前缀法则和范围查询中索引失效的情况外,还有一些情况索引会失效

7.4.1 对索引列进行函数运算

不要在索引列上进行函数运算操作,否则索引将会失效

sql 复制代码
explain
select *
from tb_user
where substring(phone, 10, 2) = '15';

可以看到,执行计划中的 type 字段为 ALL ,说明索引失效了

7.4.2 字符串不加单引号

根据字符串类型的字段查找数据时,如果不加引号,索引将失效

案例一:根据 phone 字段查找数据(phone 字段为 varchar 类型,但查找时提供的是 integer )

sql 复制代码
explain
select *
from tb_user
where phone = 17799990015;

可以看到,执行计划中的 type 字段为 ALL ,说明索引失效了(执行计划中显示可能会用到 index_user_phone 索引,但实际上没有用到)


案例二:根据 profession、age、status 字段查找数据(字段顺序为 profession、age、status ,status 字段的类型为 char ,但查找时提供的是 integer )

sql 复制代码
explain
select *
from tb_user
where profession = '软件工程'
  and age >= 31
  and status = 0;

可以看到,key_len 的大小为 49 ,说明索引中的 profession 字段和 age 字段都生效了,但是 status 字段没生效

7.4.3 头部模糊匹配

如果仅仅是尾部模糊匹配,索引不会失效,但如果是头部模糊匹配,索引将会失效,在大数据量的情况下,一定要规避头部模糊匹配

我们通过两个 SQL 语句来演示头部模糊匹配导致索引失效的情况

sql 复制代码
explain
select *
from tb_user
where profession like '软件%';

sql 复制代码
explain
select *
from tb_user
where profession like '%软件';

7.4.4 用 or 连接条件

用 or 连接条件,如果用 or 连接的条件中某些列有索引,而某些列没有索引,那么涉及的索引都不会被用到

我们通过两个 SQL 语句来演示用 or 连接条件导致索引失效的情况

sql 复制代码
explain
select *
from tb_user
where age = 27
  or id = 10;
sql 复制代码
explain
select *
from tb_user
where phone = '17799990009'
   or age = 27;

由于 age 字段没有索引,所以即使 id 字段、phone 字段有索引,索引也会失效,如果想让索引生效,需要针对于 age 字段也要建立一个索引


需要注意的是,即使针对多个字段建立了联合索引,但是 or 连接的某些条件使用了联合索引的某些字段,但没有为具体的某个字段建立索引,索引也会失效(即使符合最左前缀法则)

sql 复制代码
explain
select *
from tb_user
where profession = '软件工程' and age = 31
   or status = '0';

7.4.5 数据分布影响

如果 MySQL 评估使用索引比全表扫描更慢,则不使用索引

我们先查看 tb_user 表中的所有数据

sql 复制代码
select * from tb_user;

通过以下五个 SQL 语句演示在数据分布影响的情况下索引失效的情况

sql 复制代码
explain
select *
from tb_user
where phone >= '17799990000';

sql 复制代码
explain
select *
from tb_user
where phone >= '17799990010';

sql 复制代码
explain
select *
from tb_user
where phone >= '17799990013';

sql 复制代码
explain
select *
from tb_user
where profession is null;

sql 复制代码
explain
select *
from tb_user
where profession is not null;

7.5 SQL 提示

SQL提示,是优化数据库的一个重要手段,简单来说,就是在 SQL 语句中加入一些人为的提示来达到优化操作的目的

SQL提示有三种方式:

use index:建议 MySQL 使用某个索引(),具体使用哪个索引还是由 MySQL 评定

ignore index:提示 MySQL 忽略某个索引(),具体使用哪个索引还是由 MySQL 评定

force index:强制 MySQL 使用某个索引()


上面的描述可能不太好理解,我们通过以下案例来理解 SQL 提示

我们知道,tb_user 表中已经针对 profession、age、status 字段创建了一个联合索引 index_profession_age_status ,如果我们针对 profession 字段单独创建一个索引,那 MySQL 会选用哪一个索引呢

我们先为 profession 字段单独创建一个索引

sql 复制代码
create index index_profession on tb_user (profession);

接着运行两次以下 SQL 语句,查看MySQL 会选用哪一个索引

sql 复制代码
explain
select *
from tb_user
where profession = '软件工程';

可以看到,可能用到的索引有 index_profession_age_status 和 index_profession ,但 MySQL 实际使用的索引是 index_profession

那我们可不可以指定让 MySQL 使用 index_profession 索引呢,当然可以,我们可以在 SQL 语句前加上 use index 指令,建议 MySQL 用我们指定的索引

sql 复制代码
explain
select *
from tb_user
         use index (index_profession)
where profession = '软件工程';

可以看到,MySQL 接受了我们的建议

ignore index 指令和 force index 的使用情况同理

sql 复制代码
explain
select *
from tb_user
         ignore index (index_profession)
where profession = '软件工程';
sql 复制代码
explain
select *
from tb_user
         force index (index_profession)
where profession = '软件工程';

7.6 覆盖索引与回表查询

尽量使用覆盖索引(查询使用了索引,并且需要返回的列在该索引中已经全部能够找到),避免使用 select * 语句

为了方便大家更好地理解覆盖索引,我们先做一个测试

测试前,先把之前创建的 index_profession 索引去掉

sql 复制代码
drop index index_profession on tb_user;

运行以下三条 SQL 语句

sql 复制代码
explain
select id, profession, age, status
from tb_user
where profession = '软件工程'
  and age = 31
  and status = '0';

sql 复制代码
explain
select id, profession, age, status, name
from tb_user
where profession = '软件工程'
  and age = 31
  and status = '0';

sql 复制代码
explain
select *
from tb_user
where profession = '软件工程'
  and age = 31
  and status = '0';

从 explain 执行计划的前几个字段看不出什么差别,这个时候就需要关注 Extra 字段了

Extra 字段的取值与含义(不同版本的 MySQL 取值可能有所不同):

  • Using index condition:查找使用了索引,但是需要回表查询数据
  • Using where、Using index:查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表查询数据

下面有一个示例图,方便大家理解覆盖索引与回表查询

我们分别为 id 字段(同时也是主键)和 name 字段建立了索引,其中 id 字段是主键索引,name 字段是唯一索引

聚集索引中存的是整一行数据,而二级索引存的是 id 值,更准确地来说,是主键

第一条 SQL 语句和第二条 SQL 语句都使用了覆盖索引,因为这两条 SQL 语句要查询的字段在索引中都能找到,而第三条 SQL 语句由于索引中没有 name 字段,所以需要回表查询才能够获取 name 字段的数据

7.7 前缀索引

当字段类型为字符串(varchar,text 等)时,有时候需要为很长的字符串建立索引,这会让索引占用的磁盘空间变得很大,导致查询时浪费大量的磁盘 IO 资源,影响查询效率

此时可以只根据字符串的一部分前缀建立索引,这样可以大大节约索引空间,从而提高索引效率


创建前缀索引的语法:create index index_name on table_name(column(n));,其中 n 为前缀的长度

那前缀长度设置为多少比较合适呢

  • 可以根据索引的选择性来决定,选择性是指不重复的索引值(基数)和数据表的记录总数的比值,索引选择性越高则查询效率越高
  • 唯一索引的选择性是 1,这是最好的索引选择性,性能也是最好的

以 tb_user 表为例,email 字段的选择性为 1 ,具体可以用以下 SQL 语句查看

sql 复制代码
select count(distinct (email)) / count(*)
from tb_user;

那如果我想根据 email 字段建立一个前缀索引,减少索引占用的磁盘空间,该怎么确定前缀的长度呢

可以用以下 SQL 语句不断截取 email 字段的前 n 个字符,不断测试前缀的测试性,在选择性与占用的磁盘空间之间做出平衡

注意:MySQL 中的 substring 函数的下标是从 1 开始的

sql 复制代码
select count(distinct (substring(email, 1, 4))) / count(*)
from tb_user;

经过测试,为 email 字段建立前缀为 5 的前缀索引是比较好的选择

sql 复制代码
create index index_email on tb_user (email(5));

创建索引后也可以看到前缀的长度

sql 复制代码
show index from tb_user;

执行 SQL 语句,发现也使用了新创建的前缀索引

sql 复制代码
explain
select *
from tb_user
where email = 'daqiao666@sina.com';

我们再来了解一下前缀索引的执行流程

需要注意的是,前缀索引在找到符合前缀的数据时,并不会立即返回,而是会继续比对除了前缀后面的的数据(因为辅助索引只是根据前五个字符查找,不能保证结果的准确性)

如果与目标值不同,会通过叶子结点间的双向链表找到下一个叶子节点,直到找到与目标值相同的数据才返回,如果没有找到目标值或者下一个叶子节点的前缀已经不符合规则,则返回空

7.8 单列索引与联合索引

  • 单列索引即一个索引只包含单个列
  • 联合索引即一个索引包含了多个列

在业务场景中,如果存在多个查询条件,考虑针对于查询字段建立索引时,建议建立联合索引(并且联合索引中字段的顺序是有讲究的,具体参考本文的 最左前缀法则 章节),而非单列索引


先来看看单表索引的情况

phone 字段和 name 字段都分别建立了单列索引,我们根据 phone 字段和 name 字段查找数据

sql 复制代码
explain
select id, phone, name
from tb_user
where phone = '17799990010'
  and name = '韩信';

可以看到,可能使用的索引有 index_user_phone 和 index_user_name ,但实际上只用到了 index_user_phone 索引,

因为没有使用 name 字段的索引,所以会涉及到回表查询(Extra 字段为 NULL)


我们再为 phone 字段和 name 字段建立一个联合索引

sql 复制代码
create unique index index_phone_name
    on tb_user (phone, name);

再次执行一样的 SQL 语句

可以看到,MySQL 依然选择了使用单列索引,但这不是我们想看到的,我们可以建议 MySQL 使用我们刚创建的联合索引

sql 复制代码
explain
select id, phone, name
from tb_user
         use index (index_phone_name)
where phone = '17799990010'
  and name = '韩信';

可以看到,MySQL 接受了我们的建议,key_len 的长度也变成了 248 ,Extract 字段为 Using index ,没有回表查询


联合索引的情况大致如下

8. 索引的设计原则

  1. 针对于数据量较大(如何定义大数据量,需要根据具体的业务场景,通常是 100 万以上),且查询比较频繁的表建立索引
  2. 针对常用于排序操作(order by)和分组操作(group by)、作为查询条件(where)的字段建立索引
  3. 选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,索引的效率越高
  4. 如果是字符串类型的字段,且字段的长度较长,可以建立前缀索引
  5. 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,同时可以避免回表查询,提高查询效率
  6. 控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率
  7. 如果索引列不能存储 NULL 值,在创建表时使用 NOT NULL 约束,当优化器知道每列是否包含 NULL 值时,可以更好地确定哪个索引用于查询是最高效的
相关推荐
Ahern_11 分钟前
Oracle 普通表至分区表的分区交换
大数据·数据库·sql·oracle
夜半被帅醒30 分钟前
MySQL 数据库优化详解【Java数据库调优】
java·数据库·mysql
不爱学习的啊Biao44 分钟前
【13】MySQL如何选择合适的索引?
android·数据库·mysql
破 风1 小时前
SpringBoot 集成 MongoDB
数据库·mongodb
Rverdoser1 小时前
MySQL-MVCC(多版本并发控制)
数据库·mysql
醒了就刷牙1 小时前
黑马Java面试教程_P9_MySQL
java·mysql·面试
m0_748233641 小时前
SQL数组常用函数记录(Map篇)
java·数据库·sql
dowhileprogramming1 小时前
Python 中的迭代器
linux·数据库·python
0zxm2 小时前
08 Django - Django媒体文件&静态文件&文件上传
数据库·后端·python·django·sqlite
橘子师兄4 小时前
如何在自己的云服务器上部署mysql
运维·服务器·mysql