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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云解析DNS-重点域名监控,免费拨测 20万次(价值200元)
RDS MySQL DuckDB 分析主实例,集群系列 8核16GB
简介: 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     |
+-----+-------------+

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
目录
相关文章
|
3月前
|
缓存 关系型数据库 BI
使用MYSQL Report分析数据库性能(下)
使用MYSQL Report分析数据库性能
149 3
|
3月前
|
关系型数据库 MySQL 数据库
自建数据库如何迁移至RDS MySQL实例
数据库迁移是一项复杂且耗时的工程,需考虑数据安全、完整性及业务中断影响。使用阿里云数据传输服务DTS,可快速、平滑完成迁移任务,将应用停机时间降至分钟级。您还可通过全量备份自建数据库并恢复至RDS MySQL实例,实现间接迁移上云。
|
3月前
|
关系型数据库 MySQL 分布式数据库
阿里云PolarDB云原生数据库收费价格:MySQL和PostgreSQL详细介绍
阿里云PolarDB兼容MySQL、PostgreSQL及Oracle语法,支持集中式与分布式架构。标准版2核4G年费1116元起,企业版最高性能达4核16G,支持HTAP与多级高可用,广泛应用于金融、政务、互联网等领域,TCO成本降低50%。
|
3月前
|
关系型数据库 MySQL 数据库
阿里云数据库RDS费用价格:MySQL、SQL Server、PostgreSQL和MariaDB引擎收费标准
阿里云RDS数据库支持MySQL、SQL Server、PostgreSQL、MariaDB,多种引擎优惠上线!MySQL倚天版88元/年,SQL Server 2核4G仅299元/年,PostgreSQL 227元/年起。高可用、可弹性伸缩,安全稳定。详情见官网活动页。
|
3月前
|
关系型数据库 分布式数据库 数据库
阿里云数据库收费价格:MySQL、PostgreSQL、SQL Server和MariaDB引擎费用整理
阿里云数据库提供多种类型,包括关系型与NoSQL,主流如PolarDB、RDS MySQL/PostgreSQL、Redis等。价格低至21元/月起,支持按需付费与优惠套餐,适用于各类应用场景。
|
3月前
|
SQL 关系型数据库 MySQL
Mysql数据恢复—Mysql数据库delete删除后数据恢复案例
本地服务器,操作系统为windows server。服务器上部署mysql单实例,innodb引擎,独立表空间。未进行数据库备份,未开启binlog。 人为误操作使用Delete命令删除数据时未添加where子句,导致全表数据被删除。删除后未对该表进行任何操作。需要恢复误删除的数据。 在本案例中的mysql数据库未进行备份,也未开启binlog日志,无法直接还原数据库。
|
3月前
|
Ubuntu 安全 关系型数据库
安装与配置MySQL 8 on Ubuntu,包括权限授予、数据库备份及远程连接指南
以上步骤提供了在Ubuntu上从头开始设置、配置、授权、备份及恢复一个基础但完整的MySQL环境所需知识点。
425 7
|
3月前
|
缓存 监控 关系型数据库
使用MYSQL Report分析数据库性能(上)
最终建议:当前系统是完美的读密集型负载模型,优化重点应放在减少行读取量和提高数据定位效率。通过索引优化、分区策略和内存缓存,预期可降低30%的CPU负载,同时保持100%的缓冲池命中率。建议每百万次查询后刷新统计信息以持续优化
229 6
|
3月前
|
缓存 监控 关系型数据库
使用MYSQL Report分析数据库性能(中)
使用MYSQL Report分析数据库性能
155 1
|
3月前
|
关系型数据库 MySQL 数据库
阿里云数据库RDS支持MySQL、SQL Server、PostgreSQL和MariaDB引擎
阿里云数据库RDS支持MySQL、SQL Server、PostgreSQL和MariaDB引擎,提供高性价比、稳定安全的云数据库服务,适用于多种行业与业务场景。

热门文章

最新文章

推荐镜像

更多