赞
踩
导航:
本文一些内容需要聚簇索引、非聚簇索引、B+树、覆盖索引、索引下推等前置概念,虽然本文有简单回顾,但详细可以参考下文的【MySQL高级篇】
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
目录
3.3.1.9.没覆盖索引时,is not null、not like无法使用索引
3.3.1.10.“OR”前后存在非索引列或不同索引列,导致索引失效
3.3.5.4 待排序数量大时,尽管索引没失效,索引效率不如filesort
3.4.5 使用 LIMIT N,少用 LIMIT M, N
MySQL调优主要分为三个步骤:监控报警、 排查慢SQL、MySQL调优。
在MySQL调优过程中,首先第一步是发现问题,而发现慢SQL的场景可以是用户访问时查询慢,也可以是通过监控工具监控到慢SQL。
监控工具(例如Prometheus+Grafana)监控MySQL,发现查询性能变慢时,可以报警提醒运维人员:
其他监控工具:
查看慢查询次数:
- # 临时关闭慢查询日志,如果想永久关闭,需要修改my.ini或my.cnf配置文件
- show status like 'slow_queries';
查询慢查询日志是否打开:
SHOW VARIABLES LIKE 'slow_query_log';
开启慢查询日志,修改慢查询阈值:
- set slow_query_log='ON'; #开启慢查询日志
- set long_query_time = 1; #设置慢查询阈值
可以通过mysqldumpslow命令,分析慢查询日志,找到最慢的几条语句:
mysqldumpslow 命令的具体参数如下:
示例: 按照查询时间排序,查看前五条慢查询SQL 语句
#命令行,按照查询时间排序,查看前五条 慢查询SQL 语句 mysqldumpslow -s t -t 5 /var/lib/mysql/xxx-slow.log
explan分析sql执行计划(访问类型、记录条数、索引长度等);主要关注字段:
执行计划各个列的作用
id 每个SELECT子句或者join操作都会被分配一个唯一的编号,编号越小优先级越高,id相同的语句可以被认为是一组。id为NULL表示独立的子查询,子查询优先级都比主查询高。 select_type 查询的类型。主查询(primary)、普通查询(simple)、联合查询、子查询(subquery)、derived(from表临时子查询)、union(union后查询)、union result() table 表名。显示当前这行的数据是哪个表的。 partitions 匹配的分区信息。如果表未分区则为NULL。 type 访问类型,根据索引、全表扫描等方法来执行查询的优化策略。all(全表扫描),ref(命中非唯一索引),const(命中主键/唯一索引)、range(范围索引查询)、index_merge(使用多个索引)、 system(一行记录时,快速查询)。 possible_keys 可能用到的索引。列出MySQL能够使用哪些索引来查询。
如果该列只有一个possible_keys,通常意味着这个查询是高效的。
如果这个列有多个possible_keys,并且MySQL只使用了其中一个,则需要考虑是否需要在该列上增加一个联合索引。
key 实际上使用的索引。如果没有明确的指定KEY,MySQL会根据查询条件自动选择最优的索引。 key_len 实际使用到索引的字节数长度。越短表示越快,一般表示索引字段越小越好。 ref 当使用索引列等值查询时,与索引列进行等值匹配的对象信息。常量等值查询const, 表达式/函数使用到时func,关联查询显示关联字段名 rows 预估的需要读取的记录条数。数值越小越好,表示结果集越小,查询越高效。 filtered 某个表经过搜索条件过滤后剩余记录条数的百分比。这个值越小越好,说明可通过索引直接返回数据。 Extra 额外信息。看有没有走索引,还是全表扫描了。一般搭配type字段看。Using index(使用到覆盖索引)、Using where(使用了回表,在服务器层面(即SQL层)应用WHERE条件过滤记录,而不是在存储引擎层面利用索引进行过滤。)、Using temporary(临时表存储结果集.排序/分组会使用)、Using filesort(排序操作未用索引)、Using join buffer(连接条件未用索引)、Impossible where(where约束语句可能有问题导致没有结果集)
MySQL 8.0引入了explain analyze命令,相比explain,它提供的是实际的查询计划,而explain提供的是预估查询计划。
explain和explain analyze的区别:
示例:
人员表联查部门表:
EXPLAIN ANALYZE SELECT * FROM personnel p LEFT JOIN department d on p.department=d.id查询计划结果:
-> Nested loop left join (cost=915.25 rows=1980) (actual time=0.333..14.500 rows=2453 loops=1) -> Table scan on p (cost=222.25 rows=1980) (actual time=0.283..8.625 rows=2453 loops=1) -> Filter: (p.DEPARTMENT = d.ID) (cost=0.25 rows=1) (actual time=0.002..0.002 rows=1 loops=2453) -> Single-row index lookup on d using PRIMARY (ID=p.DEPARTMENT) (cost=0.25 rows=1) (actual time=0.002..0.002 rows=1 loops=2453)结果分析:
第一行:
Nested loop left join (cost=915.25 rows=1980) (actual time=0.333..14.500 rows=2453 loops=1)
Nested loop left join: 执行的最外层操作,表示使用嵌套循环的左连接。
成本估计: (cost=915.25 rows=1980):预计消耗915.25ms并返回1980行。
实际时间: (actual time=0.333..14.500 rows=2453 loops=1):实际读取第一行平均花费0.333ms,返回所有行平均花费14.500ms,共循环调用该迭代器1次,返回2453行。
第二行:
Table scan on p (cost=222.25 rows=1980) (actual time=0.283..8.625 rows=2453 loops=1)
Table scan on p: 对人员表的全表扫描。
成本估计: (cost=222.25 rows=1980):预计消耗222.25ms并返回1980行。
实际时间: (actual time=0.283..8.625 rows=2453 loops=1):实际读取第一行平均花费0.283ms,返回所有行平均花费8.625ms,共循环调用该迭代器1次,返回2453行。
第三行:
Filter: (p.DEPARTMENT = d.ID) (cost=0.25 rows=1) (actual time=0.002..0.002 rows=1 loops=2453)
Filter: (p.DEPARTMENT = d.ID): 执行对 md_gams_jc_department 表中 p.DEPARTMENT = d.ID 条件的过滤操作。
成本估计: (cost=0.25 rows=1):预计消耗0.25ms并返回1行。
实际时间: (actual time=0.002..0.002 rows=1 loops=2453):实际过滤操作平均花费0.002ms,共循环调用该迭代器2453次,返回1行。
第四行:
Single-row index lookup on d using PRIMARY (ID=p.DEPARTMENT) (cost=0.25 rows=1) (actual time=0.002..0.002 rows=1 loops=2453)
Single-row index lookup on d using PRIMARY (ID=p.DEPARTMENT): 对部门表使用主键索引进行单行查找,其中 ID=p.DEPARTMENT。
成本估计: (cost=0.25 rows=1):预计消耗0.25ms并返回1行。
实际时间: (actual time=0.002..0.002 rows=1 loops=2453):实际查找操作平均花费0.002ms,共循环调用该迭代器2453次,返回1行。
缓冲池:MySQL的缓冲池被分为多个不同的缓存池,其中包括:
缓冲池是主内存中的一部分空间,用来缓存已使用的表和索引数据。缓冲池使得经常被使用的数据能够直接在内存中获得,从而提高速度。
缓冲池的淘汰策略:
LRU算法。MySQL的缓冲池默认使用的是LRU(最近最少使用)淘汰策略,它会优先缓存最近使用的数据。当缓冲池的空间不足时,MySQL会将最不常用的数据从缓冲池中替换出去,以腾出空间缓存新的数据。
lru算法底层原理:
底层是双向链表(因为经常要移动元素),链表首部是最常使用元素,尾部是最少使用元素。
每次刚访问的数据会移动到链表首部,刚添加的数据也会添加到链表首部。超出maxmemory会淘汰链表尾部元素,它也最长时间没有被使用的数据。
缓冲池相关参数:
MySQL的缓存设置包括多个参数,其中比较常见的缓存参数包括以下几个:
MyISAM缓冲池大小:key_buffer_size:该参数用来设置MyISAM索引的缓存大小。如果应用程序中涉及到大量的索引查询,可以适当提高该值。一般来说,key_buffer_size占用总内存的1/4到1/3比较合适。
InnoDB缓冲池大小:innodb_buffer_pool_size:该参数用来设置InnoDB缓冲池的大小。InnoDB存储引擎使用缓冲池来缓存数据和索引文件。如果InnoDB表的读写频次较高,建议将该值设置为物理内存的70%到80%。
排序缓冲区大小:sort_buffer_size:该参数用来设置排序缓冲区大小。如果查询中涉及到ORDER BY或GROUP BY操作,可以适当提高该值。一般来说,sort_buffer_size占用总内存的1/4到1/3比较合适。
读取缓冲区大小:read_buffer_size和read_rnd_buffer_size:这两个参数是用来设置读取缓冲区大小的,默认值为128 KB。如果应用程序中经常进行大文件的读取操作,可以适当提高这两个参数。
binlog大小:binlog_cache_size:该参数是用来设置二进制日志的缓存大小。如果应用程序中需要持久化一些数据,可以开启二进制日志,并适当调整该参数。
参数配置方法:
1.查看当前缓冲池参数:
show VARIABLES like 'key_buffer_size';
2.修改缓冲池参数:
SET GLOBAL key_buffer_size = 67108864; -- 64MB
- [mysqld]
- key_buffer_size = 64M
Redis是一个基于内存的NoSQL数据库,MySQL是一个基于磁盘的关系型数据库。
我们知道,内存的读写速度是远高于磁盘的,所以对于一些多读少写的热点数据,搭配Redis存储数据,可以极大地提高数据的访问速度。
Redis特点:
- 数据库:Redis是一款基于键值对的、线程安全的NoSQL数据库;
- 内存读写性能:它在内存中读写性能非常高,每秒可以处理超过百万次的读写操作。
- 服务端线程安全,客户端线程不安全:Redis服务端是线程安全的,永远只有主线程一个线程进行读写,不需要任何的同步机制。虽然Redis6.0增加了多线程的模型,但多线程目的只是为了处理网络的IO事件,读写指令的执行依然由主线程自己处理。Redis客户端层面线程不安全,要引入原子指令(例如INCR是给数值原子性加1)、分布式锁、lua脚本保证Redis的原子操作。
Redis读写为什么不采用多线程?
- CPU不是瓶颈:Redis在内存中读写性能非常高,CPU不是Redis的瓶颈,无需使用多线程。
- 担心加锁影响性能:多线程情况下,想实现线程安全必须加锁,加锁将极大地影响性能。
为什么单线程还读写性能这么高?
- 基于内存:Redis是基于内存的,内存的读写速度非常快;
- 上下文切换:单线程避免了不必要的上下文切换和竞争条件;
- IO多路复用:底层采用NIO(非阻塞IO),NIO采用IO多路复用技术,一个线程通过多路复用器处理多个连接。IO多路复用技术选用epoll调用模型,红黑树存所有事件,链表存就绪事件。epoll_wait函数链表,通知应用程序读写操作。
Redis的瓶颈:
- 内存:因为读写在内存中进行,内存大小会影响Redis性能。可以通过加内存、读写分离优化性能。
- 网络带宽:网络 IO是Redis最大瓶颈,也就是客户端和服务端之间的网络传输延迟。Redis6.0引入了网络IO多线程模型,提高了性能瓶颈。
功能:键过期、事务、lua脚本(基于C语言,性能快)、持久化机制。
事务:
- 实现方式:MULTI(开启事务,将命令都放进队列里),EXEC(执行事务),DISCARD(取消事务,清空队列)。
- 不支持回滚:在语法正确的情况下,Redis事务一定会执行成功。只有语法错误时才会导致事务失败,而语法问题应该在开发时就避免,所以为了提高性能,Redis事务不支持回滚。事务是一个原子操作,要么全部执行,要么全不执行。
- 不完全满足ACID特性:Redis只满足隔离性和持久性,不满足原子性和一致性。
- 原子性:事务的所有操作,要么全部成功,要么全部失败。Redis不满足原子性,单个 Redis 命令的执行是原子性的,但事务失败后无法回滚。
- 一致性:事务前后,数据库的约束没有被破坏,保持前后一致。Redis连约束这个概念都没有。
- 隔离性:操作同一资源的并发事务之间相互隔离,不会互相干扰。Redis满足隔离性,因为redis server是单线程的,串行化执行事务,肯定是满足隔离性的。
- 持久性:事务的结果最终一定会持久化到数据库,宕机等故障也无法影响。Redis在开启aof并指定立刻持久化命令时,满足持久性。rdb模式会丢失部分数据,不满足持久性。
数据类型: string、hash、 list、set(集合)、zset(有序集合)
应用场景:缓存热点且不经常修改的数据、计数器、限时业务、分布式锁(set nx)、队列等。
持久化机制:
- 数据备份机制RDB(默认):数据每隔一段时间写进磁盘rdb文件,故障后从文件读。可以在redis.conf配置多少秒内多少key修改时自动bgsave。占CPU和内存但恢复快,不能恢复完整数据。save命令是主进程立即执行一次RDB,其他所有命令进程阻塞。bgsave是子进程fork主进程,阻塞并拷贝一份主进程的页表(虚拟内存到物理内存的映射关系),然后子进程写数据到rdb文件,主进程继续处理用户请求,
- 追加文件机制AOF:命令日志按指定频率(默认立刻,在redis.conf配置为缓存一秒)写进磁盘aof文件,可以按条件(redis.conf配置,比上次重写aof文件超过多少百分比时自动重写、aof文件超过多大自动重写)自动重写aof文件中的命令(多次更新同一数据只有最近一次更新有效),故障后从文件读命令恢复数据。不占CPU和内存占IO,能恢复完整或故障1s前的数据但恢复慢。
在很多情景下,我们已经对数据库的索引、参数等数据进行了优化,但当数据库并发量高、数据量大时,就需要考虑优化服务器配置、分库分表等方案,直观地提高数据库的性能。
优化方案:
除了以上说的缓冲池参数外,我们还可以通过其他参数,对MySQL进行优化。
优化方案:
- # 临时关闭慢查询日志,如果想永久关闭,需要修改my.ini或my.cnf配置文件
- SET GLOBAL slow_query_log = 'OFF';
对于不再使用的表、数据、日志、缓存等,应该及时清理,避免占用过多的MySQL资源,从而提高MySQL的性能。
- # 删除这些表:
- DROP TABLE table_name;
- # 保留表结构但删除所有数据
- delete from table_name;
一些场景下,某个时期之前的数据都不再需要,可以清理这些数据:
DELETE FROM table_name WHERE createTime < NOW() - INTERVAL 30 DAY;
创建一个MySQL事件来定期清理过期数据:
- CREATE EVENT clean_up_event
- ON SCHEDULE EVERY 1 DAY
- DO
- DELETE FROM table_name WHERE created_at < NOW() - INTERVAL 30 DAY;
- # 清理2023年之前的日志
- PURGE BINARY LOGS BEFORE '2023-01-01 00:00:00';
RESET QUERY CACHE;
或者修改配置文件,禁用查询缓存以避免潜在的性能问题:
- [mysqld]
- query_cache_type = 0
- query_cache_size = 0
在 MySQL 数据库中,OPTIMIZE TABLE 是一个重要的命令,用于优化表的性能和空间利用。通过重新组织表的存储结构,去除碎片、重建索引,OPTIMIZE TABLE 可以帮助提高查询性能、减少存储空间占用以及减少数据碎片。
OPTIMIZE TABLE命令:
OPTIMIZE TABLE table_name;
优化原理:
删除delete语句留下来的垃圾碎片。使用delete语句删除数据时,delete语句只会将记录的位置或者数据页标记为"可复用",但是数据库磁盘文件的大小不会改变,即表空间不会被回收,此时使用该命令可以释放空间,压缩数据文件。
底层原理:
执行OPTIMIZE TABLE命令后,MySQL会进行以下几个步骤:
MySQL 的Optimizer(优化元件)在优化SQL语句时,首先需要收集一些相关信息,其中就包括表的cardinality(散列程度),它表示某个索引对应的列包含多少个不同的值——如果cardinality大大少于数据的实际散列程度,那么索引就基本失效了。
ANALYZE TABLE命令:
ANALYZE TABLE table_name;
对不同存储引擎的效果:
对于以上的清理垃圾方案,可以写一个定时任务,定期统一清理垃圾数据、优化表的存储空间和索引。
方案一:创建cron作业
1.编辑crontab
使用crontab命令编辑当前用户的cron作业列表。对于系统级别的作业,可以使用sudo运行crontab。
crontab -e
或者,为特定用户编辑cron作业:
sudo crontab -u username -e
2.编写cron作业
在打开的编辑器中,添加新的cron作业,每行代表一个作业,执行指定路径下的脚本:
0 2 * * * /path/to/cleanup_script.sh
cron表达式格式如下:
* * * * * command-to-be-executed - - - - - | | | | | | | | | +----- Day of the week (0 - 7) (Sunday=0 or 7) | | | +------- Month (1 - 12) | | +--------- Day of the month (1 - 31) | +----------- Hour (0 - 23) +------------- Minute (0 - 59)
3.示例清理脚本
/path/to/cleanup_script.sh
- #!/bin/bash
-
- # MySQL credentials
- USER="your_username"
- PASSWORD="your_password"
- DATABASE="your_database"
-
- # 清理过期数据
- mysql -u $USER -p$PASSWORD -e "DELETE FROM table_name WHERE created_at < NOW() - INTERVAL 30 DAY;" $DATABASE
-
- # 清理二进制日志
- mysql -u $USER -p$PASSWORD -e "PURGE BINARY LOGS BEFORE NOW() - INTERVAL 7 DAY;"
-
- # 优化表
- mysql -u $USER -p$PASSWORD -e "OPTIMIZE TABLE table_name;" $DATABASE
方案二:使用Spring定时任务
spring根据定时任务的特征,将定时任务的开发简化到了极致。怎么说呢?要做定时任务总要告诉容器有这功能吧,然后定时执行什么任务直接告诉对应的bean什么时间执行就行了,就这么简单,一起来看怎么做
步骤①:开启定时任务功能,在引导类上开启定时任务功能的开关,使用注解@EnableScheduling
- @SpringBootApplication
- //开启定时任务功能
- @EnableScheduling
- public class Springboot22TaskApplication {
- public static void main(String[] args) {
- SpringApplication.run(Springboot22TaskApplication.class, args);
- }
- }
步骤②:在task包下定义Bean,在对应要定时执行的操作上方,使用注解@Scheduled定义执行的时间,执行时间的描述方式还是cron表达式
- @Component
- public class MyBean {
- @Scheduled(cron = "0/1 * * * * ?")
- public void print(){
- // 具体清理垃圾的逻辑
- }
- }
如何想对定时任务进行详细配置,可以通过配置文件进行
- spring:
- task:
- scheduling:
- pool:
- size: 1 #任务调度线程池大小 默认 1
- thread-name-prefix: ssm_ #调度线程名称前缀 默认 scheduling-
- shutdown:
- await-termination: false #线程池关闭时等待所有任务完成
- await-termination-period: 10s #调度线程关闭前最大等待时间,确保最后一定关闭
总结
spring task需要使用注解@EnableScheduling开启定时任务功能
为定时执行的的任务设置执行周期,描述方式cron表达式
详细参考:
InnoDB:支持外键和事务,行锁适合高并发,缓存索引和数据,内存要求高(因为要缓存索引和记录),适合存大数据量,增删改性能更优(行级锁高并发),耗费磁盘(因为有多个非聚簇索引,索引可能比记录空间还大)。
B+每个元素的结构:
InnoDB叶节点存数据各列的值:
因为聚簇索引树数据页存的元素第一个值是主键,而且聚簇索引树是默认创建的,所以如果我没有另外创建索引时,或者创建了其他索引但没发生覆盖索引时,用主键查询是最快的,例如select * from student where id=xx快过select * from student where age=xx。
当然聚簇索引并不一定是最快的,例如给age字段创建了索引,那么虽然select * from
student where id=xx快过select * from student where age=xx(因为要回表聚簇索引树查所有字段),但是select age from student where age=xx快过select age from student where id=xx,因为发生了覆盖索引,直接在非聚簇索引树就查到了age,没必要再回表聚簇索引树查其他字段信息。
MyISAM:不支持外键和事务,表锁不适合高并发,缓存索引和数据地址,内存要求低(因为不用缓存记录),查询性能更优(因为查询时InnoDB要维护MVCC一致,而且多缓存了记录),节省磁盘(因为磁盘不存完整记录)。
对比 | InnoDB | MyISAM |
特点 | 支持外键和事务 | 不支持外键和事务 |
行表锁 | 行锁,操作时只锁某一行,不对其它行有影响, 适合高并发的操作 | 表锁,即使操作一条记录也会锁住整个表,不适合高并发的操作 |
缓存 | 缓存索引和数据,对内存要求较高,而且内存大小对性能有决定性的影响 | 只缓存索引,不缓存真实数据 |
关注点 | 事务:并发写、事务、更大资源 | 性能:节省资源、消耗少、简单业务、查询快 |
默认使用 | 5.5及其之后 | 5.5之前 |
读写分离:读写分离能有效提高查询性能。读写分离基于MySQL的主从同步,一台主库负责写,多台从库负责读,每次主库发生写操作后,通过binlog和relay log,将修改操作同步到从库,从而保持主库和从库的数据一致性。
主从同步:一台或多台MySQL数据库(slave,即从库)从另一台NySQL数据库(master,即主库)进行日志的复制,然后再解析日志并应用到自身,最终实现从库的数据和主库的数据保持一致。MySQL主从复制是NySQL数据库自带功能,无需借助第三方工具。
主从同步实现步骤:
主从同步的延时问题;
- [mysqld]
- # 多线程复制(MySQL 5.7及以上版本):设置并行线程数
- slave_parallel_workers = 4
- # 开启半同步复制(MySQL 5.5及以上版本)
- rpl_semi_sync_master_enabled = 1
- rpl_semi_sync_slave_enabled = 1
- # 同步延时时间:1秒超时。如果频繁写,则适当缩小;如果频繁读少写,则适当增大,以降低服务器压力
- rpl_semi_sync_master_timeout = 1000
复制原理:
主从库数量:
概念:
拆分原则:
数据量增长情况 | 数据表类型 | 优化核心思想 |
---|---|---|
数据量为千万级,是一个相对稳定的数据量 | 状态表 | 能不拆就不拆读需求水平扩展 |
数据量为千万级,可能达到亿级或者更高 | 流水表 | 业务拆分,面向分布式存储设计 |
数据量为千万级,可能达到亿级或者更高 | 流水表 | 设计数据统计需求存储的分布式扩展 |
数据量为千万级,不应该有这么多的数据 | 配置表 | 小而简,避免大一统 |
分库分表步骤:
MySQL调优:数据量能稳定在千万级,近几年不会到达亿级,其实是不用着急拆的,先尝试MySQL调优,优化读写性能。
目标评估:评估拆几个库、表,举例: 当前20亿,5年后评估为100亿。分几个表? 分几个库?解答:一个合理的答案,1024个表,16个库按1024个表算,拆分完单表200万,5年后为1000万.1024个表*200w≈100亿
表拆分:
业务层拆分:混合业务拆分为独立业务、冷热分离
数据层拆分:
按日期拆分:这种使用方式比较普遍,尤其是按照日期维度的拆分,其实在程序层面的改动很小,但是扩展性方面的收益很大。例如
按主键范围拆分:例如【1,200w】主键在一个表,【200w,400w】主键在一个表。优点是单表数据量可控。缺点是流量无法分摊,写操作集中在最后面的表。
中间表映射:表随意拆分,引入中间表记录查询的字段值,以及它对应的数据在哪个表里。优点是灵活。确定是引入中间表让流程变复杂。
hash切分:sharding_key%N。优点是数据分片均匀,流量分摊。缺点是扩容需要迁移数据,跨节点查询问题。例如日志表拆分成logs_0、logs_1、logs_2,每次读写日志时,将用户的id转成md5哈希值,然后%3,就可以得出具体要在哪个日志表查询。
按分区拆分:hash,range等方式。不建议,因为数据其实难以实现水平扩展。
sharding_key(分表字段)选择:尽量选择查询频率最高的字段,然后根据表拆分方式选择字段。
代码改造:修改代码里的查询、更新语句,以便让其适应分库分表后的情况。
数据迁移:最简单的就是停机迁移,复杂点的就是不停机迁移,要考虑增量同步和全量同步的问题。
全量同步:老库到新库的数据迁移,要控制好迁移效率,解决增量数据的一致性。
增量同步:老库迁移到新库期间,新增删改命令的落库不能出错
数据一致性校验和补偿:假设采用异步双写方案,在迁移完成后,逐条对比新老库数据,一致则跳过,不一致则补偿:
灰度切读:灰度发布指黑(旧版本)与白(新版本)之间,让一些用户继续用旧版本,一些用户开始用新版本,如果用户对新版本没什么意见,就逐步把所有用户迁移到新版本,实现平滑过渡发布。原则:
停老用新:下线老库,用新库读写。
混合业务分表:
根据业务逻辑,将不同的业务数据分开存储在不同的表中。每个业务模块的数据单独存储,减少了单表的大小和查询的复杂度。
冷热数据分表:
将频繁访问的“热数据”和不常访问的“冷数据”分开存储。这种策略有助于提高热数据的查询性能,并且在存储和备份方面更加灵活。
对于复杂的数据库设计,使用关系表是一种常见的方法,特别是在多对多的关系中,例如学生表和课程表、商品表和商品属性表、用户和角色表、作者和书籍表、部门和人员表。
示例:
部门表 (departments):
人员表 (employees):
部门员工关联表 (department_employees):
关系表的优点:
数据库三范式:
①可能查询出现空指针问题;
②导致聚合函数不准确,因为它会忽略null
③不能用“=”判断,只能用is null判断;
④null和其他值运算只能是null,可能让你不小心把它当成0;
⑤null值比空字符更占用空间,空值长度是0,null长度是1bit;
⑥不覆盖索引情况下,is not null无法用索引
反范式:为提高查询效率,可添加不常更新的字段为冗余字段。
反范式化是数据库设计的一种策略,通过在设计中引入冗余数据来提高查询性能,简化查询操作,或满足其他业务需求。
使用场景:
将多读少写的字段,增加为冗余字段,从而不再需要每次都连表查询这个字段。需要注意每次数据更新时必须同步这个冗余字段。
示例:
部门表 (departments):
人员表 (employees):
部门员工关联表 (department_employees):
注意:
增加冗余字段后,当这个冗余字段对应的数据改动后,必须同步更改这个冗余字段。
例如成绩表除了有student_id字段外,增加冗余字段student_name,因为学生名基本不会变化,但在修改姓名的接口里,修改学生名后要同步修改成绩表的student_name字段。
整数类型:
考虑好数值范围,前期可以使用int保证稳定性。非负数类型要用UNSIGNED;同样字节数,存储的数值范围更大。主键一般使用bigint,布尔类型tinint
能整数就不要用文本类型:
跟文本类型数据相比,大整数往往占用更少的存储空间。
避免使用TEXT、BLOB数据类:
这两个大数据类型,排序时不能使用临时内存表,只能使用磁盘临时表,效率很差,建议别用,或分表到单独扩展表里。LongBlob类型能存储4G文件;
避免使用枚举类型:
因为枚举类型排序很慢。
使用TIMESTAMP存储时间:
TIMESTAMP使用4字节,DATETIME使用8个字节,同时TIMESTAMP具有自动赋值以及自动更新的特性。 缺点是只能存到2038年,MySQL5.6.4版本可以参数配置,自动修改它为BIGINT类型。
DECIMAL存浮点数:
Decimal类型为精准浮点数,在计算时不会丢失精度,尤其是财务相关的金融类数据。占用空间由定义的宽度决定,每4个字节可以存储9位数字,并且小数点要占用一个字节。可用于存储比bigint更大的整型数据。
本文使用MySQL版本:
5.7.41
学员表插 50万 条,班级表 插 1万 条。
步骤0:建库
- CREATE DATABASE test;
- USE test;
步骤1:建表
- CREATE TABLE `class` (
- `id` INT(11) NOT NULL AUTO_INCREMENT,
- `className` VARCHAR(30) DEFAULT NULL,
- `address` VARCHAR(40) DEFAULT NULL,
- `monitor` INT NULL ,
- PRIMARY KEY (`id`)
- ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-
- CREATE TABLE `student` (
- `id` INT(11) NOT NULL AUTO_INCREMENT,
- `stuno` INT NOT NULL ,
- `name` VARCHAR(20) DEFAULT NULL,
- `age` INT(3) DEFAULT NULL,
- `classId` INT(11) DEFAULT NULL,
- PRIMARY KEY (`id`)
- #CONSTRAINT `fk_class_id` FOREIGN KEY (`classId`) REFERENCES `t_class` (`id`)
- ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
步骤2:设置参数
set global log_bin_trust_function_creators=1; # 不加global只是当前窗口有效。
步骤3:创建函数
保证每条数据都不同。
- DELIMITER //
-
- CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255)
- DETERMINISTIC
- NO SQL
- BEGIN
- DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
- DECLARE return_str VARCHAR(255) DEFAULT '';
- DECLARE i INT DEFAULT 0;
- WHILE i < n DO
- SET return_str = CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));
- SET i = i + 1;
- END WHILE;
- RETURN return_str;
- END //
-
- DELIMITER ;
随机产生班级编号
- DELIMITER //
-
- CREATE FUNCTION rand_num (from_num INT ,to_num INT) RETURNS INT(11)
- DETERMINISTIC
- NO SQL
- BEGIN
- DECLARE i INT DEFAULT 0;
- SET i = FLOOR(from_num +RAND()*(to_num - from_num+1)) ;
- RETURN i;
- END //
-
- DELIMITER ;
步骤4:创建存储过程
- #创建往stu表中插入数据的存储过程
- DELIMITER //
- CREATE PROCEDURE insert_stu( START INT , max_num INT )
- BEGIN
- DECLARE i INT DEFAULT 0;
- SET autocommit = 0; #设置手动提交事务
- REPEAT #循环
- SET i = i + 1; #赋值
- INSERT INTO student (stuno, name ,age ,classId ) VALUES
- ((START+i),rand_string(6),rand_num(1,50),rand_num(1,1000));
- UNTIL i = max_num
- END REPEAT;
- COMMIT; #提交事务
- END //
- DELIMITER ;
- #假如要删除
- #drop PROCEDURE insert_stu;
创建往class表中插入数据的存储过程
- #执行存储过程,往class表添加随机数据
- DELIMITER //
- CREATE PROCEDURE `insert_class`( max_num INT )
- BEGIN
- DECLARE i INT DEFAULT 0;
- SET autocommit = 0;
- REPEAT
- SET i = i + 1;
- INSERT INTO class ( classname,address,monitor ) VALUES
- (rand_string(8),rand_string(10),rand_num(1,100000));
- UNTIL i = max_num
- END REPEAT;
- COMMIT;
- END //
- DELIMITER ;
- #假如要删除
- #drop PROCEDURE insert_class;
步骤5:调用存储过程
class
- #执行存储过程,往class表添加1万条数据
- CALL insert_class(10000);
stu
- #执行存储过程,往stu表添加50万条数据
- CALL insert_stu(100000,500000);
创建删除索引的存储过程
- DELIMITER //
- CREATE PROCEDURE `proc_drop_index`(dbname VARCHAR(200),tablename VARCHAR(200))
- BEGIN
- DECLARE done INT DEFAULT 0;
- DECLARE ct INT DEFAULT 0;
- DECLARE _index VARCHAR(200) DEFAULT '';
- DECLARE _cur CURSOR FOR SELECT index_name FROM
- information_schema.STATISTICS WHERE table_schema=dbname AND table_name=tablename AND
- seq_in_index=1 AND index_name <>'PRIMARY' ;
- #每个游标必须使用不同的declare continue handler for not found set done=1来控制游标的结束
- DECLARE CONTINUE HANDLER FOR NOT FOUND set done=2 ;
- #若没有数据返回,程序继续,并将变量done设为2
- OPEN _cur;
- FETCH _cur INTO _index;
- WHILE _index<>'' DO
- SET @str = CONCAT("drop index " , _index , " on " , tablename );
- PREPARE sql_str FROM @str ;
- EXECUTE sql_str;
- DEALLOCATE PREPARE sql_str;
- SET _index='';
- FETCH _cur INTO _index;
- END WHILE;
- CLOSE _cur;
- END //
- DELIMITER ;
执行存储过程
CALL proc_drop_index("数据库名","表名");
练习:删除student表的所有索引:
CALL proc_drop_index("test","student");
验证:
创建索引:
CREATE INDEX idx_stuno_age on student(stuno,age);查看索引:
SHOW INDEX FROM student;
删除所有索引:
CALL proc_drop_index("test","student");
再次查看索引:
SHOW INDEX FROM student;
详细请参考:
全值匹配指的是查询条件完全匹配索引中的所有列。例如,如果一个索引包含列 A
和 B
,那么查询条件应该包括这两个列才能完全匹配这个索引。
查询age and classId and name时,(age,classId,name)索引比(age,classId)快。
验证:
创建联合索引
CREATE INDEX idx_stuno_age on student(stuno,age);
全值匹配:
EXPLAIN SELECT * FROM student WHERE stuno=1 and age=2;
可以看到type是ref,即命中非唯一索引。
联合索引把频繁查询的列放左。例如索引(a,b,c),只能查(a,b,c),(a,b),(a)。
验证:
保持创建上面idx_stuno_age联合索引
没符合最佳左前缀原则:
EXPLAIN SELECT * FROM student WHERE age=2;
可以看到type是all,即全表扫描。
如果主键不有序,需要查找目标位置再插入,并且如果目标位置所在数据页满了就必须得分裂页,造成性能损耗。可以选择自增策略或MySQL8.0有序UUID策略。
计算:
当对索引列进行计算时,MySQL 不能使用索引。因为索引存储的是列的原始值,而计算改变了这些值。
验证:
EXPLAIN SELECT * FROM student WHERE stuno+1=2
可以看到type是all,全表扫描,没有走索引。
如果是等号右边的计算,则索引不受影响,依然生效,因为这个计算预先就能计算得出:
EXPLAIN SELECT * FROM student WHERE stuno=2+1
可以看出type是ref,命中非唯一索引:
函数:
在索引列上使用函数会导致索引失效,因为 MySQL 不能预先计算索引列的函数值并存储在索引中。
验证:
EXPLAIN SELECT * FROM student WHERE abs(stuno)=3
可以看到type是all,全表扫描,没有走索引。
如果是等号右边的函数,则索引不受影响,依然生效,因为这个计算预先就能计算得出:
EXPLAIN SELECT * FROM student WHERE stuno=abs(2.3)
可以看出type是ref,命中非唯一索引:
隐式转换导致索引失效:
当查询条件中的数据类型与索引列的数据类型不匹配时,MySQL 会进行隐式类型转换。这种类型转换会导致 MySQL 无法利用索引,从而导致全表扫描,影响查询性能。
使用不同字符集时,也会触发类型隐式转换,导致索引失效。
注意:MySQL官方文档有提到:
In all other cases, the arguments are compared as floating-point (double-precision) numbers. For example, a comparison of string and numeric operands takes place as a comparison of floating-point numbers.
也就是说,对比的时候,MySQL 自身会有一套基本的规则来对应不同类型数据的比较,而字符串与数字的对比中,字符串会被转换成双精度浮点型数字之后再进行对比。
例如name=123,而不是name='123',则会导致索引失效。
而stuno='12abc3',而不是stuno=123,则并不会导致索引失效。
验证:
保持创建索引idx_stuno_age、idx_name
EXPLAIN SELECT * FROM student WHERE name = '123';
可以看到type是ref,这是走索引的。
而下面触发类型隐式转换,type是all,全表扫描,没有走索引(虽然字符串可以转数字,但数字不能转字符串,会导致索引失效):
EXPLAIN SELECT * FROM student WHERE name = 123;
显式转换导致索引失效:
显式类型转换是指在查询条件中使用 CAST 或 CONVERT 函数将数据类型转换为匹配索引列的数据类型。这种方式同样会导致索引失效。
验证:
给name字段创建索引:
CREATE INDEX idx_name on student(name);
正常查询name会走索引,type是ref,命中非唯一索引:
EXPLAIN SELECT * FROM student WHERE name = '123';
将name类型转换成char,type是all,全表扫描:
当查询条件使用了范围条件(例如 >、<、>=、<=、BETWEEN)时,会导致索引右边的列失效,因为范围查询会阻止 MySQL 使用复合索引的后续列。
例如:(a,b,c)联合索引,查询条件a,b,c,如果b使用了范围查询,那么b右边的c索引失效。所以建议把需要范围查询的字段放在最后。
索引下推:
索引下推(ICP,Index Condition Pushdown)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优化方式。它可以使范围查询右侧索引失效的列,依然可以在索引树上过滤,而不需要回表。
如果开启了索引下推,范围条件右边的列实际上是索引失效的,但是可以直接在这颗联合索引树上直接过滤右边的这些字段。
例如:索引(name,age),查询name like 'z%' and age and address,'z%'是模糊查询,实际上也是范围查询。使用索引下推时,在联合索引树查询时不止查name,还会判断后面的age。而如果关闭了索引下推,联合索引里范围查询后面的字段age不能在联合索引树里直接条件判断,必须回表到主键索引树后,以之前过滤结果id查找到对应数据,再过滤age列。
注意:这里举例没有使用'%z',因为左模糊查询会使整个索引失效,也就不会用到索引下推了。
验证:
继续使用idx_stuno_age索引
关闭索引下推,范围查询右侧的列索引失效:
SET optimizer_switch='index_condition_pushdown=off'; EXPLAIN SELECT * FROM student WHERE stuno > 123 and stuno<129 and age=2;可以看到,type是range,即范围查询,extra是using where,即用到了回表,age字段是根据name过滤结果的id,回表到主键索引数过滤的:
打开索引下推,范围查询右侧的列依然在索引树过滤:
SET optimizer_switch='index_condition_pushdown=on'; EXPLAIN SELECT * FROM student WHERE stuno > 123 and stuno<129 and age=2;对age字段使用范围查询,发现type是range,即命中范围查询,extra是using index condition,即使用了索引下推。因为age在联合索引里,所以直接在联合索引所在的非聚簇索引树中即可完成过滤 age=2,而不需要回表:
tip:范围查询过滤量过小时,查询优化器连范围索引也不用:
对stuno使用范围查询stuno>123,发现type是all,即全表扫描。因为stuno>123的记录有50w条,索引直接全表扫描。:
EXPLAIN SELECT * FROM student WHERE stuno > 123 and stuno<129 and age=2;
因为“不等于”不能精准匹配,全表扫描二级索引树再回表效率不如直接全表扫描聚簇索引树。但使用覆盖索引时,联合索引数据量小,加载到内存所需空间比聚簇索引树小,且不需要回表(因为直接在非聚簇索引树全表扫描,比在聚簇索引树全表扫描要快),索引效率优于全表扫描聚簇索引树。
覆盖索引:一个索引包含了满足查询结果的数据就叫做覆盖索引,不需要回表等操作。
聚簇索引和非聚簇索引:
验证:
没覆盖索引,不等于导致索引失效:
EXPLAIN SELECT * FROM student WHERE stuno <>3;
type是all,全表扫描
有覆盖索引,即使不等于,索引依然生效,因为只查这两个字段,直接在联合索引树上全表扫描,肯定比在主键索引树上全表扫描快。
EXPLAIN SELECT stuno FROM student WHERE stuno <>3;
type是range,即范围查询,extra是using index,即用到了覆盖索引,extra又是using where,即因为不等于导致索引失效,所以在联合索引树上进行了全表扫描:
覆盖索引+走索引:
EXPLAIN SELECT stuno FROM student WHERE stuno =3;
type是ref,命中非唯一索引,extra是using index,即使用了覆盖索引
因为“左模糊查询”不能精准匹配,全表扫描二级索引树再回表效率不如直接全表扫描聚簇索引树。但使用覆盖索引时,联合索引数据量小,加载到内存所需空间比聚簇索引树小,且不需要回表(因为直接在非聚簇索引树全表扫描,比在聚簇索引树全表扫描要快),索引效率优于全表扫描聚簇索引树。
覆盖索引:一个索引包含了满足查询结果的数据就叫做覆盖索引,不需要回表等操作。
例如LIKE '%abc'。因为字符串开头都不能精准匹配。
验证:
跟上面范围查询导致索引失效同理,此处不再赘述。
因为“is not null、not like”不能精准匹配,全表扫描二级索引树再回表效率不如直接全表扫描聚簇索引树。但使用覆盖索引时,联合索引数据量小,加载到内存所需空间比聚簇索引树小,且不需要回表(因为直接在非聚簇索引树全表扫描,比在聚簇索引树全表扫描要快),索引效率优于全表扫描聚簇索引树。
覆盖索引:一个索引包含了满足查询结果的数据就叫做覆盖索引,不需要回表等操作。
验证:
跟上面范围查询导致索引失效同理,此处不再赘述。
or前后是不同索引列时,整个语句的索引失效。例如a=x or b=x
or前后是同一索引列时,命中范围索引。例如a=x or a=xx
or前后是不同或相同的非索引列时,索引失效。例如联合索引(a,b),查询a=x and (c=x or c=xx)
验证:
清除并重新创建索引:
CALL proc_drop_index("test","student"); CREATE INDEX idx_stuno_age_classid on student(stuno,age,classid);or前后是不同列时,索引就一定会失效:
EXPLAIN SELECT * FROM student WHERE stuno =3 and age=2 or classid=3
可以看到type是all,全表扫描,即使stuno和age都在联合索引树的前两级:
or前后是同一列时,命中范围索引:
EXPLAIN SELECT * FROM student WHERE stuno =3 or stuno=2 and age=3;
type是range,extra是索引下推。索引下推就是把stuno=3和stuno=2,两个条件其中一个条件在联合索引树上走索引判断,另一个条件在此树的过滤结果基础上再过滤,而不需要在MySQL服务器上再回表过滤。
or前后是同一列时,命中范围索引,它后面的列依然在联合索引树上判断:
EXPLAIN SELECT * FROM student WHERE stuno =3 or stuno=2 and age=3;
key_len是9,说明age也继续在联合索引树上走索引:
or前后是非索引列,索引失效:
EXPLAIN SELECT * FROM student WHERE name ='3' or name='3';
可以看到全表扫描:
建议utf8mb4,不同的字符集进行比较前需要进行 转换 会造成索引失效。
例如创建表时给不同字段设置不同字符集,再判断这些字段时会导致索引失效:
例如下面表,给name_utf8和name_latin1创建联合索引,再where name_utf8=name_latin1时会导致索引失效:
CREATE TABLE student2 ( id INT(11) NOT NULL AUTO_INCREMENT, name_utf8 VARCHAR(20) CHARACTER SET utf8 DEFAULT NULL, name_latin1 VARCHAR(20) CHARACTER SET latin1 DEFAULT NULL, ) ENGINE=InnoDB;
详细请参考:
详细请参考:
外连接查询时,右表就是被驱动表,建议加索引。
原因:因为MySQL连接查询底层是先查过滤条件下的左表,按左表查询的结果,再以连接字段作为条件查询结果,所以右表的连接字段加索引,将极大地提高查询性能。
驱动表与被驱动表:
- 驱动表:在连接操作中,驱动表是首先被读取的表。MySQL会从驱动表中读取数据行,然后在被驱动表中寻找匹配的行。
- 被驱动表:被驱动表是连接操作中第二个被读取的表。对于驱动表中的每一行,MySQL会在被驱动表中寻找匹配的行。
验证:
被驱动表连接字段没索引:
下面SQL驱动表是班级表,被驱动表是学生表,学生表的连接字段classId没有创建索引,所以查询性能不如上面的SQL:
EXPLAIN SELECT * FROM class c LEFT JOIN student s on c.id=s.classId;
这个连接查询相当于先查驱动表班级表,再查被驱动表学生表。两个type都是all,被驱动表学生表的条件是连接字段classId,这个字段没有加索引,所以type是all。
查询时长:
被驱动表连接字段有索引:
创建索引并分析:
-- 因为被驱动表连接字段是classid,所以给他创建索引 CREATE INDEX idx_classid on student(classid); EXPLAIN SELECT * FROM class c LEFT JOIN student s on c.id=s.classId;我们可以看到,被驱动表学生表走了索引,type是ref,即命中非唯一索引。
查询时长:
结论:可以看到,查询时长从180秒优化到了0.6秒。被驱动表的连接字段,加索引和不加索引,性能区别还是很大的。
注意:我这里被驱动表用的是学生表,而不是班级表,因为如果用班级表作为被驱动表,连接字段将成为被驱动表的id,id主键默认加唯一索引,不方便验证。
在MySQL连接查询时,建议用小表驱动大表,即“小表 left join 大表”,或者“小表 right join 大表”。
小表驱动大表是为了减少连接次数:
因为同样两张表相互连接,小表驱动大表,可以有效减少连接的次数。上一节有说过,连接查询的原理是先查左表,再根据连接字段查右表,然后过滤右表的条件。因为相比普通的查询,连接查询要左表右表都查一次,肯定没有只查一次快,所以连接次数越少越好,所以要用小表驱动大表。
连接次数验证:
现有两个表A与B ,表A有200条数据,表B有20万条数据 ;
按照循环的概念举个例子
- for(200条){
- for(20万条){
- ...
- }
- }
- for(20万){
- for(200条){
- ...
- }
- }
所以:
验证-不加索引(学生表50w数据,班级表1w数据):
先清除所有索引:
为了防止索引带来的影响,先去掉所有索引:
CALL proc_drop_index("test","student");
大表驱动小表:
为了避免主键唯一索引对结果的影响,我们连接字段不使用id,而使用classId和monitor。
下面SQL驱动表是学生表,被驱动表是班级表,连接字段是班级表的monitor。
学生比班级多,符合大表驱动小表:
SELECT * FROM student s LEFT JOIN class c on s.classId=c.monitor;
可以看到查询时长是206s。
小表驱动大表:即班级表驱动学生表
SELECT * FROM class c LEFT JOIN student s on s.classId=c.monitor;
可以看到查询时长只有189s:
结论:可以看出使用小表驱动大表后,查询时长缩短了16s。
验证-加索引:
CREATE INDEX idx_classid on student(classid); CREATE INDEX idx_monitor on class(monitor);大表驱动小表:学生比班级多,符合大表驱动小表
SELECT * FROM student s LEFT JOIN class c on s.classId=c.monitor;
查询时长:
小表驱动大表:即班级表驱动学生表
SELECT * FROM class c LEFT JOIN student s on s.classId=c.monitor;
结论:可以看出使用小表驱动大表后,查询时长由0.6s优化到了0.2s
两个表JOIN字段数据类型保持绝对一致。防止MySQL查询优化器隐式的自动类型转换导致索引失效。
索引失效上面3.3.1.5有详细说,此处不再赘述。
验证:
-- 修改classId 字段类型 ALTER TABLE student MODIFY COLUMN classId VARCHAR(255); -- 删除所有索引 CALL proc_drop_index("test","student"); -- 创建classId 字段索引 CREATE INDEX idx_classid on student(classid);学生表的classid字段设置成varchar类型,而班级表的id是int类型,再使用下面SQL将这两个字段连接起来,就会导致索引失效。
EXPLAIN SELECT * FROM class c LEFT JOIN student s on s.classId=c.monitor;
可以看到被驱动表学生表的查询计划中,type是all,即全表扫描,索引失效了:
结论:连接查询时,两个表的连接字段必须保持类型一致。
然后我们把classid字段类型改回来:
ALTER TABLE student MODIFY COLUMN classId INT;
详细请参考:
优化方案:
子查询原理:
子查询是指在一个SQL语句中嵌套另一个完整的SQL查询。它可以作为主查询的一部分,也可以作为WHERE、FROM或HAVING子句的一部分。子查询的执行顺序是先执行子查询,然后将其结果作为外部查询的条件或数据源。
验证:子查询比关联查询快的场景
查询所有班长的学号、姓名
关联查询:
SELECT DISTINCT s.* from class c LEFT JOIN student s on c.monitor=s.id;
查询时长:
子查询:
SELECT * FROM student WHERE id in (SELECT DISTINCT monitor FROM class)
查询时长:
验证:子查询比关联查询慢的场景
查询所有学生及其对应的班级名称
关联查询:
SELECT s.name, c.address FROM student s JOIN class c ON s.classId = c.id;子查询:
SELECT name, (SELECT address FROM class WHERE id = s.classId) AS address FROM student s;
在复杂SQL中,同一个子查询语句可能在整个SQL中多次出现,这就导致了性能浪费。这种情况下,结合Java代码多次查询而不用子查询,可以使这个重复子查询语句只查一次,从而提高性能。
通常情况下,我们可能想,多次查询肯定没有一次查询,因为MySQL查询两次肯定就有两次I/O调用过程,而查询一次也就只有一次调用过程。
但其实还是需要具体情况具体分析,如果是简单SQL,只调用了一次子查询,那肯定是子查询快,当同一个子查询语句可能在整个SQL中多次出现时,用Java代码,肯定是更快的,虽然IO次数多,但这么多次子查询缩减成了一次,性能是快了的。
这也是日常开发中的一个原则,任何性能优化都需要看具体的业务场景。
如果一个复杂SQL中,多次用到了同一个子查询,可以尝试将其抽离出来,优化成临时表。这样可以避免重复计算、减少查询次数,从而提高查询性能。
并且可维护性也有效提高,每次修改时只需要修改这一个临时表,而不需要手动一个个修改子查询语句。
详细请参考:
结论:
MySQL支持索引排序和FileSort排序,索引保证记录有序性,性能高,推荐使用。
FileSort排序是内存中排序,当数据量大时,查询优化器会产生临时文件,在磁盘里对数据排序。我们知道,磁盘的IO速度是远低于内存的,所以它的性能一般不如索引排序,而且排序过程中会占用大量CPU。
当然,任何事不是绝对的,一些情况FileSort可能效率高。例如没覆盖索引的左模糊、“不等于”、not null等索引失效情况下,全表扫描效率比非聚簇索引树遍历再回表更高。
创建索引:
CREATE INDEX idx_name on student(name);
验证:索引失效时FileSort更快
EXPLAIN SELECT * FROM student WHERE name like '%34' order by name;
可以看到type是all,全表扫描,extra是using filesort,即使用了FileSort排序,全表扫描排序:
覆盖索引时索引排序更快
而如果我们使用了覆盖索引,在非聚簇索引树查询时,将不需要回表,所以使用了索引排序:
EXPLAIN SELECT name FROM student WHERE name like '%34' order by name;
可以看到type是index,即索引树上全表扫描,extra是using index,覆盖索引:
当where后的条件符合最左前缀原则时,要让排序也走索引,需要排序字段也符合最佳左前缀原则。例如索引(a,b,c),查询where a=1 order by a,b,c走索引,而where a=1 order by b,c不走索引。
验证:
创建索引:
CREATE INDEX idx_stuno_age_classid on student(stuno,age,classid);
标准的排序字段符合最左前缀:
EXPLAIN SELECT * FROM student WHERE stuno =3 and age=2 order by stuno;
可以看到type是ref,即命中非唯一索引,extra是空:
排序字段不符合最左前缀-使用索引下推:
将排序字段由学号改为班号
EXPLAIN SELECT * FROM student WHERE stuno =3 and age=2 order by classid;
可以看到type是ref,即命中非唯一索引,extra是using index condition,即使用索引下推。索引下推是MySQL5.6支持的,当过滤的字段在索引树上并且索引失效时,将直接在索引树上走过滤,而不需要在MySQL服务器回表过滤。
排序字段不符合最左前缀-不使用索引下推:
将排序字段由学号改为名字
EXPLAIN SELECT * FROM student WHERE stuno =3 and age=2 order by name;
可以看到type是ref,即命中非唯一索引,extra是using filesort,即使用了FileSort排序。
排序顺序必须要么全部DESC,要么全部ASC,尽量不要使用混合排序。因为索引树的数据以一个顺序排列的,如果一些字段升序,一些字段降序,会导致整体性能较差。
验证:
全升序,运行时间0.025s
EXPLAIN SELECT * FROM student WHERE stuno =3 and age=2 order by stuno ,age,classid;
乱序,运行时间0.071s
SELECT * FROM student WHERE stuno =3 and age=2 order by stuno desc ,age,classid desc;
待排序数据量大约超过一万个,就不走索引走filesort了。建议用limit和where过滤,减少数据量。数据量很大时,索引排序完需要回表根据这些过滤后数据的id查所有数据,性能很差,还不如FileSort在内存中排序效率高。
注意:并不是说使用limit一定会走索引排序,关键看的是数据量,数据量过大时优化器会使用FileSort排序。
前面3.3.1.6 有提到,范围查询会使后续的列索引失效,当然也会令排序无法走索引。
验证:
索引排序:
EXPLAIN SELECT * FROM student WHERE stuno =3 order by stuno;
范围查询导致排序索引失效:
EXPLAIN SELECT * FROM student WHERE stuno >3 order by stuno;
当【范围条件】和【group by 或者 order by】的字段出现二选一时,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上。
因为MySQL底层是,先where过滤,按过滤结果再分组、排序,所以优先给where后的字段加过滤,过滤掉主要的数据,然后再分组或排序,性能可以快很多。
这样即使范围查询导致排序索引失效,效率依然比只索引排序字段时候高。如果只能过滤一点点,那就优先索引放到排序字段上。
无法使用 Index 排序时,需要对 FileSort 方式进行调优。
1.排序缓冲区大小:
sort_buffer_size。增加 sort_buffer_size 可以让更多的数据在内存中排序,从而减少对磁盘的依赖,提高性能。
SET GLOBAL sort_buffer_size = 1048576; -- 设置为1MB,根据需求调整
查看当前排序缓冲区大小:
SHOW VARIABLES LIKE 'sort_buffer_size';
2.排序数据最大长度:
max_length_for_sort_data。增大这个值可以让 MySQL 采用更有效的排序方式,但需要权衡内存使用量。
SET GLOBAL max_length_for_sort_data = 1024; -- 设置为1KB,根据需求调整
跟排序基本一个思路。
排序分组都比较耗费cpu,能不用就不用。
当需要过滤数据时,核心思路是能在where前过滤就别在having后过滤。因为where效率高于having。where是分组前过滤,having是分组后过滤。
深分页(Deep Pagination):指在数据库查询中,通过 LIMIT 和 OFFSET 子句来分页获取数据时,偏移量(OFFSET)较大的情况。例如“limit 200000,10” ,即查询学生表中第200000~200010 的数据。
一般情况下,偏移量到达一万就可以称为深分页。但实际情况下还是需要根据具体的字段数量和类型去进行判断,例如一个表里只有两个int型字段,那么可能要十万以上才能算作深分页。
优化方法:
验证:
需求:返回第200000~200010 的记录
常规查询:
EXPLAIN SELECT * FROM student ORDER BY id limit 200000,10
可以看到运行时长0.09s,因为排序字段是id,所以走了主键索引树,type是index。
主键有序的表根据主键排序,先过滤再排序:
直接查范围之后的几个数据。
EXPLAIN SELECT * FROM student WHERE id > 200000 LIMIT 10;
运行时长0.048s,可以看到性能变快。因为走范围索引后只对10个元素进行了排序,而常规查询方案相当于对200010条数据排序了。
分析查询计划,可以看到,type是range,即范围索引,extra是using where ,即回表了(因为select *)。
主键不有序的表根据主键排序,先给主键分页,然后内连接原表:
当前表内连接排序截取后的主键表,连接字段是主键。因为查主键是在聚簇索引树查,不用回表,排序和分页很快
EXPLAIN SELECT * FROM student t,(SELECT id FROM student ORDER BY id LIMIT 200000,10) a WHERE t.id = a.id;
可以看到运行时长明显缩短。
主键有序的表根据非主键排序:
得到上一页最后一条记录x,那么目标页码的所有记录id都比x.id小(因为逆序,且排序依据其实是age,id,主键自增),目标页码的所有记录age都比x.age小或等于。
EXPLAIN SELECT * FROM student WHERE id<#{x.id} AND age>=#{x.age} ORDER BY age DESC LIMIT 10;
验证:
常规方案:0.378s
SELECT * FROM student ORDER BY age,id limit 200000,10
优化方案:0.031s
SELECT * FROM student WHERE id>481690 AND age>=20 ORDER BY age,id LIMIT 10;
详细请参考:
一个索引包含了满足查询结果的数据。因为不需要回表,所以查询效率高。覆盖索引时“左模糊”和“不等于”不能让索引失效。
示例:
- #没覆盖索引的情况下,左模糊查询导致索引失效
- CREATE INDEX idx_age_name ON student(age, NAME);
- EXPLAIN SELECT * FROM student WHERE NAME LIKE '%abc';
覆盖索引:一个索引包含了满足查询结果的数据就叫做覆盖索引,不需要回表等操作。
索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数据,那就不需要读取行了。
覆盖索引是非聚簇索引的一种形式,它包括在查询里的SELECT、JOIN和WHERE子句用到的所有列 (即建索引的字段正好是覆盖查询条件中所涉及的字段)。简单说就是, 索引列+主键 包含 SELECT 到 FROM之间查询的列 。
例如(email(6)),给字符串前缀而不是整个字符串添加索引,前缀长度要根据区分度和长度进行取舍。
示例:
MySQL是支持前缀索引的。默认地,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字符串。
- mysql> alter table teacher add index index1(email);
- #或
- mysql> alter table teacher add index index2(email(6));
这两种不同的定义在数据结构和存储上有什么区别呢?下图就是这两个索引的示意图。
如果使用的是index1(索引包含整个字符串),执行顺序是这样的:
这个过程中,只需要回主键索引取一次数据,所以系统认为只扫描了一行。
如果使用的是index2(索引包含字符串前缀email(6)),执行顺序是这样的:
也就是说使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。前面 已经讲过区分度,区分度越高越好。因为区分度越高,意味着重复的键值越少。
ICP(索引下推):在使用范围查询时,允许 MySQL 在存储引擎层面(如 InnoDB)利用索引树来过滤数据,而不是将数据传递给 MySQL 服务器层再进行过滤,从而减少了需要从存储引擎传递到服务器层的数据量。
简而言之,索引下推可以使范围查询右侧索引失效的列,依然可以在索引树上过滤。
例如索引(name,age),查询name like 'z%' and age and address,'z%'是模糊查询,实际上也是范围查询。使用索引下推时,在联合索引树查询时不止查name,还会判断后面的age。而如果关闭了索引下推,联合索引里范围查询后面的字段age不能在联合索引树里直接条件判断,必须回表到主键索引树后,以之前过滤结果id查找到对应数据,再过滤age列。
注意:这里举例没有使用'%z',因为左模糊查询会使整个索引失效,也就不会用到索引下推了。
索引下推(ICP,Index Condition Pushdown)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优化方式。
举例:
验证:
删除所有索引:
CALL proc_drop_index("test","student");
创建索引
CREATE INDEX idx_name_age ON student(name,age);
开启索引下推:
SET optimizer_switch='index_condition_pushdown=on';
索引下推查询计划分析:
#索引成功;MySQL5.6引入索引下推,where后面的name和age都在联合索引里,可以又过滤又索引,不用回表,索引生效 EXPLAIN SELECT * FROM student WHERE `name` like 'bc%' AND age=30;可以看出,type是range,即范围索引,extra是using index condition,即使用了索引下推。因为 age在联合索引(name,age)树里,所以即使name是范围查询导致右侧的age索引失效了,依然可以在这个索引树中过滤age条件:
关闭索引下推:
SET optimizer_switch='index_condition_pushdown=off';
关闭索引下推后查询计划分析:
#范围查询导致右侧列age索引失效,因为关闭了索引下推 EXPLAIN SELECT * FROM student WHERE `name` like 'bc%' AND age=30;可以看到,type是range,即使用了范围索引,extra是using where,即使用了回表。在走name列范围查询后,age列索引失效,转为回表,将name过滤后的数据id作为条件,在主键索引数中过滤age条件:
结论:读多写少用唯一索引,读少写多用普通索引+代码逻辑中去维护唯一性。
普通索引和唯一索引的区别:
写缓存(change buffer):
当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话, 在不影响数据一致性的前提下, InooDB会将这些更新操作缓存在change buffer中 ,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行change buffer中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。
merge :将change buffer中的操作应用到原数据页,得到最新结果的过程称为 merge 。除了访问这个数据页会触发merge外,系统有后台线程会定期merge。在数据库正常关闭(shutdown)的过程中,也会执行merge 操作。
如果能够将更新操作先记录在change buffer, 减少读磁盘 ,语句的执行速度会得到明显的提升。而且, 数据读入内存是需要占用 buffer pool 的,所以这种方式还能够 避免占用内存,提高内存利用率。
唯一索引的更新就不能使用change buffer ,实际上也只有普通索引可以使用。
做好区分:
- 读数据用的是缓冲池buffer pool;
- 重做日志有个redo log buffer,是将缓冲池里更新的数据写入redo log buffer,事务提交时根据刷盘策略,将redo log buffer刷盘到redo log file或page cache。
详细请参考:
遵循小表驱动大表原则,左边表小就是EXISTS,左边表大就用IN。
EXISTS:适用于左边表较小的情况,因为EXISTS子查询在找到满足条件的记录后会立即返回,适合处理小表驱动大表的场景。
SELECT * FROM large_table WHERE EXISTS (SELECT 1 FROM small_table WHERE small_table.id = large_table.id);
IN:适用于左边表较大的情况,因为IN子查询会先执行并将结果集缓存下来,再与外部查询匹配,适合处理大表驱动小表的场景。
SELECT * FROM small_table WHERE id IN (SELECT id FROM large_table);
在使用InnoDB存储引擎时,COUNT(1),COUNT(*)时,查询优化器会优先选用有索引的、占用空间最小的二级索引树进行统计,只有找不到非聚簇索引树时才会采用使用聚簇索引树统计。
当然也能COUNT(最小空间二级索引字段),但很麻烦,需要你自己找到最小空间并且建了索引的字段,而且也要考虑null值,不如交给优化器自动选择。
在使用MyISAM存储引擎时,就无所谓了,用哪个时间复杂度都是O(1)。
SELECT COUNT(*) FROM table_name;
明确字段查询:避免使用SELECT *,明确列名不仅可以提高查询解析速度,还可以利用覆盖索引,减少回表操作。select *可能会多查一些字段,提高了一些网络传输负担,而且杜绝了覆盖索引的可能性,性能较差。
SELECT id, name, age FROM student WHERE id = 123;
数据库引擎的通用查询流程:
- 解析 SQL 语句:数据库引擎先将 SQL 语句解析成内部的执行计划,包括了查询哪些数据表、使用哪些索引、如何连接多个数据表等信息。
- 优化查询计划:数据库引擎对内部的执行计划进行优化,根据查询的复杂度、数据量和系统资源等因素,选择最优的执行计划。
- 执行查询计划:数据库引擎根据执行计划,通过 I/O 操作读取数据表的数据,进行数据过滤、排序、分组等操作,最终返回结果集。
- 缓存查询结果:如果查询结果集比较大或者查询频率较高,数据库引擎会将查询结果缓存在内存中,以加速后续的查询操作。
MySQL执行一条select语句时会经过的流程:
- 连接器:主要作用是建立连接、管理连接及校验用户信息。
- 查询缓冲:查询缓冲是以key-value的方式存储,key就是查询语句,value就是查询语句的查询结果集;如果命中直接返回。
- 8.0版本废弃:注意,MySQL 8.0已经删除了查询缓冲。从MySQL 5.6版本开始,官方将Query Cache设置为了默认关闭。
- 原因:官方给出的原因是这个功能比较鸡肋,而且减少性能的可变性确实通常比提高峰值吞吐量更重要,尤其是在生产环境中。稳定的性能可以确保用户体验的一致性,并减少系统出现瓶颈或宕机的风险。
- 方案:官方给出了所替代的解决方案建议——使用第三方工具客户端缓存ProxySQL 来代替Query Cache。
- 分析器:词法句法分析生成语法树。
- 优化器:指定执行计划,选择查询成本最小的计划。
- 执行器:根据执行计划,从存储引擎获取数据,并返回客户端
ProxySQL:
- 基本介绍:一个MySQL中间件,一个高性能的 MySQL 代理,一个用 C++ 开发的轻量级产品。旨在提高 MySQL 服务器的性能、可伸缩性和可用性。MySQL官方推荐的Query Cache替换方案。
- 同类产品:DBproxy、MyCAT、OneProxy
- 安装配置:
- 安装:
sudo yum install proxysql
配置: 修改/etc/proxysql.cnf 直接配置,或者访问管理接口配置:
-- 连接到管理接口 mysql -u admin -padmin -h 127.0.0.1 -P 6032 -- 添加MySQL服务器 INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES (0, '192.168.1.100', 3306); -- 加载配置到运行时 LOAD MYSQL SERVERS TO RUNTIME; -- 保存配置到磁盘 SAVE MYSQL SERVERS TO DISK;- 功能:
- 查询缓存
- 负载均衡:支持自动摘除宕机的DB
- 读写分离
-- 主库 INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES (10, '192.168.1.101', 3306); -- 从库 INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES (20, '192.168.1.102', 3306); INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES (20, '192.168.1.103', 3306); -- 将所有写操作(INSERT、UPDATE、DELETE)定向到主库 INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup) VALUES (1, 1, '^INSERT', 10); INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup) VALUES (2, 1, '^UPDATE', 10); INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup) VALUES (3, 1, '^DELETE', 10); -- 将所有读操作(SELECT)定向到从库 INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup) VALUES (4, 1, '^SELECT', 20); -- 加载并保存配置 LOAD MYSQL SERVERS TO RUNTIME; SAVE MYSQL SERVERS TO DISK;- 实时监控
- 连接池
- 动态加载配置
- 访问控制
- ProxySQL集群
当进行全表扫描并且明确时,使用LIMIT可以在达到指定数量后停止扫描,减少不必要的开销。
例如根据学号查询学生,根据身份证号查询人,根据订单号查询订单,当我们明确知道需要精准查询时,用Limit 1 总错不了。
当然如果走了唯一索引,就无需用limit了,查到对应记录会直接返回;如果走了普通索引,并且对应记录重复数据很多的话,用limit也会提高一些性能。
- -- 根据学号(假设学号是按班级隔离的)和班级号精准查询学生
- SELECT * FROM student where stuno =23 and classid=1 LIMIT 1;
避免大偏移量的LIMIT:在大表或M值较大时,LIMIT M, N的性能较差,因为需要扫描并丢弃前M条记录。可以通过记录上次查询的最大ID来优化分页。
SELECT * FROM large_table WHERE id > last_id ORDER BY id ASC LIMIT 10;
多使用COMMIT:长事务会持有锁和占用资源较长时间,拆分为小事务并频繁COMMIT可以释放锁、减少资源占用。
示例:
- @Transactional
- public void fun(){
- // 1.查询a
- // 2.查询b
- // 3.数据处理
- // 4.保存c表
- // 5.保存b表
- }
优化成:
-
- @Transactional
- public void fun(){
- // 1.查询a
- // 2.查询b
- // 3.数据处理
- // 4.落库
- savaFun();
- }
-
- public void savaFun(){
- // 1.保存c表
- // 2.保存d表
- }
确保WHERE条件明确:在执行UPDATE或DELETE操作前,先SELECT一下并不会让性能变差,它可以确保有明确的WHERE条件,避免误操作和全表扫描。
- UPDATE student SET age = age + 1 WHERE id = 123;
- DELETE FROM student WHERE id = 123;
阿里规约:
【强制】 数据订正(特别是删除、修改记录操作)时,要先 select ,避免出现误删除,确认无误才能执行更新语句。
UNION ALL:UNION ALL 和 UNION 都用于组合两个或多个查询结果集。UNION ALL在组合时,不进行去重操作,比UNION更快,适用于不需要去重的场景。
- SELECT id, name FROM table1
- UNION ALL
- SELECT id, name FROM table2;
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。