本文主要是介绍一张900w的数据表,怎么把原先要花费17s执行的SQL优化到300ms?(转),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
转自 一次SQL查询优化原理分析(900W+数据,从17s到300ms)
如需要请跳转至原文查看
问题
有一张财务流水表,未分库分表,目前的数据量为9555695,分页查询使用到了limit,优化之前的查询耗时16 s 938 ms (execution: 16 s 831 ms, fetching: 107 ms),按照下文的方式调整SQL后,耗时347 ms (execution: 163 ms, fetching: 184 ms);
操作:查询条件放到子查询中,子查询只查主键ID,然后使用子查询中确定的主键关联查询其他的属性字段;
原理:减少回表操作
-- 优化前SQLSELECT 各种字段FROM `table_name`WHERE 各种条件LIMIT 0,10
-- 优化后SQLSELECT 各种字段FROM `table_name` main_taleRIGHT JOIN (SELECT 子查询只查主键FROM `table_name`WHERE 各种条件LIMIT 0,10;) temp_table ON temp_table.主键 = main_table.主键
前言
首先说明使用的MYSQL版本:
表结构:
id为自增主键,val为非唯一索引。
数据准备:
-- 创建表
CREATE table `test`(id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,val int(10) UNSIGNED NOT NULL DEFAULT 0,source int(10) UNSIGNED NOT NULL DEFAULT 0
) -- 创建val字段索引
CREATE INDEX MUL ON test(val)-- 查看表结构
desc test-- 编辑循环插入数据的存储过程 500W条数据
drop PROCEDURE callback
DELIMITER //
CREATE PROCEDURE callback()
begindeclare num int;set num = 0;repeatINSERT INTO test(val, source)VALUES(4,4);set num = num + 1;until num >= 5000000end repeat;
end;
//
--调用存储过程 出入数据
CALL callback();
灌入大量数据,共500万:
我们知道,当limit offset rows中的offset很大时,会出现效率问题: 30W数据在8.0版本查找得时间会较短,若要看到效果,可改为300W
为了达到相同的目的,我们一般会改写成如下语句:
select * from test a inner join (select id from test where val=4 limit 300000,5) b on a.id=b.id;
时间相差很明显。
为什么会出现上面的结果?我们看一下select * from test where val=4 limit 300000,5;的查询过程:
查询到索引叶子节点数据。
根据叶子节点上的主键值去聚簇索引上查询需要的全部字段值。
类似于下面这张图:
像上面这样,需要查询300005次索引节点,查询300005次聚簇索引的数据,最后再将结果过滤掉前300000条,取出最后5条。MySQL耗费了大量随机I/O在查询聚簇索引的数据上,而有300000次随机I/O查询到的数据是不会出现在结果集当中的。
肯定会有人问:既然一开始是利用索引的,为什么不先沿着索引叶子节点查询到最后需要的5个节点,然后再去聚簇索引中查询实际数据。这样只需要5次随机I/O,类似于下面图片的过程:
证实
下面我们实际操作一下来证实上述的推论:
为了证实select * from test where val=4 limit 300000,5是扫描300005个索引节点和300005个聚簇索引上的数据节点,我们需要知道MySQL有没有办法统计在一个sql中通过索引节点查询数据节点的次数。我先试了Handler_read_*系列,很遗憾没有一个变量能满足条件。
我只能通过间接的方式来证实:
InnoDB中有buffer pool。里面存有最近访问过的数据页,包括数据页和索引页。所以我们需要运行两个sql,来比较buffer pool中的数据页的数量。预测结果是运行select * from test a inner join (select id from test where val=4 limit 300000,5); 之后,buffer pool中的数据页的数量远远少于select * from test where val=4 limit 300000,5;对应的数量,因为前一个sql只访问5次数据页,而后一个sql访问300005次数据页。
select * from test where val=4 limit 300000,5
mysql> select index_name,count(*) from information_schema.INNODB_BUFFER_PAGE where INDEX_NAME in('val','primary') and TABLE_NAME like '%test%' group by index_name;
Empty set (0.04 sec)
可以看出,目前buffer pool中没有关于test表的数据页。
mysql> select * from test where val=4 limit 300000,5;
+---------+-----+--------+
| id | val | source |
+---------+-----+--------+|
| 3327622 | 4 | 4 |
| 3327632 | 4 | 4 |
| 3327642 | 4 | 4 |
| 3327652 | 4 | 4 |
| 3327662 | 4 | 4 |
+---------+-----+--------+
5 rows in set (26.19 sec)mysql> select index_name,count(*) from information_schema.INNODB_BUFFER_PAGE where INDEX_NAME in('val','primary') and TABLE_NAME like '%test%' group by index_name;
+------------+----------+
| index_name | count(*) |
+------------+----------+
| PRIMARY | 4098 |
| val | 208 |
+------------+----------+2 rows in set (0.04 sec)
可以看出,此时buffer pool中关于test表有4098个数据页,208个索引页。
select * from test a inner join (select id from test where val=4 limit 300000,5) ;为了防止上次试验的影响,我们需要清空buffer pool,重启mysql。
mysql> select index_name,count(*) from information_schema.INNODB_BUFFER_PAGE where INDEX_NAME in('val','primary') and TABLE_NAME like '%test%' group by index_name;
Empty set (0.03 sec)
运行sql:
mysql> select * from test a inner join (select id from test where val=4 limit 300000,5) b on a.id=b.id;
+---------+-----+--------+---------+
| id | val | source | id |
+---------+-----+--------+---------+
| 3327622 | 4 | 4 | 3327622 |
| 3327632 | 4 | 4 | 3327632 |
| 3327642 | 4 | 4 | 3327642 |
| 3327652 | 4 | 4 | 3327652 |
| 3327662 | 4 | 4 | 3327662 |
+---------+-----+--------+---------+
5 rows in set (0.09 sec)mysql> select index_name,count(*) from information_schema.INNODB_BUFFER_PAGE where INDEX_NAME in('val','primary') and TABLE_NAME like '%test%' group by index_name;
+------------+----------+
| index_name | count(*) |
+------------+----------+
| PRIMARY | 5 |
| val | 390 |
+------------+----------+
2 rows in set (0.03 sec)
我们可以看明显的看出两者的差别:第一个sql加载了4098个数据页到buffer pool,而第二个sql只加载了5个数据页到buffer pool。符合我们的预测。也证实了为什么第一个sql会慢:读取大量的无用数据行(300000),最后却抛弃掉。
而且这会造成一个问题:加载了很多热点不是很高的数据页到buffer pool,会造成buffer pool的污染,占用buffer pool的空间。 遇到的问题
为了在每次重启时确保清空buffer pool,我们需要关闭innodb_buffer_pool_dump_at_shutdown和innodb_buffer_pool_load_at_startup,这两个选项能够控制数据库关闭时dump出buffer pool中的数据和在数据库开启时载入在磁盘上备份buffer pool的数据。
分析原因
① select * from test where val=4 limit 3000000,5;
② select * from test a inner join (select id from test where val=4 limit 3000000,5) b on a.id=b.id;
③ select id from test where val=4 limit 3000000,5;
通过explain mysql语句对比分析:①使用*号查询时,只是单单的使用了MUL索引,并没有使用到主键索引
EXtra列表指出:使用②语句时候 ,不需要回表查询。
using index :性能提升;索引覆盖(覆盖索引)。原因:不读取原文件,只从索引文件中获取数据(不需要回表查询)。只要使用到的列,全部都在索引中
select * from 最大的问题是可能会多出一些不用的列,这就杜绝了索引覆盖的可能性,导致查询的成本几何级增高(索引覆盖:就是select的数据列只用从索引中就能够取得,不必从数据表中读取,换句话说查询列要被所使用的索引覆盖。)
本文转自:
一张900w的数据表,怎么把原先要花费17s执行的SQL优化到300ms?
一次SQL查询优化原理分析(900W+数据,从17s到300ms)
参考文章:
https://blog.csdn.net/qq_41267618/article/details/89476196
https://blog.csdn.net/u013308490/article/details/83001060(MySQL索引实现原理分析)
这篇关于一张900w的数据表,怎么把原先要花费17s执行的SQL优化到300ms?(转)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!