跳到主要内容

SQL慢查询优化

问题复现

select a.*
from scenic_spot_audio a
LEFT JOIN scenic_spot_audio_type b on a.category_id = b.id
where a.scenic_id = 11253
and a.type = 1
and b.foreign_name = '中文'

建表语句:

create table scenic_spot_audio
(
id bigint auto_increment
primary key,
category_id varchar(100) null,
type varchar(100) null,
scenic_id varchar(255) null,
audio varchar(255) null,
is_default varchar(10) null,
audio_name varchar(255) null,
is_tencent varchar(2) null,
language varchar(20) null,
sound_sex varchar(20) null,
season varchar(20) null,
special varchar(20) null,
duration varchar(20) null,
introduce text null
);

create table scenic_spot_audio_type
(
id bigint auto_increment
primary key,
name varchar(100) null,
pic varchar(255) null,
foreign_name varchar(100) null
);

可以看到除了主键索引之外没有其他索引

先查询一次大概多少数据:

select count(*) as audio_count
from scenic_spot_audio;
select count(*) as audio_type_count
from scenic_spot_audio_type;

结果是:

audio_count=591090
audio_type_count=14

再来看这条SQL语句的执行时间,开启查询时间,然后执行SQL

SHOW VARIABLES LIKE 'profiling';
SET profiling = 1;
SHOW PROFILES;
0.68286025,"/* ApplicationName=IntelliJ IDEA 2023.3.6 */ SELECT a.*
FROM scenic_spot_audio a
LEFT JOIN scenic_spot_audio_type b ON a.category_id = b.id
WHERE a.scenic_id = 11253
AND a.type = 1
AND b.foreign_name = '中文'"

或者navicat中也可以看查询时间,可以看到大概在0.7-0.8s左右

image-20240805145539773

优化过程

使用explain查看执行计划

idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEbnullALLPRIMARYnullnullnull1410Using where
1SIMPLEanullALLnullnullnullnull6249280.1Using where; Using join buffer (Block Nested Loop)

Using where:

  • MySQL 在服务器端过滤行,而不仅仅依赖于索引条件。即使有索引可用,但还需要进一步筛选符合 WHERE 子句条件的行。

Using join buffer (Block Nested Loop):

  • 表示 MySQL 使用了连接缓冲区。这通常发生在没有足够的索引来高效地执行连接操作时,MySQL 会将一个表的结果存储在一个缓冲区中,然后用该缓冲区与另一个表进行连接。这通常表明连接操作效率较低,可以通过添加适当的索引来优化。

可以看到两条执行计划都是走的全表扫描:

优化方式:

首先针对scenic_spot_audio_type表,即便数据很少,我们也尽量走索引:

create index scenic_spot_audio_type_foreign_name_index
on scenic_spot_audio_type (foreign_name);

此时的执行计划:

idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEbnullrefPRIMARY,scenic_spot_audio_type_foreign_name_indexscenic_spot_audio_type_foreign_name_index403const1100Using index
1SIMPLEanullALLnullnullnullnull6249280.1Using where; Using join buffer (Block Nested Loop)

可以看到现在第二张表走索引了type: ref (使用索引)Using index 表示这个查询是覆盖索引查询,即查询可以直接从索引中获取所需的数据,而不需要访问表的行数据。

针对第一张表,我们考虑联合索引:在 scenic_idtypecategory_id字段上创建复合索引,以加速过滤条件的匹配

CREATE INDEX idx_scenic_spot_audio_full ON scenic_spot_audio(scenic_id, type, category_id);
idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEbnullrefPRIMARY,scenic_spot_audio_type_foreign_name_indexscenic_spot_audio_type_foreign_name_index403const1100Using index
1SIMPLEanullALLidx_scenic_spot_audio_scenic_id_typenullnullnull6249280.1Using where; Using join buffer (Block Nested Loop)

可以看到依然走全表。

仔细观察,发现建表语句的时候使用的全是varchar类型,而我查询的时候用的是数字类型,

在字段类型与查询条件不一致的情况下,数据库需要对数据进行类型转换,这会导致全表扫描而不是使用索引

我们此时将查询的SQL语句全用字符串而不是数字:

EXPLAIN
SELECT a.*
FROM scenic_spot_audio a
INNER JOIN scenic_spot_audio_type b ON a.category_id = b.id
WHERE a.scenic_id = '11253'
AND a.type = '1'
AND b.foreign_name = '中文';

观察执行计划:

idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEbnullrefPRIMARY,scenic_spot_audio_type_foreign_name_indexscenic_spot_audio_type_foreign_name_index403const1100Using index
1SIMPLEanullrefidx_scenic_spot_audio_fullidx_scenic_spot_audio_full1426const,const310Using index condition

此时我们可以看到两张表都走索引了,执行SQL语句

image-20240805151537389

可以看到时间从0.8s降低到0.034s,性能提高了20倍左右。

考虑到表中还有一个introduce字段,这个字段是对景点信息的介绍,是个大文本,因此也不太适合放同一张表,最好去做一个分表操作,防止数据量继续加大的时候影响性能。此处不在考虑这个问题。

总结

优化数据库查询的主要思路是看执行计划走不走索引,不走索引全盘扫会慢很多

如果索引字段是字符串类型,但是在条件查询中,输入的参数是整型的话,执行计划的结果发现这条语句会走全表扫描。