业务会经常使用关系型数据库,习惯上使用SQL型数据库来存储来存储冷数据,Redis等缓存中间件来存储热数据。
慢SQL=慢查+慢写
慢SQL主要分为两类,具体的界定下文会提到:
- 慢查:查询语句执行时间过长
- 慢写:插入、更新、删除操作耗时过长
慢SQL对性能的影响有以下几点:
- 性能下降:慢SQL的执行时间过长,占用数据库的资源和处理能力。慢SQL往往伴随着大量的行扫描。可能会导致其他SQL耗时增长,进而导致整体性能下降。
- 资源锁定:慢SQL可能会锁定部分资源,导致其他SQL等待,造成数据库阻塞。
- 系统负载增加:占用大量的CPU资源和内存,并发量增加可能会造成响应缓慢甚至系统崩溃。
一般来说,1秒以上的SQL就算做慢SQL,如果是核心链路,这个标准会更加严格。
如果要治理慢SQL,就要确定治理的优先级:
- 慢SQL是否在核心链路
如果在核心链路,慢SQL的耗时要求需要更严格。
需要关注是否有执行放大的场景(指不仅会导致单次请求响应放缓,还可能影响到其他请求。)
并且评估在未来大促场景,是否有潜在的瓶颈风险。 - 慢SQL发生的时间点
关注慢SQL发生的时间点,是否和自身业务场景高峰期重叠。 - 慢SQL性能影响的评估
- 执行次数(越高风险越大 大于10次/天为较严重)
- 平均最大耗时(>2s为较严重)
- 平均扫描行数(>1000为较严重)
为了更好地预防和治理慢SQL,这里介绍一下SQL执行的过程。
SQL执行流程
此处以MySQL,InnoDB数据引擎为例,介绍一下一条SQL主要的执行流程:
- SQL优化(解析和语法等价优化)
- 代价优化(分析条件表达可走的一切索引,根据预估扫描行数,常量表检测等选择最优路径)
- 额外优化
知道了SQL有这些优化,就可以减少每一步优化的负担:
- 避免相同语句由于书写格式的不同,导致多次语法解析
- 避免隐式转换,导致索引失效
- 避免使用存储过程、触发器、视图等
- 避免不走索引的order by
- 避免使用大表的join
- 避免在数据库中进行数学运算等等
刚提到了代价模型,是mysql通过量化的数值指标来衡量。
代价模型将操作分为Server和Engine两层。Server层主要占用CPU,Engine主要占用IO。优化器会使用代价模型对索引的成本和全表扫描做对比。
基于代价模型,可以做一些规避:
- 设计索引时注意索引的识别度
- 避免聚合函数、数学函数
SQL慢查询中,一般有两种情况:
- 索引设置的不合理
- 有定时同步任务,在全量的拉取数据,查询没有分页或者存在深分页的问题
使用EXPLAIN语句分析SQL
通过在SQL开头添加一个explain语句,下面是需要关注的列
| 列名 | 描述&关注点 |
|---|---|
| id | 表示查询中执行select子句或者操作表的顺序,id的值越大,代表越先执行。 |
| select_type | 表示select查询的类型:普通查询、联合查询、子查询 |
| type | 查询使用了何种类型,是一个非常重要的指标: system > const > eq_ref > ref > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL 如果看到 type=ALL,说明在”全表扫描”,这就是问题所在! |
| possible_key | 表示在MySQL中通过哪些索引,能让我们在表中找到想要的记录。但不一定会是最终查询数据时被用到的索引 |
| key | 区别于possible_key,key是查询中实际使用的索引,如果没有索引,显示为NULL。当 type 为 Index_merge 时,可能会使用多个索引 |
| key_len | 表示使用到的索引长度,原则上长度越短越好 单列索引:需要计算整个索引长度 多列索引:需要计算查询中具体 |
| rows | 以表的统计信息和索引使用情况,估算需要读取的行数 |
| filtered | 记录表里符合记录的百分比。 |
| extra | 需要关注 file sort – 没有使用索引:导致MySQL需要对全部数据进行排序 – 内存不足:mysql有一个参数,sort_buffer_size 控制排序时使用的内存大小。如果排序内存超过了这个值,就在fs中进行排序 – 数据量过大:mysql可能会将排序转为使用文件进行排序 |
explain分析关注点:
- 关注key的实际命中情况,对比已有的index,符合预期?
- rows,关注扫描行数体量。关注业务增长量
- extra,关注是否有 file sort 裂化
- explain执行建议在测试环境就提前分析验证。
总结
有一些行业里治理的优秀经验,根据不同问题可能产生的阶段进行分类:
库表阶段:
- 根据业务场景做好分库分表,降低单笔数据量。不同业务场景,提前做好数据倾斜的预防(防止单笔超过1000万行,大小控制在50G以内)
- 表字段控制在20个以内,并尽可能的都提供一个默认值
- 单笔索引数量不要超过5个,单笔索引过多会影响更新效率
- 索引创建字段的基数/区分度要尽可能的高
查询一个字段的区分度为以下公式count(distinct 列名) / count(*) - 尽量创建覆盖索引
语句执行阶段:
- 规避聚合函数、隐式转换。尽量用简单的语句查询。
- 不要join大表。
- 避免不走所有的 order by,避免大范围的排序,引发 file sort
- 不要使用
insert on duplicate key update或者insert ignore语句。如果是RR的事务隔离级别,插入时容易产生gap锁 - 规避深分页和添加where > ? 的条件。尤其是定时任务归档、或者跑报表的场景。同时跑报表的场景建议使用离线数据。
- 所有语句编写之后,可以使用 show index from xxx。查看表的现有索引结构,然后用 explain 分析语句索引结构,然后用 explain 分析语句索引的命中情况。