如何优化慢SQL?

前言

前几天帮公司解决线上慢SQL告警问题,遇到了几个case。

下面我会结合case案例分析自己这段时间在工作上遇到的慢查询谈谈数据库如何优化慢查询。

一般我们遇到的慢sql都是索引没有正确使用导致的,所以我先介绍下索引相关知识

索引介绍

索引概念

排好序的快速查找的数据结构(我们平时说的索引,如果没有特别指明,都是指B树,其中聚集索引、次要索引、覆盖索引、复合索引、前缀索引、唯一索引默认使用的都是B+树索引,除B+树这种类型的索引外还有哈希索引等)

索引优缺点

优点:

  • 查找 :提高数据检索效率,降低IO成本。

  • 排序:通过索引对数据进行排序,降低排序成本,降低cpu消耗

缺点:

  • 实际上索引也是一张表,该表保存了主键与索引字段,并指向索引的记录,所以索引列也需要占空间。

  • 更新表时(insert、update、delete)不仅要保存数据还要更新保存索引文件新添加的索引列。

索引分类

  • 单值索引(单列索引):一个索引只包含单个列,一个表中可以有多个单列索引。

  • 唯一索引:索引列必须唯一,但可以允许有空值

  • 复合索引:一个索引包含多个列

索引结构

  • BTree索引

  • Hash索引

  • full-text全文检索

  • R-Tree索引

哪些情况要建索引

  • 主键自动建主键索引

  • 频繁作为查询条件的字段应该创建索引

  • 查询中与其他表关联的字段,外键关系建立索引

  • 在高并发下倾向建立组合索引

  • 查询中的排序字段,排序字段若通过索引去访问将大大提高排序速度

  • 查询中统计或者分组的数据

哪些情况不适合建索引

  • 频繁更新的字段

  • where条件用不到的字段不创建索引

  • 表记录太少

  • 经常增删改的表

  • 数据重复太多的字段,为它建索引意义不大(假如一个表有10万,有一个字段只有T和F两种值,每个值的分布概率大约只有50%,那么对这个字段的建索引一般不会提高查询效率,索引的选择性是指索引列的不同值数据与表中索引记录的比,,如果一个表中有2000条记录,表中索引列的不同值记录有1980个,这个索引的选择性为1980/2000=0.99,如果索引项越接近1,这个索引效率越高)

explain字段分析

explain是排查慢sql的一种最常用的手段

mysql> EXPLAIN SELECT 1;

[图片上传失败...(image-5d524b-1700187059535)]

id:表示select子句或者操作的顺序

  • id相同:执行顺序自上而下

  • id不同:id值越大优先级越高,越先被执行

  • id相同不同:id越大越先执行,相同的自上而下执行

select_type:主要是区分普通查询、联合查询、子查询等。

  • SIMPLE:简单的select查询,不包含子查询与union

  • PRIMARY:查询中包含复杂的子部分,最外层会被标记为primary

  • SUBQUERY:在select或者where列表中包含了子查询

  • DERIVED:在from列表中包含的子查询衍生表

  • UNION:若第二个select出现在union之后,则被标记为union

  • UNION RESESULT:从union表获取结果的select

table:这一行数据是哪个表的数据

type:查询中使用了何种类型

结果值从最好到最坏:system>const>eq_ref>ref>fulltext>ref_or_null>index_merge>unique_subquery>index_subquery>range>index>all

  • 一般来说,得保证查询至少达到range级别,最好能到达ref

  • system:表只有一行记录(等于系统表),这是const类型的特例,平时不会出现

  • const:表示通过索引一次就能够找到

  • eq_ref:唯一性索引扫描,对于每个索引键,表示只有一条记录与之匹配,常见于主键或唯一索引扫描

  • ref:非唯一性索引扫描,返回匹配某个单独值的所有行

  • range:只检索给定范围的行,使用一个索引来选择行,一般就是在where语句中出现了between、<、>、in等的查询

  • index:index比all快,因为index是从索引中读取,all是从硬盘中读取

  • all:遍历全表才能找到

possible_key:显示可能应用在这张表中的索引,但实际上不一定用到

key:实际上使用的索引,如果没有则为null

key_len:表示索引中使用的字节数(可能使用的,不是实际的),可通过该列查询中使用的索引的长度,在不损失精确性的情况下,长度越短越好

ref:显示索引的哪一列被用到,如果可能的话是一个常数,哪些常量被用于查找索引列上的值

rows:大致估算找出所需的记录要读取的行数

Extra:包含不适合在其他列中显示,但十分重要的的额外信息

  • Using filesort 说明mysql会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取,mysql中无法利用索引完成的排序成为文件排序

  • Using temporary 使了用临时表保存中间结果,mysql在对查询结果排序时使用了临时表,常见于排序order by 和分组查询group by

  • Using index 表示相应的select操作中使用了覆盖索引,避免访问了表的数据行,效率高

  • Using where 表明使用了where进行过滤

  • Using join buffer 使用了连接缓存

  • impossible where 如果where子句的值总是false,不能用来获取任何元组

  • select table optimized away 在没有group by子句的情况下,基于索引优化min/max操作或者对于myisam存储引擎优化count(*)操作,不必等到执行阶段再进行计算

