5. SQL编程之三剑客只能孤独求败吗?

2018-12-27  本文已影响0人  Alvinzane

这里所说的孤独求败其实有二层意思:

  1. 作为金庸名著系列里的人物, 拥有至高无上的武功,却很少在江湖中行走
  2. 作为形容词,SQL编程在percona手里做出了无人匹敌的工具,让percona觉得甚是寂寞无奈

MySQL中SQL编程中function,procedure和trigger就好比是三剑客, 在我看来, 它目前的处境就是孤独求败!

一,对SQL编程认识的常见误区

先来讲个故事:<把家都搬过去>
话说一家鞋厂派了两个业务员去某地做市场调研,他俩回来以后分别给BOSS做了汇报:

故事不长,其道理显而易见,同样的市场, 有人看到的是"困境",有人看到的是"商机", 这是眼光的问题.

吴老师在课上也讲过小马过河的故事,道理非常简单, 不要道听途说, 遇到问题或疑惑时, 应该自己去思考, 在实践中寻找答案. 当别人说不推荐使用SQL编程时, 就应该去思考:

  1. 为什么不推荐?
  2. 不推荐是不是在说不反对呢?
  3. 那什么样的场景是不推荐?
  4. 什么样的场景又鼓励呢?
  5. 什么样的场景还得必须使用呢?

其实我本没有计划写这篇文章的,但看到很多小伙伴的还是有小马心态,还是伟哥常说的那句话,好人做到底,送佛送到西吧. 接下来,我们开始一起来寻找答案吧.

二,使用SQL编程的利与弊

如果你不太了解MySQL的function,procedure及trigger, 建议你先按顺序看完之前的教程,再回来看这一章节,便能更深入的理解下面的内容.

三剑客之function

使用function的好处可以说是谁用谁知道, 在前面的教程里, 我们已经看到用function来简化select的结构,把原来的三个left join直接变成了直接select主表,回顾一下:

# 原SQL的样子:
select 
  a.order_id,       
  b.address_name  as address_name_shipper,
  c.address_name  as address_name_receiver,
  d.address_name  as address_name_notify
from t_order a
  left join t_address b on a.address_id_shipper=b.address_id
  left join t_address c on a.address_id_receiver=c.address_id
  left join t_address d on a.address_id_notify=d.address_id
where order_id = 1;

# 用function改写后的样子:
select 
  order_id,
  f_get_address_name(address_id_shipper) as address_name_shipper,
  f_get_address_name(address_id_receiver) as address_name_receiver,
  f_get_address_name(address_id_notify) as address_name_notify
from t_order 
where order_id = 1;

# 更多的例子会以后的章节中看到

上面只是一个简单的例子, 以下是我总结的一些心得:

一言以蔽之,使用function的好处就是简化select,消除不必要的join,提高可读性.

有利就于弊哦, 那使用function有什么不好的地方呢?

有性能强迫症的同学可能要问了, 高并发怎么办法呀, 会不会拖后腿? 性能损耗是肯定是有的, 主要是看你怎么去选择了.

关于function最后再给大家留个思考题:
function适合在分库分表的场景下使用吗?

三剑客之procedure

可以把procedure理解决多个DQL和DML的混合体, 并且节省了网络的开销. 性能的优势是显而易见的,另外在团队协作层面,也有以下好处:
1. 一个团队中负责schema设计通常是更懂数据库小组
2. schema设计小组更清楚数据变化和流向,更懂得如何优化数据
3. 把核心的业务从app层面切分出来,由schema设计小组来开发procedure,人员分工更合理
4. schema设计小组和app开发小组可以利用接口,同时开发, 并可以分工测试

procedure的缺点主要就体现在debug和单元测试上面:
1. 在Oracle上面是提供了debug储存过程的api,利于专业工具是可以完成单步调度的功能,如Toad for Oracle
2. MySQL好像并不支持debug这个功能, 对于复杂逻辑的 储存过程,调试起来很不方便,所以也太建议写的太复杂了
3. 在编程语言中都有单元测试框架可以保证代码的质量,而SQL编程中,单元测试只能自己动手做

三剑客之trigger

触发器的设计允许DBA可以在最低层对数据再设立一道关卡, 有很多奇葩的场景都可以用trigger来协助我们进行处理, 举一个栗子:

曾经有一个客户投诉我们的系统"不稳定", 有些关帐的数据, 过一段时间会自己改变. 且客户一再坚定的说不是他人为修改的.
* 关帐的数据的本来就锁, 除非有权限的人取消关帐后才可以修改.
* Oh, My God. 作为一个老司机, 第一直觉就是客户他自己搞错了, 但是God都说了有问题, 那不得查一下吗?
* 于是开始推测各种可能性, 把认为有可以造成这个原因的地方都处理了,OK, 升级吧, 然后通知God说问题处理好了.
* 平静几天后, God又说了问题还在存在.
* 好吧,再继续查,继续升级, God继续说还有问题, 如此反复
* 终于有一天,伟哥实在受不了,决定.... 放大招! 放大招! 放大招!
* 于是我就给某个表的某个字段,严格看管起来,并且加上一把特殊锁, 只有打电话给我才能解开,并且留了电话.
* 就这样平静的过了好久,我都几乎快忘记这件事了,突然有一天接到一个电话:
* 你是阿伟吗? 我是xx公司的开发,负责God财务系统对接模块,我在错误日志查到,系统有个问题只有你才能解决....
* 终于查出问题了, 原来跟本就不是我们系统的问题, 当初就忽略了第三方系统出错的可能性.

相信遇到这种奇葩问题的人,我不是第一个,也不会是最后一个.

trigger的好处是切入点足够多,足够灵活, 且功能强大.

非要说trigger的缺点的话,那就是会产生额外的性能开销. 这里先暂时不展开讨论.

三,SQL编程的常见场景

function的场景相对单一些, 最主要就是处理复杂SQL的时候, 用funtion来消除一些不必要的join. 提高SQL的可读性和可维护性. 可读性好了, 犯错的机率自然也小了. 对于高并发的OLTP业务中, 很少有用武之地, 在一个轻量的OLAP业务中则可以大显身手.

procedure由于它本身也尽似于编程语言, 能做出的功能非常多, 需要在性能,成本考量之间找到平衡, 成本主要来源与业务复杂度导致的degug成本和测试成本. 特别适合以下场景:

  1. 批量数据生成,如测试数据
  2. 批量数据更新
  3. 大量数据查询后聚合,如各种财务统计,结帐等等

trigger能在最底层同时拿到新旧两个版本的数据,还能获取其它表的数据(前提不能跨实例),也能发起DML,并且自动触发,可以中断事务的提交,其灵活性和功能性不言而喻, 能完成的功能就更多了, 典型的场景如下:

四,关于性能问题

总会有一些人患有性能强迫症?! 性能恐惧症?! 我也不知道用什么词来形容比较好, 总会担心:

就像买电脑硬件一样,一定要买最顶配的, 要把性能追求到极致, 总要追求压测工具的得分力压群雄. 不少人因此买了各种发烧级硬件回来,实际上后来发现除了比别人多了"煮泡面"的功能外, 也没什么优越感.

以我个人的观点, 对于数据库而言, 当其未成为业务的频颈之前, 其稳定性,维护的便利性,开发的友好性远比追求性能重要, 而不必在上线前, 就开始操心性能的问题. 在现在的PCIE年代, 对于中小型公司而言, 可能更多考虑的不是如何拆分,反而是如何合并.

