MySQL 数据结构优化与索引细节解析:打造高效数据库的优化秘笈(二)

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云解析 DNS,旗舰版 1个月
简介: MySQL 数据结构优化与索引细节解析:打造高效数据库的优化秘笈(二)

索引匹配方式

关于 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 值,所以索引结构十分紧凑,才让哈希索引查找速度非常快

哈希索引使用的限制如下:

  1. 只包含了哈希值、行指针,而不存储字段值,索引不能使用索引中的值来避免读取行,每次查询必须要先匹配到哈希值,然后再读取行指针,再根据行指针去读取我们实际的数据
  2. 数据不是按照索引值顺序存储的,所以无法排序
  3. 不支持部分列查询,哈希索引是使用索引的全部内容来计算哈希值
  4. 访问哈希索引数据非常快,除非有很多的哈希冲突,当出现哈希冲突时,存储引擎必须遍历链表中所有的行指针,逐行进行比较,直到找出所有符合条件的行
  5. 哈希冲突比较多时,维护的代价也会很高

当需要大量的 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

聚簇、非聚簇索引

所谓的聚簇索引并不是单独的索引类型,而是一种数据存储方式聚簇索引指的是数据、索引列紧凑的存储在一起;非聚簇索引指的是数据、索引分开存储

聚簇索引优点:

  1. 可以把相关数据保存在一起
  2. 数据访问更快,数据、索引保存在同一颗树中
  3. 使用覆盖索引扫描的查询可以直接使用叶子节点的主键值

聚簇索引缺点:

  1. 聚簇数据最大限度提高了 IO 密集型应用的性能,若数据全部放在内存,那么聚簇索引就没有优势
  2. 插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式
  3. 更新聚簇索引列代价很高,会强制将每个被更新的行移动到新的位置(在数据表已经堆积了很多数据再加索引导致的,最好是先暂停表数据的使用,在停用的状态下添加索引
  4. 基于聚簇索引的表在插入新行或者主键被更新导致需要移动行的时候,可能面临页分裂问题
  5. 聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏或者由于页分裂导致数据存储不连续的时候

覆盖索引

一个索引包含所有需要查询的字段值,称之为覆盖索引

并非所有索引的类型都可以称之为覆盖索引,覆盖索引必须要存储索引列的值

不同的存储引擎实现覆盖索引的方式不同,不是所有的存储引擎都支持覆盖索引,Memory 不支持覆盖索引

覆盖索引优点:

  1. 索引条目通常远小于数据行大小,若只需要读取索引,那么 MySQL 就会极大减少数据的访问量
  2. 索引是按照列值顺序存储的,所以对于 IO 密集型的范围查询会比从一行一行读取数据的 IO 少得多
  3. MyISAM 存储引擎在内存中只缓存索引,数据依赖于操作系统来缓存,因此访问数据需要进行一次系统调用,可能会导致严重的性能问题
  4. 由于 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 测试,如下:

  1. 查询最常见的城市列表,发现每个值出现了 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     |
+-----+-------------+

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
4天前
|
SQL 关系型数据库 MySQL
深入解析MySQL的EXPLAIN:指标详解与索引优化
MySQL 中的 `EXPLAIN` 语句用于分析和优化 SQL 查询,帮助你了解查询优化器的执行计划。本文详细介绍了 `EXPLAIN` 输出的各项指标,如 `id`、`select_type`、`table`、`type`、`key` 等,并提供了如何利用这些指标优化索引结构和 SQL 语句的具体方法。通过实战案例,展示了如何通过创建合适索引和调整查询语句来提升查询性能。
40 9
|
9天前
|
缓存 关系型数据库 MySQL
MySQL 索引优化以及慢查询优化
通过本文的介绍,希望您能够深入理解MySQL索引优化和慢查询优化的方法,并在实际应用中灵活运用这些技术,提升数据库的整体性能。
50 18
|
8天前
|
缓存 关系型数据库 MySQL
MySQL 索引优化以及慢查询优化
通过本文的介绍,希望您能够深入理解MySQL索引优化和慢查询优化的方法,并在实际应用中灵活运用这些技术,提升数据库的整体性能。
17 7
|
7天前
|
存储 关系型数据库 MySQL
double ,FLOAT还是double(m,n)--深入解析MySQL数据库中双精度浮点数的使用
本文探讨了在MySQL中使用`float`和`double`时指定精度和刻度的影响。对于`float`,指定精度会影响存储大小:0-23位使用4字节单精度存储,24-53位使用8字节双精度存储。而对于`double`,指定精度和刻度对存储空间没有影响,但可以限制数值的输入范围,提高数据的规范性和业务意义。从性能角度看,`float`和`double`的区别不大,但在存储空间和数据输入方面,指定精度和刻度有助于优化和约束。
|
7天前
|
缓存 关系型数据库 MySQL
MySQL 索引优化与慢查询优化:原理与实践
通过本文的介绍,希望您能够深入理解MySQL索引优化与慢查询优化的原理和实践方法,并在实际项目中灵活运用这些技术,提升数据库的整体性能。
27 5
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
71 2
|
2月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
76 0
|
2月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
62 0
|
2月前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
66 0
|
2月前
|
安全 Java 程序员
Collection-Stack&Queue源码解析
Collection-Stack&Queue源码解析
86 0

推荐镜像

更多
下一篇
DataWorks