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

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云解析 DNS,旗舰版 1个月
简介: MySQL 数据结构优化与索引细节解析:打造高效数据库的优化秘笈(三)
  1. 查找最频繁出现的城市前缀,先从 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 是否还有继续发生变化即可.

  1. 第二种方式有时并不那么准确能够计算出前缀,可以通过这种方式来进行判断,识别它的选择性占比率,如下:
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 bygroup 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`)
  1. 使用 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 |
+----+-------------+--------+------------+------+---------------+-------------+---------+-------+------+----------+-----------------------+
  1. 如下查询不会触发索引排序,因为 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 |
+----+-------------+--------+------------+------+---------------+------+---------+------+-------+----------+-----------------------------+
  1. 如下查询使用了两种不同的排序方向 > 升序、降序,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 |
+----+-------------+--------+------------+------+---------------+------+---------+------+-------+----------+-----------------------------+
  1. 如下查询中引用了一个非索引列
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 |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+

通过对比可以发现,当发生类型转换以后会导致索引失效,所以尽量确保索引的类型

建立索引

更新十分频繁,数据区分度不高的字段上不宜建立索引

  1. 更新会变更 B+ 树,更新频繁的字段上建立索引会大大降低数据库的性能
  2. 类似于性别这种区分不大的字段,建立索引是没有任何意义的,不能有效的过滤数据
  3. 一般区分度在 80% 以上的就可以建立索引,区分度可以使用 count(distinct(列名))/count(*) 来计算

创建索引的列,不允许为 null,查询条件也不能为 null,否则会得到不符合预期的结果,造成 SQL 执行效率极速下降

区分度不高也有例外,一般在业务重试表,对业务消息进行重试时,一般会将需要重试的消息查询出来,进行重试,通过对消息的处理状态列 + 索引,然后结合 LIMIT 限制行数据,可以提高这部分的执行效率

JOIN 表

通过表 Join 连接时,最好不要超过三张表,因为需要 join 字段,数据类型必须保持一致 > 来自于阿里云编码规范;因为在进行多表联查时会造成查询较慢,小表(表数据比较少)JOIN 大表(表数据比较多)效率会相当高

MySQL 提供了三种 JOIN 算法,如下:

  1. Simple Nested-Loop Join:每次把第一张表里面的数据行记录取出来,然后再去匹配第二张的每行记录

    以上那种方式是一行一行去匹配,这种方式效率比较低,所以一般情况下不推荐使用这种方式
  2. Index Nested-Loop Join:这种方式是使用表中的索引进行相关的匹配操作

1、要求匹配表 S 上有索引,可以通过索引来减少比较次数,加速查询

2、在查询时,驱动表 R 会通过关联字段的索引进行查找,当在索引上找到符合的值,再回表的进行查询,也就是只有当匹配到索引以后才会进行回表查询

3、若匹配表 S 关联键是主键的话,性能会非常高,若不是主键,要进行多次回表查询,先关联索引,然后通过二级索引的主键 ID 去进行回表操作,性能上比索引是主键要慢

  1. 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 个,大部分应用场景下都需要创建组合索引,但组合索引的列个数不宜太多,列太多会导致占用太多的存储空间,从而会导致树深度变深,数据检索效率变低

避免错误概念

  1. 索引越多越好
  2. 过于早优化,在不了解系统的情况下进行优化

索引监控

索引监控信息,用于判别索引的使用情况

  • 显示全局的索引读取记录
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,订阅一波不再迷路

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!

相关实践学习
如何在云端创建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
MySQL 索引优化与慢查询优化:原理与实践
通过本文的介绍,希望您能够深入理解MySQL索引优化与慢查询优化的原理和实践方法,并在实际项目中灵活运用这些技术,提升数据库的整体性能。
27 5
|
17天前
|
存储 缓存 数据库
数据库索引采用B+树不采用B树的原因?
B+树优化了数据存储和查询效率,数据仅存于叶子节点,便于区间查询和遍历,磁盘读写成本低,查询效率稳定,特别适合数据库索引及范围查询。
33 6
|
26天前
|
关系型数据库 MySQL Java
MySQL索引优化与Java应用实践
【11月更文挑战第25天】在大数据量和高并发的业务场景下,MySQL数据库的索引优化是提升查询性能的关键。本文将深入探讨MySQL索引的多种类型、优化策略及其在Java应用中的实践,通过历史背景、业务场景、底层原理的介绍,并结合Java示例代码,帮助Java架构师更好地理解并应用这些技术。
26 2
|
27天前
|
存储 缓存 数据库
数据库索引采用B+树不采用B树的原因
B+树相较于B树,在数据存储、磁盘读写、查询效率及范围查询方面更具优势。数据仅存于叶子节点,便于高效遍历和区间查询;内部节点不含数据,提高缓存命中率;查询路径固定,效率稳定;特别适合数据库索引使用。
28 1
|
12天前
|
关系型数据库 MySQL 数据库
Python处理数据库:MySQL与SQLite详解 | python小知识
本文详细介绍了如何使用Python操作MySQL和SQLite数据库,包括安装必要的库、连接数据库、执行增删改查等基本操作,适合初学者快速上手。
81 15
|
6天前
|
SQL 关系型数据库 MySQL
数据库数据恢复—Mysql数据库表记录丢失的数据恢复方案
Mysql数据库故障: Mysql数据库表记录丢失。 Mysql数据库故障表现: 1、Mysql数据库表中无任何数据或只有部分数据。 2、客户端无法查询到完整的信息。
|
13天前
|
关系型数据库 MySQL 数据库
数据库数据恢复—MYSQL数据库文件损坏的数据恢复案例
mysql数据库文件ibdata1、MYI、MYD损坏。 故障表现:1、数据库无法进行查询等操作;2、使用mysqlcheck和myisamchk无法修复数据库。

推荐镜像

更多
下一篇
DataWorks