关于SQL编程的性能问题, 它不像binlog那样属于非0即1的选择, 它的取舍更加灵活, 比如, 在项目开发的初期, 完全可以为了开发效率和团队分工配合, 优先考虑使用SQL编程, 将项目按时甚至提前投入生产, 是首要考虑的问题. 业务爆发后, 就算真的瓶颈点出在SQL编程, 再将相关的业务逻辑放到app中处理也是很简单的事件, 之前也有提到过SQL编程一般都是由schema设计小组来完成, 所以SQL的质量更有保证, app开发的同学在移值的过程,也是一个学习的过程. 也利于团队的成长.

更何况, 合理的使用SQL编程, 综合来看, 反而更有利于提高性能, 因为在RBR他们是天然的"组提交", 有可以有效的合并IO,对复制也量种加强.
在介绍trigger章节的最后留了一个问题, 就是当trigger产生其它DML时, MySQL会跟据触发时机, 把额外的DML产生的events合并到自身表的events前后,binlog传到slave上执行时,也并不会触发从库上相同的trigger,我们可以解折一下binlog文件,一探究竟:

#mysqlbinlog -vv mysql-bin.000017 | tail -50
BEGIN
/*!*/;
# at 7836
#181220  3:09:36 server id 3306100  end_log_pos 7888     Table_map: `db_dba`.`t2` mapped to number 308
# at 7888
#181220  3:09:36 server id 3306100  end_log_pos 7948     Table_map: `db_dba`.`t2_history` mapped to number 310
# at 7948
#181220  3:09:36 server id 3306100  end_log_pos 7995     Write_rows: table id 310
# at 7995
#181220  3:09:36 server id 3306100  end_log_pos 8094     Update_rows: table id 308 flags: STMT_END_F

BINLOG '
8AcbXBN0cjIANAAAANAeAAAAADQBAAAAAAEABmRiX2RiYQACdDIABgMDDwMPAwQoACgAHg==
8AcbXBN0cjIAPAAAAAwfAAAAADYBAAAAAAEABmRiX2RiYQAKdDJfaGlzdG9yeQAGAwMDDwMPBCgA
KAA8
8AcbXB50cjIALwAAADsfAAAAADYBAAAAAAAAAgAG/9wDAAAAAwAAAAZweXRob24=
8AcbXB90cjIAYwAAAJ4fAAAAADQBAAAAAAEAAgAG///AAwAAAGECAAAKc3Nzc3Nzc3Nzc2ECAAAG
cHl0aG9uAwAAAMADAAAAYQIAAApzc3Nzc3Nzc3NzYQIAAAN0b28EAAAA
'/*!*/;
### INSERT INTO `db_dba`.`t2_history`
### SET
###   @1=3 /* INT meta=0 nullable=0 is_null=0 */
###   @2=3 /* INT meta=0 nullable=0 is_null=0 */
###   @3=NULL /* INT meta=0 nullable=1 is_null=1 */
###   @4=NULL /* VARSTRING(40) meta=40 nullable=1 is_null=1 */
###   @5=NULL /* INT meta=0 nullable=1 is_null=1 */
###   @6='python' /* VARSTRING(40) meta=40 nullable=1 is_null=0 */
### UPDATE `db_dba`.`t2`
### WHERE
###   @1=3 /* INT meta=0 nullable=0 is_null=0 */
###   @2=609 /* INT meta=0 nullable=1 is_null=0 */
###   @3='ssssssssss' /* VARSTRING(40) meta=40 nullable=1 is_null=0 */
###   @4=609 /* INT meta=0 nullable=1 is_null=0 */
###   @5='python' /* VARSTRING(40) meta=40 nullable=1 is_null=0 */
###   @6=3 /* INT meta=0 nullable=0 is_null=0 */
### SET
###   @1=3 /* INT meta=0 nullable=0 is_null=0 */
###   @2=609 /* INT meta=0 nullable=1 is_null=0 */
###   @3='ssssssssss' /* VARSTRING(40) meta=40 nullable=1 is_null=0 */
###   @4=609 /* INT meta=0 nullable=1 is_null=0 */
###   @5='too' /* VARSTRING(40) meta=40 nullable=1 is_null=0 */
###   @6=4 /* INT meta=0 nullable=0 is_null=0 */
# at 8094
#181220  3:09:36 server id 3306100  end_log_pos 8121     Xid = 160
COMMIT/*!*/;
SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /* added by mysqlbinlog */ /*!*/;
DELIMITER ;
# End of log file
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;

