Commit 223de1a7 authored by 王雷's avatar 王雷 😹

add (SQL)优化分析与方法.md

parent 8bdf749c
### SQL 优化分析与方法
-- 王雷 2019年1月24日 --
> **前言**:要做好 sql 优化,我们先聊一聊在编写 sql 时要经常用到的一些关键词,看看这些关键词的用途,以及应该如何使用;然后讨论下,一条 SQL 是如何被执行的,再根据 SQL 的执行规范说一说应该怎么写出高效的 SQL,最后拿出一个栗子,来分析下应该如何对问题 SQL 进行优化。
#### 一、先说一说什么是 SQL
我们先看一看百度百科中关于 SQL 的描述
> 结构化查询语言 (Structured Query Language) ,简称 SQL,是一种特殊目的的编程语言,是一种数据库查询和程序设计语言,用于存取数据以及查询、更新和管理关系数据库系统;同时也是数据库脚本文件的扩展名。
标准的 SQL 包含 6 个组成部分,分别是:
- 数据查询语言(DQL):用来检索数据,是最常用的 SQL,基本关键词(select)
- 数据操作语言(DML):用来修改数据,如插入、更新、删除,基本关键词(insert,update,delete)
- 事务处理语言(TPL):用来确保事务操作正常有效,基本关键词(commit,rollback)
- 数据控制语言(DCL):用来进行数据权限控制,一般用来分配用户、角色以及数据可访问范围,基本关键词(grant,deny)
- 数据定义语言(DDL):用来定义数据格式、关系,基本关键词(create table)
- 指针控制语言(CCL):用来控制数据游标在不同数据行之间跳转,基本关键词(cursor)
  **我们常说的优化,大部分所指的就是对使用数据查询语言编写的 SQL 在执行效率方面进行优化。**
  当然,sql 优化也不单单只有 DQL 优化,有时,为了满足效率预期,我们也需要对其他方面进行调整。
#### 二、名词解释
  在讲解如何优化 SQL 之前,我们先介绍几个关键字,这些关键字大家在日常的开发中肯定经常用到,但具体是做什么用的恐怕就不那么清楚了,所以,让我们先从这些“最熟悉的陌生人”开始,对我们的工作内容做一个深度了解。
1. select:这个是 DQL 最常见的关键字,我们基本上用在了所有的查询开始的地方,在 DQL 语句中,select 表示一个查询的开始或者一个子查询的开始,紧跟在 ```select``` 后面的就是我们要从数据库中提取后返回给调用方的数据项,当 ```select``` 后面跟随一个 ```*``` 时,表示我们要将所有查询到的数据项都返回;
2. from:99.999% 的 DQL 中都会包含 ```from``` 关键字,它用来标识我们要查询的数据的来源,```from``` 后面可以是数据表(table),视图(view)甚至是另一个 DQL ***(子查询)***```from``` 后面可以跟随一个或多个数据来源,使用 “,” (西文半角逗号)进行分割;
> 在某些数据库中,如 MySQL,在调用一个函数或直接返回一个自定义常量时可以不写 from 关键字,如我们配置在数据库连接池中,为连接对象验活的语句 ```select 1```,或者直接调用一个函数,如查询当前数据库服务器时间 ```select now()```,但不是所有的数据库都支持省略 from 关键字,如 ORACLE,所以为了保证数据库兼容,我们要求不能省略 ```from``` 关键字,像上面两个语句,要改写为 ```select 1 from dual``` 和 ```select now() from dual```,dual 是数据库中提供的一个只有一列的系统表,一般我们称之为伪表或虚拟表。
3. inner join:用来做两个数据来源的关联使用,使用 on 关键字来进行数据关联,一般用在多表(视图)查询上,一般 inner join 也可以写为逗号分隔的方式,然后将 on 条件转义到 where 中;使用 inner join 只有当数据来源存在时,才会返回数据;
4. join:join 就是对 inner join 的简写
5. ***left join***:左外连接,也可简称左连接,当使用 left join 进行关联时,处于 left join 后方的数据来源如果不存在满足条件的数据记录,但其他数据来源存在相关记录,则返回除此来源外的其他数据项,此来源数据项返回 null
6. right join:右外连接,也可简称右连接,数据返回情况如 left join 相似,但区别是只要 right join 后方的数据来源存在符合条件的记录即返回数据;
7. full join:可以看做是 left join ∪ right join,也就是只要有满足条件的数据,就会返回,这个用的比较少;
8. where:条件起始关键字,跟随在 where 关键字后的就是数据查询条件,我看大家在写 sql 经常会在 where 后面跟一个 1=1,貌似是方便了查询条件的拼写,但是,这个在某些数据库或者引擎下是会影响执行效率的,应该尽量避免;
9. in:一般用在一个数据项符合多个条件时,但是,in 操作很慢,如果可能的话,还是写 exists 子查询比较好,还有,如果要使用的 in 条件选项很多的情况,jdbc 可能是无法执行的,如 1000 个以上,这时就要考虑其他方法了;
10. ***like***:做模糊查询时使用,使用左右 “%” 来进行匹配,如果使用左侧 % ,查询的时候用不上索引;
11. group by:数据项分组,一般配合聚合函数,如 sum avg 等使用,如果需要对分组结果在进行筛选的话,需要配合 having 关键字;
12. order by:结果排序;
#### 四、优化策略
  不同的数据库甚至同一个数据库不同的数据引擎在 SQL 处理上回存在差异,而咱们最常用的数据库是 MySQL,所以,咱们的优化策略主要针对的就是 MySQL,其他类型的数据库优化方案大家可以 Google 或者百度。
  要想做好 SQL 优化,我们需要先明白 MySQL 在执行一个 SQL 时的顺序,一条 SQL 是有多个完成不同任务的关键字组成的,而一个最完整的 SQL 大概是下面这样:
