尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

写了十年SQL,聊聊“集合思维“这件事

写了十年SQL,聊聊“集合思维“这件事 上周Code Review组里一个小伙子写了段代码我盯着看了大概几分钟。需求不复杂从订单表里找出过去30天内每个用户最近一笔订单的金额。他的做法是查出来一个按用户ID排序的列表然后在Java里用for循环遍历拿个Map记着这个用户我见过没有没见过就把金额塞进结果集。逻辑对不对对。能不能跑能。我给他打回去了。不是因为他写得烂。是因为我十年前也这么写过我知道这条路走到底是什么样子。先说个事故2019年我在一家做电商中台的公司。有个同事写了个报表查询逻辑是先查出所有商户ID然后在Java里循环每个商户ID去查一次交易流水算出汇总金额再拼到一个大List里返回给前端。商户少的时候没事。但是遇到业务高峰期商户数从二千多涨到三万二。接口响应时间从200毫秒干到了47秒。数据库连接池打满后面排队的请求全部超时级联故障整个交易查询挂了将近二十分钟。复盘的时候大家讨论了一堆要不要加缓存、要不要做分页、要不要限流。但最根上的问题其实就一个他不该用三万两千次查询去解决一个一次查询就能解决的问题。SELECT merchant_id, SUM(amount) AS totalFROM transactionsWHERE created_at NOW() - INTERVAL 30 daysGROUPBY merchant_id;就这一条。数据库内部怎么聚合、走哪个索引、用HashAggregate还是SortAggregate那是PostgreSQL的事。你不需要替它操心。那个同事后来跟我说他当时脑子里根本没有这是一整个集合我可以一次性操作这个概念。他看到的就是三万两千个商户我得一个一个算。这就是问题所在。不是不会写SQL是脑子里的模型不对。我理解的集合思维带过几个新人之后我发现大部分人写SQL卡壳不是语法不熟是思维还没从处理一条数据切换到描述一组数据。我一般跟他们说你写SQL的时候别想这一行想这一堆。比如WHERE。你不是在检查某一行满不满足条件然后决定要不要。你是在定义一个子集的边界。满足条件的元素属于这个子集不满足的不属于。就这么简单。比如JOIN。你不是在拿左边的每一行去右边找匹配。你是在说这两个集合之间存在某种对应关系关系由这个等式定义。比如GROUP BY。你不是在把相同的归到一起然后算个数。你是在说我要把原来那个集合投影到另一个维度上去个体消失留下类别和聚合值。这些说法听着抽象但你一旦脑子里真转过来这个弯写SQL的手感完全不一样。你不再是在操作数据你是在描述数据的结构。JOIN不是拼表刚开始用JOIN的时候我脑子里的画面是两张Excel表我拿个VLOOKUP把右边的列拽到左边来。这个理解不能说错但它让你把JOIN想小了。JOIN真正在做的事情是两个集合之间建立关系。SELECT o.id, p.nameFROM orders oJOIN products p ON o.product_id p.id这不是把products表的name列贴到orders表旁边。这是在说orders里的每一条记录和products里的某条记录之间存在一种对应关系这种关系由product_id id来定义。区别在哪在于前者是操作后者是描述。前者你得关心怎么贴、贴到哪、贴不上怎么办。后者你只管说这俩东西有这个关系剩下的系统去处理。而且你仔细想想LEFT JOIN、RIGHT JOIN、FULL JOIN这些变体本质上是在回答一个集合论的问题当关系不是对所有元素都成立的时候那些没有关系的元素你要不要保留这其实是个挺哲学的问题。一个客户从来没下过单他算不算你的客户LEFT JOIN说算INNER JOIN说不算。你的业务语义决定了你选哪个。GROUP BY 让我想到一个事GROUP BY用久了有一天我突然觉得它挺像抽象这个动作。SELECT dept, COUNT(*), AVG(salary)FROM employeesGROUPBY dept;执行这条语句之前表里是几千个具体的人。执行之后人没了。剩下的是研发部87人平均23k、市场部42人平均18k。具体的张三李四被抹掉了留下的是群体的统计特征。这跟写代码时的抽象其实是一回事。你写一个UserService里面不关心具体是哪个用户你关心的是用户这个概念有哪些操作。GROUP BY就是在数据层面做这件事我不关心个体了我关心的是类别和类别的聚合属性。不过反过来也有意思。窗口函数就是GROUP BY的反面——它让你在不丢失个体的前提下把群体信息附加到个体上。SELECT name, salary, AVG(salary) OVER (PARTITION BY dept) AS dept_avgFROM employees;张三还是张三工资还是他的工资。但他旁边多了一列他部门的平均工资。他作为个体的信息保留了同时他所属群体的信息也挂在他身上了。不需要GROUP BY把所有人压扁也不需要再JOIN回来。一步到位。我第一次看懂窗口函数的时候确实愣了一下觉得这个设计真的很聪明。求求你 别替数据库瞎操心了这个我走了不少弯路。有段时间我特别沉迷于优化SQL。EXPLAIN看执行计划手动调JOIN顺序把子查询拆成临时表加各种hint。后来我们组来了个DBA看了我一通操作说了句让我记到现在的话你这条SQL的意图我看不明白。你把它改得这么碎优化器反而不好做。你先把你想干嘛说清楚性能的事我来。他说得对。很多优化其实是在用命令式思维强奸声明式语言。你手动指定了JOIN顺序等于告诉优化器你别想了按我说的来。但大多数时候优化器比你聪明。它知道数据分布知道索引情况知道哪种join算法在当前数据量下最快。你要做的就是把意图表达准确、表达清晰。就像你写需求文档写得越清楚开发越少来烦你。你写得含含糊糊还非要指定实现细节那最后出来的东西大概率不是你想要的。当然真到了性能瓶颈该干预还是要干预。但那是DBA的活不是每个写业务SQL的人都需要操心的。关于性能这个事有人可能觉得你说了半天集合思维那性能呢一条大SQL跑不动怎么办这个问题我分两面说。一面是大多数时候你写的那种一条大SQL根本跑不动不了。现代数据库的查询优化器处理几十个JOIN、几层子查询、窗口函数是它的本职工作。你把它拆成八条小SQL在应用层拼反而多了七次网络往返、七次连接获取释放还丢了优化器做全局优化的机会。另一面是真到了数据量上亿、查询复杂度很高的时候确实需要拆。但拆的方式不是回到for循环而是把大集合运算分解成几个小集合运算。物化视图、中间表、分区裁剪这些还是集合思维只是粒度变了。我见过最糟糕的情况是有人用性能优化当借口把一条清晰的SQL拆成了十几步Java操作每步查一小块数据在内存里拼。最后代码又臭又长改一个需求要动七八个地方。性能也没好到哪去因为瓶颈根本不在他想的那个地方。所以我的建议是先把意图写清楚跑一下看执行计划。真慢了再针对性地调。别提前优化更别用优化的名义把清晰的逻辑搞成一团浆糊。一个我常拿来举例的场景业务方说我要看每个品类下销量排名前3的商品。过程式思维的人会怎么做大概率是查出所有商品按品类分组每组内部排序取前三个。落到代码里可能是一个嵌套循环加一个计数器。集合思维怎么写SELECT *FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDERBY sales DESC) AS rn FROM products) tWHERE rn 3;这条语句里没有循环没有计数器没有取前三个就停。它做的事情是给整个集合里的每个元素标上一个它在所属品类内的销量排名然后过滤。整个过程是静态的、一次性的、全局的。不是走一步看一步是先给所有东西贴好标签然后按标签筛。我带新人的时候会让他们先写Java版本再写SQL版本然后对比。不是为了证明SQL更好是为了让他们感受到两种思维模型的差异。有些人很快能转过来有些人要磨一阵子。这跟聪明不聪明没关系就是习惯问题。但我也不是什么都用集合思维干了这么多年我越来越觉得xx思维好这种话要少说。集合思维在数据查询、报表、ETL这些场景里确实是最自然的选择。但你要是写一个状态机写一个审批流引擎写一个需要严格时序控制的业务逻辑你硬套集合思维反而会把自己绕进去。还有一种情况数据量特别小逻辑特别简单你就是想快速出个结果。这时候你写个循环处理一下比在那纠结SQL怎么写更省时间。工程上没那么多洁癖。我现在的判断标准大概是这样的如果这个操作的核心是筛选、关联、聚合用SQL用集合思维。如果这个操作的核心是状态流转、顺序依赖、副作用用过程式代码。如果数据量小到可以忽略怎么快怎么来。没有什么思维是万能的。资深和初级的区别不在于你掌握了多高级的方法论在于你知道什么时候该用什么、什么时候不该用。带人这些年的一些观察我带过大概七八个应届生和初、中级工程师观察下来有个规律集合思维转得快的人通常数学还行或者接触过函数式编程。不是因为他们多聪明是他们脑子里已经有映射、变换、整体操作这些概念了SQL只是换了个语法。转得慢的人通常是C或者Java写得多脑子里根深蒂固的是变量、赋值、循环、分支这套东西。你让他写SELECT ... WHERE ...他能写但你让他写个稍微复杂的子查询或者窗口函数他就开始试图在脑子里模拟执行一行一行地跑。我的办法是让他们多读别人写的好SQL。不是看语法书是看真实的、生产环境里的、解决具体业务问题的SQL。看多了慢慢就能感受到那种描述结构而不是模拟过程的味道。还有一个笨办法拿到需求先别写代码拿纸画。画两个圈代表两个集合画箭头代表JOIN关系画虚线框代表WHERE过滤。画完了再翻译成SQL。一开始慢但比直接上手写然后改来改去要快。最后那天那个code review我最后跟那小伙子说的是你回去想想这个需求里每个用户最近一笔订单这句话能不能用SQL直接表达出来而不是在Java里用if判断我是不是第一次见到这个用户。他后来改了一版用了ROW_NUMBER十几行Java变成了一条SQL。跑了一下快了大概六倍。他跟我说确实不用想那么复杂。我说对。大多数时候不用想那么复杂。你把你要什么说清楚数据库会帮你把怎么做搞定。这就是集合思维最实际的好处不是它多优雅多哲学是它让你少写代码、少维护代码、少出bug。别的都是虚的这个是真的。往期精彩DWS 宽表和 ADS 宽表不是一回事数据仓库中的全维度宽表复用利器还是治理陷阱数仓中的指标覆盖率到底该怎么衡量组里只有一行有值却要填给所有行——分组「稀疏值回填」的窗口函数范式Doris LATERAL VIEW explode_split 实战优雅实现“一行转多行”数仓小文件治理从源头预防到存量兜底的全链路实践SQL 分组有序等额分摊扣减问题最优解法面试问数据仓库各层模型数量分布规律与判断原则是什么avia数据开发游戏数仓主要有哪些指标对于指标波动有什么好的提升方式阿里大数据开发面试星型和雪花模型的trade-off是什么?实际中如何选
返回列表