SQL优化系统解析

发布于:2025-07-30 ⋅ 阅读:(16) ⋅ 点赞:(0)

MySQL的安装就不讲述了, 本篇文章着重讲解sql优化

本篇是对B站颜群老师视频讲解的笔记梳理, 感兴趣的可以去看下老师的原视频: SQL优化

MySQL原理

1. MySQL逻辑分层: 连接层->服务层->引擎层->存储层(如图)

  • 连接层:提供与客户端连接的服务
  • 服务层:提供各种用户使用的接口(select…)/提供各种sql优化器(mysql query optimizer)
  • 引擎层:提供了各种存储数据的方式(InnoDB和MyIsam)
  • 存储层:存储数据
    在这里插入图片描述
  • InnoDB引擎(默认使用): 事务优先, 适合高并发操作, 采用的是行锁
  • MyISAM: 性能优先, 采用的是表锁

2. 查看数据库引擎

支持哪些引擎的命令

show engines;

查看当前使用的引擎

show variables like '%storage_engine%';

指定数据库对象的引擎

create table tb(....)
ENGINE=MYISAM AUTO_INCREMENT=1
DEFAULT CHARSET=UTF8MB4;

3.SQL优化

3.1 原因: 性能低, 执行时间太长, 等待时间太久, SQL语句欠佳(连接查询), 索引失效, 服务器参数设置不合理(缓冲, 线程数…)

  • SQL的编写过程:
select distinct...from...join...on...where...group by...having...order by...limit...
  • SQL的解析过程
from...on...join...where...group by...having...select distinct...order by...limit...
  • SQL优化,主要是优化索引
    索引相当于书的目录, 是帮助mysql高效获取数据的数据结构(树: B树, Hash树)
    在这里插入图片描述
    在这里插入图片描述
  • 索引的弊端
    1. 索引本身很大, 可以存放在内存/硬盘(一般为硬盘)
    2. 索引不是所有情况都适用(少量数据/频繁更新的字段/很少使用的字段并不适合索引)
    3. 索引会降低增删改的效率
  • 索引的优势
    1. 提高查询效率, 降低IO使用率
    2. 降低CPU使用率(…order by age desc, 因为B树索引本身就是一个好排序的结构, 因此可以在排序时直接使用)

4. 索引

4.1 索引分类

  • 主键索引: 不能重复, 不能是null, 是一种约束, 例如id
  • 唯一索引: 不能重复, 可以是null, 是一种索引
  • 单值索引: 单列, 一个表可以有多个单值索引
  • 复合索引: 多个列构成的索引, 相当于二级目录

附: 主键索引和唯一索引的区别

  1. 主键是一种约束, 而唯一索引是一种索引, 二者在本质上市不同的
  2. 主键可以被其他表作为外键引用, 而唯一索引不能
  3. 主键列不允许空值, 唯一索引列允许空值
  4. 一张表里可以多个唯一索引, 但是只有一个主键
  5. 主键创建后一定包含一个唯一索引, 而唯一索引不一定是主键

4.2 创建索引

  • 方式一: create 索引类型 索引名 on 表(字段)
单值索引 : create index dept_index on tb(dept);
唯一索引:  create unique index name_index on tb(name);
复合索引:  create index dept_name_index on tb(dept,name);
  • 方式二: alter table 表名 索引类型 索引名(字段)
单值索引: alter table tb add index dept_index(dept);
唯一索引: alter table tb add unique index name_index(name);
复合索引: alter table tb add index dept_name_index(dept,name);

4.3 删除索引

drop index 索引名 on 表名;
drop index name_index on tb;

4.4 查询索引

show index from 表名;
show index from 表名 \G

5. SQL性能问题

  1. 分析SQL的执行计划: explain, 可以模拟sql优化器执行SQL语句, 从而让开发人员知道自己编写的SQL状况
  2. MySQL查询优化器会干扰我们的优化
  3. 优化方法, 官网https://dev.mysql.com/doc/refman/8.0/en/optimize-overview.html
  4. 查询执行计划: explain + SQL语句, 如图:在这里插入图片描述
  5. 字段详解
    id: 编号
    select_type: 查询类型
    table: 表名
    partitions: 分区
    type: 类型
    possible_keys: 预测使用到的索引
    key: 实际使用到的索引
    key_len: 实际使用到的索引长度
    ref: 表之间的引用
    rows: 通过索引查询到的数据量
    filtered: 返回结果的行数占读取行数的百分比
    Extra: 额外的信息

