复杂SQL治理实践 | 京东物流技术团队

一、前言

软件在持续的开发和维护过程中,会不断添加新功能和修复旧的缺陷,这往往伴随着代码的快速增长和复杂性的提升。若代码库没有得到良好的管理和重构,就可能积累大量的技术债务,包括不一致的设计、冗余代码、过时的库和框架以及不再使用的功能。这些因素都会导致软件结构的脆弱,增加系统出错的可能性,我们俗称为“代码腐化”,持续性的重构是一种好的解决方案。SQL也是我们常用的代码语言,虽然SQL本身作为一种标准化的查询语言不会"腐化",但是使用SQL编写的数据库应用程序、查询和架构确实可能会因时间推移而面临类似于代码腐化的问题。

平台技术部一直坚持做稳定性建设,其中慢SQL就作为一个核心指标在治理。在治理进入深水区时,就会啃到因“SQL腐化”引入的复杂SQL治理这种硬骨头。本文以一个案例为依托来看看怎样像重构Java等高级编程语言一样来重构SQL。

二、JDL路由系统复杂SQL治理案例

路由规划是为保障客户体验,依据产品需求及时效目标,设计物流网络中每个节点的操作时长,然后通过节点互相串联保障全程链通且综合最优,同步输出规划方案并指导运营现场操作,双向校验优化,实现路由规划与实际运营的不断趋合。

简言之,路由系统支持的路由规划就是在做基于物流网络运营的运筹优化,网络是基础。而网络的基础又是线路,必然对线路的操作会“千奇百怪”。

1.问题SQL

select count(*) total_count 
from (
select * 
from (
select 
a.line_store_goods_id as line_resource_id, a.group_num as group_num, 
a.approval_erp as approval_erp, a.approval_person as approval_person, 
a.approval_status as approval_status, a.approval_time as approval_time, 
a.approval_remark as approval_remark, a.master_slave as master_slave, 
a.parent_line_code as parent_line_code, a.remarks as remarks, a.operator_time, 
a.same_stowage as same_stowage, b.start_org_id, b.start_org_name, 
b.start_province_id, b.start_province_name, b.start_city_id, b.start_city_name, 
b.start_node_code, b.start_node_name, b.end_org_id, b.end_org_name, 
b.end_province_id, b.end_province_name, b.end_city_id, b.end_city_name, 
b.end_node_code, b.end_node_name, b.depart_time, b.arrive_time, b.depart_wave_code, 
b.arrive_wave_code, b.enable_time, b.disable_time, a.store_enable_time, 
a.store_disable_time, a.update_name operator_name, b.line_code, 
b.line_type, b.transport_type, IF(a.store_enable_time > b.enable_time, 
IF(a.store_enable_time > c.enable_time, a.store_enable_time, c.enable_time), 
IF(b.enable_time > c.enable_time, b.enable_time, c.enable_time)) 
as insect_start_time, IF(a.store_disable_time < b.disable_time, 
IF(a.store_disable_time < c.disable_time, a.store_disable_time, c.disable_time), 
IF(b.disable_time < c.disable_time, b.disable_time, c.disable_time)) 
as insect_end_time 
FROM (
select * FROM line_store_goods WHERE yn = 1 and master_slave = 1) a 
join (
select start_org_id, start_org_name, start_province_id, start_province_name, 
start_city_id, start_city_name, start_node_code, start_node_name, end_org_id, 
end_org_name, end_province_id, end_province_name, end_city_id, end_city_name, 
end_node_code, end_node_name, depart_time, arrive_time, depart_wave_code, 
arrive_wave_code, line_code, line_type, transport_type, min(enable_time) 
as enable_time, max(disable_time) as disable_time 
from line_resource where line_code in (
select line_code from line_store_goods WHERE yn = 1 ) 
and yn=1 group by line_code) b 
ON a.line_code = b.line_code and a.start_node_code = b.start_node_code 
join (
select line_code,start_node_code, min(enable_time) as enable_time, 
max(disable_time) as disable_time from line_resource 
WHERE yn = 1 group by line_code) c 
ON a.parent_line_code = c.line_code and a.start_node_code = c.start_node_code) temp 
WHERE start_node_code = '311F001' and disable_time > '2023-11-15 00:00:00' 
and enable_time < disable_time) t_total;

这是一段运行在生产上的复杂SQL案例,通过慢SQL指标统计识别出来。一眼看过去毫无头绪(说明不仅性能差,而且可读性差,那么必然可维护性差),非功能性指标总是存在很强的关联性

2.开始治理

step1.格式化

对工程人员而言:要重构,格式化很重要,保证一定的可读性

select count(*) total_count from (select * from (select a.line_store_goods_id as line_resource_id, a.group_num as group_num, a.approval_erp as approval_erp, a.approval_person as approval_person, a.approval_status as approval_status, a.approval_time as approval_time, a.approval_remark as approval_remark, a.master_slave as master_slave, a.parent_line_code as parent_line_code, a.remarks as remarks, a.operator_time, a.same_stowage as same_stowage, b.start_org_id, b.start_org_name, b.start_province_id, b.start_province_name, b.start_city_id, b.start_city_name, b.start_node_code, b.start_node_name, b.end_org_id, b.end_org_name, b.end_province_id, b.end_province_name, b.end_city_id, b.end_city_name, b.end_node_code, b.end_node_name, b.depart_time, b.arrive_time, b.depart_wave_code, b.arrive_wave_code, b.enable_time, b.disable_time, a.store_enable_time, a.store_disable_time, a.update_name operator_name, b.line_code, b.line_type, b.transport_type, IF(a.store_enable_time > b.enable_time, IF(a.store_enable_time > c.enable_time, a.store_enable_time, c.enable_time), IF(b.enable_time > c.enable_time, b.enable_time, c.enable_time)) as insect_start_time, IF(a.store_disable_time < b.disable_time, IF(a.store_disable_time < c.disable_time, a.store_disable_time, c.disable_time), IF(b.disable_time < c.disable_time, b.disable_time, c.disable_time)) as insect_end_time FROM (select * FROM line_store_goods WHERE yn = 1 and master_slave = 1) a join (select start_org_id, start_org_name, start_province_id, start_province_name, start_city_id, start_city_name, start_node_code, start_node_name, end_org_id, end_org_name, end_province_id, end_province_name, end_city_id, end_city_name, end_node_code, end_node_name, depart_time, arrive_time, depart_wave_code, arrive_wave_code, line_code, line_type, transport_type, min(enable_time) as enable_time, max(disable_time) as disable_time from line_resource where line_code in (select line_code from line_store_goods WHERE yn = 1 ) and yn=1 group by line_code) b ON a.line_code = b.line_code and a.start_node_code = b.start_node_code join (select line_code,start_node_code, min(enable_time) as enable_time, max(disable_time) as disable_time from line_resource WHERE yn = 1 group by line_code) c ON a.parent_line_code = c.line_code and a.start_node_code = c.start_node_code) temp WHERE start_node_code = '311F001' and disable_time > '2023-11-15 00:00:00' and enable_time < disable_time) t_total;

经过格式化之后,能简单判断出SQL的功能是检索满足某条件的线路数量统计。

注意:格式化作为一个重要的工具可以在任意阶段发生作用。

step2.分层拆解

·level0

select count(*) total_count from t_total

·level1 - t_total

select * from temp 
WHERE start_node_code = '311F001' 
and disable_time > '2023-11-15 00:00:00' 
and enable_time < disable_time

·level2 - temp

select 
a.line_store_goods_id as line_resource_id, a.group_num as group_num, a.approval_erp as approval_erp, a.approval_person as approval_person, a.approval_status as approval_status, a.approval_time as approval_time, a.approval_remark as approval_remark, a.master_slave as master_slave, a.parent_line_code as parent_line_code, a.remarks as remarks, a.operator_time, a.same_stowage as same_stowage, b.start_org_id, b.start_org_name, b.start_province_id, b.start_province_name, b.start_city_id, b.start_city_name, b.start_node_code, b.start_node_name, b.end_org_id, b.end_org_name, b.end_province_id, b.end_province_name, b.end_city_id, b.end_city_name, b.end_node_code, b.end_node_name, b.depart_time, b.arrive_time, b.depart_wave_code, b.arrive_wave_code, b.enable_time, b.disable_time, a.store_enable_time, a.store_disable_time, a.update_name operator_name, b.line_code, b.line_type, b.transport_type, IF(a.store_enable_time > b.enable_time, IF(a.store_enable_time > c.enable_time, a.store_enable_time, c.enable_time), IF(b.enable_time > c.enable_time, b.enable_time, c.enable_time)) as insect_start_time, IF(a.store_disable_time < b.disable_time, IF(a.store_disable_time < c.disable_time, a.store_disable_time, c.disable_time), IF(b.disable_time < c.disable_time, b.disable_time, c.disable_time)) as insect_end_time 
FROM join_table

·level3 - join_table

(select * FROM line_store_goods WHERE yn = 1 and master_slave = 1) a 
join (select start_org_id, start_org_name, start_province_id, start_province_name, start_city_id, start_city_name, start_node_code, start_node_name, end_org_id, end_org_name, end_province_id, end_province_name, end_city_id, end_city_name, end_node_code, end_node_name, depart_time, arrive_time, depart_wave_code, arrive_wave_code, line_code, line_type, transport_type, min(enable_time) as enable_time, max(disable_time) as disable_time from line_resource where line_code in (select line_code from line_store_goods WHERE yn = 1 ) and yn=1 group by line_code) b ON a.line_code = b.line_code and a.start_node_code = b.start_node_code 
join (select line_code,start_node_code, min(enable_time) as enable_time, max(disable_time) as disable_time from line_resource WHERE yn = 1 group by line_code) c ON a.parent_line_code = c.line_code and a.start_node_code = c.start_node_code

·level4 - a,b,c

select * FROM 
line_store_goods 
WHERE yn = 1 
and master_slave = 1
select start_org_id, start_org_name, start_province_id, start_province_name, start_city_id, start_city_name, start_node_code, start_node_name, end_org_id, end_org_name, end_province_id, end_province_name, end_city_id, end_city_name, end_node_code, end_node_name, depart_time, arrive_time, depart_wave_code, arrive_wave_code, line_code, line_type, transport_type, min(enable_time) as enable_time, max(disable_time) as disable_time 
from line_resource 
where line_code in (select line_code from line_store_goods WHERE yn = 1 ) 
and yn=1 
group by line_code
select line_code,start_node_code, min(enable_time) as enable_time, max(disable_time) as disable_time 
from line_resource 
WHERE yn = 1 
group by line_code

step3.重构

对于Java程序员而言,《重构 - 改善既有代码的设计》一书应该不陌生。重构的核心在设计原则(“道”&“法”);但是工具包(“术”)同样重要,指导具体落地。

工具包准备:

•层级合并 减少临时表个数

•条件下推 减少检索行数&临时表大小

•join优化 减少检索行数&临时表大小

•子查询删除 减少临时表个数

•子查询与join的相互转换 减少检索行数

重构1 - 层级合并

level0 & level1

如下两个SQL执行效果一致,但是性能表现会有很大差异。

select count(*) total_count from (select * from temp where a = "1")
select count(*) from temp where a = "1"

第二种方式的性能表现会更好一些。原因如下:

1.减少查询计算开销: 在第二种方式中,直接对表进行 count(*) 统计,不需要额外的子查询和临时表操作,可以减少计算的开销。

2.减少内存占用: 第一种方式需要在内存中创建一个临时表来存储子查询的结果,而第二种方式直接对原表进行统计,不需要额外的内存占用。

3.减少磁盘 IO: 第二种方式可以直接利用表的索引进行 count(*) 统计,而第一种方式可能需要额外的磁盘 IO 来处理子查询和临时表的操作。

因此,一般情况下,推荐使用第二种方式来进行 count()统计,以获得更好的性能表现。当然,在实际情况中,也需要根据具体的业务场景和数据量来综合考虑,有时候使用子查询的方式也是必要的,但总体来说,直接对原表进行 count() 统计会更高效。

重构2 - 条件下推

start_node_code = ‘311F001’ 直接下推至level4

SQL的执行是流程化的,从执行层视角看,涉及时空资源消耗最关键的有两类:1-时间(行记录扫描)、2-空间(临时表)。

简化来看,问题SQL的执行过程是子查询形成临时表,而后基于临时表做各种形式的计算(过滤、联合)。

通过条件下推,可以将过滤动作尽可能前置,减少后续过程临时表的大小。

重构3 - join优化

按个人喜好进行格式化

条件下推

剥离冗余字段,冗余字段在SQL优化过程中是一个影响易读性的干扰信息,剥离冗余字段给工程人员一个干净的画板来尽情施为

删除无效条件。join的on条件中start_node_code条件因为条件下推已经不再是有效条件。注意,此处为了行文方便做了一定的简化,理论上之前的剥离冗余字段理论上需要包含start_node_code字段查询,在此步骤之后变为冗余字段后被剥离

删除无效子查询。此时从上往下看,表a和表b存在一个奇怪的现象 - 使用了两个类似功能(子查询和join),两者的功能完全一致。题外话:此案例作为反面教材真心不错。 涉及两者的优劣决策,个人做取舍的两个点是性能和可读性。在此案例中功能实现场景特别简单,join的可读性明显更好,在条件限定后扫描行数基本一致,但子查询多一个临时表;综合考量会删除子查询。

合并冗余join。继续从上往下看,表b和表c看起来一模一样。再次重复题外话:此案例作为反面教材真心不错。

等价条件替换,再次删除冗余字段

经过优化后的join语句,可读性发生了很大的变化 - 简单的双表关联查询。

