1. SQL是经久不衰的基础

能经过时间考验的SQL,其优点毋庸置疑。

对于日常处理数据的朋友们(BI顾问,数据开发,数仓建模,数据研发,ETL工程师,AI工程师等),SQL更是一项非常重要的基础技能。

这里就不再列举SQL的优点了(很多),而只谈谈SQL使用中的一些问题,这里是系列文章的开篇:复杂SQL不易理解。

2. 讲故事

先讲个故事来示例,注:

  • 示例中的表和场景都是经过简化的,实际中可能复杂非常多
  • 示例的SQL都不保证是最优的写法
  • 示例中的表结构也只是示例作用

数据开发工程师小吴在一家零售企业工作,他最近的工作就是帮助运营小胡分析客户画像。

公司有2张表,都是直接存储在最简单好用的 Postgresql 12.2 数据库中:

  • orders: 订单表
  • customers: 客户表

具体内容如下:

orders:

customers

2.1 Step1 - 需要统计每个 customer_id 的总消费额

小吴快速的写了个SQL:

SELECT 

注:小吴是处女座的,所以SQL还是要经过排版的, 数据也是排好序的。

得到了如下结果:

2.2 Step2 - 加上客户名和过滤掉非正常用户

小胡很快给出了反馈:

  1. 虽然你是开发,你熟悉于直接用ID称呼客户,但是我不习惯, 我需要看中文名字
  2. 这个客户ID 2, 我记得很清楚, 是我们的测试用户,上次我们上线后,我就把它从数据库中标记 is_delete 为 True 了,你需要去除掉

小吴说:好的

在解决了如下问题后:

  1. 查阅了JOIN的几种语法
  2. 通过表别名解决了错误:column reference "customer_id" is ambiguous
  3. 通过 max() 解决了错误:column "customers.customer_name" must appear in the GROUP BY clause or be used in an aggregate function

得到了如下SQL (注意:修改散落在多个地方)

SELECT orders.customer_id,MAX(customer_name) AS customer_name,SUM(unit * unit_price * (1 - discount)) AS total_sales
FROM orders JOIN customers
ON orders.customer_id = customers.customer_id
WHERE customers.is_delete=False
GROUP BY orders.customer_id
ORDER BY total_sales DESC

得到结果:

2.3 Step3 - 复杂的任务来了,要把客户分等级了

运营同学在阿里进修了一门《人人都可以当运营》课程,回来对数据小吴说:小吴呀,我们的会员体系要做起来呀,会员是我们以后上市的支柱,即使对我们的天使轮也是非常有用的呀。而且我学到了:“一定要结合客户所在地做会员分级”,所以,我决定:

  1. 对于所在地在”上海“的客户: 如果他/她的消费额 >= 300, 那么他/她是白金会员,如果在区间 [100, 300), 则是黄金会员,否则就是普通会员
  2. 对于所在地为”杭州“的客户: 如果他/她的消费额 >= 250, 那么他/她是白金会员,如果在区间 [80, 250), 则是黄金会员,否则就是普通会员

小吴这下要好好考虑这个问题了。

2.3.1 同一层SQL上改

首先,他试着在上步骤的SQL中,直接把会员等级这个直接算出来,

SELECT orders.customer_id,MAX(customer_name) AS customer_name,SUM(unit * unit_price * (1 - discount)) AS total_sales,CASE city WHEN '上海' THEN CASE WHEN SUM(unit * unit_price * (1 - discount)) >= 300 THEN '白金'WHEN SUM(unit * unit_price * (1 - discount)) >= 100 THEN '黄金'ELSE '普通' ENDWHEN '杭州' THENCASE WHEN SUM(unit * unit_price * (1 - discount)) >= 250 THEN '白金'WHEN SUM(unit * unit_price * (1 - discount)) >= 80 THEN '黄金'ELSE '普通' ENDEND as customer_rank
FROM orders JOIN customers
ON orders.customer_id = customers.customer_id
WHERE customers.is_delete=False
GROUP BY orders.customer_id
ORDER BY total_sales DESC

得到结果:

