专题描述 讲述Ecbil项目典型复杂业务场景的存储过程优化步骤,帮助研发团队提升存储过程的编程能力和相关的优化技术技巧。
问题提出 优化存储过程发现SQL写法、事务提交、事务大小控制、索引设计等方面存在问题,下文对其中典型问题做案例分析。
解决方案 1 1.存储过程/函数内部的SQL语句优化

例如:
INSERT INTO tt_express_invoice ( 
EXPRESS_INVOICE_CODE, 
............
(SELECT DISTINCT 
e.express_invoice_code, 
e.bill_code, 
e.cust_code, 
e.cust_dept, 
e.vip_code, 
e.vip_dept, 
e.start_dt, 
e.end_dt, 
e.gl_dt, 
e.bill_start_dt, 
e.bill_end_dt, 
e.BILLING_VERSION_NO, 
e.bill_period, 
e.bill_type, 
e.BILL_JOB_BATCH_NO, 
e.bill_batch_no 
FROM 
tt_express_invoice e 
WHERE e.`invoice_batch_no` = v_invoice_batch_no 
AND NOT EXISTS 
(SELECT 

FROM 
tt_express_invoice t 
WHERE t.`cust_code` = e.`cust_code` 
AND t.`cust_dept` = e.`cust_dept` 
AND t.`bill_start_dt` = e.`bill_start_dt` 
AND t.`bill_end_dt` = e.`bill_end_dt` 
AND t.`invoice_batch_no` = v_invoice_batch_no 
AND t.`FEE_TYPE_CODE` = V_EXPRESS_SPECIAL_ZK)) e, 
tt_special_rebate s 
WHERE e.`cust_code` = s.`customer_code` 
AND e.`cust_dept` = s.`customer_dept` 
AND e.`bill_start_dt` = s.`start_dt` 
AND e.`bill_end_dt` = s.`end_dt` 
AND s.`billing_flag` = 1 
AND s.`rebate_type` = 1 
GROUP BY e.express_invoice_code, 
e.bill_code, 
e.cust_code, 
e.cust_dept, 
e.vip_code, 
e.vip_dept, 
e.start_dt, 
e.end_dt, 
e.bill_start_dt, 
e.bill_end_dt, 
e.billing_version_no, 
e.bill_period, 
e.bill_type, 
e.gl_dt, 
bill_job_batch_no ;

需要优化GROUP BY分组条件,去掉不需要的分组条件。将
group by子句 优化为
GROUP BY e.express_invoice_code ;
2 结合存储过程业务逻辑的SQL优化 

#查询是否有需要结算的电商快递的特殊折扣数据 
SELECT 
COUNT(1) INTO v_special_express_count 
FROM 
tt_special_rebate s 
WHERE s.rebate_type = 1 
AND billing_flag = 0 ; 

IF v_special_express_count > 0 AND V_EXPRESS_SPECIAL_ZK IS NOT NULL 

优化如下: 
SELECT 
special_rebate_id INTO v_special_express_count 
FROM 
tt_special_rebate s 
WHERE s.rebate_type = 1 AND billing_flag = 0 
LIMIT 1; 

-- tt_special_rebate 全网几万条数据 
-- special_rebate_id 是表tt_special_rebate的主键,是BIGINT整型
3 变量定义

存储过程/函数内部建议慎重使用SESSION级别的全局变量,全部使用局部变量,

且自行定义的局部变量都统一设置默认值

例如:
  #程序运行位置
DECLARE v_line_no SMALLINT DEFAULT 0 ;
4 异常处理定义

例如以事务为例,可以找到对应的错误变量,在MySQL手册上有专门的章节介绍:
-- 事务中不允许执行的命令 
Error: 1179 SQLSTATE: 25000 (ER_CANT_DO_THIS_DURING_AN_TRANSACTION) 
Message: You are not allowed to execute this command in a transaction 

-- 正在执行提交的时候出错 
Error: 1180 SQLSTATE: HY000 (ER_ERROR_DURING_COMMIT) 
Message: Got error %d during COMMIT 

-- 正在执行回滚的时候出错 
Error: 1181 SQLSTATE: HY000 (ER_ERROR_DURING_ROLLBACK) 
Message: Got error %d during ROLLBACK 

-- 异常处理的定义格式 
DECLARE error_during_commit CONDITION FOR 1180; 
DECLARE CONTINUE HANDLER FOR error_during_commit 
BEGIN 
-- body of handler 
END;
5 事务控制

阅读Ecbile存储过程都发现没有事务开启却有事务提交命令。

研发团队主要是担心DBA团队的参数设置为默认不提交,

故建议每个存储过程中设置本次连接的提交状态,命令行:

SET SESSION autocommit=1;

若存储过程内部需要开启事务,

再执行STRAT TRANSACTION(注:不要使用SET SESSION autocommit=0; 属于早期模式)

6 事务大小

从业务逻辑和数据完整性角度出发,

控制事务的大小,不一定要把所有的执行步骤都放到一个事务中,

这样容易导致资源占用、锁争用和死锁发生。

7 存储过程内的SQL语句所用索引优化

存储过程都是属于MySQL服务端执行的,

比应用程序处理消耗更多的数据库资源,

故一定要确保每条SQL语句的执行效率最高,除前面提到的SQL语句写法外,

还确保SQL语句用到的索引合理,例如:

ALTER TABLE tt_warehouse_order_temp ADD 

INDEX idx_ino_vno_cc_pcode(inner_order_no,version_no,

cust_code,pay_dept_code,order_dt); 

ALTER TABLE tt_warehouse_order_temp DROP INDEX 

IDX_ORDER_NO_WAREHOUSE_TEMP; 

8 存储过程/函数执行的资源消耗可从以下几个方面进行考虑:

1).实现的业务复杂度
2).内部复杂SQL和大数据量处理SQL的资源使用
3).存储过程/函数是否并发执行
4).存储过程/函数的执行频率
9 并发锁争用
UPDATE 
tt_warehouse_head h, 
(SELECT 
h.`head_id` ,p.`full_begin_dt`,p.`full_end_dt` 
FROM 
tt_warehouse_head h, 
tt_warehouse_cust_period p 
WHERE h.`oms_version` = 1 
AND h.`deal_process_no` = 0 
AND p.`dept_code` = h.`dept_code` 
AND p.`cust_code` = h.`cust_code` 
AND p.`end_dt` = p.`full_end_dt` 
AND p.`full_begin_dt` <= h.`order_dt` 
AND p.`full_end_dt` >= h.`order_dt` 

ORDER BY h.update_tm ASC           

-- 增加排序条件以减少锁争用和死锁发生 

LIMIT 5000) c 
SET 
h.`deal_process_no` = process_no, 
h.`peroid_sdt` = c.`full_begin_dt`, 
h.`peroid_edt` = c.`full_end_dt` 
WHERE c.head_id = h.head_id ; 