5.1 准备数据

课程表

create table course(
	cid int(3),
	cname varchar(20),
	tid int(3)
);

教师表

create table teacher(
	tid int(3),
	tname varchar(20),
	tcid int(3)
);

教师信息表

create table teacher_card(
	tcid int(3),
	tcdesc varchar(200)
);

插入数据

insert into course values(1,'java',1);
insert into course values(2,'html',1);
insert into course values(3,'sql',2);
insert into course values(4,'web',3);

insert into teacher values(1,'tz',1);
insert into teacher values(2,'tw',2);
insert into teacher values(3,'tl',3);

insert into teacher_card values(1,'tzdesc') ;
insert into teacher_card values(2,'twdesc') ;
insert into teacher_card values(3,'tldesc') ;

5.2 执行过程分析

5.2.1 id
select tc.tcdesc from course c join teacher t on c.tid = t.tid join teacher_card tc on t.tcid = tc.tcid where c.cid = 2 or tc.tcid = 3

在这里插入图片描述
id值相同, 从上往下顺序执行, t - c -tc
表的执行顺序, 因数量的个数改变而改变的原因: 笛卡尔积

a  b  c
4  3  2  =  2*3*4 = 6*4 = 24
		 =	3*4*2 = 12*2 = 24

查询原则: 数据小的表, 优先查询
id值不同: id值越大, 越优先查询(本质: 在嵌套时, 先查内层, 再查外层)

子查询 + 多表: 查询教授SQL课程的老师的描述

-- 查询教授sql的老师tid
select tid from course where cname = 'sql';
-- 查询该老师的tcid
select tcid from teacher where tid = (select tid from course where cname = 'sql');
-- 查询该tcid对应老师的描述
select tcdesc from teacher_card where tcid = (select tcid from teacher where tid = (select tid from course where cname = 'sql'));

在这里插入图片描述
id值有相同, 又有不同; id值越大越优先, id值相同, 从上往下顺序执行

5.2.2 select_type: 查询类型

PRIMARY: 包含子查询SQL中的主查询(最外层)
SUBQUERY:包含子查询SQL中的子查询(非最外层)
SIMPLE:简单查询(不包含子查询, union)
DERIVED: 衍生查询(使用到了临时表)
UNION:

  • 在from子查询中只有一张表
explain select cr.cname from(select * from course where tid in (1,2)) cr;
  • 在from子查询中, 如果有table1 union table2, 则table1就是derived, table2就是union
explain select cr.cname from (select * from course where tid = 1 union select * from course where tid = 2) cr;

在这里插入图片描述
UNION RESULT:告知开发人员, 哪些表之间存在union查询

5.2.3 type: 索引类型, 类型

级别对比: system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
其中, system,const只是理想情况, 实际能达到ref > range

5.2.3.1 system(忽略): 只有一条数据的系统表, 或衍生表只有一条数据的主查询
5.2.3.2 const: 仅仅能查到一条数据的sql, 用于primary key 或 unique 索引(类型与索引类型有关)
-- 建表插入数据
create table test01(
	tid int(3),
	tname varchar(20)
);

insert into test01 values(1,'a');
commit;
-- 增加索引并解析
alter table test01 add constraint tid_pk primary key(tid) ;
explain select * from (select * from test01) t where tid = 1;

在这里插入图片描述

5.2.3.3 eq_ref: 唯一性索引, 对于每个索引键的查询, 返回匹配唯一行数据(有且只有1个, 不能多, 不能0), 常见于唯一索引和主键索引
lalter table teacher_card add constraint pk_tcid primary key(tcid);
alter table teacher add constraint uk_tcid unique index(tcid);
explain select t.tcid from teacher t, teacher_card tc where t.tcid = tc.tcid;

以上SQL, 用到的索引是t.tcid, 即teacher表中的tcid字段;
除非teacher表的数据个数与连接查询的数据个数一致, 则有可能满足eq_ref级别, 否则无法满足
在这里插入图片描述

5.2.3.4 ref: 非唯一性索引, 对于每个索引键的查询, 返回匹配的所有行(0条或多条)

准备数据:

insert into teacher values(4,'tz',4);
insert into teacher_card values(4,'tz222');

测试:

alter table teacher add index index_name(tname);
explain select * from teacher where tname = 'tz';

在这里插入图片描述

5.2.3.5 range: 检索指定范围的行, where后面是一个范围查询(between, >, <, >=, <=)
alter table teacher add index tid_index(tid);
explain select * from teacher t where t.tid in (1,2);
explain select * from teacher t where t.tid < 3;

特殊: in有时候会失效, 从而转为无索引 all
在这里插入图片描述
在这里插入图片描述

5.2.3.6 index: 查询全部索引中数据
explain select tid from teacher;

tid是索引, 只需要扫描索引表,不需要所有表中的数据
在这里插入图片描述

5.2.3.7 all: 查询全部表中的数据
explain select cid from course;

cid 不是索引, 需要全表扫描
在这里插入图片描述

5.2.4 possible_keys: 可能用到的索引, 是一种预测, 不准
alter table course add index index_cname_index(cname);

如果possible_keys/key是null, 则说明没用索引

explain select tc.tcdesc from teacher_card tc, course c, teacher t where c.tid = t.tid and t.tcid = tc.tcid and c.cname = 'sql';

在这里插入图片描述

5.2.5 key: 实际使用到的索引(具体可参考上面的)
5.2.6 key_len: 索引的长度

作用: 用于判断复合索引是否被完全使用(a,b,c)

create table test_kl(
	name char(20) not null default ''
);
alter table test_kl add index index_name(name);
explain select * from test_kl where name = '';

在这里插入图片描述
在utf8mb4编码中, 一个字符占4个字节

alter table test_kl add column name1 char(20); -- name1可以为null
-- 如果索引字段可以为null, 则会使用1个字节用于标识
explain select * from test_kl where name1 = ''; -- null
-- 增加一个符合索引
alter table test_kl add index name_name1_index(name,name1);
explain select * from test_kl where name1 = ''; -- null
explain select * from test_kl where name = ''; -- 80
-- 可以为null
alter table test_kl add column name2 varchar(20);
alter table test_kl add index name2_index(name2);
-- 83 = 20 * 4 = 80 + 1(null) + 2(2个字节标识可变长度)
explain select * from test_kl where name2 = '';
5.2.7 ref: 注意与type中的ref值区分

作用: 指明当前所参照的字段

select ...where a.c = b.x;(其中b.x可以是常量, const)
alter table course add index tid_index(tid);
explain select * from course c, teacher t where c.tid = t.tid and t.tname = 'tw';

在这里插入图片描述

5.2.8 rows: 被索引优化查询的数据条数(实际通过索引而查询到的数据条数)
explain select * from course c, teacher t where c.tid = t.tid and t.tname = 'tz';

在这里插入图片描述

5.2.9 Extra: 额外的
5.2.9.1 using filesort: 性能消耗大, 需要额外的一次排序(查询), 常见于order by语句中

排序:先查询
单值索引

create table test02(
	a1 char(3),
    a2 char(3),
    a3 char(3),
    index idx_a1(a1),
    index idx_a2(a2),
    index idx_a3(a3)
);

explain select * from test02 where a1 = '' order by a1;

在这里插入图片描述

explain select * from test02 where a1 = '' order by a2; -- using filesort

小结: 避免using filesort 的出现, where哪些字段, 就order by哪些字段

复合索引

drop index idx_a1 on test02;
drop index idx_a2 on test02;
drop index idx_a3 on test02;
alter table test02 add index idx_a1_a2_a3(a1,a2,a3);
explain select * from test02 where a1 = '' order by a3; -- 跨列使用
explain select * from test02 where a2 = '' order by a3; -- 跨列使用
explain select * from test02 where a1 = '' order by a2; -- 有序使用
explain select * from test02 where a2 = '' order by a1; -- 无序使用

小结: 避免using filesort的方法, where和order by 按照复合索引的顺序使用, 不要跨列或者无序使用

5.2.9.2 using temporary: 性能损耗大, 用到了临时表, 一般出现在group by语句中
explain select a1 from test02 where a1 in ('1','2','3') group by a1;
explain select a1 from test02 where a1 in ('1','2','3') group by a2; -- using temporary

在这里插入图片描述
规避原则: 查询哪些列, 就根据哪些列group by

5.2.9.3 using index: 性能提升, 索引覆盖

原因: 不读取源文件, 只从索引文件中获取数据(不需要回表查询), 只要使用到的列全部在索引中, 就是索引覆盖
例如: test02表中有一个复合索引(a1, a2, a3)

explain select a1,a2 from test02 where a1 = '' or a2 = ''; -- using index
drop index idx_a1_a2_a3 on test02;
alter table test02 add index idx_a1_a2(a1,a2);
explain select a1,a3 from test02 where a1 = '' or a3 = '';
-- 如果用到了索引覆盖(using index), 会对possible_keys 和key造成影响:
-- a. 如果没有where, 则索引只出现在key中;
-- b. 如果有where, 则索引出现在key和possible_keys中
explain select a1,a2 from test02 where a1 = '' or a2 = '';
explain select a1,a2 from test02;
5.2.9.4 using where(需要回表查询)

假设age是索引列, 但查询语句select age, name from …where age = … 此语句中必须回原表查Name, 因此会显示using where

explain select a1,a3 from test02 where a3 = ''; -- a3需要回表查询
5.2.9.5 impossible where : where 子句永远为false
explain select * from test02 where a1 = 'x' and a1 = 'y';

6. 优化案例(单表优化, 双表优化, 三表优化)

6.1 单表优化

create table book(
	bid int(4) primary key,
    name varchar(20) not null,
    authorid int(4) not null,
    publicid int(4) not null,
    typeid int(4) not null
);

insert into book values(1,'tjava',1,1,2);
insert into book values(2,'tc',2,1,2);
insert into book values(3,'wx',3,2,1);
insert into book values(4,'math',4,2,3);
6.1.1 查询authorid 为1并且 typeid 为2或3的bid
explain select bid from book where authorid = 1 and (typeid in (2,3)) order by typeid desc;
-- 优化,加索引
alter table book add index idx_bta(bid,authorid,typeid);
-- 虽然是using index, 但是也有回表查询的情况

在这里插入图片描述

-- 索引一旦进行升级优化, 需要将之前废弃的索引删掉,防止干扰
drop index idx_bta on book;
-- 根据sql实际解析的顺序, 调整索引的顺序
alter table book add index idx_atb(authorid,typeid,bid);
-- 再次优化(之前是index级别): 因为范围查询in有时候会失效, 因此交换索引的顺序,将typeid in(2,3)放在最后

在这里插入图片描述

6.1.2 小结
  1. 最佳左前缀, 保证索引定义与使用的顺序一致性
  2. 索引需要逐步优化
  3. 将含in的范围查询放在where条件的最后, 防止索引失效
6.1.3 本例中同时出现了using where(需要回原表); using index(不需要回原表);

原因:
where authorid = 1 and typeid in (2, 3)中, authorid 在索引(authorid,typeid,bid)中, 因此不需要回原表(直接在索引表中就能查到); 而typeid虽然也在索引(authorid,typeid,bid)中, 但是含in的范围查询以及or的条件连接查询已经使该typeid索引失效, 因此相当于没有typeid这个索引, 所以需要回原表(using where)
例如以下没有了in, 则不会出现using where

explain select bid from book where authorid = 1 and typeid = 2 order by typeid desc;

在这里插入图片描述
还可以通过key_len属性证明in可以使索引失效

6.2 双表优化

create table teacher2(
	tid int(4) primary key,
    cid int(4) not null
);

insert into teacher2 values(1,2);
insert into teacher2 values(2,1);
insert into teacher2 values(3,3);

create table course2(
	cid int(4),
    cname varchar(20)
);

insert into course2 values(1,'java');
insert into course2 values(2,'python');
insert into course2 values(3,'kotlin');
6.2.1 左连接
explain select * from teacher2 t left join course2 c on t.cid = c.cid where c.cname = 'java';

Q: 索引往哪张表加?
A: 小表驱动大表, 索引建立在经常使用的字段上(本题t.cid = c.cid可知, cid字段使用频繁, 因此给该字段加上索引), 一般情况下, 对于左外连接, 给左表加索引, 右外连接, 给右表加索引
小表: 10条数据

