百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

「每天一道面试题」MySQL索引优化与SQL优化

bigegpt 2024-08-04 11:38 9 浏览

MySQL索引优化与SQL优化

概述

日常在 CURD 的过程中,都避免不了跟数据库打交道,大多数业务都离不开数据库表的设计和 SQL 的编写,那如何让你编写的SQL语句性能更优呢?先来整体看下 MySQL 逻辑架构图:



MySQL 整体逻辑架构图可以分为 Server 和存储引擎层。

Server层

Server 层涵盖了 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),以及存储过程、触发器、视图等跨存储引擎的实现也在这一层来实现。

  • 连接器:负责跟客户端建立连接、获取权限、维持和管理连接。
  • 分析器:SQL 词法分析,SQL语法分析。
  • 优化器:索引选择,选择一个执行效率高的,生成执行计划。
  • 执行器:操作引擎,返回执行结果。
  • 查询缓存:执行 SQL 语句之前,先查缓存,缓存结果可能是以 key-value 对方式存储的,key 是查询的语句,value 是查询的结果。

存储引擎层

负责数据的存储和提取,是一种插件式的架构方式。支持 InnoDB、MyISAM、Memory 等多个存储引擎。MySQL 5.5.5 版本开始默认存储引擎是 InnoDB,也是目前常用的存储引擎。

数据准备

我们使用如下语句,创建测试的数据表:

CREATE TABLE `user_haicoder` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(24) NOT NULL DEFAULT '' COMMENT '用户姓名',
  `user_age` int(11) NOT NULL DEFAULT 0 COMMENT '用户年龄',
  `user_level` varchar(20) NOT NULL DEFAULT '' COMMENT '用户等级',
  `register_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
  PRIMARY KEY (`id`),
  KEY `idx_userName_userAge_userLevel` (`user_name`,`user_age`,`user_level`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';

我们使用如下语句插入数据:

INSERT INTO user_haicoder(user_name, user_age, user_level, register_time)VALUES('嗨客网', 30, 'A', NOW());
INSERT INTO user_haicoder(user_name, user_age, user_level, register_time)VALUES('haicoder', 31, 'B', NOW());
INSERT INTO user_haicoder(user_name, user_age, user_level, register_time)VALUES('hai', 31, 'C', NOW());

索引优化案例

全值匹配

按索引字段顺序匹配使用,我们执行如下 sql:

explain select * from user_haicoder where user_name = '嗨客网';

执行完毕后,如下图所示:


通过 explain 分析,type 为 ref,使用索引,效率高。key_len 为 74,根据 key_len 计算规则,如果字段类型为 varchar(n),并且是 utf-8 编码格式,则 key_len=3n+2,where 后使用了 user_name,则 key_len=3*24+2=74,证明索引用到了联合索引的第一个字段 user_name,从 ref 也可以看到一个 const。

使用联合索引两个字段时:

explain select * from user_haicoder where user_name = '嗨客网' and user_age = 30;

执行完毕后,如下图所示:



同样 type 为 ref,使用联合索引三个字段时:

explain select * from user_haicoder where user_name = '嗨客网'and user_age = 30 and user_level = 'A';

执行完毕后,如下图所示:



同样 type 为 ref。

最佳左前缀法则

如果建的是联合索引,要遵循最左前缀法则。要想使用索引,where 后面的条件需要从索引的最左前列开始并且不跳过索引中的列使用。我们执行如下查询语句:

explain select * from user_haicoder where user_name = '嗨客网'and user_age = 30 and user_level = 'A';

执行完毕后,如下图所示:



按照索引字段顺序使用,三个字段都使用了索引。我们再次执行如下查询语句:

explain select * from user_haicoder where user_age = 30 and user_level = 'A';

执行完毕后,如下图所示:



直接跳过 user_name 使用索引字段,索引无效,未使用到索引。我们再次执行如下查询语句:

explain select * from user_haicoder where user_age = 30 and user_level = 'A' and user_name = '嗨客网';

执行完毕后,如下图所示:



where 后面查询条件顺序是 user_age、user_level、user_name 与我们建的索引顺序 user_name、user_age、user_level 不一致,为什么还是使用了索引,这是因为 MySql 底层优化器给咱们做了优化。但是,咱们最好还是按顺序使用索引。

不要在索引列上做任何操作

不要在索引列上做任何操作,比如计算、使用函数、自动或手动进行类型转换,会导致索引失效,从而使查询转向全表扫描。我们执行如下查询语句:

explain select * from user_haicoder where user_name = '嗨客网';

执行完毕后,如下图所示:



wher 条件直接使用索引字段 user_name 用到了索引。我们再次执行如下查询语句:

explain select * from user_haicoder where right(user_name, 1) = '嗨';

执行完毕后,如下图所示:



where 条件使用计算后的索引字段 user_name,没有使用索引,索引失效。

存储引擎不能使用范围条件右边的索引列

我们执行如下查询语句:

explain select * from user_haicoder where user_name = '嗨客网' and user_age = 30 and user_level = 'A';

执行完毕后,如下图所示:



三个列都使用 “=” 号,顺序使用三个字段,三个字段都使用了索引。我们再次执行如下查询语句:

explain select * from user_haicoder where user_name = '嗨客网' and user_age > 30 and user_level = 'A';

执行完毕后,如下图所示:



将 user_age修 改为 ">" 之后,Extra 为 Using index condition,表明索引没有被完全使用,并且 key_len 由 140 降为 78,说明最后一个字段 user_level 没有使用索引。即范围之后索引全无效。

但是如果我们把 “>” 变为 ">=" 呢?我们再次执行如下查询语句:

explain select * from user_haicoder where user_name = '嗨客网' and user_age >= 30 and user_level = 'A';

执行完毕后,如下图所示:



加上等号后,key_len 变为了 140,但是 Using index condition 确又表明索引没有被完全使用,只能说明在满足 user_age 为 “=” 号条件时全部索引使用,否则,范围之后的索引失效。

尽量使用覆盖索引

尽量使用覆盖索引(只访问索引的查询(索引列包含查询列)),减少 select * 语句。我们执行如下查询语句:

explain select * from user_haicoder where user_name = '嗨客网' and user_age = 30 and user_level = 'A';

执行完毕后,如下图所示:



Extra 显示 "Using index condition",表示查询的列未被索引列覆盖,并且 where 筛选条件是索引的前导列,说明用到了索引,但是部分字段未被索引列覆盖,必须通过 “回表” 来实现,所以不是纯粹地用到了索引,也不是完全没用到索引。我们再次执行如下查询语句:

explain select user_name, user_age from user_haicoder where user_name = '嗨客网' and user_age  = 30 and user_level = 'A';

执行完毕后,如下图所示:



* 换成索引列,查询时使用了索引,用索引列覆盖查询的 *, 叫做覆盖索引。

不等于无法使用索引

mysql 在使用不等于(!= 或者 <>)的时候无法使用索引会导致全表扫描。我们执行如下查询语句:

explain select user_name, user_age from user_haicoder where user_name = '嗨客网';

执行完毕后,如下图所示:



使用了索引,现在我们再次执行如下查询语句:

explain select user_name, user_age from user_haicoder where user_name != '嗨客网';

执行完毕后,如下图所示:



Extra 显示 Using whre Using index,表示查询的列被索引列覆盖,但是 where 后面条件未使用索引,说明无法直接通过索引查找查询到符合条件的数据。

is null,is not null也无法使用索引

我们执行如下查询语句:

explain select * from user_haicoder where user_name = '嗨客网';

执行完毕后,如下图所示:



使用了索引。我们执行如下查询语句:

explain select user_name, user_age from user_haicoder where user_name is not null;

执行完毕后,如下图所示:



未使用索引。

like以通配符开头

like 以通配符开头(like '%aaa')mysql 索引失效会变成全表扫描操作。现在我们执行如下查询语句:

explain select * from user_haicoder where user_name like '%A';

执行完毕后,如下图所示:



% 开头,未使用索引。我们再次执行如下查询语句:

explain select * from user_haicoder where user_name like 'A%';

执行完毕后,如下图所示:



% 结尾,使用了索引。

union、in、or都能够命中索引,建议使用in

union 能够命中索引,并且 MySQL 耗费的 CPU 最少:

select * from user_haicoder where user_level='A'
union all
select * from user_haicoder where user_level='B';

in 能够命中索引,查询优化耗费的 CPU 比 union all 多,但可以忽略不计,一般情况下建议使用 in:

select * from user_haicoder where user_level in ('A', 'B');

or 新版的 MySQL 能够命中索引,查询优化耗费的 CPU 比 in 多,不建议频繁用 or:

select * from user_haicoder where user_level='A' or user_level='B';

order by和group by

如果有 order by、group by 的场景,请注意利用索引的有序性:

  1. order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 file_sort 的情况,影响查询性能。例如对于语句 where a=? and b=? order by c,可以建立联合索引(a,b,c)。
  2. 如果索引中有范围查找,那么索引有序性无法利用,如 WHERE a > 10 ORDER BY b;,索引 (a,b) 无法排序。

join查询

进行 join 联表查询的字段需要建立索引,join 最好不要超过三个表。需要 join 的字段,数据类型必须一致,多表关联查询时,保证被关联的字段需要有索引。

left join 是由左边决定的,左边的数据一定都有,所以右边是我们的关键点,建立索引要建右边的。当然如果索引在左边,可以用 right join。

单表索引建议控制在5个以内

索引不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引。一个表的索引数较好不要超过 6 个。

SQL语句优化

  1. 用具体的字段列表代替 "select *",不要返回用不到的任何字段。
  2. 如果明确知道只有一条结果返回,limit 1 能够提高效率。
  3. 利用延迟关联或者子查询优化超多分页场景,MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。示例如下,先快速定位需要获取的 id 段,然后再关联:select a.* from 表1 a,(select id from 表1 where 条件 limit 100000,20 ) b where a.id=b.id;
  4. 对于多张大数据量的表 JOIN,要先分页再 JOIN,否则逻辑读会很高,性能很差。
  5. 避免在 where 子句中使用 or 来连接查询条件,如果一个字段有索引,一个字段没有索引,将导致引擎放弃使用索引而进行全表扫描。
  6. 对于连续的数值,能用 between 就不要用 in 了,尽量使用 exists 代替 in。
  7. 优化 Group by,使用 where 子句替换 Having 子句。避免使用 having 子句,having 只会在检索出所有记录之后才会对结果集进行过滤,这个处理需要排序分组,如果能通过 where 子句提前过滤查询的数目,就可以减少这方面的开销。on、where、having 这三个都可以加条件的子句,on 是最先执行,where 次之,having 最后。提高 GROUP BY 语句的效率, 可以通过将不需要的记录在 GROUP BY 之前过滤掉。# 低效 SELECT JOB, AVG(SAL) FROM EMP GROUP by JOB HAVING JOB = ‘PRESIDENT' OR JOB = ‘MANAGER' # 高效 SELECT JOB, AVG(SAL) FROM EMP WHERE JOB = ‘PRESIDENT' OR JOB = ‘MANAGER' GROUP by JOB
  1. 使用 union all 替换 union当 SQL 语句需要 union 两个查询结果集合时,这两个结果集合会以 union all 的方式被合并,然后再输出最终结果前进行排序。如果用 union all 替代 union,这样排序就不是不要了,效率就会因此得到提高。需要注意的是,UNION ALL 将重复输出两个结果集合中相同记录。
  2. 尽量使用数字型字段若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能。引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。
  3. 写出统一的SQL语句对于以下两句 SQL 语句,很多人都认为是相同的。不过数据库查询优化器则认为是不同的,虽然只是大小写不同,但必须进行两次解析,生成 2 个执行计划。所以应该保证相同的查询语句在任何地方都一致,多一个空格都不行。select * from dual select * From dual
  4. where 子句中索引列使用参数,也会导致索引失效因为 SQL 只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:select id from t where num=@num -- 不能使用索引 select id from t with(index(索引名)) where num=@num --可以改为强制查询使用索引:
  5. 使用复合索引须遵守最左前缀原则复合索引必须使用到最左边字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。
  6. 当需要全表删除且无需回滚时,使用 Truncate 替代 delete。
  7. 使用表的别名当在 SQL 语句中连接多个表时, 使用表的别名并把别名前缀用于每个 Column 上,这样可以减少解析的时间并减少那些由 Column 歧义引起的语法错误。
  8. 避免使用耗费资源的操作:带有 DISTINCT, UNION, MINUS, INTERSECT, ORDER BY 的 SQL 语句,会启动 SQL 引擎执行耗费资源的排序功能,DISTINCT 需要一次排序操作,而其他的至少需要执行两次排序。通常带有 UNION, MINUS, INTERSECT 的 SQL 语句都可以用其他方式重写,如果你的数据库的 SORT_AREA_SIZE 调配得好, 使用 UNION , MINUS, INTERSECT 也是可以考虑的, 毕竟它们的可读性很强。
  9. 尽量避免向客户端返回大数据量。
  10. Update 语句,如果只更改 1、2 个字段,不要 Update 全部字段,否则频繁调用会引起明显的性能消耗,同时带来大量日志。
  11. 应尽可能的避免更新聚簇索引数据列,因为聚簇索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。
  12. 尽量使用表变量来代替临时表。
  13. 考虑使用 “临时表” 暂存中间结果。临时表并不是不可使用,适当地使用它们可以使某些查询更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。将临时结果暂存在临时表,后面的查询就在临时表中查询了,这可以避免程序中多次扫描主表,也大大减少了程序执行中 “共享锁” 阻塞 “更新锁”,减少了阻塞,提高了并发性能。但是,对于一次性事件,较好使用导出表。
  14. 在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先 create table,然后 insert。
  15. 如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。
  16. 避免频繁创建和删除临时表,以减少系统表资源的消耗。
  17. 尽量避免使用游标,因为游标的效率较差。与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括 “合计” 的例程通常要比使用游标执行的速度快。
  18. 在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。
  19. 尽量避免大事务操作,提高系统并发能力。
  20. 在运行代码中,尽量使用 PreparedStatement 来查询,不要用 Statement。



相关推荐

最全的MySQL总结,助你向阿里“开炮”(面试题+笔记+思维图)

前言作为一名编程人员,对MySQL一定不会陌生,尤其是互联网行业,对MySQL的使用是比较多的。对于求职者来说,MySQL又是面试中一定会问到的重点,很多人拥有大厂梦,却因为MySQL败下阵来。实际上...

Redis数据库从入门到精通(redis数据库设计)

目录一、常见的非关系型数据库NOSQL分类二、了解Redis三、Redis的单节点安装教程四、Redis的常用命令1、Help帮助命令2、SET命令3、过期命令4、查找键命令5、操作键命令6、GET命...

netcore 急速接入第三方登录,不看后悔

新年新气象,趁着新年的喜庆,肝了十来天,终于发了第一版,希望大家喜欢。如果有不喜欢看文字的童鞋,可以直接看下面的地址体验一下:https://oauthlogin.net/前言此次带来得这个小项目是...

精选 30 个 C++ 面试题(含解析)(c++面试题和答案汇总)

大家好,我是柠檬哥,专注编程知识分享。欢迎关注@程序员柠檬橙,编程路上不迷路,私信发送以下关键字获取编程资源:发送1024打包下载10个G编程资源学习资料发送001获取阿里大神LeetCode...

Oracle 12c系列(一)|多租户容器数据库

作者杨禹航出品沃趣技术Oracle12.1发布至今已有多年,但国内Oracle12C的用户并不多,随着12.2在去年的发布,选择安装Oracle12c的客户量明显增加,在接下来的几年中,Or...

flutter系列之:UI layout简介(flutter-ui-nice)

简介对于一个前端框架来说,除了各个组件之外,最重要的就是将这些组件进行连接的布局了。布局的英文名叫做layout,就是用来描述如何将组件进行摆放的一个约束。在flutter中,基本上所有的对象都是wi...

Flutter 分页功能表格控件(flutter 列表)

老孟导读:前2天有读者问到是否有带分页功能的表格控件,今天分页功能的表格控件详细解析来来。PaginatedDataTablePaginatedDataTable是一个带分页功能的DataTable,...

Flutter | 使用BottomNavigationBar快速构建底部导航

平时我们在使用app时经常会看到底部导航栏,而在flutter中它的实现也较为简单.需要用到的组件:BottomNavigationBar导航栏的主体BottomNavigationBarI...

Android中的数据库和本地存储在Flutter中是怎样实现的

如何使用SharedPreferences?在Android中,你可以使用SharedPreferencesAPI来存储少量的键值对。在Flutter中,使用Shared_Pref...

Flet,一个Flutter应用的实用Python库!

▼Flet:用Python轻松构建跨平台应用!在纷繁复杂的Python框架中,Flet宛如一缕清风,为开发者带来极致的跨平台应用开发体验。它用最简单的Python代码,帮你实现移动端、桌面端...

flutter系列之:做一个图像滤镜(flutter photo)

简介很多时候,我们需要一些特效功能,比如给图片做个滤镜什么的,如果是h5页面,那么我们可以很容易的通过css滤镜来实现这个功能。那么如果在flutter中,如果要实现这样的滤镜功能应该怎么处理呢?一起...

flutter软件开发笔记20-flutter web开发

flutterweb开发优势比较多,采用统一的语言,就能开发不同类型的软件,在web开发中,特别是后台式软件中,相比传统的html5开发,更高效,有点像c++编程的方式,把web设计出来了。一...

Flutter实战-请求封装(五)之设置抓包Proxy

用了两年的flutter,有了一些心得,不虚头巴脑,只求实战有用,以供学习或使用flutter的小伙伴参考,学习尚浅,如有不正确的地方还望各路大神指正,以免误人子弟,在此拜谢~(原创不易,转发请标注来...

为什么不在 Flutter 中使用全局变量来管理状态

我相信没有人用全局变量来管理Flutter应用程序的状态。毫无疑问,我们的Flutter应用程序需要状态管理包或Flutter的基本小部件(例如InheritedWidget或St...

Flutter 攻略(Dart基本数据类型,变量 整理 2)

代码运行从main方法开始voidmain(){print("hellodart");}变量与常量var声明变量未初始化变量为nullvarc;//未初始化print(c)...