在多并发执行这条语句时,若没有对查询结果集进行排序,

容易造成不同连接之间取得的数据有交集,造成锁争用和死锁。

10 游标处理

1).为游标定义预处理,判断数据是否处理完毕
2).定义好控制游标移动的循环
3).游标执行完毕一定要销毁,避免资源未释放
11 调用的其他存储过程/函数

优化的存储过程调用到的其他存储过程或函数,其内部如何执行的也要分析优化
12

函数 建议明确申明类型

: | { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES 

SQL DATA }

13 存储过程定义者

建议为存储过程指定定义者为应用程序调用的帐号名称,例如  

DEFINER = 'ecbil'

知识点 1

只有当优化之后的group by子句中的列能唯一标识一行数据

,且优化之前子句中其他列与其在同一张表中才能进行group by去重优化。

2

见MySQL5.6手册1.8.1 MySQL Extensions to Standard SQL章节中文字“You don't need to name all selected columns in th

e GROUP BY clause. This gives better

performance for some very specific, but quite normal queries. 

See Section 12.19, “Functions and

Modifiers for Use with GROUP BY Clauses”.”
而oracle的语法:使用GROUP BY分组计算或去重时,SELECT子句中出现的字段必须在GROUP BY中出现;
3

详见手册:12.19.3 MySQL Handling of GROUP BY

,文中指出你可以使用这一特征(group by语句中不必出现select语句中所有的列),

避免不必要的列排序和分组获得更好的性能。

4

优化后的SQL可以避免二次排序,优化之前需要根据多个字段进行排序后到聚簇索引中取出数据,而优化后的SQL因为express_invoice_code是主键

,在innodb引擎中聚簇索引按照主键排序。

5

解决方案2中因为有判断条件IF v_special_express_count > 0 AND V_EXPRESS_SPECIAL_ZK IS NOT NULL

 所以进行等价优化,优化之后只需要查到一条数据,

可以避免查找出所有数据做判断是否执行后续语句,提高效率。

6 合理控制事务大小可以减少锁竞争发生。
7

存储过程/函数内部建议慎重使用SESSION级别的全局变量,全部使用局部变量

,且自行定义的局部变量都统一设置默认值。当使用的变量没有被销毁时,

新申明的变量默认初始值即为最后设定的值。

8 MySQL手册中 Appendix B Errors, Error Codes, and Common Problems部分解释了报错含义。存储过程中主要用到的错误编码有以下几种:
-- 事务中不允许执行的命令 
Error: 1179 SQLSTATE: 25000 (ER_CANT_DO_THIS_DURING_AN_TRANSACTION) 
Message: You are not allowed to execute this command in a transaction 

-- 正在执行提交的时候出错 
Error: 1180 SQLSTATE: HY000 (ER_ERROR_DURING_COMMIT) 
Message: Got error %d during COMMIT 

-- 正在执行回滚的时候出错 
Error: 1181 SQLSTATE: HY000 (ER_ERROR_DURING_ROLLBACK) 
Message: Got error %d during ROLLBACK 
9  异常处理的定义格式 
DECLARE error_during_commit CONDITION FOR 1180; 
DECLARE CONTINUE HANDLER FOR error_during_commit 
BEGIN 
-- body of handler 
END;
10

参数设置为默认提交的命令行:SET SESSION autocommit=1;设定此参数

,可以避免存储过程中未提交的部分资源占用。

11

存储过程都是属于MySQL服务端执行的,比应用程序处理消耗更多的数据库资源

,故一定要确保每条SQL语句的执行效率最高,除前面提到的SQL语句写法外,

还确保SQL语句用到的索引合理。

12

CONSTAINSSQL | NO SQL | READS SQL DATA |

 MODIFIES SQL DATA指定程序使用SQL语句的限制

CONSTAINS SQL:子程序包含SQL,但不包含读写数据的语句,默认
NO SQL:子程序中不包含SQL语句
READS SQL DATA:子程序中包含读数据的语句
MODIFIES SQL DATA:子程序中包含了写数据的语句
13

调用存储过程的用户需要有EXECUTE权限

,最终执行存储过程的用户也即存储过程定义者要具备存储过程定义语句中相关的各种权限。

存储过程中默认按DEFINER的权限执行,

可以通过SQL SECURITY选择按照创建者权限执行还是调用者权限执行。

14

NOT EXISTS(SELECT ......)语句中

,可以在select子句后加LIMIT条件做等价转换,避免产生过多查询结果集。

15 SYSDATE()为不确定函数,即select sysdate(),sysdate()...;中,可能查询结果不同。