大表: 300条数据

where 小表 x10 = 大表y300 ; – 循环了10次

​ 大表y 300 = 小表x 10; – 循环了300次

select ...where 小表.x10=大表.x300 ;
	for(int i=0;i<小表.length10;i++)
	{
		for(int j=0;j<大表.length300;j++)
		{
			...
		}
	}
	

select ...where 大表.x300=小表.x10 ;
	for(int i=0;i<大表.length300;i++)
	{
		for(int j=0;j<小表.length10;j++)
		{
			...
		}
	}
	
-- 以上2个for循环, 最终都会循环3000次;但是对于双层循环来说, 一般建议将数据量小的循环放外层, 数据量大的循环放内层
-- 当编写...on t.cid = c.cid时, 将数据量小的表放左边(假设此时t表数据量小)
alter table teacher2 add index index_teacher2_cid(cid) ;
alter table course2 add index index_course2_cname(cname);

Using join buffer:extra中的一个选项,作用:Mysql引擎使用了 连接缓存。

6.3 三张表优化A B C

a. 小表驱动大表
b. 索引建立在经常查询的字段上
create table test03(
	a1 int(4) not null,
    a2 int(4) not null,
    a3 int(4) not null,
    a4 int(4) not null
);

alter table test03 add index idx_a1_a2_a3_4(a1,a2,a3,a4) ;
-- 推荐写法,因为索引的使用顺序(where后面的顺序), 和复合索引的顺序保持一致
explain select a1,a2,a3,a4 from test03 where a1 = 1 and a2 = 2 and a3 = 3 and a4 = 4;
-- 虽然编写顺序和索引顺序不一致, 但是sql在真正执行前, 经过了sql优化器的调整,结果与上条是一致的
explain select a1,a2,a3,a4 from test03 where a4 = 1 and a2 = 2 and a3 = 3 and a1 = 4;
-- 以上两个sql, 使用了全部的复合索引

explain select a1,a2,a3,a4 from test03 where a1 = 1 and a2 = 2 and a4 = 4 order by a3; 
-- 以上sql用到了a1,a2两个索引,该两个字段不需要回表查询using index; 而a4因为跨列使用,造成该索引失效,需要回表查询, 因此是where;以上可以通过key_len验证

explain select a1,a2,a3,a4 from test03 where a1 = 1 and a4 = 4 order by a3; 
-- 以上sql出现了using filesort(文件排序, "多了一次额外的查找排序";) 不要跨列使用(where和order by拼起来, 不要跨列使用)

explain select a1,a2,a3,a4 from test03 where a1 = 1 and a4 = 4 order by a2, a3;
-- 以上sql不会出现using filesort
c. 总结
  1. 如果(a,b,c,d)复合索引和使用的顺序全部一致, 且不跨列使用, 则复合索引全部使用, 如果部分一致(且不跨列使用), 则使用的部分索引. 例如: select a,c where a =... and b = ... and c = ...
  2. where 和order by 拼起来, 不需要跨列使用
  3. usring temporary : 需要额外多使用一张表, 一般出现在group by语句中, 已经有表了, 但是不适用, 必须再来一张表
explain select * from test03 where a2 = 2 and a4 = 4 group by a2,a4; -- 没有using temporary
explain select * from test03 where a2 = 2 and a4 = 4 group by a3;
  1. 解析过程: from...on...join...where...group by...having...select...distinct...order by...limit...

7.避免索引失效的一些原则

7.1 复合索引

  1. 不要跨列使用或者无序使用
  2. 尽量使用全索引匹配

7.2 不要在索引上进行任何操作(计算, 函数, 类型转换), 否则索引失效

select ... where A.x = ...; -- 假设A.x是索引
-- 不要进行对索引进行操作, 如下
select .. where A.x * 3 = ...;
explain select * from book where authorid = 3 and typeid = 2;
-- 以上sql用到了'a','t'2个索引
explain select * from book where authorid = 3 and typeid * 2 = 2; 
-- 以上sql用到了'a'1个索引
explain select * from book where authorid * 2 = 3 and typeid = 2;
-- 以上sql用到了0个索引, 对于复合索引,如果左边失效,右侧全部失效

