- 前言
- 数据结构
- HASH
- Binary Search Trees、AVL Trees
- Red/Black Trees
- B Trees
- B+ Trees
- 数据存储
- InnoDB
- MyISAM
- 索引优化
- 索引匹配方式
- 哈希索引
- 组合索引
- 聚簇、非聚簇索引
- 覆盖索引
- 优化细节(important)
- 数据库勿做计算
- 尽量主键查询
- 前缀索引
- 索引扫描排序
- 子查询
- 范围列查询
- 强制类型转换
- 建立索引
- JOIN 表
- LIMIT 限制输出行
- 单表索引数量
- 单表索引数量
- 避免错误概念
- 索引监控
- 总结
前言
在上篇博文:MySQL 性能调优及生产实战篇(一) 提到了数据建模方案及数据类型的优化方案,简要说明了一些索引的基本知识及分类、技术名词,该篇博文会从以下几点来对 MySQL 调优部分进行分析:
- 索引数据结构、优化细节
- 大数据量查询优化
- 海量数据解耦优化处理
数据结构
InnoDB、MyISAM 存储引擎底层索引使用的 B+ Tree,Memory 存储引擎使用的 hash
推荐一个数据结构可视化网站:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
HASH
hash 表存在于就是一个数组,然后在每一个数组下可以添加一个数据桶,以链表的方式进行实现。hash 表有对应的下标值,从 0 开始进行排列,若想要往 hash 表里放数据的话,必须要经过散列算法,然后计算出对应的位置,将数据放入到指定的位置中,而散列算法最简单的实现就是取模运算
hash 表方式看起来虽然很好,能够通过对应的下标位置定位到某一条记录,但是需要注意 hash 表同样也有明显的缺点
会占用大量的内存空间
,在每次使用 hash 表时需要将大量的数据加载到内存中,此时是非常浪费内存空间的,所以在 MySQL Memory 存储引擎中使用 hash 索引,像 InnoDB 这种存储引擎支持自适应 hash,但它是由 MySQL 自主控制的- 在进行数据查询时都是等值查询,首先通过 Key 计算出 hash 值,然后定位到某一个位置,进行 Key 比较,但是大部分公司用的都是基于范围查询,而 hash 表在进行范围查询时必须挨个匹配,这样查询会比较浪费时间,所以不太合适。最终可以得出结论:
若每次查询都是等值查询,那么 hash 表方式查询是是比较快的,若是基于范围查询,hash 方式查询是比较慢的
- 使用 hash 表存储数据时,需要设计比较优秀的 hash 算法,若算法设计不合理的话,会导致数据散列不均匀,浪费比较多的存储空间,同时在数据查询时会导致查询效率较低
基于以上缺点,在 MySQL InnoDB、MyISAM 存储引擎并没有使用 hash 表来存储数据,而 Memory 存储引擎使用了 hash 表这样的方式,注意索引的数据结构选择与存储引擎是息息相关的
Binary Search Trees、AVL Trees
基于二叉树会产生一条腿长一条腿短,这样的话很明显会编程挨个对比的过程,而当插入的数据越来越多时,会导致链表越长,这样的效率一定是比较低的,大于根节点值往右边塞 < 小于根节点值往左边塞 >
造成上述问题,最关键的原因:树的左右分支不够平衡,因此后续才有了二叉平衡树,即 AVL 树
,如下图:
AVL 树要求左子树、右子树之间的高度之差不能超过 1,因此在进行数据插入时会造成 N 个旋转操作来保持树的平衡
,所以在进行数据插入时效率比较低,查询的效率会比较快,这样的话可以理解为损失部分性能来满足查询性能的提升,但会引起插入、删除需求比较多时,如何解决呢?插入数据的越来越多,会造成树越来越深,从而会造成查询效率降低
Red/Black Trees
红黑树也基于二叉平衡树,只不过不是严格意义上的平衡树;在 AVL 树中,要求左右子树的高度之差不能超过 1,但红黑树的要求是最长子树只要不超过最短子树的两倍路径长度就好,如下图:
通过以上的分析,可以得出一个结论:无论是那种类型的二叉树,最终都会存在一个问题,随着数据量的增加,树的层数就会增加,那么就会造成 IO 次数越多,从而影响数据读取的效率
B Trees
B 树的数据结构特点如下:
- 所有键值分布在整棵树中
- 搜索有可能在非叶子节点结束(有可能在度为 0 或度为 1 的节点结束)在关键字全局内作一次查找,性能逼近于
二分查找
- 每个节点最多拥有 M 个子树,根节点至少有 2 个子树
- 分支节点至少拥有 M/2 颗子树(除根节点、叶子节点都是分支节点)
- 所有叶子节点都在同一层,每个叶子节点最多有 M-1 Key,并且以升序排列
在 MySQL 使用 B 树结构进行数据存储时,如下图:
每一个树节点都占用一个磁盘块,一个节点上有两个升序排序的关键词(如:16、34)
以及三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。两个关键词(16、34)划分成三个范围域对应三个指针所指向的子树数据范围域,以根节点 16、34
为例子,P1 指针指向的子树数据范围小于 16,P2 指针指向的子树数据范围为 16~34,P3 指针指向的子树数据范围大于 34
比如,要找出关键词等于 28,查找关键词过程如下:
- 通过根节点找到磁盘块 1,读入内存【磁盘 I/O 操纵第一次】
- 比较关键字 28 在区间(16~34)找到磁盘块 1 中的 P2 指针
- 通过 P2 指针找到磁盘块 3,读入内存【磁盘 I/O 操纵第二次】
- 比较关键字 28 在区间(25~31)找到磁盘块 3 中的 P2 指针
- 通过 P2 指针找到磁盘块 8,读入内存【磁盘 I/O 操纵第三次】
- 在磁盘块 8 中的关键词列表,找到关键字 28
B 树作为存储索引的数据结构,缺点如下:
- 每个节点都有 Key,同时也包含了 data,但每个页的存储空间是有限的,若 data 比较大的话会导致每个节点存储的 Key 数量变小
- 当存储数据量很大时会导致深度越大,同时就会增大查询时的
磁盘 IO 次数
,进而影响了查询性能 - InnoDB 存储引擎,默认情况下读取的是 16 KB,一共会读取三个磁盘块,意味着一共读取了 48 KB 数据,假设说上面这些 P 指针、节点 Key 都不需要占用额外的存储空间,一条数据占用 1 KB,那意味着当前节点里面最多存储 16 条数据,下一个磁盘块也是 16 条,第三个磁盘块也是 16 条,最终的总数也就是 4096 条,故而这个支撑的数据量太少了!
B+ Trees
B+ 树是在 B 树基础之上作的一种优化,如下:
- B+ 树每个节点可以包含更多的节点,这样做的原因是:为了降低树的高度、将数据范围变为多个区间,区间越多,数据检索越快
- 非叶子节点存储 Key,叶子节点存储 Key、数据
- 叶子节点之间通过指针相互连接在一起(符合磁盘的预读特性)顺序查询性能更高
在 B+ 树有两个头指针:一个指向根节点,另一个指向关键词最小的叶子节点,但所有叶子节点(数据节点)之间是一种链式环结构;因此可以对 B+ Tree 进行两种查找运算:
- 对于主键的范围查询、分页查询
- 从根节点开始,进行随机查找
叶子节点负责存储数据,非叶子节点不存储数据,能保证尽可能多的存储数据,查找数据的方式不变,可以进行计算一下三层的 B+ 树能存储多少数据
读取数据仍然是 16 KB,假设:P 指针、节点 Key 占用 10 字节,那么 16 KB = 16*1000/10,结果为 1600,第二层也是 1600,第三层还是 1600,最终的结果:40960000,可达到千万级别,而刚刚 B 树为 4096,完全不是量级的数据
数据存储
在上述中,是对 MySQL 索引结构的统一描述,但对于不同的存储引擎来说,虽然使用的都是基于 B+ Tree 数据结构,但在实际存储数据时是完全不一样的
InnoDB
在 InnoDB 存储引擎中,数据、索引是放在一起的,因此你看到的只有 idb 文件,其中既能存储实际的数据,又可以存储索引数据,因此当查询索引时,能够直接从叶子节点中获取需要的数据行,如下图:
[root@trench study]# cat /etc/my.cnf
# 找到 datadir 配置所在的文件目录
[root@trench study]# pwd
/var/lib/mysql/study
[root@trench study]# ll
total 1152
# 表结构文件
-rw-r----- 1 mysql mysql 8586 May 16 22:22 course.frm
-rw-r----- 1 mysql mysql 98304 May 16 22:24 course.ibd
InnoDB 通过 B+ Trees 结构对主键创建索引,然后在叶子节点中存储记录,若没有主键,那么就选择唯一键,后台就会生成一个 6 字节的 row_id
作为主键
若创建索引的键是其他字段,那么叶子节点存储的是该记录的主键,然后再通过主键索引找到对应的记录,这叫做回表
MyISAM
在 MyISAM 存储引擎中,数据文件、索引文件是分开存储的,所以能看到两个文件,后缀分别是:MYI、MYD,因此在进行数据检索时,需要读取两个文件,在索引的数据结构中存储的是实际的数据行地址,如下图:
[root@trench study]# pwd
/var/lib/mysql/study
[root@trench study]# ll
total 1064
# 表结构文件
-rw-r----- 1 mysql mysql 8586 May 16 22:36 course.frm
-rw-r----- 1 mysql mysql 120 May 16 22:36 course.MYD
-rw-r----- 1 mysql mysql 2048 May 16 22:36 course.MYI
索引优化
上一篇讲到了索引的基本概念、索引的分类以及索引相关的技术名词
在 MySQL 官网中,给了数据库、表案例 > sakila 数据库,如下:https://dev.mysql.com/doc/index-other.html,后续会基于该数据库表进行索引优化演示
索引匹配方式
关于 explain 关键字各个列描述可以阅读:
MySQL 内置的监控工具介绍及使用篇
首先创建好表结构,并设置好对应的索引
CREATE TABLE `member` (
`id` bigint(10) not null primary key auto_increment,
`nick_name` varchar(32) not null default '' comment '昵称',
`real_name` varchar(32) not null default '' comment '真实姓名',
`phone` varchar(11) not null default '' comment '手机号',
`age` int not null default 0 comment '年龄',
`level_id` bigint(10) not null default 1 comment '等级id',
`level_name` varchar(32) not null default 1 comment '等级名称',
`register_time` datetime not null default current_timestamp comment '注册时间'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
alter table `member` add index idx_name_phone_level(nick_name,phone, level_id);
新增一部分测试数据
insert into `member`
(nick_name,real_name,phone,age,level_id,level_name) values
('January','一月','1111111101',18,1,'青铜'),
('February','二月','1111111102',18,2,'白银'),
('March','三月','1111111103',18,3,'黄金'),
('April','四月','1111111104',18,3,'黄金'),
('May','五月','1111111105',18,3,'黄金'),
('June','六月','1111111106',18,3,'黄金'),
('July','七月','1111111107',18,4,'铂金'),
('August','八月','1111111108',18,5,'钻石'),
('September','九月','1111111109',18,5,'钻石'),
('October','十月','1111111110',18,5,'钻石'),
('November','十一月','1111111111',18,6,'翡翠'),
('December','十二月','1111111112',18,7,'大师');
全值匹配
全值匹配:索引内的值都是等值查询,例如:
mysql> explain select * from `member` where phone='1111111101' and level_id=1 and nick_name='January';
+----+-------------+--------+------------+------+----------------------+----------------------+---------+-------------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+----------------------+----------------------+---------+-------------------+------+----------+-------+
| 1 | SIMPLE | member | NULL | ref | idx_phone_level_name | idx_name_phone_level | 184 | const,const,const | 1 | 100.00 | NULL |
+----+-------------+--------+------------+------+----------------------+----------------------+---------+-------------------+------+----------+-------+
最左前缀匹配
最左前缀匹配:匹配组合索引列的部分列,例如:
mysql> explain select * from `member` where phone='1111111101' and nick_name='January';
+----+-------------+--------+------------+------+----------------------+----------------------+---------+-------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+----------------------+----------------------+---------+-------------+------+----------+-------+
| 1 | SIMPLE | member | NULL | ref | idx_name_phone_level | idx_name_phone_level | 176 | const,const | 1 | 100.00 | NULL |
+----+-------------+--------+------------+------+----------------------+----------------------+---------+-------------+------+----------+-------+
如上,只会匹配到 nick_name、phone 字段
列前缀匹配
列前缀匹配:匹配某一列值的开头部分,例如:
mysql> explain select * from `member` where nick_name like 'Ja%';
+----+-------------+--------+------------+-------+----------------------+----------------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+-------+----------------------+----------------------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | member | NULL | range | idx_name_phone_level | idx_name_phone_level | 130 | NULL | 1 | 100.00 | Using index condition |
+----+-------------+--------+------------+-------+----------------------+----------------------+---------+------+------+----------+-----------------------+
虽然是模糊匹配方法,但也用到了索引,但以下这种方式是使用不到索引的
mysql> explain select * from `member` where nick_name like '%nuary';
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | member | NULL | ALL | NULL | NULL | NULL | NULL | 12 | 11.11 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
范围匹配
范围匹配:查询某一个范围的数据,例如:
mysql> explain select * from `member` where nick_name > 'May';
+----+-------------+--------+------------+-------+----------------------+----------------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+-------+----------------------+----------------------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | member | NULL | range | idx_name_phone_level | idx_name_phone_level | 130 | NULL | 3 | 100.00 | Using index condition |
+----+-------------+--------+------------+-------+----------------------+----------------------+---------+------+------+----------+-----------------------+
精确匹配某一列、范围匹配另一列
精确匹配某一列、范围匹配另一列:第一列的值全值匹配,另外的列部分匹配,例如:
mysql> explain select * from `member` where nick_name = 'May' and phone > '1111111101';
+----+-------------+--------+------------+-------+----------------------+----------------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+-------+----------------------+----------------------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | member | NULL | range | idx_name_phone_level | idx_name_phone_level | 176 | NULL | 1 | 100.00 | Using index condition |
+----+-------------+--------+------------+-------+----------------------+----------------------+---------+------+------+----------+-----------------------+
访问索引
访问索引:查询时只需要访问索引,从二级索引树可以拿到所有需要的数据,无需通过主键再次去回表查询,本质上就是覆盖索引
,例如:
mysql> explain select id,phone,level_id,nick_name from `member` where phone='1111111101' and level_id=1 and nick_name='January';
+----+-------------+--------+------------+------+----------------------+----------------------+---------+-------------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+----------------------+----------------------+---------+-------------------+------+----------+-------------+
| 1 | SIMPLE | member | NULL | ref | idx_name_phone_level | idx_name_phone_level | 184 | const,const,const | 1 | 100.00 | Using index |
+----+-------------+--------+------------+------+----------------------+----------------------+---------+-------------------+------+----------+-------------+
Extra 列显示 Using index
,则表达直接可以在索引取回
哈希索引
在 InnoDB、MyISAM 存储引擎中使用 B+ 树存储索引,MySQL Memory 存储引擎中显示支持哈希索引,它基于 Hash 表实现,只有精确匹配所有列的查询才有效,哈希索引自身只需存储对应的 hash 值,所以索引结构十分紧凑,才让哈希索引查找速度非常快
哈希索引使用的限制如下:
- 只包含了哈希值、行指针,而不存储字段值,索引不能使用索引中的值来避免读取行,每次查询必须要先匹配到哈希值,然后再读取行指针,再根据行指针去读取我们实际的数据
- 数据不是按照索引值顺序存储的,所以无法排序
- 不支持部分列查询,哈希索引是使用索引的全部内容来计算哈希值
- 访问哈希索引数据非常快,除非有很多的哈希冲突,当出现哈希冲突时,存储引擎必须遍历链表中所有的行指针,逐行进行比较,直到找出所有符合条件的行
- 哈希冲突比较多时,维护的代价也会很高
当需要大量的 URL,并且通过 URL 进行搜索查找,若使用 B+ 树,存储的内容就会变的很大
select id,url from url_base where url = '';
此时,可以利用 CRC32 对 url 作哈希算法,使用以下的查询方式:
select id,url from url_base where url_crc = CRC32('');
此查询性能较高的原因 > 使用体积很小的索引来完成查找
组合索引
通常在实际生产开发情况下,会选择多列值作为索引,也就是组合索引
,又称之为复合索引
;在使用组合索引时,要注意最左匹配原则,当创建组合索引之后,进行列值匹配时,从左到右匹配
创建一张表,将表中 a,b,c 三列作为组合索引,注意不同查询语句的索引匹配情况,如下:
语句 | 索引是否匹配 |
---|---|
where a=3 | 是,使用了 a |
where a=3 and b=5 | 是,使用了 a,b |
where a=3 and b=5 and c=4 | 是,使用了 a,b,c |
where b=5、where c=4 | 否 |
where a=3 and c=4 | 是,使用了 a |
where a=3 and b > 10 and c=7 | 是,使用了 a,b |
where a=3 and b like ‘%xx%’ and c=7 | 是,使用了 a |
聚簇、非聚簇索引
所谓的聚簇索引并不是单独的索引类型,而是一种数据存储方式
,聚簇索引指的是数据、索引列紧凑的存储在一起;非聚簇索引指的是数据、索引分开存储
聚簇索引优点:
- 可以把相关数据保存在一起
- 数据访问更快,数据、索引保存在同一颗树中
- 使用覆盖索引扫描的查询可以直接使用叶子节点的主键值
聚簇索引缺点:
- 聚簇数据最大限度提高了 IO 密集型应用的性能,若数据全部放在内存,那么聚簇索引就没有优势
- 插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式
- 更新聚簇索引列代价很高,会强制将每个被更新的行移动到新的位置(
在数据表已经堆积了很多数据再加索引导致的,最好是先暂停表数据的使用,在停用的状态下添加索引
) - 基于聚簇索引的表在插入新行或者主键被更新导致需要移动行的时候,可能面临
页分裂问题
- 聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏或者由于页分裂导致数据存储不连续的时候
覆盖索引
一个索引包含所有需要查询的字段值,称之为覆盖索引
并非所有索引的类型都可以称之为覆盖索引,覆盖索引必须要存储索引列的值
不同的存储引擎实现覆盖索引的方式不同,不是所有的存储引擎都支持覆盖索引,Memory 不支持覆盖索引
覆盖索引优点:
- 索引条目通常远小于数据行大小,若只需要读取索引,那么 MySQL 就会极大减少数据的访问量
- 索引是按照列值顺序存储的,所以对于 IO 密集型的范围查询会比从一行一行读取数据的 IO 少得多
- MyISAM 存储引擎在内存中只缓存索引,数据依赖于操作系统来缓存,因此访问数据需要进行一次系统调用,可能会导致严重的性能问题
- 由于 InnoDB 是聚簇索引,所以覆盖索引对 InnoDB 特别有用
当发起一个被索引覆盖的查询时,在 explain > extra 列可以看到 using index
信息,此时就使用了覆盖索引
mysql> explain select store_id,film_id from inventory;
+----+-------------+-----------+------------+-------+---------------+----------------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-----------+------------+-------+---------------+----------------------+---------+------+------+----------+-------------+
| 1 | SIMPLE | inventory | NULL | index | NULL | idx_store_id_film_id | 3 | NULL | 4581 | 100.00 | Using index |
+----+-------------+-----------+------------+-------+---------------+----------------------+---------+------+------+----------+-------------+
在大多数存储引擎中,覆盖索引只能覆盖那些只访问索引中部分列的查询;不过,可以进一步的进行优化,可以使用 InnoDB 二级索引来覆盖查询
例如:actor 表使用 InnoDB 存储引擎,并在 last_name 字段有索引,虽然该索引的列不包含主键 actor_id,但仍然能够对 actor_id 作覆盖查询,
二级索引叶子节点也以主键作为唯一 Key 存储了起来
mysql> explain select actor_id,last_name from actor where last_name='HOPPER';
+----+-------------+-------+------------+------+---------------------+---------------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------------+---------------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | actor | NULL | ref | idx_actor_last_name | idx_actor_last_name | 182 | const | 2 | 100.00 | Using index |
+----+-------------+-------+------------+------+---------------------+---------------------+---------+-------+------+----------+-------------+
优化细节(important)
数据库勿做计算
数据库勿计算:当使用索引列进行查询时,尽量不要使用表达式,把计算逻辑放到代码业务层而不是在数据库层操作
mysql> explain select * from actor where actor_id=4;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | actor | NULL | const | PRIMARY | PRIMARY | 2 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
mysql> explain select * from actor where actor_id+1=4;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | actor | NULL | ALL | NULL | NULL | NULL | NULL | 200 | 100.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
如上,当 where 条件中包含了表达式以后,不会使用对应的索引列
尽量主键查询
尽量使用主键查询,而不是使用其他索引,因此主键查询不会触发回表查询 > explain 分析出来会是 const
,效率极高
前缀索引
前缀索引:当创建的索引字符串比较长时,可以考虑使用索引前缀 > 创建索引,来提高数据检索的效率
1、有时需要索引很长的字符串,这会让索引变得很大且很慢,通常情况下,可以使用
某个列开始的部分字符串
,这样可以大大节约索引空间,从而提高索引效率,但这会大大降低索引的选择性
;索引选择性越高则查询效率越高
,因为选择性更高的索引可以让 MySQL 在查找时过滤掉更多的行
2、一般情况下某个列前缀的选择性也是足够高的,足以满足查询的性能,但是对于 Blob、Text、Varchar 类型列,必须使用前缀索引,因为 MySQL 不允许索引这些列的完整长度,使用该方法的诀窍在于要选择足够长的前缀以此来保证较高的选择性
举例如下,先创建好对应的表结构、数据:
# 创建数据表
create table citydemo(city varchar(50) not null);
insert into citydemo(city) select city from city;
# 重复执行 5 次,以下的 SQL 语句
insert into citydemo(city) select city from citydemo;
# 随机更新城市表中的名称
update citydemo set city=(select city from city order by rand() limit 1);
数据准备好以后,进行 SQL 测试,如下:
- 查询最常见的城市列表,发现每个值出现了 40 次以上
mysql> select count(*) cnt,city from citydemo group by city order by cnt desc limit 10;
+-----+-------------+
| cnt | city |
+-----+-------------+
| 73 | London |
| 49 | Chatsworth |
| 48 | Ikerre |
| 47 | Tychy |
| 46 | Santa Rosa |
| 45 | Alessandria |
| 45 | Akron |
| 44 | Tartu |
| 44 | Kuwana |
| 44 | Tsuyama |
+-----+-------------+
- 查找最频繁出现的城市前缀,先从
5 字符前缀
开始,发现比原来出现的次数更多,可以分别截取多个字符,查看城市出现的次数频率
mysql> select count(*) as cnt,left(city,5) as pref from citydemo group by pref order by cnt desc limit 10;
+-----+-------+
| cnt | pref |
+-----+-------+
| 115 | South |
| 97 | Santa |
| 80 | Saint |
| 75 | Londo |
| 75 | Valle |
| 69 | San F |
| 69 | al-Qa |
| 67 | Shimo |
| 67 | Xiang |
| 63 | Chang |
+-----+-------+
10 rows in set (0.02 sec)
mysql> select count(*) as cnt,left(city,6) as pref from citydemo group by pref order by cnt desc limit 10;
+-----+--------+
| cnt | pref |
+-----+--------+
| 97 | Santa |
| 75 | London |
| 75 | Valle |
| 69 | San Fe |
| 53 | Santia |
| 50 | Hanoi |
| 48 | Deba H |
| 48 | La Pla |
| 46 | Saint |
| 46 | Crdoba |
+-----+--------+
10 rows in set (0.02 sec)
mysql> select count(*) as cnt,left(city,7) as pref from citydemo group by pref order by cnt desc limit 10;
+-----+---------+
| cnt | pref |
+-----+---------+
| 75 | Valle d |
| 75 | London |
| 69 | San Fel |
| 53 | Santiag |
| 50 | Hanoi |
| 48 | Deba Ha |
| 48 | La Plat |
| 46 | Bucures |
| 46 | Saint L |
| 46 | Crdoba |
+-----+---------+
10 rows in set (0.02 sec)
mysql> select count(*) as cnt,left(city,8) as pref from citydemo group by pref order by cnt desc limit 10;
+-----+----------+
| cnt | pref |
+-----+----------+
| 75 | Valle de |
| 75 | London |
| 69 | San Feli |
| 53 | Santiago |
| 50 | Hanoi |
| 48 | Deba Hab |
| 48 | La Plata |
| 46 | Bucurest |
| 46 | Saint Lo |
| 46 | Crdoba |
+-----+----------+
10 rows in set (0.02 sec)
通过上述查询结果,可以发现,当前缀=7 时,前缀的选择性接近于完整列的选择性,只要比对它的 cnt
是否还有继续发生变化即可.
- 第二种方式有时并不那么准确能够计算出前缀,可以通过这种方式来进行判断,识别它的选择性占比率,如下:
mysql> select count(distinct left(city,3))/count(*) as sel3, -> count(distinct left(city,4))/count(*) as sel4, -> count(distinct left(city,5))/count(*) as sel5,
-> count(distinct left(city,6))/count(*) as sel6,
-> count(distinct left(city,7))/count(*) as sel7,
-> count(distinct left(city,8))/count(*) as sel8
-> from citydemo;
+--------+--------+--------+--------+--------+--------+
| sel3 | sel4 | sel5 | sel6 | sel7 | sel8 |
+--------+--------+--------+--------+--------+--------+
| 0.0239 | 0.0293 | 0.0305 | 0.0309 | 0.0310 | 0.0310 |
+--------+--------+--------+--------+--------+--------+
因此,可以使用字符串前缀=7
来创建索引:
alter table citydemo add key(city(7));
此处创建好以后,当使用 city 索引列进行条件查询时会发现效率可以极大提升
注意:前缀索引是一种能够使索引更小更快的有效方法,但也有缺点:MySQL 无法使用前缀索引作
order by
、group by
索引扫描排序
使用索引扫描作排序,MySQL 有两种方式可以生成有序的结果,通过排序操作或索引顺序进行扫描 > 若 explain 出来的 type=index
,则说明 MySQL 使用了索引扫描来进行了排序
扫描索引本身是很快的,因为只需要从一条索引记录移动到紧接着的下一条记录,但索引不能覆盖查询所需要的全部列,那么就不得不扫描一条索引记录就得回表查询一次对应的行数据;基础上都是随机 IO,因此按索引顺序读取数据的速度要比顺序扫描全表慢
MySQL 可以使用同一个索引既能满足排序,又可以用于查找行;若有可能的话,设计索引时应当尽可能地同时满足这两项任务
当索引列顺序跟 order by 子句顺序完全一致,并且所有列的顺序方式都一样的话,MySQL 才能使用索引来对结果进行排序;若查询需要关联多张表,则只有当 order by 子句引用的字段全部为第一张表时,才能使用索引作排序;order by 子句与查询的限制是一样的,要满足索引的最左匹配原则
,否则,MySQL 都需要执行顺序操作,无法使用索引排序
举例如下,使用 sakila > rental 表 > rentail_data、inventory_id、customer_id
列上索引名:rentail_data
UNIQUE KEY `rental_date` (`rental_date`,`inventory_id`,`customer_id`)
- 使用 rental_data 索引为下面的查询作排序
mysql> explain select rental_id,staff_id from rental where rental_date='2005-05-25' order by inventory_id,customer_id;
+----+-------------+--------+------------+------+---------------+-------------+---------+-------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+-------------+---------+-------+------+----------+-----------------------+
| 1 | SIMPLE | rental | NULL | ref | rental_date | rental_date | 5 | const | 1 | 100.00 | Using index condition |
+----+-------------+--------+------------+------+---------------+-------------+---------+-------+------+----------+-----------------------+
- 如下查询不会触发索引排序,因为
rental_data 被重复使用了
,一般 where、order by 是组合使用的
mysql> explain select rental_id,staff_id from rental where rental_date>'2005-05-25' order by rental_date,inventory_id;+----+-------------+--------+------------+------+---------------+------+---------+------+-------+----------+-----------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+------+---------+------+-------+----------+-----------------------------+
| 1 | SIMPLE | rental | NULL | ALL | rental_date | NULL | NULL | NULL | 16008 | 50.00 | Using where; Using filesort |
+----+-------------+--------+------------+------+---------------+------+---------+------+-------+----------+-----------------------------+
- 如下查询使用了两种不同的排序方向 > 升序、降序,rental_data 使用了范围查询
mysql> explain select rental_id,staff_id from rental where rental_date>'2005-05-25' order by inventory_id desc,customer_id asc;
+----+-------------+--------+------------+------+---------------+------+---------+------+-------+----------+-----------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+------+---------+------+-------+----------+-----------------------------+
| 1 | SIMPLE | rental | NULL | ALL | rental_date | NULL | NULL | NULL | 16008 | 50.00 | Using where; Using filesort |
+----+-------------+--------+------------+------+---------------+------+---------+------+-------+----------+-----------------------------+
- 如下查询中引用了一个非索引列
mysql> explain select rental_id,staff_id from rental where rental_date>'2005-05-25' order by inventory_id,staff_id;
+----+-------------+--------+------------+------+---------------+------+---------+------+-------+----------+-----------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+------+---------+------+-------+----------+-----------------------------+
| 1 | SIMPLE | rental | NULL | ALL | rental_date | NULL | NULL | NULL | 16008 | 50.00 | Using where; Using filesort |
+----+-------------+--------+------------+------+---------------+------+---------+------+-------+----------+-----------------------------+
子查询
union all、in、or,推荐使用 in 关键字子查询,如下:
可以看到执行 in 时效率是较高的,当然这个没有绝对,要根据实际的执行情况来进行判断,绝大部分情况 in 是比较节省时间的,所以推荐使用 in 方式
or 关键字有时候会引起索引失效,会造成扫描表中大部分无效的行数据,比如:
where a = x or b =y;
要是 a、b 两列都加了索引,b 索引列就无法使用到,当表数据量增大时,这条 SQL 会造成扫描的条数据飙升,从而导致引发慢 SQL 查询
范围列查询
范围列可以使用索引,当使用范围列可以进行索引的匹配,但是范围列后面的列就无法用到索引,索引最多用于一个范围列
在创建复合、组合索引时,要结合所有的 SQL 一起观察,有出现列是范围查询的,最好将
它放到最后面
,以避免那些常量值的索引列无法使用索引去加快查询
强制类型转换
强制类型转换会触发全表扫描
create table user(id int,name varchar(10),phone varchar(11));
alter table user add index idx_phone(phone);
使用强转前、强转后作比对,如下:
mysql> explain select * from user where phone = '15980212312';
+----+-------------+-------+------------+------+---------------+-----------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+-----------+---------+-------+------+----------+-------+
| 1 | SIMPLE | user | NULL | ref | idx_phone | idx_phone | 36 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+-----------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
mysql> explain select * from user where phone = 15980212312;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | user | NULL | ALL | idx_phone | NULL | NULL | NULL | 1 | 100.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
通过对比可以发现,当发生类型转换以后会导致索引失效,所以尽量确保索引的类型
建立索引
更新十分频繁,数据区分度不高的字段上不宜建立索引
- 更新会变更 B+ 树,更新频繁的字段上建立索引会大大降低数据库的性能
- 类似于性别这种区分不大的字段,建立索引是没有任何意义的,不能有效的过滤数据
- 一般区分度在 80% 以上的就可以建立索引,区分度可以使用
count(distinct(列名))/count(*)
来计算
创建索引的列,不允许为 null,查询条件也不能为 null,否则会得到不符合预期的结果,造成 SQL 执行效率极速下降
区分度不高也有例外,一般在业务重试表,对业务消息进行重试时,一般会将需要重试的消息查询出来,进行重试,
通过对消息的处理状态列 + 索引,然后结合 LIMIT 限制行数据
,可以提高这部分的执行效率
JOIN 表
通过表 Join 连接时,最好不要超过三张表,因为需要 join 字段,数据类型必须保持一致 > 来自于阿里云编码规范
;因为在进行多表联查时会造成查询较慢,小表(表数据比较少)JOIN 大表(表数据比较多)效率会相当高
MySQL 提供了三种 JOIN 算法,如下:
- Simple Nested-Loop Join:每次把第一张表里面的数据行记录取出来,然后再去匹配第二张的每行记录
以上那种方式是一行一行去匹配,这种方式效率比较低,所以一般情况下不推荐使用这种方式 - Index Nested-Loop Join:这种方式是使用表中的索引进行相关的匹配操作
1、要求匹配表 S 上有索引,可以通过索引来减少比较次数,加速查询
2、在查询时,驱动表 R 会通过关联字段的索引进行查找,当在索引上找到符合的值,再回表的进行查询,也就是只有当匹配到索引以后才会进行回表查询
3、若匹配表 S 关联键是主键的话,性能会非常高,若不是主键,要进行多次回表查询,先关联索引,然后通过二级索引的主键 ID 去进行回表操作,性能上比索引是主键要慢
- Block Nested-Loop Join:表示每次查询时将 R 驱动表里面的一些数据优先放入到内存中,然后通过从内存中获取数据来进行匹配操作
若有索引,会选取第二种方式进行 JOIN,若 JOIN 列没有索引,就会采用Block Nested-Loop Join
,可以看到中间有个 JOIN BUFFER 缓冲区,将 R 驱动表的所有 JOIN 相关的列都先缓存到 JOIN BUFFER 中,然后批量与匹配表 S 进行匹配,将第一种的方式处理的多次合并为一次,降低了匹配表 S 访问频率;默认情况下join_buffer_size=256k
,查找时 MySQL 会将所有需要的列缓存到 JOIN BUFFER 当中,包括 SELECT 查询列,而不是仅仅只缓存关联列;在有 N 个 JOIN 关联 SQL,会在执行时分配 N-1 个 JOIN BUFFER
在使用第三种方式时,会消耗内存,所以在使用时有以下需要注意的点,如下:
1、JOIN BUFFER 会缓存所有参与查询的列而不是只有 JOIN 列,所以在查询时指定你需要查询的列,而不是 SELECT *
2、可以调整 join_buffer_size 缓存大小
3、join_buffer_size 默认值为 256K,join_buffer_size 最大值在 MySQL 5.1.22 版本前是 4G-1,而之后的版本在 64 位操作系统下申请大于 4G JOIN BUFFER 空间
4、使用 Block Nested-Loop Join 算法需要开启优化器管理配置,optimizer_switch > block_nested_loop=ON
,默认是开启的
LIMIT 限制输出行
LIMIT:主要用来限制输出的行数据,在进行一系列 SQL 调优步骤后,其实最核心的就是减少数据 IO 量,因此在很多场景下能使用 LIMIT 尽量使用 LIMIT,这样能保证返回的数据量最少,数据量少了,查询数据的效率才会有提升
单表索引数量
单表索引的数量建议在 5 个以内,当我们给表创建索引时,并不是说每一个列都创建索引之后,在读取数据的时候就一定快,要通过实际的情况来决定,在很多的场景下,创建的索引越多,反而会导致数据的文件越大,那么在进行数据访问时效率就会降低,因此在 《高性能 MySQL 调优》强调了单表索引尽量控制在 5 个以内
,当然在很多场景下,索引个数是可能超过 5 个的,根据实际的情况再决定
单表索引数量
组合、复合索引字段数不允许超过 5 个,大部分应用场景下都需要创建组合索引,但组合索引的列个数不宜太多,列太多会导致占用太多的存储空间,从而会导致树深度变深,数据检索效率变低
避免错误概念
- 索引越多越好
- 过于早优化,在不了解系统的情况下进行优化
索引监控
索引监控信息,用于判别索引的使用情况
- 显示全局的索引读取记录
show global status like 'Handler_read%';
- 显示当前会话级别的索引读取记录
show status like 'Handler_read%';
对以上打印的参数描述如下:
- Handler_read_first:索引中第一条被读取的次数
- Handler_read_key:通过索引读取数据的次数,此选项数值如果很高,那么可以说明系统高效地使用到了索引,一切运转良好
- Handler_read_last:通过索引读取最后一行的请求数
- Handler_read_next:通过索引读取下一行的请求数,若查询语句中使用范围查询或索引扫描来查询索引列,该列增加
- Handler_read_prev:通过索引顺序读取前一行的请求数,该读取方式主要用于优化
ORDER BY column DESC
- Handler_read_rnd:从固定位置读取数据的次数,若你正执行大量查询并需要对结果进行排序该值就会比较高,那么你可能使用了大量全表扫描的查询或者没有正确使用索引
- Handler_read_rnd_next:从数据文件读取下一行的请求数,如果你正在进行大量的表扫描,该值就会比较高,通常说明你的表索引不正确或写的 SQL 没有利用到索引
总结
该篇博文从零到一讲解了数据库索引使用到的数据结构以及它与存储引擎之前的关联关系,为什么要使用 B+ Trees 而不使用 B Trees?数据库表文件的存储方式:聚簇、非聚簇;说到了索引的类型以及这方面是如何去进行优化的,最重要的是,优化细节这个章节,不仅仅如何告知优化、生产如何调优、调优的细节如何处理、如何避免生产慢 SQL;最后,通过索引监控命令得知了系统使用 SQL 情况!希望你能够喜欢!后续的大数据量查询优化、海量数据解耦优化处理
敬请期待~
如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!
推荐专栏:Spring、MySQL,订阅一波不再迷路
大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!