MySQL进阶篇2_SQL优化、锁

发布于:2025-05-10 ⋅ 阅读:(10) ⋅ 点赞:(0)

1 SQL优化

讲一些SQL语句的性能优化

1.1插入数据优化

  • 普通插入

1.采用批量插入(一次插入的数据不建议超过1000条)

INSERT INTO tb_test VALUES(1, 'Tom'), (2, 'Cat'), (3, 'Jerry');

2.手动提交事务
默认情况下,mysql执行单个insert语句会开启事务,insert执行完结束
这样单个insert语句事务就加入我们声明的事务,将所有事务变成一个事务,节省事务开启结束时间

START TRANSACTION;
INSERT INTO tb test VALUES(1, 'Tom'), (2, 'Cat'), (3, 'Jerry');
INSERT INTO tb test VALUES(4, 'Tom'), (5, 'Cat'), (6, 'lerry');
INSERT INTO tb test VALUES(7, 'Tom'), (8, 'Cat'), (9, 'lerry');
COMMIT;

在这里插入图片描述

3.主键顺序插入
主键顺序插入性能优于主键乱序插入(涉及到树的查找和优化)。-主键优化,看完主键结构即可理解

  • 大批量插入:

load命令插入

-- 客户端连接服务端时,加上参数  -–local-infile
 mysql –-local-infile  -u  root  -p
 -- 设置全局参数local_infile为1,开启从本地加载文件导入数据的开关
 
set  global  local_infile = 1;
-- 执行load指令将准备好的数据,加载到表结构中
load  data  local  infile  '/root/sql1.log'  into  table  tb_user  fields  
terminated  by  ','  lines  terminated  by  '\n' ; 

视频操作

1.2主键优化

数据组织方式:在innodb存储引擎中,表数据都是根据主键顺序存放的,这种存储方式的表称为索引组织表。

非叶子节点的索引和叶子节点的数据都存放在page里,一个extent(区)-1MB中可以存放64个page(页)-16KB
InnoDB的内存组织结构

页分裂

除了新增页,新申请的空间,正常页至少包含两条数据
正常你按主键顺序插入
在这里插入图片描述
不按主键顺序插入的话会出现页分裂
比如现在要插入50
在这里插入图片描述
但是1页满了,所以要申请新一页来存储,将50放入,需要将1页的后50%元素(即23和47)移入3页和50凑成一页,这个就叫页分裂

为什么要数据重新分配?-将原页后50%和新增数据合并(问AI)
在这里插入图片描述然后他们页之间进行一下排序,1-3-2这样
在这里插入图片描述

页合并

如果一个数据源恰删数据删除后只剩50%不到数据(占页空间大小一半不到),会发出合并请求,找两边的页,如果恰好两侧的页也有不足50%的页,这两个页的数据就会合并
在这里插入图片描述

主键设计原则

  • ①满足业务需求的情况下,尽量降低主键长度。

  • ②插入数据时,尽量选择顺序插入,选择使用auto_increment自增主键。

  • ③尽量不要使用uuid做主键或者其他自然主键,如身份证号。(但是由于分布式的影响现在基本都是用雪花算法这种)

  • ④业务操作时,尽量避免对主键的修改。

1.3order by设计优化

MySQL的排序,有两种方式:
Using filesort:通过表的索引或全表扫描,读取满足条件的数据行,然后在排序缓冲区sort buffer中完成排序操作,所有不是通过索引直接返回排序结果的排序都叫 FileSort 排序。
Using index:通过有序索引顺序扫描直接返回有序数据,这种情况即为 using index,不需要额外排序,操作效率高。

对于以上的两种排序方式,Using index的性能高,我们在优化排序操作时,尽量要优化为 Using index

create  index  idx_user_age_phone_aa  on  tb_user(age [asc/desc],phone [asc/desc]);
explain select  id,age,phone from tb_user order by age, phone

