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

本文涉及的产品
RDS AI 助手,专业版
RDS MySQL DuckDB 分析主实例,集群系列 4核8GB
RDS DuckDB + QuickBI 企业套餐,8核32GB + QuickBI 专业版
简介: 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,订阅一波不再迷路

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

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
目录
相关文章
|
6月前
|
存储 人工智能 NoSQL
AI大模型应用实践 八:如何通过RAG数据库实现大模型的私有化定制与优化
RAG技术通过融合外部知识库与大模型,实现知识动态更新与私有化定制,解决大模型知识固化、幻觉及数据安全难题。本文详解RAG原理、数据库选型(向量库、图库、知识图谱、混合架构)及应用场景,助力企业高效构建安全、可解释的智能系统。
|
7月前
|
关系型数据库 MySQL 数据库
阿里云数据库RDS费用价格:MySQL、SQL Server、PostgreSQL和MariaDB引擎收费标准
阿里云RDS数据库支持MySQL、SQL Server、PostgreSQL、MariaDB,多种引擎优惠上线!MySQL倚天版88元/年,SQL Server 2核4G仅299元/年,PostgreSQL 227元/年起。高可用、可弹性伸缩,安全稳定。详情见官网活动页。
1239 152
|
6月前
|
SQL 存储 监控
SQL日志优化策略:提升数据库日志记录效率
通过以上方法结合起来运行调整方案, 可以显著地提升SQL环境下面向各种搜索引擎服务平台所需要满足标准条件下之数据库登记作业流程综合表现; 同时还能确保系统稳健运行并满越用户体验预期目标.
346 6
|
7月前
|
关系型数据库 MySQL 分布式数据库
阿里云PolarDB云原生数据库收费价格:MySQL和PostgreSQL详细介绍
阿里云PolarDB兼容MySQL、PostgreSQL及Oracle语法,支持集中式与分布式架构。标准版2核4G年费1116元起,企业版最高性能达4核16G,支持HTAP与多级高可用,广泛应用于金融、政务、互联网等领域,TCO成本降低50%。
|
7月前
|
关系型数据库 分布式数据库 数据库
阿里云数据库收费价格:MySQL、PostgreSQL、SQL Server和MariaDB引擎费用整理
阿里云数据库提供多种类型,包括关系型与NoSQL,主流如PolarDB、RDS MySQL/PostgreSQL、Redis等。价格低至21元/月起,支持按需付费与优惠套餐,适用于各类应用场景。
|
7月前
|
SQL 关系型数据库 MySQL
Mysql数据恢复—Mysql数据库delete删除后数据恢复案例
本地服务器,操作系统为windows server。服务器上部署mysql单实例,innodb引擎,独立表空间。未进行数据库备份,未开启binlog。 人为误操作使用Delete命令删除数据时未添加where子句,导致全表数据被删除。删除后未对该表进行任何操作。需要恢复误删除的数据。 在本案例中的mysql数据库未进行备份,也未开启binlog日志,无法直接还原数据库。
|
算法 测试技术 C语言
深入理解HTTP/2:nghttp2库源码解析及客户端实现示例
通过解析nghttp2库的源码和实现一个简单的HTTP/2客户端示例,本文详细介绍了HTTP/2的关键特性和nghttp2的核心实现。了解这些内容可以帮助开发者更好地理解HTTP/2协议,提高Web应用的性能和用户体验。对于实际开发中的应用,可以根据需要进一步优化和扩展代码,以满足具体需求。
1244 29
|
前端开发 数据安全/隐私保护 CDN
二次元聚合短视频解析去水印系统源码
二次元聚合短视频解析去水印系统源码
514 4
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
移动开发 前端开发 JavaScript
从入门到精通:H5游戏源码开发技术全解析与未来趋势洞察
H5游戏凭借其跨平台、易传播和开发成本低的优势,近年来发展迅猛。接下来,让我们深入了解 H5 游戏源码开发的技术教程以及未来的发展趋势。

推荐镜像

更多
下一篇
开通oss服务