step4.结果的理论验证

select count(*) from ((select line_code FROM line_store_goods WHERE yn = 1 and parent_line_code = line_code and master_slave = 1 and start_node_code = '311F001') ajoin(select line_code, min(enable_time) as enable_time, max(disable_time) as disable_time from line_resource where yn=1 and start_node_code = '311F001' group by line_code) bON a.line_code = b.line_code
) where disable_time > '2023-11-15 00:00:00' and enable_time < disable_time

重构后的SQL具备良好的可读性,基于此很容易反推出SQL的业务功能。基于此与其理论应用场景做是否匹配的理论判断很重要。有的时候生产上的SQL不一定是正确的,因为部分场景下可用性并不完全等价于正确性。

step5.索引优化

大量索引优化的文章可参考,此处不再赘述。

step6.结果的测试验证

与代码重构一样,测试通过永远是变更的正确性保证。较为特殊的是SQL改造后功能测试和性能测试都是必要的。

3.效果对比

| | 优化前 | 优化后 |
| 嵌套层级 | 4 | 1 |
| 多表join | 3 | 2 |
| 子查询 | 7 | 2 |
| 耗时 | 4.75s | 0.6s |

三、写在最后

重构的原则具备普适性,但是工具包每个人都有自己用的顺手的一套,没必要完全趋同。

另外,上面的技术能不用就不用,好的前置设计胜过事后的十八般武艺。

作者:京东物流 崔立群

来源:京东云开发者社区 自猿其说 Tech 转载请注明来源

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/655785.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Go语言中HTTP代理的请求和响应过程

在Go语言中&#xff0c;HTTP代理的实现涉及对请求和响应的拦截、转发和处理。下面将详细介绍这个过程。 请求过程&#xff1a; 客户端发起请求&#xff1a;客户端&#xff08;例如浏览器或其他应用程序&#xff09;发送HTTP请求到代理服务器。建立连接&#xff1a;代理服务器…

C++核心编程:类和对象 笔记

4.类和对象 C面向对象的三大特性为:封装,继承,多态C认为万事万物都皆为对象&#xff0c;对象上有其属性和行为 例如&#xff1a; 人可以作为对象&#xff0c;属性有姓名、年龄、身高、体重...,行为有走、跑、跳、说话...车可以作为对象&#xff0c;属性有轮胎、方向盘、车灯…

Django配置websocket时的错误解决

基于移动群智感知的网络图谱构建系统需要手机app不断上传数据到服务器并把数据推到前端标记在百度地图上&#xff0c;由于众多手机向同一服务器发送数据&#xff0c;如果使用长轮询&#xff0c;则实时性差、延迟高且服务器的负载过大&#xff0c;而使用websocket则有更好的性能…

go基础-垃圾回收+混合写屏障GC全分析