在这里插入图片描述
现在我们建立联合索引默认升序
如果order by age desc,phone desc;会多显示一个Backward index scan;-反向扫描索引
也是用了索引,性能方面没什么影响
在这里插入图片描述

  • 1.两个都升序或者两个都降序,走联合索引
  • 2.要遵循最左前缀原则,单phone排序,但是没有单phone索引,有age,phone联合索引就不行
  • 3.如果要求按先phone后age进行排序,但是生成的索引是先age后phone,那么还是出现using filesort;创建索引前后字段结构要和排序前后字段结构要相同(和索引底层结构有关)
  • 4.如果要求按先按age升序排列,后按phone降序排列,此时生成的索引是先age后phone,那么还是出现using filesort,因为创建索引的时候默认collation为A,即为asc,按升序排列的。

联合索引,它不是单独的按谁排列的,它是根据两个字段一起排列的合并结果,数据列是phone和age的共同排序结果,所以这个用不了反向扫描

一个升序一个降序,需要重新创建索引。
创建如下索引即可解决

create index index_age_pho_ad on tb_user(age asc,phone desc);
  • 5.最好使用覆盖索引,如果表数据量太少都可能不回表查询,直接全表扫了
  • 6.如果不可避免地出现filesort,可以适当增大排序缓冲区大小sort_buffer_size(默认256k)

1.4group by设计优化

group by也要遵循最左前缀使用原则

没有创建索引时候

explain select profession,count(*) from  tb_user group by profession;

using temporary-使用临时表,效率不高
在这里插入图片描述
创建索引后

create index idx_user_pro_age_sta on tb_user(profession,status);

explain select profession,count(*) from  tb_user group by profession;

这样就会用到索引
在这里插入图片描述

explain select age,count(*) from  tb_user group by age;

这样的话,既会使用到索引也会使用到临时表

具体内部怎么执行?
为什么用索引:因为全表扫描和查索引差不多,索引不用回表查age就在索引结构中,索引这里只起到提供数据的作用
具体执行过程
在这里插入图片描述

在这里插入图片描述
如果两个字段都用(有最左且连续)

explain select profession,age,count(*) from  tb_user group by profession,age;

正常using index
在这里插入图片描述

思考下面这条sql的执行计划是怎么样的

explain select age,count(*) from tb_user where 
profession='软件工程' group by age;

正常使用index,为什么和之前的单用age分组不一样呢?
因为满足最左前缀使用法则,where 筛选中用了profession

小理解

看完上面那个思考
我认为group by order by
其实和where后面字段一样,只要用的话,都会用索引
order by和group by列顺序和联合索引顺序一致的时候,才是最高性能的时候

1.5limit设计优化

无论你是根据索引还是根据全表查(Using filesort)都要获取前20000条数据
而且limit实际一般会和我们的order by联合使用
这样一般就用的是索引,因为最后是一张链表嘛,需要一个一个遍历元素
注:Using filesort虽然说是照物理结构读取,但是底层物理结构不是数组,其实还是通过B+树叶子节点的链表查询,这里的叶子节点其实是页(Page),获取到页内后,逐行读取页内数据(计数器累加行数,再到下一个页累加,直到达到 offset=20000),然后读取后10个再输出
在这里插入图片描述
我们可以通过连表查询的方式
我们可以通过覆盖索引+子查询

SELECT * FROM your_table ORDER BY id LIMIT 50000, 10;

先查询对应的主键id,再根据主键联表查

SELECT * FROM your_table 
INNER JOIN (
  SELECT id FROM your_table ORDER BY id LIMIT 50000, 10
) AS tmp where your_table.id=tmp.id;

有的同志可能又疑问了,这不还是根据链表查询吗?
还是要查前50000条数据啊,为什么下面的就比上面的效率高呢

  • 1.仅读取索引字段(如 id):索引存储在独立的 B+树中,体积更小,通常可完全载入内存。
    尽管这个是主键索引,下面是data,但是我们只读取索引字段id即可,不访问data,而select *则需要访问叶子节点的数据字段data
  • 2.是这条sql完全不需要回表查询,因为id是主键
  • 3.顺序访问:如果顺序插入(无修改),主键索引的叶子节点物理地址就会连续,扫描时是 连续 I/O(高效)。

所以子查询会很快,子查询查出来后联表根据id条件查,那也很快
在这里插入图片描述

顺序IO和随机IO小疑惑

这样说吧,顺序IO和随机IO是不由我们自己决定的
我们要做的就是,最好顺序插入数据,减少页分裂现象
页分裂会123页换成132页,原来123逻辑物理地址相同,现在132逻辑物理就不同,物理地址不同,就需要移动磁头1-2-3本来顺序移动磁头,现在1-3-2要从1-3移动一次3-2又要移动一次-这就变成随机IO了,效率就会低。

顺序I/O的优势体现在:

  • 磁盘寻道时间减少:磁头不需要频繁移动
  • 预读机制有效:数据库可以提前读取后续数据块
  • 缓存命中率提高:连续的数据块更容易被保留在内存中

1.6count设计优化

问题: InnoDB引擎执行count(*)的时候,需要把数据一行一行地从引擎中读出来,然后累积计数
: MyISAM引擎把一个表地总行数存在了磁盘上,没有where条件地时候,就会直接返回这个数,效率很高。
优化思路: 自己计数,在Redis中保存一个数来记录

count的几种用法
count()会一行一行数据进行判断,如果count函数的参数不是NULL,累计值就加1,否则不加

用法
count(*) ——总记录数,不取值直接累加

count(主键) ——取主键值,直接按行累加,但不用判断null

count(字段) ——加NOT NULL取字段值按行累加(不是累加字段值,是累加一个字段算一行的总行数,但是会取出字段值),没加NOT NUL约束情况时,如果NULL不加

count(1) ——InnoDB引擎遍历整张表,但不取值。服务层对于返回的每一行,放一个数字“1”,直接按行进行累加。

优化: count(*) ~ count(1) > count(主键id) > count(字段)

理论count(*)最快

1.7update优化

update主要就是避免表锁,将表锁降级为行锁

没有索引就会锁住整张表

优化: 根据索引字段进行更新,并且索引字段不能失效,否则行锁就会升级为表锁,影响并发性能

关于隐式事务事务的DML操作

默认autocommit=1
每个 DML 语句都会隐式开启一个事务,并在执行后立即自动提交。

INSERT INTO users (name) VALUES ('Alice'); -- 隐式开启事务 → 执行 → 自动提交
UPDATE users SET name='Bob' WHERE id=1;     -- 隐式开启事务 → 执行 → 自动提交

下面的DDL和隐式事务,这种情况不多,涉及元数据锁
这里展示DML和DDL在一个事务
在这里插入图片描述
如果不是一个事务DDL会等带DML的事务提交后再进行操作,如下图
在这里插入图片描述

innodb引擎中锁的分类

  • 全局锁:锁定数据库中所有的表
  • 表级锁:每次操作锁住整张表
  • 行级锁:每次操作锁住对应的行数据

全局锁

全局锁就是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的DML的写语句,DDL语句,已经更新操作的事务提交语句都将被阻塞。

其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。
备份Mysql数据库
1.可以在navicat直接转存sql文件

2.可以使用Mysql自带的 mysqldump 工具来备份数据库。该工具可以生成 SQL 脚本文件,包含数据库中所有表和数据的语句

mysqldump -u [username] -p [database_name] > [backup_file].sql
# [username] 是 MySQL ⽤户名
#[database_name] 是需要备份的数据库名称,[backup_file].sql 是备份的⽂件名。

恢复Mysql数据库
1.一般都是直接把sql文件拖入navicat

2.如果要恢复数据库,前提要有备份的文件。可以运行以下命令:

mysql -u [username] -p [database_name] < [backup_file].sql
# [username] 是 MySQL ⽤户名
# [database_name] 是需要恢复的数据库名称,[backup_file].sql 是备份的文件名。

为什么备份库需要给库上全局锁?
因为可能几个表之间有关联,导致数据的逻辑不匹配
比如:备份库存表之后,扣减库存,又生成订单,再备份订单,这样造成了库存表没有去减1,但是多了个订单对不上库存了

使用mysqldump的时候不会自动使用全局锁,而是使用表锁,通过 LOCK TABLES 对每个表依次加读锁(READ LOCK)备份期间该表只读(其他会话写入会阻塞)
所以我们要自己实现全局锁

语法
1). 加全局锁

 flush tables with read lock ;

2). 数据备份

 mysqldump  -uroot –p1234  itcast > itcast.sql

3). 释放锁

 unlock tables ;

特点
数据库中加全局锁,是一个比较重的操作,存在以下问题:

  • 如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆。
  • 如果在从库上备份,那么在备份期间从库不能执行主库同步过来的二进制日志(binlog),会导致主从延迟。

在InnoDB引擎中,我们可以在备份时加上参数 --single-transaction 参数来完成不加锁的一致性数据备份。

 mysqldump  --single-transaction  -uroot –p123456  itcast > itcast.sql

在这里插入图片描述

表级锁

介绍
表级锁,每次操作锁住整张表。锁定粒度大,发生锁冲突的概率最高并发度最低。应用在MyISAM、InnoDB、BDB等存储引擎中。

对于表级锁,主要分为以下三类:

  • 表锁
  • 元数据锁(meta data lock,MDL)
  • 意向锁

表锁