-- 单索引(不适用最佳左前缀)
drop index idx_atb on book;
alter table book add index idx_authorid(authorid);
alter table book add index idx_typeid(typeid);
explain select * from book where authorid = 1 and typeid = 2;

7.3 复合索引不能使用"!=“, “is (not) null”, 否则自身以及右侧索引全部失效, 如果复合索引中有”>", 则自身以及右侧索引全部失效

explain select * from book where authorid = 1 and typeid = 2;
explain select * from book where authorid != 1 and typeid = 2;
explain select * from book where authorid != 1 and typeid != 2;
-- sql优化,是一种概率层面的优化. 至于是否实际使用了我们的优化, 需要通过explain进行推测
-- 体验概率情况(>,<,=):原因是服务层中有sql优化器,可能会影响我们的优化
drop index idx_authorid on book;
drop index idx_typeid on book;
alter table book add index idx_book_at(authorid,typeid);
explain select * from book where authorid = 1 and typeid = 2; -- 复合索引at全部使用
explain select * from book where authorid > 1 and typeid = 2; -- 复合索引如果有>, 则自身和右侧索引全部失效
explain select * from book where authorid = 1 and typeid > 2; -- 复合索引全部使用
-- 明显的概率问题 --
explain select * from book where authorid < 1 and typeid = 2; -- 复合索引只用到了1个索引
explain select * from book where authorid < 10 and typeid = 2; -- 复合索引全部失效

7.4 补救: 尽量使用索引覆盖(using index)

-- 假设表中有复合索引(a,b,c), 尽量以下面这种方式去写
select a,b,c from ... where a = ... and b = ...and c = ...;

7.5 like尽量以"常量"开头, 不要以"%"开头, 否则索引失效

select * from ... where name like '%x%'; -- name索引失效
explain select * from teacher where tname like '%x%'; -- tname索引失效
explain select * from teacher where tname like 'x%';
explain select tname from teacher where tname like '%x%'; -- 如果必须要用like'%X%'进行模糊查询, 可以使用索引覆盖挽救一部分.

7.6 尽量不要使用类型转换(显示/隐式), 否则索引失效

explain select * from teacher where tname = 'abc';
explain select * from teacher where tname = 123; -- 程序底层将123 -> '123', 进行了类型转换, 因此索引失效

7.7 尽量不要使用or, 否则会使索引失效

explain select * from teacher where tname = '' or tcid > 1; -- or左侧的tname将失效

8. 一些其他的优化方法

8.1 exist和in

select ... from table where exists (子查询);
select ... from table where column in (子查询);
-- 如果主查询的数据集大, 则使用in, 效率高
-- 如果子查询的数据集大, 则使用exists, 效率高
-- exists语法: 将主查询的结果, 放到子查询结果中去校验(看子查询是否有数据,如果有数据, 则校验成功), 如果符合校验, 则保留数据
select tname from teacher where exists (select * from teacher);
-- 等价于 select tname from teacher
select tname from teacher where exists (select * from teacher where tid = 9999);

-- in语法
select ... from table where id in (1,3,5);

8.2 order by 优化

using filesort 有两种算法(根据io的次数, io消耗性能): 单路排序, 双路排序

MySQL4.1 之前默认使用双路排序,:
双路排序: 扫描2次磁盘

  1. 从磁盘读取排序字段, 对排序字段进行排序(在buffer中进行的排序)
  2. 扫描其他字段
    MySQL4.1之后默认使用单路排序: 只读一次(全部字段), 在buffer中进行排序, 但这种单路排序会有一定的隐患(不一定真的是单路1次io, 有可能多次io).
    原因:`如果数据量特别大, 则无法将所有字段的数据一次性读取完毕, 因此会进行单路排序; 单路排序在使用时, 如果数据量大, 可以考虑调整buffer的大小
set max_length_for_sort_data = 1024; -- 单位为byte

如果max_length_for_sort_data值太低, 则mysql会自动从单路切换到双路(太低: 需要排序的字段的总大小超过了max_length_for_sort_data定义的字节数)

提高order by 查询的策略:

  1. 选择使用单路还是双路, 调整buffer容量的大小
  2. 避免select * ......
  3. 复合索引: 不要跨列使用, 避免using filesort
  4. 保证全部的排序字段, 排序的一致性(都是升序或者降序)

网站公告

今日签到

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