更详细的内容,请看我之前的文章:

最完整的Explain总结,SQL优化不再困难

索引失效

  • 应该尽量全值匹配

  • 复合最佳左前缀法则(第一个索引不能掉,中间不能断开

  • 不在索引列上做任何操作(计算、函数、类型转换)会导致索引失效而转向全表扫描

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

  • 尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少select*

  • mysql在使用不等于(!=或者<>)的时候无法使用索引会导致全表扫描

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

  • like以统配符开头

  • 字符串不加单引号

  • 少用or

order by优化

  • 避免filesort,尽量在索引上进行排序,遵照最佳左前缀原则

filesort有两种排序:

  • 双路排序:两次磁盘扫描

  • 单路排序:一次性读取保存在内存中,没拉完的数据再次拉

  • 单路排序总体好于双路排序

  • 优化策略:1、增大sort_buffer_size参数的设置,2、增大max_length_for_sort_data参数的设置,尽可能一次拿到内存

Case分析

案例一

in中参数太多

select * from goods_info where goods_status = ? and id in(11,22,33......)

in中id数据量比较多,导致查询的数据量比较大,这是一个比较常见的慢查询类型,并且往往在业务数据量比较少的时候这条语句不是慢查;

因为参数传进一个List集合,当参数比较多的时候,可以采用在业务层把List集合拆分为多个长度较小的集合,分多次查询,具体每一次拆长度为多少,可能需要具体根据业务及数据量进行评估

我的解决办法:业务代码增加拆分集合操作,LIMIT_SIZE设置为1000

List<List<Integer>> partitionGoodsIdList = Lists.partition(goodsIdList, LIMIT_SIZE);

当SQL的查询参数过多,我觉得可以考虑使用上述拆分的方式

案例二

返回的查询结果过多

select from goods where goods_status = ? and poi_id = ?

解决办法:将SQL修改为分页查询,并在业务代码上修改为分页查询,修改后的SQL语句如下:

select from goods where goods_status = 1 and poi_id = 11 and goods_id > 22 order by goods_id limit 2000

通过分页的方式可以降低数据量,避免慢查询,但是会从而导致一次查询请求,增加为多次查询请求,对于limit的大小需要谨慎评估

案例三

order by慢查询

SELECT * FROM order FORCE INDEX (orderid)  WHERE orderId = 11 AND status IN (0,22) ORDER BY id ASC ;

该SQL由于强制指定了使用orderId索引,但条件中并没有orderId,导致产生全表扫描(type: ALL);

如下为问题SQL的执行计划:

556730e61e3b623d64b217ecf9d1ea2b.png

直接原因是最终传给SQL查询函数的参数,orderId没有加入where子句,但forceindex一直生效

案例四

join慢查询

select * from useract join userinfo order by useracct.id desc limit 11;

对sql进行explain可以发现,因为忘写了join的on条件,这是扫全表sql,如下图:

c403c23d78f7201a3ae6bce2ad643b41.png

我们首先看type级别两个表的级别都是ALL,说明该条语句没有用到索引,做了全表扫描是最差的情况

优化:

0f6c0ed499fc53549de49459a67f5501.png

案例五

不同索引尝试

select id from goods_info where id > ? and activity_id = ? and goods_switch in(?+) limit ?
select id from goods_info where id > 123991510 and activity_id = 0 and goods_switch in (2,3) limit 1000

通过执行计划可知,该语句走的是activity_id和主键的索引,但是这种命中率比较低,大量的数据被goods_switch筛掉

解决办法:在不确定最优的索引的情况下,可以在测试环境下,分别添加不同的索引,观察执行计划及语句的执行时间。

尝试强制走主键索引,效果不佳;尝试添加activity_id_id的联合索引,效果不佳;尝试添加activity_id,goods_switch的联合索引,问题解决!

所以在不确定哪种索引是最优时,可以尝试建立不同的索引,观察语句在不同索引情况下的执行情况进行权衡。

案例六

MySQL选错索引

select * from goods_info
where goods_source = ? and goods_switch != ? and id > ? order by id limit ?

select * from goods_info  
where goods_source = 2 and goods_switch != 8 and id > 12395070 order by id limit 1000

这条语句从语句本身猜测使用的是主键索引,但是查看该语句的执行计划,发现走的索引是idx_goods_source,即走了goods_source的单列索引!

解决办法:修改SQL语句,强制走主键索引,查看执行计划,走了主键索引,查询时间大大降低。

正常情况下MySQL会选择最优的索引,但是有时候也会选错,MySQL的优化器会依据扫描行数、是否排序,索引区分度来选择最优的索引,并且扫描行数不一定完成准确,只是MySQL的一个预估值

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容