2.3.2 重构

小吴突然想起了自己在从事“数据工程师”之前,自己在某电商公司还做过两年"软件工程师",当时的研发经理,天天用发音不太准的英语告诉小吴:

“ Do Not Repeat Yourself!

虽然没直接问研发经理,不过爱好学习的小吴猜测经理可能是从小吴也看过的经典著作《重构》 (《Refactoring》)中看来的。

带上“软件工程师”的帽子后,小吴看看自己写的SQL,除了感慨“同样是工程,为啥SQL工程和软件工程差别咋就这么大呢”。也发现了上面SQL还有不少问题:

  1. 重复的内容也太多了, 比如计算消费总额的时候, 不停的写 SUM(unit * unit_price * (1 - discount))
  2. 嵌套的CASE WHEN也太复杂(虽然小吴分别用了CASE WHEN的两种写法,但是并没有感觉到茴香豆的几种写法所带来的快感),另外,如果以后客户不光是“上海”,“杭州”了怎么办?

所以,小吴仔细重构了一版

SELECTcustomer_id,customer_name,total_sales,CASE WHEN total_sales >= baijin_bar THEN '白金'WHEN total_sales >= huangjin_bar THEN '黄金'ELSE '普通'END as customer_rank
FROM (SELECT orders.customer_id,MAX(customer_name) AS customer_name,MAX(city) AS city,SUM(unit * unit_price * (1 - discount)) AS total_salesFROM orders JOIN customersON orders.customer_id = customers.customer_idWHERE customers.is_delete=FalseGROUP BY orders.customer_idORDER BY total_sales DESC
) t1 JOIN (VALUES ('上海', 300, 100),('杭州', 250, 80))AS rank_dict(city, baijin_bar, huangjin_bar)
ON t1.city = rank_dict.city

得到结果:

小吴看到:

  1. 没有重复的计算“消费额”的逻辑
  2. 关于会员等级的计算, 通过查表的方式解决了不同城市不同计算方法的问题。

虽然:

  • SQL多了一层子查询
  • 也请忽略程序员常见的中英文结合的名字, 比如: baijin_bar(白金会员入门门槛), huangjin_bar

小吴看着SQL很满意,向欣赏一件艺术品一样欣赏了10分钟,并额外花了5分钟调整了一下缩进和空格, 觉得自己同时是:

  • 写SQL最好的程序员
  • 写程序最好的SQL工程师

2.3.3 冲突

客户觉得自己收到了重视,营业额多了2个百分点,公司很高兴, 多找了一个数据开发工程师大吴来一起做数据(写SQL)。

大吴第一天来找小吴熟悉之前写的SQL,但是大吴花了半天时间仍没有理解到底小吴写的SQL是啥。因为:

  • 业务需求是逐步增加的
  • SQL是那种写的时候知道自己在做什么,但是写好后就不知道每个地方都是做了什么了。

不过大吴经验丰富,很快和小吴达成了如下共识,并说是实现了小吴很欣赏的“逻辑隔离”。他们每做一个来自运营小胡的新需求,就在之前的SQL上套上一层以上SQL,经过一段时间, SQL变为:

--- add by Da Wu
SELECT col1,col2,col3
FROM (--- add by XiaoWu, feature 123SELECT col3, col4FROM (
--- add by Da Wu
SELECT col5,col6,col7,col8
FROM
(-- add by XiaoWu, skip check...........................................................................
)ttt) t99
) ttabc

当SQL行数超过了200 行,小吴觉得好像这样不太好,不过大吴告诉小吴:别着急,我之前所在的银行, 普通的SQL都有几千行,我们这算小菜一碟。

另外,小吴在向大吴提出了几次缩进要求(每行要比上一个逻辑块空出4个空格,不要写TAB)后,也不再提了,因为随着层级太多, 每行开头有几百个空格也实在是对不齐了。而且小吴也听过之前关于LISP程序员的程序最后一页全是“)))))))”的笑话。于是,小吴继续空4个空格写,大吴继续不留空格写逻辑,两个人竟仿佛达到了像一起工作多年的伙伴一样的默契。