可以看出对于before update,t2_history的数据产生在t2的前面, 如果换到app程层处理, 如果不显式启用事务, 则会产生两个trx逐个提交.

而对于procedure相对于app的批量提交,在通信层面就占尽了优势,就不在多说.

唯有function的使用,相比等价joinSQL多出解析过程,在性能上有所下降, 下面是两个可用于快速测试的储存过程两个功能等价的写法的耗时, 大家都自行去测试吧, 就先不给出我自己的结论了, 留给大家在论坛中讨论:

function改写join的性能损耗是利大于弊,还是弊大于利, 能在高并发中使用吗? 并讲出你的理由.

DELIMITER $$

DROP PROCEDURE IF EXISTS P_TEST_CALL_FUNCTION_TIMES$$
CREATE PROCEDURE `P_TEST_CALL_FUNCTION_TIMES`(IN I_TOTAL INT)
BEGIN
  -- 功能: 测试function的执行耗时
  
  DECLARE C_N1 INT DEFAULT 0;
  DECLARE C_ADDRESS_NAME_SHIPPER VARCHAR(50);
  
  -- 循环
  WHILE C_N1 < I_TOTAL DO
    -- 赋值, 不能写成 C_N1++
    SET C_N1 = C_N1 + 1;
    
    -- 调用测试SQL
    SELECT
      F_GET_ADDRESS_NAME(ADDRESS_ID_SHIPPER) AS ADDRESS_NAME_SHIPPER,
      F_GET_ADDRESS_NAME(ADDRESS_ID_RECEIVER) AS ADDRESS_NAME_RECEIVER,
      F_GET_ADDRESS_NAME(ADDRESS_ID_NOTIFY) AS ADDRESS_NAME_NOTIFY
    INTO
      C_ADDRESS_NAME_SHIPPER,C_ADDRESS_NAME_SHIPPER,C_ADDRESS_NAME_SHIPPER
    FROM t_order
    WHERE ORDER_ID = 1;

  END WHILE;

END$$

DROP PROCEDURE IF EXISTS `P_TEST_CALL_JOIN_TIMES`$$
CREATE PROCEDURE `P_TEST_CALL_JOIN_TIMES`(IN I_TOTAL INT)
BEGIN
  -- 功能: 测试function的执行耗时
  
  DECLARE C_N1 INT DEFAULT 0;
  DECLARE C_ADDRESS_NAME_SHIPPER VARCHAR(50);
  
  -- 循环
  WHILE C_N1 < I_TOTAL DO
    -- 赋值, 不能写成 C_N1++
    SET C_N1 = C_N1 + 1;
    
    -- 调用测试SQL
    SELECT      
      B.ADDRESS_NAME  AS ADDRESS_NAME_SHIPPER,
      C.ADDRESS_NAME  AS ADDRESS_NAME_RECEIVER,
      D.ADDRESS_NAME  AS ADDRESS_NAME_NOTIFY
    INTO
      C_ADDRESS_NAME_SHIPPER,C_ADDRESS_NAME_SHIPPER,C_ADDRESS_NAME_SHIPPER
    FROM t_order A
      LEFT JOIN t_address B ON A.ADDRESS_ID_SHIPPER=B.ADDRESS_ID
      LEFT JOIN t_address C ON A.ADDRESS_ID_RECEIVER=C.ADDRESS_ID
      LEFT JOIN t_address D ON A.ADDRESS_ID_NOTIFY=D.ADDRESS_ID
    WHERE ORDER_ID = 1;

  END WHILE;

END$$

DELIMITER ;

返回目录

上一篇下一篇

猜你喜欢

热点阅读