对于表锁,分为两类
1.表共享读锁(read lock

  • 所有客户端都只能读数据,不能写数据

2.表独占写锁(write lock

  • 只有上锁的客户端可以读写数据,其他都不能读写数据。

读锁
在这里插入图片描述
写锁:
在这里插入图片描述
语法

加锁:lock tables 表名... read / write。
释放锁:unlock tables / 客户端断开连接释放锁。

说实话其实表锁不常用(个人感觉)

元数据锁

meta data lock , 元数据锁,简写MDL

MDL加锁过程是系统自动控制,无需显式使用,在访问一张表的时候会自动加上

这里的元数据,可以简单理解为就是一张表的表结构, 也就是说,某一张表涉及到未提交的事务时,是不能够修改这张表的表结构的。

在MySQL5.5中引入了MDL,当对一张表进行增删改查的时候,加MDL读锁(共享)除EXCLUSIVE的其他元数据锁;当对表结构进行变更操作的时候,加MDL写锁(排他)EXCLUSIVE

MDL读锁:防止DDL语句,即修改表结构
MDL写锁:防止DML和DQL即增删改查

当事务执行提交后,就会释放MDL读锁

注:所有除EXCLUSIVE元数据锁,其目的都是防止表结构被修改,不管是SHARED_READ或者其他的都是这样,图片第一列是表锁,上表锁也会上一个元数据锁,表锁是防止写/读(上一小节),而元数据锁是防止DDL语句修改表结构,达到协同作用
在这里插入图片描述

意向锁

为了避免 DML 在执行时,加的行锁表锁的冲突,在 InnoDB 中引入了意向锁,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查。

假如没有意向锁,客户端一对表的某一行加了行锁后,客户端二想给表加表锁时,会从第一行数据,检查到最后一行数据查找是否有行锁。如果没有,则会添加表锁,如果存在,则不添加。这种方法效率非常低

有了意向锁之后,客户端一在执行 DML 操作时,会对涉及的行加行锁,同时也会对该表加上意向锁
在这里插入图片描述
而其他客户端,在对这张表加表锁的时候,会根据该表上所加的意向锁来判定是否可以成功加表锁,而不用逐行判断行锁情况了。

意向锁和表锁兼容即可加
意向锁和表锁不兼容即不可加

意向锁类型和兼容状态
分类:

  • 意向共享锁 (IS): 由语句 select ... lock in share mode(普通 select 不加锁) 添加 。 与表共享读锁 (read lock) 兼容,与表独占写(write lock)互斥。
  • 意向排他锁 (IX): 由insert、update、delete、select…for update添加 。与表锁共享锁 (read) 及排他锁 (write) 都互斥,意向锁之间不会互斥。

和我的理解一样,就是有一个意向锁,标识了整张表中行锁的状态,所以上表锁的时候,只需要检查意向锁即可判断能否加标锁

select * from score where id=1 lock in share mode; 

会给这行加上行锁的共享锁,同时为score这张表加上意向共享锁,此时可以给表加读锁,但是不能加写锁。

意向锁会在事务提交后解锁

行级锁

介绍
行级锁,每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在InnoDB存储引擎中。

InnoDB的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。
所以update的优化,需要加索引防止表锁

对于行级锁,主要分为以下三类:

  • 行锁(Record Lock):锁定单个行记录的锁,防止其他事务对此行进行 updatedelete。在RC(读未提交)、RR(读已提交)隔离级别下都支持。
    在这里插入图片描述
  • 间隙锁(Gap Lock):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在 RR 隔离级别下支持。
    在这里插入图片描述
  • 临键锁(Next-Key Lock):行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap。在RR隔离级别下支持
    在这里插入图片描述

行锁
InnoDB 实现了以下两种类型的行锁:

  • 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排它锁。
  • 排他锁(X):允许获取排他锁的事务更新数据,阻止其他事务获得相同数据集的共享锁和排他锁。

两种行锁的兼容情况如下:
在这里插入图片描述
常见的SQL语句,在执行时,所加的行锁如下:
在这里插入图片描述
默认情况下,InnoDB在REPEATABLE READ(可重复读)事务隔离级别运行,InnoDB使用next-key-临键锁进行搜索和索引扫描,以防止幻读

  1. 针对唯一索引进行检索时,对已存在的记录进行等值匹配时,将会自动优化为行锁。
  2. 对于 行锁类型:排他锁 来说InnoDB的行锁是针对于索引加的锁,不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,此时就会升级为表锁

间隙锁&临键锁
默认情况下,InnoDB在 REPEATABLE READ 事务隔离级别运行,InnoDB 使用 next-key 锁进行搜索和索引扫描,以防止幻读。

  • 针对唯一索引进行检索时,对不存在的记录进行等值匹配时,将会自动优化为间隙锁。
现在只有id=1,3,8...
beginupdate stu set age = 10 where id = 5;//此时没有id=5的数据
这里就不会加行锁了,而会加间隙锁
锁住3-8之间的间隙,该间隙锁不会锁38对应的记录,锁的只是间隙
此时insert into stu values(7,'ruby',18);是阻塞的,因为73-8中间,需要等待间隙锁释放
  • 索引上的等值查询(普通索引),向右遍历时最后一个值不满足查询需求时,next-key lock 退化为间隙锁。
比如说现在有age为非唯一索引,age有1,3,7,.....
begin;
select * from stu where age = 3 lock in share mode;
由于age为非唯一索引,不确定age=3的有几个,也不确定事务未提交前,是否别的事务会插入age=3的数据
所以此时,有三个锁
1.锁住age=3这一行的行锁,共享锁S
2.锁住1-3之间间隙的间隙锁
3.锁住3-7之间的间隙锁
就是把等值数据和其旁边间隙锁住
  • 索引上的范围查询(唯一索引) ,会访问到不满足条件的第一个值为止。
现在有age 19,25;后面就没了
begin;
select * from stu where id>=19 lock in share mode;
此时会有三个锁
1.先锁19这一行的行锁
2.19-25之间的临键锁,锁25行和19-25之间的间隙
3.临键锁,锁25-正无穷之间的间隙和正无穷这个值(???有疑问)

注意:间隙锁唯一目的是防止其他事务插入间隙。间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁。

锁的释放条件

所有隐式生效的锁,都会在事务提交后解锁
所有显示上的锁,都需要自己执行解锁语句


网站公告

今日签到

点亮在社区的每一天
去签到