垃圾回收(Garbage Collection&#xff0c;简称GC)是编程语言中提供的自动的内存管理机制&#xff0c;自动释放不需要的对象&#xff0c;让出存储器资源&#xff0c;无需程序员手动执行。 Golang中的垃圾回收主要应用三色标记法&#xff0c;GC过程和其他用户goroutine可并发运行…

[Tcpdump] 网络抓包工具使用教程

往期回顾 海思 tcpdump 移植开发详解海思 tcpdump 移植开发详解 前言 上一节&#xff0c;我们已经讲解了在海思平台如何基于静态库生成 tcpdump 工具&#xff0c;本节将作为上一节的拓展内容。 一、tcpdump 简介 「 tcpdump 」是一款强大的网络抓包工具&#xff0c;它基于…

Vue学习笔记之生命周期函数

生命周期示意图如下所示&#xff1a; beforeCreate&#xff1a;组件初始化之前触发该事件created&#xff1a;组件初始化完毕触发该事件beforeMount&#xff1a;Vue应用对象挂载DOM结点之前触发该事件mounted&#xff1a;DOM结点挂载成功之后触发该事件beforeUpdate&#xff1a…

Springboot 快速集成 ES

1、Springboot 官网给出的版本选择标准 2、选择版本依赖 我的 elasticsearch 服务版本为 7.17.13&#xff0c;所以 springboot 版本我选用 2.7.10 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies<…

【Linux】wait()和waitpid()函数

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;Linux系列专栏&#xff1a;Linux基础 &#x1f525; 给大家…

CRG设计之复位

1. 前言 CRG(Clock and Reset Generation&#xff0c;时钟复位生成模块) 模块扮演着关键角色。这个模块负责为整个系统提供稳定可靠的时钟信号&#xff0c;同时在系统上电或出现故障时生成复位信号&#xff0c;确保各个模块按预期运行。简而言之&#xff0c;CRG模块就像是SoC系…

网工每日一练(1月30日)

试题1 以太网中的帧属于 &#xff08;B&#xff09; 协议数据单元。 A、物理层 B、数据链路层 C、网络层 D、应用层 试题2 在Linux 系统中&#xff0c;采用 &#xff08;B&#xff09; 命令查看进程输出的信息&#xff0c;得到下图所示的结果。系统启动时最先运行的进程是 &…

黑盒测试用例的具体设计方法(7种)

7种常见的黑盒测设用例设计方法&#xff0c;分别是等价类、边界值、错误猜测法、场景设计法、因果图、判定表、正交排列。 &#xff08;一&#xff09;等价类 1.概念 依据需求将输入&#xff08;特殊情况下会考虑输出&#xff09;划分为若干个等价类&#xff0c;从等价类中选…

项目交付后,PM该如何做复盘总结?

2023已经收尾&#xff0c;那些让我们或焦灼、或紧急、或喜悦、或悲伤的项目也都交付完毕了。为了更好的总结工作成果与反思&#xff0c;各家单位开始一边排练年会舞蹈一边要求员工做出项目交付后复盘方案了&#xff0c;那么&#xff0c;怎样的复盘才会让项目工作更加明确&#…

如何在群晖NAS部署office服务实现多人远程协同办公编辑文档

文章目录 本教程解决的问题是&#xff1a;1. 本地环境配置2. 制作本地分享链接3. 制作公网访问链接4. 公网ip地址访问您的分享相册5. 制作固定公网访问链接 本教程解决的问题是&#xff1a; 1.Word&#xff0c;PPT&#xff0c;Excel等重要文件存在本地环境&#xff0c;如何在编…

Web3技术革新:重新定义在线体验

互联网的不断演进塑造了我们的数字生活&#xff0c;而Web3技术的涌现正带来一场前所未有的变革。本文将深入探讨Web3技术的创新&#xff0c;以及它如何重新定义和提升我们的在线体验。 Web3技术的基本概念 Web3是互联网的第三个时代&#xff0c;它将去中心化、区块链、智能合约…

解决import Jetson.GPIO报错“权限错误”

在导入Jetson.GPIO模块时出现权限错误&#xff0c;可能是由于缺少适当的权限或设备权限问题。以下是一些建议&#xff1a; 使用sudo&#xff1a; 尝试使用sudo来运行你的Python脚本或解释器&#xff0c;以获取足够的权限&#xff1a; sudo python your_script.py请注意&#xf…

(一)PySpark3:安装教程及RDD编程(非常详细)

目录 一、pyspark介绍 二、PySpark安装 三、RDD编程 1、创建RDD 2、常用Action操作 ①collect ②take ③takeSample ④first ⑤count ⑥reduce ⑦foreach ⑧countByKey ⑨saveAsTextFile 3、常用Transformation操作 ①map ②filter ③flatMap ④sample ⑤d…

【Linux】—— 信号的产生

本期&#xff0c;我们今天要将的是信号的第二个知识&#xff0c;即信号的产生。 目录 &#xff08;一&#xff09;通过终端按键产生信号 &#xff08;二&#xff09;调用系统函数向进程发信号 &#xff08;三&#xff09;由软件条件产生信号 &#xff08;四&#xff09;硬件…

会计分录的概念和应用

目录 一. 会计分录的概念二. 会计分录的分类三. 会计分录的应用 \quad 一. 会计分录的概念 \quad 会计分录是指对每笔经济业务列示其应借记和应贷记账户及其金额的一种记录。 会计分录的基本要素 ( 1 )账户及其所属明细账户名称(或会计科目及其所属明细科目名称) (2 )记账方向…

Linux系统——正则表达式

有一段时间本机访问量过高&#xff0c;如何查看日志提取出访问量前十的信息 1.使用提取命令&#xff08;cut、awk、sed&#xff09;提取出ip地址的那一列 2.使用sort按数字排序&#xff0c;将相同的地址整合到一起 3.使用uniq -c统计出数量 4.使用sort 数字 数字倒序排序 5.最…

【React教程】(2) React之JSX入门与列表渲染、条件渲染详细代码示例

目录 JSX环境配置基本语法规则在 JSX 中嵌入 JavaScript 表达式在 JavaScript 表达式中嵌入 JSXJSX 中的节点属性声明子节点JSX 自动阻止注入攻击在 JSX 中使用注释JSX 原理列表循环DOM Elements 列表渲染语法高亮 条件渲染示例1&#xff1a;示例2&#xff1a;示例3&#xff08…