```sql
select
< select_list >
from
< left_table > < join_type >
join
< right_table > ON < join_condition >
where
< where_condition >
group by
< group_by_list >
having
< having_condition >
order by
< order_by_condition >
limit < limit_number >
```
&emsp;&emsp;咱们跳过最底层的 SQL 检查和解析部分,直接看最终的执行顺序
```sql
-- 上面那个 SQL 在 MySQL 引擎中的执行顺序是这样滴
from
<left_table>
on
<join_condition> <join_type>
join
<right_table>
where
<where_condition>
group by
<group_by_list>
having
<having_condition>
select
<select_list>
order by
<order_by_condition>
limit
<limit_number>
```
&emsp;&emsp;很诧异是不是,和咱们写的 SQL 顺序发生了很大的变化,但却很符合查询的数据处理逻辑,先需要知道在什么地方获取数据,然后根据 on 条件进行数据关联,再通过 where 条件筛选掉不符合条件的数据,接着进行分组、筛选分组结果,然后根据要求在筛选出需要返回的数据项,最后根据取数限定,返回一定量的数据。
&emsp;&emsp;从 SQL 的执行顺序咱们可以看出,这就是要给在逐渐筛选数据的过程,一步一步的减少最终返回的数据量,而这也正是我们优化 SQL 的目标,让能大量减少数据量的筛选条件先执行,然后逐步细化调整;
&emsp;&emsp;常用的 SQL 优化方法如下:
1. 减少子查询的使用,尽量使用 join 的方式连接数据源,当一个查询中存在子查询的时候,数据库引擎会先将子查询提取出来执行,然后将结果在使用虚拟表或临时表的方式嵌入到原始 SQL 中,这将严重影响执行效率;
2. 如非必要,所有的数据表关联都选择 (inner) join 的方式,不要使用 left join 或 right join;
3. 表关联条件(on)尽量使用索引列,索引将会显著提升查询效率,但索引也不是万能的,维护索引需要占用大量的存储空间,而且在插入或更新数据值会同时更新索引,会影响写数据的速度;
4. 索引尽量创建在会对外进行关联的数据列上,如在销售模块中,我们有订单表,订单明细表,客户信息表,其中订单表中有客户 ID,订单明细表中有订单 ID,那么订单表中的客户 ID 和订单明细表中的订单 ID 就应该创建索引;
5. 创建外键,这是一把双刃剑,有外键关系的表在进行关联时相对于索引还有一定的提升,但使用了外键在对数据进行增删改的时候有一定的顺序影响,因此我不建议大家使用外键。
6. 条件类型,在执行查询时,作为变量的条件要尽可能的与要比对的数据列类型一致,减少数据库引擎转换数据类型的时间;
7. 如果要参与比对的条件需要用到条件,则尽量避免在数据列上使用,这样会造成索引失效,而形成全表扫描,增加数据量;
8. 如果查询条件中对同一个数据列与多个选项,不要使用 or 进行条件关联,而是使用 in;
9. 如果查询条件中是一个变量可能符合多个数据列,可以将 in 反过来写,如,订单表中,想要查找 **未交费****未开票****未发货** 的数据,则条件可以写为 ***where 'N' in (pay_status, invoice_status, send_status)***
10. 如果查询条件包含较多的 or,可以使用 union all 来分解为多个 and 条件的 sql,避免因为使用 or 关系造成索引失效;
11. 避免不必要的分组和排序;
12. 如非比较,减少 like 的使用,like 比较在大部分情况下无法利用到索引;
13. 从我了解到的资料看,MySQL 在解析 from 中的表和 Where 中的条件时基本都是采用自做向右的方式进行处理,因此我们再写跨表查询和多条件查询时,要尽量将可以大量过滤数据的条件写在前面,将可以利用索引的条件写在前面;
14. 在使用条件时,尽量减少 not、<> 这样的比较方式;
15. 能使用 between 时,尽量别拆成多个 or 条件;
16. 能使用去重方式获取数据,就不要使用 group by 进行分组;
17. 合理利用 limit,减少一次性传输大量数据的可能;
18. 合理利用游标技术,减少反复查询;
#### 五、举个栗子 ☺
&emsp;&emsp;下面,我们拉出在 **2019年1月** 造成大规模线上问题的 SQL 来具体说明优化步骤。
```sql
-- 这个就是造成数据库 CPU 超负荷以至于停止对外服务的罪魁祸首
SELECT
ss.djxh djxh,
ci.ID cid,
ci.E_ID eid,
ss.ID sbid,
ci.SHXYDM shxydm,
ci.CUSTOMER_NAME customer_name,
db.BBLX_MC zsxm_mc,
CASE ss.TB_BZ WHEN 'Y' THEN '已同步' ELSE ( CASE ss.SBZT WHEN 'Y' THEN '已申报' ELSE '未申报' END ) END AS sbzt,
ss.SBRQ sbrq,
ss.YBTSE ybtse,
CASE ss.JKZT WHEN 'Y' THEN '已缴款' ELSE '未缴款' END AS jkzt,
ss.JKRQ jkrq,
ss.SSSQ_Q sssq_q,
ss.SSSQ_Z sssq_z,
ss.ZSXM_DM zsxm_dm,
ss.BBLX_DM bblx_dm,
ei.REAL_NAME NAME,
ss.SWJGMC swjgmc,
ss.SWJGJC swjgjc,
ss.NSRSBQ nsrsbq
FROM agency_tax.sb_sbxx ss
LEFT JOIN `customer_info` ci ON ci.ID = ss.C_ID
LEFT JOIN agency_tax.dm_bblx db ON db.BBLX_DM = ss.BBLX_DM
LEFT JOIN employee_info ei ON ei.id = ci.SERVICE_TAX_ID
LEFT JOIN (
SELECT b.num, sbxx.SB_ID, sbxx.BB_DM
FROM agency_tax.sb_sbxx_bbmx sbxx
LEFT JOIN (
SELECT COUNT(*) num, bbmx.SB_ID sbId
FROM agency_tax.sb_sbxx_bbmx bbmx
WHERE bbmx.STATE = 'Y'
GROUP BY bbmx.SB_ID
) b ON b.sbId = sbxx.SB_ID
WHERE sbxx.STATE = 'Y'
GROUP BY sbxx.ID
) a ON ss.ID = a.SB_ID
WHERE ss.state = 'Y'
AND a.num > 0
AND ci.CUSTOMER_NAME LIKE '%柏%'
AND ( ss.SBZT = 'Y' OR ss.BBLX_DM = '10411' OR ss.TB_BZ = 'Y' )
AND ci.SERVICE_TAX_ID = 696
GROUP BY ss.ID
ORDER BY ss.SSSQ_Z DESC, ss.C_ID, ss.ID
LIMIT 15
```
&emsp;&emsp;这个 sql 的业务是查询符合条件税种申报、缴款情况,并返给用户查看,在发生问题的时刻,这个 sql 涉及的数据表数据长度如下表:
| 序号 | 数据表名称 | 数据行数 |
|---|---|---|
| 1 | sb_sbxx | 254389 |
| 2 | customer_info | 53801 |
| 3 | dm_bblx | 83 |
| 4 | employee_info | 2630 |
| 5 | sb_sbxx_bbmx | 179635 |
&emsp;&emsp;我们可以看到其中数据量最多的 sb_sbxx 表,不过有 25 万行记录,但在发生问题的时刻,这条 sql 语句执行耗时却达到了惊人的 200s,以至于造成数据库服务器 CPU 占用率过高(95%),进而引发数据库服务对外停止服务,造成大面积线上事故。
&emsp;&emsp;让我们使用 EXPLAIN 关键字来查询这条 SQL 的执行计划,如果使用 Navicat 工具,可以在查询窗口上点 “解释” 按钮,来获取执行计划:
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | extra |
|---|---|---|---|---|---|---|---|---|---|
| 1 | PRIMARY | <derived2> | ALL | | | | | 53801 | Using where; Using temporary; Using filesort |
| 1 | PRIMARY | ss | eq_ref | PRIMARY,IDX_SB_SBXX_CID,IDX_SB_SBXX_CID_ZSXM,IDX_SB_SBXX_CID_ZSXM_SQ,IDX_SB_SBXX_CID_ZSXM_BBLX_SQ,IDX_SB_SBXX_SQ_STATE | PRIMARY | 4 | a.SB_ID | 1 | Using where |
| 1 | PRIMARY | db | eq_ref | PRIMARY | PRIMARY | 30 | agency_tax.ss.BBLX_DM | 1 | |
| 1 | PRIMARY | ci | eq_ref | PRIMARY | PRIMARY | 4 | agency_tax.ss.C_ID | 1 | Using where |
| 1 | PRIMARY | ei | const | PRIMARY | PRIMARY | 4 | const | 1 | |
| 2 | DERIVED | sbxx | index | PRIMARY,IDX_SB_SBXX_BBMX_SBID,IDX_SB_SBXX_SBID_ZSXM,IDX_SB_SBXX_SBID_ZSXM_SQ,IDX_SB_SBXX_SBID_ZSXM_BBLX_SQ,IDX_SB_SBXX_SBID_ZSXM_BBLX_BB_SQ | PRIMARY | 4 | | 167850 | Using where |
| 2 | DERIVED | <derived3> | ref | <auto_key0> | <auto_key0> | 4 | agency_tax.sbxx.SB_ID | 10 | |
| 3 | DERIVED | bbmx | index | IDX_SB_SBXX_BBMX_SBID,IDX_SB_SBXX_SBID_ZSXM,IDX_SB_SBXX_SBID_ZSXM_SQ,IDX_SB_SBXX_SBID_ZSXM_BBLX_SQ,IDX_SB_SBXX_SBID_ZSXM_BBLX_BB_SQ | IDX_SB_SBXX_BBMX_SBID | 4 | | 167850 | Using where |
&emsp;&emsp;我们主要关注执行计划中的 rows 这一列,这一列表示执行这条 SQL 需要扫描的数据行数,可以看到,其中对 sbxx 和 bbmx 这两数据来源都进行 16 万行的数据扫描;
&emsp;&emsp;同时根据执行计划我们也可以看出,这条查询竟然包含了两段子查询;
&emsp;&emsp;我们现在可以做一个初步的判断,这条 SQL 存在全表或大量数据扫描问题,这是我们要解决的第一个问题,出现全表扫描的原因可能是由于缺少索引或缺少有效的查询条件;
&emsp;&emsp;我们反过来看 SQL,可以看到其中的子查询是
```sql
SELECT b.num, sbxx.SB_ID, sbxx.BB_DM
FROM agency_tax.sb_sbxx_bbmx sbxx
LEFT JOIN (
SELECT COUNT(*) num, bbmx.SB_ID sbId
FROM agency_tax.sb_sbxx_bbmx bbmx
WHERE bbmx.STATE = 'Y'
GROUP BY bbmx.SB_ID
) b ON b.sbId = sbxx.SB_ID
WHERE sbxx.STATE = 'Y'
GROUP BY sbxx.ID
```
&emsp;&emsp;进一步分析发现,这个子查询竟然有嵌套了一个子查询,更令人费解的是,这个查询竟然是对同一张表,通过对 SQL 分析,这个查询的目的应该是提取符合条件的记录总数,然后对外提供查询。
&emsp;&emsp;当然,我们在没有分析业务的前提下,也不能贸然调整 SQL,所以需要进一步分析子查询数据的用途,从 SQL 中,我们可以看到子查询返回了 num、sb_id 和 bb_dm 三个字段,其中 sb_id 作为子查询与其他数据表的关联条件,num 在查询条件中出现了一次,而 bb_dm 从头至尾就没有被使用过,为了避免 SQL 是动态生成的,缺少使用条件,我们又进一步检查业务逻辑,发现仍然没有使用这个条件的位置,因此可以认为此节点无意义,可以忽略;
&emsp;&emsp;在进一步分析,发现 SQL 中使用的全部都是 left join,结合我们前面说的内容,left join 会在右侧数据表不存在符合条件的数据时,检索左侧表的内容,而从业务角度分析,在这个查询中,左侧表 sb_sbxx 并不会出现在不存在于其他报表的数据,因此可以使用 inner join 替换掉现有的 left join
&emsp;&emsp;然后,在分析查询条件,在查询条件中存在使用子查询结果的情况,同时存在 like 条件和 or 条件,根据优化策略,我们对查询条件的顺序进行调整,将可以大量过滤数据的条件前移,将 like 条件后置,同时将涉及子查询的条件先拆分出来放置到分组条件中,对 SQL 进行优化,得到以下优化结果
```sql
select
distinct
ss.djxh djxh,
ci.id cid,
ci.e_id eid,
ss.id sbid,
ci.shxydm shxydm,
ci.customer_name customer_name,
db.bblx_mc zsxm_mc,
case ss.tb_bz when 'Y' then '已同步' else ( case ss.sbzt when 'Y' then '已申报' else '未申报' end ) end as sbzt,
ss.sbrq sbrq,
ss.ybtse ybtse,
case ss.jkzt when 'Y' then '已缴款' else '未缴款' end as jkzt,
ss.jkrq jkrq,
ss.sssq_q sssq_q,
ss.sssq_z sssq_z,
ss.zsxm_dm zsxm_dm,
ss.bblx_dm bblx_dm,
ei.real_name name,
ss.swjgmc swjgmc,
ss.swjgjc swjgjc,
ss.nsrsbq nsrsbq,
count(distinct bbmx.id) num
from agency_tax.sb_sbxx ss
join `customer_info` ci on ci.id = ss.c_id
join agency_tax.dm_bblx db on db.bblx_dm = ss.bblx_dm
join employee_info ei on ei.id = ci.service_tax_id
join agency_tax.sb_sbxx_bbmx bbmx on bbmx.sb_id = ss.id
where
ci.service_tax_id = 696
and ss.state = 'Y'
and ( ss.sbzt = 'Y' or ss.bblx_dm = '10411' or ss.tb_bz = 'Y' )
and ci.customer_name like '%柏%'
group by ss.id
having count(distinct bbmx.id) > 0
order by ss.sssq_z desc, ss.c_id, ss.id
limit 15
```
&emsp;&emsp;对优化前和优化后的 SQL 分别执行,时间分别为 2s 和 300ms(多次平均),基本可以认为 SQL 完成了优化;
&emsp;&emsp;我们再次执行查询计划,发现仍然存在一个一个全表扫描,customer_info 表;
&emsp;&emsp;纵观 SQL,我们可以看到 customer_info 除了提供结果数据外,还提供了一个筛选条件 service_tax_id,而通过分析表结构发现,service_tax_id 列缺少索引,并且该列可以过滤大量数据,因此可以考虑增加索引;
&emsp;&emsp;增加索引后,再次执行优化后的 SQL,可以看到执行时间已经降低到 15ms(平均多次),至此,我们可以任务,本次 SQL 优化完成。
#### 五、总结
&emsp;&emsp;纵观问题 SQL,我们可以发现这条 SQL 基本上把所有编写 SQL 是应该避免的问题都用上了:
- 无意义的子查询
- 不正确的 join 方法
- 关键条件(字段)缺少索引
- 不必要的分组
- 查询条件顺序随意
- 过多的 or 条件(业务原因,因为影响不大,没有调整)
- 包含模糊查询(业务问题,但可以将模糊查询放到最后)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment