MySQL数据库的优化手段通常包括但不限于:
我们主要掌握:SQL查询优化
通过以下命令可以查看当前数据库在SQL语句执行方面的整体情况:
show global status like 'Com_select';
show global status like 'Com_insert';
show global status like 'Com_delete';
show global status like 'Com_update';
show global status like 'Com_______';
这些结果反映了从 MySQL 服务器启动到当前时刻,所有的 SELECT 查询总数。对于 MySQL 性能优化来说,通过查看 Com_select
的值可以了解 SELECT 查询在整个 MySQL 服务期间所占比例的情况:
Com_select
次数过高,可能说明查询表中的每条记录都会返回过多的字段。Com_select
次数很少,同时insert或delete或update的次数很高,可能说明服务器运行的应用程序过于依赖写入操作和少量读取操作。总之,通过查看 Com_select
的值,可以了解 MySQL 服务器的长期执行情况,并在优化查询性能时,帮助我们了解 MySQL 的性能瓶颈。
慢查询日志文件可以将查询较慢的DQL语句记录下来,便于我们定位需要调优的select语句。
通过以下命令查看慢查询日志功能是否开启:
show variables like 'slow_query_log';
慢查询日志功能默认是关闭的。请修改my.ini文件来开启慢查询日志功能,在my.ini的[mysqld]后面添加如下配置:
注意:slow_query_log=1表示开启慢查询日志功能,long_query_time=3表示:只要SELECT语句的执行耗时超过3秒则将其记录到慢查询日志中。
重启mysql服务。再次查看是否开启慢查询日志功能:
尝试执行一条时长超过3秒的select语句:
select empno,ename,sleep(4) from emp where ename='smith';
慢查询日志文件默认存储在:C:\dev\mysql-8.0.36-winx64\data 目录下,默认的名字是:计算机名-slow.log
通过该文件可以清晰的看到哪些DQL语句属于慢查询:
通过show profiles可以查看一个SQL语句在执行过程中具体的耗时情况。帮助我们更好的定位问题所在。
查看当前数据库是否支持 profile操作:
select @@have_profiling;
查看 profiling 开关是否打开:
select @@profiling;
将 profiling 开关打开:
set profiling = 1;
可以执行多条DQL语句,然后使用 show profiles; 来查看当前数据库中执行过的每个SELECT语句的耗时情况。
select empno,ename from emp;
select empno,ename from emp where empno=7369;
select count(*) from emp;
show profiles;
查看某个SQL语句语句在执行过程中,每个阶段的耗时情况:
show profile for query 4;
想查看执行过程中cpu的情况,可以执行以下命令:
show profile cpu for query 4;
explain命令可以查看一个DQL语句的执行计划,根据执行计划可以做出相应的优化措施。提高执行效率。
explain select * from emp where empno=7369;
id反映出一条select语句执行顺序,id越大优先级越高。id相同则按照自上而下的顺序执行。
explain select e.ename,d.dname from emp e join dept d on e.deptno=d.deptno join salgrade s on e.sal between s.losal and s.hisal;
由于id相同,反映出三张表在执行顺序上属于平等关系,执行时采用,先d,再e,最后s。
explain select e.ename,d.dname from emp e join dept d on e.deptno=d.deptno where e.sal=(select sal from emp where ename='ford');
反映出,先执行子查询,然后让e和d做表连接。
反映了mysql查询语句的类型。常用值包括:
反映了这个查询操作的是哪个表。
反映了查询表中数据时的访问类型,常见的值:
效率最高的是NULL,效率最低的是all,从上到下,从高到低。
这个查询可能会用到的索引
实际用到的索引
反映索引中在查询中使用的列所占的总字节数。
查询扫描的预估计行数。
给出了与查询相关的额外信息和说明。这些额外信息可以帮助我们更好地理解查询执行的过程。
将这个sql脚本初始化到数据库中(初始化100W条记录):t_vip.sql
根据id查询(id是主键,有索引):
select * from t_vip where id = 900000;
根据name查询(name上没有索引):
select * from t_vip where name='4c6494cb';
给name字段添加索引:
create index idx_t_user_name on t_vip(name);
再次根据name查询(此时name上已经有索引了) :
select * from t_vip where name='4c6494cb';
假设有这样一张表:
create table t_customer(
id int primary key auto_increment,
name varchar(255),
age int,
gender char(1),
email varchar(255)
);
添加了这些数据:
insert into t_customer values(null, 'zhangsan', 20, 'M', 'zhangsan@123.com');
insert into t_customer values(null, 'lisi', 22, 'M', 'lisi@123.com');
insert into t_customer values(null, 'wangwu', 18, 'F', 'wangwu@123.com');
insert into t_customer values(null, 'zhaoliu', 22, 'F', 'zhaoliu@123.com');
insert into t_customer values(null, 'jack', 30, 'M', 'jack@123.com');
添加了这样的复合索引:
create index idx_name_age_gender on t_customer(name,age,gender);
最左前缀原则:当查询语句条件中包含了这个复合索引最左边的列 name 时,此时索引才会起作用。
验证1:
explain select * from t_customer where name='zhangsan' and age=20 and gender='M';
验证结果:完全使用了索引
验证2:
explain select * from t_customer where name='zhangsan' and age=20;
验证结果:使用了部分索引
验证3:
explain select * from t_customer where name='zhangsan';
验证结果:使用了部分索引
验证4:
explain select * from t_customer where age=20 and gender='M' and name='zhangsan';
验证结果:完全使用了索引
验证5:
explain select * from t_customer where gender='M' and age=20;
验证结果:没有使用任何索引
验证6:
explain select * from t_customer where name='zhangsan' and gender='M';
验证结果:使用了部分索引
验证7:
explain select * from t_customer where name='zhangsan' and gender='M' and age=20;
验证结果:完全使用了索引
范围查询时,在“范围条件”右侧的列索引会失效:
验证:
explain select * from t_customer where name='zhangsan' and age>20 and gender='M';
验证结果:name和age列索引生效。gender列索引无效。
怎么解决?建议范围查找时带上“=”
explain select * from t_customer where name='zhangsan' and age>=20 and gender='M';
有这样一张表:
create table t_emp(
id int primary key auto_increment,
name varchar(255),
sal int,
age char(2)
);
有这样一些数据:
insert into t_emp values(null, '张三', 5000,'20');
insert into t_emp values(null, '张飞', 4000,'30');
insert into t_emp values(null, '李飞', 6000,'40');
有这样一些索引:
create index idx_t_emp_name on t_emp(name);
create index idx_t_emp_sal on t_emp(sal);
create index idx_t_emp_age on t_emp(age);
explain select * from t_emp where sal > 5000;
验证结果:使用了索引
explain select * from t_emp where sal*10 > 50000;
验证结果:索引失效
explain select * from t_emp where name like '张%';
验证结果:索引有效
explain select * from t_emp where name like '%飞';
验证结果:索引失效
explain select * from t_emp where age='20';
验证结果:索引有效
explain select * from t_emp where age=20;
验证结果:索引失效
explain select * from t_emp where name='张三' or sal=5000;
验证结果:使用了索引
将t_emp表sal字段上的索引删除:
alter table t_emp drop index idx_t_emp_sal;
再次验证:
explain select * from t_emp where name='张三' or sal=5000;
验证结果:索引失效
复制一张新表:emp2
create table emp2 as select * from emp;
给sal添加索引:
alter table emp2 add index idx_emp2_sal(sal);
验证1:
explain select * from emp2 where sal > 800;
不走索引:
验证2:
explain select * from emp2 where sal > 1000;
不走索引:
验证3:
explain select * from emp2 where sal > 2000;
走索引:
给emp2的comm字段添加一个索引:
create index idx_emp2_comm on emp2(comm);
将emp2表的comm字段值全部更新为NULL:
update emp2 set comm=null;
验证此时条件使用is null是否走索引:
explain select * from emp2 where comm is null;
验证结果:不走索引。
验证此时条件使用is not null是否走索引:
将emp2表的comm字段全部更新为非NULL:
update emp2 set comm=100;
验证此时条件使用is null是否走索引:
explain select * from emp2 where comm is null;
验证结果:走索引
验证此时条件使用is not null是否走索引:
explain select * from emp2 where comm is not null;
验证结果:不走索引
结论:走索引还是不走索引,根数据分布有很大关系,如果符合条件的记录占比较大,会考虑使用全表扫描,而放弃走索引。
当一个字段上既有单列索引,又有复合索引时,我们可以通过以下的SQL提示来要求该SQL语句执行时采用哪个索引:
查看 t_customer 表上的索引:
show index from t_customer;
可以看到name age gender三列添加了一个复合索引。
现在给name字段添加一个单列索引:
create index idx_name on t_customer(name);
看看以下的语句默认使用了哪个索引:
explain select * from t_customer where name='zhangsan';
通过测试得知,默认使用了联合索引。
如何建议使用单列索引idx_name:
explain select * from t_customer use index(idx_name) where name='zhangsan';
如何忽略使用符合索引 idx_name_age_gender:
explain select * from t_customer ignore index(idx_name_age_gender) where name='zhangsan';
如何强行使用单列索引idx_name:
explain select * from t_customer force index(idx_name) where name='zhangsan';
覆盖索引我们在讲解索引的时候已经提到过了,覆盖索引强调的是:在select后面写字段的时候,这些字段尽可能是索引所覆盖的字段,这样可以避免回表查询。尽可能避免使用 select ,因为select 很容易导致回表查询。(本质就是:能在索引上检索的,就不要再次回表查询了。)
例如:有一张表 emp3,其中 ename,job添加了联合索引:idx_emp3_ename_job,以下这个select语句就不会回表:
drop table if exists emp3;
create table emp3 as select * from emp;
alter table emp3 add constraint emp3_pk primary key(empno);
create index idx_emp3_ename_job on emp3(ename,job);
explain select empno,ename,job from emp3 where ename='KING';
如果查询语句要查找的列没有在索引中,则会回表查询,例如:
explain select empno,ename,job,sal from emp3 where ename='KING';
面试题:t_user表字段如下:id,name,password,realname,birth,email。表中数据量500万条,请针对以下SQL语句给出优化方案:
select id,name,realname from t_user where name='鲁智深';
如果只给name添加索引,底层会进行大量的回表查询,效率较低,建议给name和realname两个字段添加联合索引,这样大大减少回表操作,提高查询效率。
如果一个字段类型是varchar或text字段,字段中存储的是文本或者大文本,直接对这种长文本创建索引,会让索引体积很大,怎么优化呢?可以将字符串的前几个字符截取下来当做索引来创建。这种索引被称为前缀索引,例如:
drop table if exists emp4;
create table emp4 as select * from emp;
create index idx_emp4_ename_2 on emp4(ename(2));
以上SQL表示将emp4表中ename字段的前2个字符创建到索引当中。
使用前缀索引时,需要通过以下公式来确定使用前几个字符作为索引:
select count(distinct substring(ename,1,前几个字符)) / count(*) from emp4;
以上查询结果越接近1,表示索引的效果越好。(原理:做索引值的话,索引值越具有唯一性效率越高)
假设我们使用前1个字符作为索引值:
select count(distinct substring(ename,1,1)) / count(*) from emp4;
假设我们使用前2个字符作为索引值:
select count(distinct substring(ename,1,2)) / count(*) from emp4;
可见使用前2个字符作为索引值,能够让索引值更具有唯一性,效率越好,因此我们选择前2个字符作为前缀索引。
create index idx_emp4_ename_2 on emp4(ename(2));
执行以下的查询语句则会走这个前缀索引:
explain select * from emp4 where ename='KING';
当查询语句的条件中有多个条件,建议将这几个列创建为复合索引,因为创建单列索引很容易回表查询。
例如分别给emp5表ename,job添加两个单列索引:
create table emp5 as select * from emp;
alter table emp5 add constraint emp5_pk primary key(empno);
create index idx_emp5_ename on emp5(ename);
create index idx_emp5_job on emp5(job);
执行以下查询语句:
explain select empno,ename,job from emp5 where ename='SMITH' and job='CLERK';
ename和job都出现在查询条件中,可以给emp6表的ename和job创建一个复合索引:
create table emp6 as select * from emp;
alter table emp6 add constraint emp6_pk primary key(empno);
create index idx_emp6_ename_job on emp6(ename,job);
explain select empno,ename,job from emp6 where ename='SMITH' and job='CLERK';
对于以上查询语句,使用复合索引避免了回表,因此这种情况下还是建议使用复合索引。
注意:创建索引时应考虑最左前缀原则,主字段并且具有很强唯一性的字段建议排在第一位,例如:
create index idx_emp_ename_job on emp(ename,job);
和以下方式对比:
create index idx_emp_job_ename on emp(job,ename);
由于ename是主字段,并且ename具有很好的唯一性,建议将ename列放在最左边。因此这两种创建复合索引的方式,建议采用第一种。
复合索引底层原理:
准备数据:
drop table if exists workers;
create table workers(
id int primary key auto_increment,
name varchar(255),
age int,
sal int
);
insert into workers values(null, '孙悟空', 500, 50000);
insert into workers values(null, '猪八戒', 300, 40000);
insert into workers values(null, '沙和尚', 600, 40000);
insert into workers values(null, '白骨精', 600, 10000);
explain查看一个带有order by的语句时,Extra列会显示:using index 或者 using filesort,区别是什么?
此时name没有添加索引,如果根据name进行排序的话:
explain select id,name from workers order by name;
显然这种方式效率较低。
给name添加索引:
create index idx_workers_name on workers(name);
再根据name排序:
explain select id,name from workers order by name;
这样效率则提升了。
如果要通过age和sal两个字段进行排序,最好给age和sal两个字段添加复合索引,不添加复合索引时:
按照age升序排,如果age相同则按照sal升序
explain select id,age,sal from workers order by age,sal;
这样效率是低的。
给age和sal添加复合索引:
create index idx_workers_age_sal on workers(age, sal);
再按照age升序排,如果age相同则按照sal升序:
explain select id,age,sal from workers order by age,sal;
这样效率提升了。
在B+树上叶子结点上的所有数据默认是按照升序排列的,如果按照age降序,如果age相同则按照sal降序,会走索引吗?
explain select id,age,sal from workers order by age desc,sal desc;
可以看到备注信息是:反向索引扫描,使用了索引。
这样效率也是很高的,因为B+树叶子结点之间采用的是双向指针。可以从左向右(升序),也可以从右向左(降序)。
如果一个升序,一个降序会怎样呢?
explain select id,age,sal from workers order by age asc, sal desc;
可见age使用了索引,但是sal没有使用索引。怎么办呢?可以针对这种排序情况创建对应的索引来解决:
create index idx_workers_ageasc_saldesc on workers(age asc, sal desc);
创建的索引如下:A表示升序,D表示降序。
再次执行:
explain select id,age,sal from workers order by age asc, sal desc;
我们再来看看,对于排序来说是否支持最左前缀法则:
explain select id,age,sal from workers order by sal;
通过测试得知,order by也遵循最左前缀法则。
我们再来看一下未使用覆盖索引会怎样?
explain select * from workers order by age,sal;
通过测试得知,排序也要尽量使用覆盖索引。
order by 优化原则总结:
show variables like 'sort_buffer_size';
创建empx表:
create table empx as select * from emp;
job字段上没有索引,根据job进行分组,查看每个工作岗位有多少人:
select job,count(*) from empx group by job;
看看是否走索引了:
explain select job,count(*) from empx group by job;
使用了临时表,效率较低。
给job添加索引:
create index idx_empx_job on empx(job);
再次执行:
explain select job,count(*) from empx group by job;
效率提升了。
我们再来看看group by是否需要遵守最左前缀法则:给deptno和sal添加复合索引
create index idx_empx_deptno_sal on empx(deptno, sal);
根据部门编号分组,查看每个部门人数:
explain select deptno,count(*) from empx group by deptno;
效率很高,因为deptno是复合索引中最左边的字段。
根据sal分组,查看每个工资有多少人:
explain select sal, count(*) from empx group by sal;
使用了临时表,效率较低。
通过测试得知,group by也同样遵循最左前缀法则。
我们再来测试一下,如果将部门编号deptno(复合索引的最左列)添加到where条件中,效率会不会提升:
explain select sal, count(*) from empx where deptno=10 group by sal;
效率有提升的,这说明了,group by确实也遵循最左前缀法则。(where中使用了最左列)
数据量特别庞大时,取数据时,越往后效率越低,怎么提升?mysql官方给出的解决方案是:使用覆盖索引+子查询的形式来提升效率。
怎么解决?使用覆盖索引,加子查询
使用覆盖索引:速度有所提升
使用子查询形式取其他列的数据:
通过测试,这种方式整体效率有所提升。
主键设计原则:
insert优化原则:
insert into t_user(id,name,age) values (1,'jack',20),(2,'lucy',30),(3,'timi',22);
mysql --local-infile -uroot -p1234
set global local_infile = 1;
use powernode;
create table t_temp(
id int primary key,
name varchar(255),
password varchar(255),
birth char(10),
email varchar(255)
);
load data local infile 'E:\\powernode\\05-MySQL高级\\resources\\t_temp-100W.csv' into table t_temp fields terminated by ',' lines terminated by '\n';
文件中的数据如下:
导入表中之后,数据如下:
分组函数count的使用方式:
结论:如果你要统计一张表中数据的总行数,建议使用 count(*)
注意:
当存储引擎是InnoDB时,表的行级锁是针对索引添加的锁,如果索引失效了,或者不是索引列时,会提升为表级锁。
什么是行级锁?A事务和B事务,开启A事务后,通过A事务修改表中某条记录,修改后,在A事务未提交的前提下,B事务去修改同一条记录时,无法继续,直到A事务提交,B事务才可以继续。
有一张表:t_fruit
create table t_fruit(
id int primary key auto_increment,
name varchar(255)
);
insert into t_fruit values(null, '苹果');
insert into t_fruit values(null, '香蕉');
insert into t_fruit values(null, '橘子');
开启A事务和B事务,演示行级锁:
事务A没有结束之前,事务B卡住:
事务A结束之后,事务B继续执行:
当然,如果更新的不是同一行数据,事务A和事务B可以并发:
行级锁是对索引列加锁,以上更新语句的where条件是id,id是主键,当然有索引,所以使用了行级锁,如果索引失效,或者字段上没有索引,则会升级为表级锁:
因此,为了更新的效率,建议update语句中where条件中的字段是添加索引的。
推荐阅读: