PostgreSQL 子查询 vs CTE:把复杂 SQL 拆开写,读得懂也跑得快

17 阅读5分钟

e2962cd8-8086-4c10-b8d3-3ba81086323a.png 隔壁组的数据分析师又在群里吵起来了!

原因很简单:同一份“Top区域复购用户”的指标,两个人的SQL结果硬是对不上。打开代码一看,好家伙,全是几百行的嵌套子查询,一眼望去全是SELECTSELECT,比俄罗斯套娃还难拆。

这就是现状: 在数据圈,能写SQL的人一抓一大把,但能写出“人话SQL”的,简直比熊猫还珍贵。

今天我们不聊虚的,直接硬碰硬聊聊PostgreSQL里最让新人头秃的选择题:子查询(Subquery) vs CTE(WITH语句)。到底什么时候该用哪个?怎么写才能让代码像老黄(NVIDIA CEO)的皮衣一样利落?

看完这篇,你的SQL水平至少能甩开50%的同行。

别再像“俄罗斯套娃”那样写SQL了

先说个扎心的数据:超过60%的线上慢查询,往往源于逻辑混乱的深层嵌套。

什么概念?就是当你写到第三层子查询时,不仅数据库优化器(Optimizer)开始头晕,连你自己两天后回看代码,都得怀疑人生:“这玩意儿是我写的?”

如果你还在用这种方式写代码,赶紧停手。

我们先看子查询。它的本质,其实是**“就地解决”**。

在PostgreSQL里,子查询最擅长的场景是条件判断集合匹配。比如你想找“下过单的用户”,用EXISTS或者IN简直是神技。

EXISTS (SELECT 1 FROM orders ...)

这行代码翻译成白话就是:“我就看看有没有,有就行,不关心具体有多少。” 这种场景下,子查询效率极高,逻辑也顺畅 。

但问题来了,很多新手把子查询当万能钥匙,不管三七二十一全往WHERE里塞,或者在FROM后面无限套娃。结果就是代码逻辑像一团乱麻,维护成本直线上升。

CTE:把复杂逻辑拆成“乐高积木”

这时候,CTE(Common Table Expressions) 就该登场了。

简单来说,CTE就是用WITH关键字,把你那个巨大的SQL拆成一个个**“临时步骤表”**。这就好比做菜,先把葱姜蒜切好(CTE1),再把肉腌好(CTE2),最后下锅炒(主查询)。

这不仅仅是好不好看的问题,这是工程化思维的体现。

举个真实的业务场景:你要算“各地区销售额排名前三的店铺”。

  • 用子查询写: 你得在一个巨大的SELECT里再套一个窗口函数的SELECT,读起来像在解高数题。
  • 用CTE写:
  1. regional_sales(先算各区总销售)
  2. top_regions(再排个序)
  3. 最后主查询输出。

你看,逻辑流是从上到下的,清晰得像一篇说明文 。彭博社的数据分析大神们早就把这种写法标准化了,为的就是可读性可复用性

划重点: CTE还有一个隐藏大招——RECURSIVE(递归)。当你处理公司组织架构、商品品类树这种层级数据时,CTE基本上是唯一优雅的解法 。

选型避坑指南:到底怎么选?

“老司机,别光说不练,到底啥时候用哪个?”

别急,这里有一份价值百万的**“选型口诀”**,建议直接截图保存:

  1. 搞判断,用子查询: 如果你的需求是“是否存在”(EXISTS)、“是否在名单里”(IN)、“比谁都大”(ALL),子查询是首选。它就地生效,简洁明了 。
  2. 搞计算,用CTE: 如果你的逻辑超过两步,或者一个中间结果要被反复用到(比如上面的regional_sales既要算总额,又要算占比),必须用CTE。这能极大降低代码冗余 。
  3. 防坑预警: 千万小心NOT IN遇到NULL值!这绝对是新人最大的坑。如果你的子查询结果里有一个NULL,整个NOT IN就会失效(返回NULL而不是True/False)。这时候,请无脑转用NOT EXISTS

性能黑盒:CTE真的慢吗?

坊间传闻:“CTE会生成临时表,性能不如子查询。”

这都是哪年的老黄历了?

在现代PostgreSQL版本(尤其是PG 12以后),优化器已经非常聪明了。它会自动判断:如果你的CTE只被用了一次,它通常会把CTE**“折叠”**进主查询里一起优化,性能跟子查询没区别 。

但这里有个高阶技巧,普通人我不告诉他:

你可以手动控制CTE的物化(Materialization)

  • 如果你想强迫数据库先把CTE算完存起来(比如计算量巨大且被多次引用),加上 MATERIALIZED
  • 如果你想让外层的过滤条件“下推”到CTE里(利用索引加速),加上 NOT MATERIALIZED

这就像手动挡赛车,给了你极大的性能调优自由度。

写在最后

数据不会撒谎,代码也一样。

SQL不仅是写给机器跑的,更是写给人看的。 当你在代码里敲下WITH的那一刻,你就不再只是一个“取数工具人”,而是一个具备工程化思维的“数据开发者”。

下次再遇到几百行的“屎山代码”,别抱怨,试着按上面的套路,把它拆成3个清清爽爽的CTE。你会发现,不仅代码跑得快了,连下班都能早半小时。

现在的你,更习惯用IN还是EXISTS?在评论区晒出你遇到过最离谱的“套娃SQL”,我们评论区见!