3. 扪心自问

在2020年初,经过了一个漫长的寒假后,小吴也在长假中有了机会思考一下之前SQL的问题,于是发起了“扪心自问”

  • 写上面那些意大利面式(spaghetti)的SQL好吗?看着不太好
  • 意大利面式SQL有自己的优势吗? 有,从小吴和大吴的SQL的和谐相处可以看出还是有价值的
  • 我自己能看懂SQL所有的部分都是做什么的吗? 不能。

又带上“软件工程师”的“帽子”,小吴陷入了沉思。

3.1 是否能用 temp table 解决

小吴想了半天,最终还是放弃了。

  • 意大利面式的SQL的子查询嵌套层级实在太多了,每个临时数据都存到新的临时表中, 实在是太多空间了
  • 那么是否写一些 drop table 命令,来在该临时表不用时马上释放掉?想了想后,表示:自己也不知道啥时候临时表不用了
  • 临时表不光是占空间, 而且还没有索引,以及统计信息(statistics)等, 需要手工建立索引, 以及手工分析(ANALYZE) 来生成必要的统计信息

3.2 如何才能结合软件工程的实践

小吴又仔细读起了 PostgreSQL 的文档: https://www.postgresql.org/docs/current/index.html

突然有了灵感。 WITH Queries (Common Table Expressions): https://www.postgresql.org/docs/current/queries-with.html 好像可以。

于是小吴结合自己之前的编程经验,把这个方案详细的写了下来

4. 初步方案

大吴的意大利面SQL的写法有其优势:

  • 每次的业务需求就是一层SQL
  • 虽然放在一起比较难看,但是分开写好像会比较清晰

比如:要做到第2章的例子,小吴可以这样写:

Steps

小吴选取了最新最流行的 YAML 文件格式,而没选择之前的:INI,XML,JSON等格式,小吴也觉得自己还是挺 In Time 的。

这样, 我们就可以:

  • 把编写SQL分成:面向人的SQL和面向数据库的SQL。面向人的SQL注重可读性,面向数据库的则注重效率。这一点有点像编程中的高级语言JAVA和面向机器的汇编语言之前的关系
  • 把复杂的SQL拆分成多个小的SQL,每个小的SQL只负责一小块逻辑
  • 把各个步骤之前的SQL按照引用关系,转为一个有向无环图(Directed Acyclic Graph, DAG), 这样我们可以用比较成熟的DAG遍历来组合成最终的SQL

通过读取上面人工编写的yaml文件, 经过我们的小的程序转化后, 面向机器执行的SQL变为:

WITH step_calculate_total_sales AS (WITH step_filter_customer1 AS (SELECT *FROM customersWHERE customers.is_delete=False)SELECT orders.customer_id,MAX(customer_name) AS customer_name,MAX(city) as city,SUM(unit * unit_price * (1 - discount)) AS total_salesFROM orders JOIN step_filter_customer1ON orders.customer_id = step_filter_customer1.customer_idGROUP BY orders.customer_idORDER BY total_sales DESC
), step_rank_dict AS (SELECT *FROM(VALUES ('上海', 300, 100),('杭州', 250, 80))AS rank_dict(city, baijin_bar, huangjin_bar)
)
SELECT step_calculate_total_sales.*,CASE WHEN total_sales >= baijin_bar THEN '白金'WHEN total_sales >= huangjin_bar THEN '黄金'ELSE '普通'END as customer_rank
FROM step_calculate_total_sales JOIN step_rank_dict
ON step_calculate_total_sales.city = step_rank_dict.city

得到结果:

Yeah,成功把复杂SQL拆分成面向人的多个SQL,并最终执行时, 还是有翻译好的高效的面向机器的唯一SQL。

4.2 如何利用DAG来易化“转化程序”的书写

其实DAG是计算机领域非常成熟的概念,以 Apache DolphinScheduler 中的相关代码为例,

注:Apache DolphinScheduler是国人发起的“分布式易扩展的可视化工作流任务调度“开源项目,并已经进入Apache孵化,笔者作为早期参加者和PPMC,也非常希望能吸引更多的人士加入到DolphinScheduler的开发。DolphinScheduler的项目地址在: https://github.com/apache/incubator-dolphinscheduler

比如DolphinScheduler中的DAG类: https://github.com/apache/incubator-dolphinscheduler/blob/dev/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/graph/DAG.java

public class DAG<Node, NodeInfo, EdgeInfo> {// add node informationpublic void addNode(Node node, NodeInfo nodeInfo)public boolean addEdge(Node fromNode, Node toNode)public boolean containsNode(Node node)// whether this edge is containedpublic boolean containsEdge(Node fromNode, Node toNode)// get node descriptionpublic NodeInfo getNode(Node node)public int getNodesCount()public int getEdgesCount()public Collection<Node> getBeginNode()public Collection<Node> getEndNode()// Gets all previous nodes of the nodepublic Set<Node> getPreviousNodes(Node node)// Get all subsequent nodes of the nodepublic Set<Node> getSubsequentNodes(Node node)// Gets the degree of entry of the nodepublic int getIndegree(Node node)// whether the graph has a ringpublic boolean hasCycle()// DAG has a topological sortpublic List<Node> topologicalSort() throws Exception
}

这个流程变为:

  1. 遍历yaml中最上层数组的每个记录
  2. 对于每条记录,判断是否有前置依赖(有的话加 edge),把本身作为 node 加入 DAG
  3. 进行拓扑排序(topologicalSort)
  4. 把排序好的节点从前到后一个一个处理,通过WITH语句串起来

4.3 上面只是一种可行思路, 但是细节是魔鬼

上面的思路,感觉对Postgresql的SQL可读性做了非常棒的探索。但是,真正能用用于商业还是有很多细节的, 比如: 每个步骤的schema信息,每个步骤的预览,以及某一步的schema变化后的处理。

所以,除了自行探索,也可以使用现成的商业产品。比如:笔者所在的创业公司——观远数据,就有丰富的数据可视化和数据开发平台等多个产品,欢迎访问官网进行了解: https://www.guandata.com/

注:文中所描述的方法并不是观远系统ETL中所使用的实现方法,观远系统中有着更先进、完善的实现。

5. 想象空间

有了上面的方案, 我们可以把SQL变为可拆分,容易读懂的方式,并且每一步转化都是有注释的可以理解的小步骤。

我们还可以继续参考”软件工程“中的其它实践来管理SQL, 比如:

  1. SQL yaml文件上传到github,进行版本控制
  2. 也可以编写单元测试
  3. 通过Github的Action做CI/CD, 自动化测试等

从此SQL也逐渐软件工程起来。

正所谓:

  • 软件工程用的好,SQL写的好
  • 软件工程用的好,下班早,头发多
  • 软件工程用的好,彻底重写少

用sql写每年的第三周_SQL的弱点(1):复杂SQL不易理解,以及软件工程如何来帮忙...相关推荐

  1. 面试官:听说你sql写的挺溜的,你说一说查询sql的执行过程

    来自:非科班的科班 当希望Mysql能够高效的执行的时候,最好的办法就是清楚的了解Mysql是如何执行查询的,只有更加全面的了解SQL执行的每一个过程,才能更好的进行SQl的优化. 当执行一条查询的S ...

  2. 第三周 7.17LJY关于方法参数的一些理解

    在java中采用的是按值调用.也就是得到的是变量的一个拷贝 class Text22 {static int i = 1;public static void main(String[] args) ...

  3. 2017-2018-1 20155234第三周《信息安全系统设计基础》学习总结

    20155234第一周<信息安全系统设计基础>学习总结 教材学习内容总结 信息存储与处理 无符号数:基于传统二进制表示法,表示大于或者等于零的数字. 有符号数:以二进制补码表示. 两者之间 ...

  4. 20145233《网络对抗》 第三周 免杀原理与实践

    20145233<网络对抗> 第三周 免杀原理与实践 实验内容 理解免杀技术原理(1分) 正确使用msf编码器,veil-evasion,自己利用shellcode编程等免杀工具或技巧:( ...

  5. sql 左侧要固定最近一周的周四 怎么写_程序员啊,你怎么这么忙啊?

    萌宝我又来啦!今天的话题是 一."为什么程序员这么忙呢?" 可能因为"萌宝"是个初级程序员吧. ▲工作量庞大,时间预算却少 最近一个多月里,组里连着来了几个需求 ...

  6. 三周写出高性能的Python代码,这些小技巧你值得一试。

    1一个不上进的 Python 使用者 我是一个有 C 语言背景的开发者.最近转做了 Python,平时用 Python 还算 6,这周在给新员工分享工作之后,有个小孩跑来问我:"哥,你是学 ...

  7. 老婆离家三周,我写了一个操作系统!

    我出生于1943年,今年已经78岁了,依然战斗编程的第一线,今天给大家讲讲我当年写操作系统的故事...... 我小时候特别喜欢鼓捣电器,玩了10年. 当我去加州大学伯克利分校读电子工程的时候,我发现课 ...

  8. 他,生物系毕业,刚入职连Java都没听过,却在马云的要求下,三周写出淘宝网雏形...

    生物信息学习的正确姿势 NGS系列文章包括NGS基础.高颜值在线绘图和分析.转录组分析 (Nature重磅综述|关于RNA-seq你想知道的全在这).ChIP-seq分析 (ChIP-seq基本分析流 ...

  9. 他,生物系毕业,刚入职连 Java 都没听过,却在马云的要求下,三周写出淘宝网雏形......

    公众号关注 "GitTick" 设为 "星标",带你了解技术圈内新鲜事! 今天为大家介绍一位阿里巴巴早期的程序员--蔡景现. 在阿里内部,蔡景现可是一位传奇人物 ...

最新文章

  1. Linux内核中关于定时器Timer的应用
  2. mysql rollback函数_PHP mysqli_rollback() 函数_程序员人生
  3. BugKuCTF 杂项 这是一张单纯的图片
  4. JQuery筛选器全系列介绍
  5. boost::test
  6. CF55D Beautiful numbers
  7. 计算机专业必备基础知识500题,计算机基础知识500题
  8. 图灵机器人api接入测试
  9. 躺平即是正义,另一种幸福生活的方式
  10. 测试家里网速用什么软件,家中宽带网速多少?教你测试小妙招
  11. CSS如何使用伪元素选择器给所有的div里的文本前面添加小写罗马数字编号
  12. 中国做SaaS为什么这么难?
  13. 孙陶然:企业需要建立自己的人才标准体系
  14. 质数乘积(大数乘法+埃氏筛法)
  15. 发送测试电子邮件消息 响应服务器 550,Microsoft SMTP 服务器在第三方测试中可能显示为能够接受并中继电子邮件...
  16. 中小企业如何进行云灾备?
  17. Android Studio Chipmunk 发布啦,快来看看有什么更新
  18. 如何用一片74LS161和必要的门电路构成一个可控计数器?
  19. 笔记本电脑免拆清灰的诸多方法,怎么不拆机清灰
  20. gradle的依赖方式优化

热门文章

  1. 程序员为教师妻子开发专属应用;2020 最佳开源项目出炉;中国构建全星地量子通信网|开发者周刊
  2. CSDN 发布开源代码托管平台 GitCode
  3. 数据爆炸时代,云存储在“破圈”!
  4. 专访 | 「Smartbi 」VP徐晶:未来,BI将成为决策者的诸葛亮
  5. 百度在美国遭集体起诉;iPhone 11 成苹果最畅销机型;OpenSSL 曝高危漏洞 | 极客头条...
  6. 沙迦美国大学科研副校长赵伟:揭秘工业 4.0 核心技术 CPS 的前世今生 | 人物志...
  7. 如何实现自动化前端开发?
  8. 京东回应「被薅 7000 万、项目组全体开除」;微信朋友圈屏蔽支付宝集五福;MySQL 8.0.19 发布 | 极客头条...
  9. @程序员,这 TOP 11 物联网云平台速码!
  10. Swift 势必取代 Python?