MySQL学习笔记8——慢查询

烂漫一生 提交于 2019-12-31 09:02:41


一、简单语句的慢查询

假设表有两个字段 id 和 c,并且在里面插入了 10 万行记录。

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

delimiter ;;
create procedure idata()
begin
  declare i int;
  set i=1;
  while(i<=100000) do
    insert into t values(i,i);
    set i=i+1;
  end while;
end;;
delimiter ;

call idata();

1.查询长时间不返回

select * from t where id=1;

一般都是先执行一下 show processlist 命令,看看当前语句处于什么状态。

等 MDL 锁

在这里插入图片描述
在这里插入图片描述
复现步骤如上:
session A 通过 lock table 命令持有表 t 的 MDL 写锁,而 session B 的查询需要获取 MDL 读锁。所以,session B 进入等待状态。

这类问题的处理方式,就是找到谁持有 MDL 写锁,然后把它 kill 掉。

通过查询 sys.schema_table_lock_waits 这张表,我们就可以直接找出造成阻塞的 process id,把这个连接用 kill 命令断开即可。(MySQL 启动时需要设置 performance_schema=on,相比于设置为 off 会有 10% 左右的性能损失)
在这里插入图片描述

等 flush

在这里插入图片描述

这个状态表示的是,现在有一个线程正要对表 t 做 flush 操作。MySQL 里面对表做 flush 操作的用法,一般有以下两个:

flush tables t with read lock;

flush tables with read lock;

如果指定表 t 的话,代表的是只关闭表 t;如果没有指定具体的表名,则表示关闭 MySQL 里所有打开的表。

但是正常这两个语句执行起来都很快,除非它们也被别的线程堵住了。

所以,出现 Waiting for table flush 状态的可能情况是:有一个 flush tables 命令被别的语句堵住了,然后它又堵住了我们的 select 语句。
在这里插入图片描述

等行锁

在这里插入图片描述
显然,session A 启动了事务,占有写锁,还不提交,是导致 session B 被堵住的原因。

如果是 MySQL 5.7 版本,可以通过 sys.innodb_lock_waits 表查到是谁占着这个写锁。
在这里插入图片描述
4 号线程是造成堵塞的罪魁祸首。而干掉这个罪魁祸首的方式,就是 KILL QUERY 4 或 KILL 4。

不过,这里不应该显示“KILL QUERY 4”。这个命令表示停止 4 号线程当前正在执行的语句,而这个方法其实是没有用的。因为占有行锁的是 update 语句,这个语句已经是之前执行完成了的,现在执行 KILL QUERY,无法让这个事务去掉 id=1 上的行锁。

实际上,KILL 4 才有效,也就是说直接断开这个连接。这里隐含的一个逻辑就是,连接被断开的时候,会自动回滚这个连接里面正在执行的线程,也就释放了 id=1 上的行锁。

2.查询慢

条件没有索引

select * from t where c=50000 limit 1;

这个语句只能走 id 主键顺序扫描,因此需要扫描 5 万行。

长事务

select * from t where id=1;
在这里插入图片描述
虽然扫描行数是 1,但执行时间却长达 800 毫秒。

select * from t where id=1 lock in share mode
在这里插入图片描述
执行时扫描行数也是 1 行,执行时间是 0.2 毫秒。

在这里插入图片描述
在这里插入图片描述

session B 更新完 100 万次,生成了 100 万个回滚日志 (undo log)。

带 lock in share mode 的 SQL 语句,是当前读,因此会直接读到 1000001 这个结果,所以速度很快;而 select * from t where id=1 这个语句,是一致性读,因此需要从 1000001 开始,依次执行 undo log,执行了 100 万次以后,才将 1 这个结果返回。

注意,undo log 里记录的其实是“把 2 改成 1”,“把 3 改成 2”这样的操作逻辑。


思考题

1、select * from t where id=1 lock in share mode;由于 id 上有索引,所以可以直接定位到 id=1 这一行,因此读锁也是只加在了这一行上。但如果是下面的 SQL 语句,

begin;
select * from t where c=5 for update;
commit;

这个语句序列是怎么加锁的呢?加的锁又是什么时候释放呢?

: 由于c字段没有加索引,所以会全表扫描,并且每一行都加上行锁,还会加上间隙锁(RR级别下)。
读提交隔离级别下,在语句执行完成后,是只有行锁的。而且语句执行完成后,InnoDB 就会把不满足条件的行行锁去掉。当然了,c=5 这一行的行锁,还是会等到 commit 的时候才释放的。

可重复读隔离级别下,所有的锁都是在commit时释放。


2、假设现在表里面,有 100 万行数据,其中有 10 万行数据的 b 的值是’1234567890’

CREATE TABLE `table_a` (
  `id` int(11) NOT NULL,
  `b` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `b` (`b`)
) ENGINE=InnoDB;

select * from table_a where b='1234567890abcd';
这时候,MySQL 会怎么执行呢?

:最理想的情况是,MySQL 看到字段 b 定义的是 varchar(10),那肯定返回空呀。可惜,MySQL 并没有这么做。

那要不,就是把’1234567890abcd’拿到索引里面去做匹配,肯定也没能够快速判断出索引树 b 上并没有这个值,也很快就能返回空结果。但实际上,MySQL 也不是这么做的。

这条 SQL 语句的执行很慢,流程是这样的:

  1. 在传给引擎执行的时候,做了字符截断。因为引擎里面这个行只定义了长度是 10,所以只截了前 10 个字节,就是’1234567890’进去做匹配;这样满足条件的数据有 10 万行;
  2. 因为是 select *, 所以要做 10 万次回表;
  3. 但是每次回表以后查出整行,到 server 层一判断,b 的值都不是’1234567890abcd’;
  4. 返回结果是空。

参考资料:林晓斌——MySQL实战45讲

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!