一、子查询与CTE的基本概念

1.1 什么是子查询?

子查询是嵌套在其他查询中的查询语句,本质是“用一个查询的结果作为另一个查询的输入”。根据是否依赖外部查询,分为两类:

  • 非相关子查询:可独立执行,不依赖外部查询的任何值(比如“先统计各地区销售额,再过滤超过100万的地区”);
  • 相关子查询:依赖外部查询的字段值(比如“计算每个订单对应的客户平均订单金额”)。

示例:非相关子查询

-- 统计销售额超100万的地区(非相关子查询)
SELECT region, total_sales
FROM (
    SELECT region, SUM(amount) AS total_sales  -- 子查询:统计各地区销售额
    FROM orders
    GROUP BY region
) AS regional_sales
WHERE total_sales > 1000000;

示例:相关子查询

-- 计算每个订单的客户平均订单金额(相关子查询)
SELECT o.order_id, o.amount,
       (SELECT AVG(amount)
        FROM orders 
        WHERE customer_id = o.customer_id) AS avg_customer_order  -- 依赖外部的o.customer_id
FROM orders o;

1.2 什么是CTE(公共表表达式)?

CTE(Common Table Expression)用WITH子句定义,是命名的临时结果集,用于简化复杂查询的逻辑结构。它的核心特性是物化(默认生成临时表),且只执行一次(即使多次引用)。

示例:基础CTE

-- 用CTE实现“销售额超100万的地区”
WITH regional_sales AS (
    SELECT region, SUM(amount) AS total_sales  -- CTE:定义临时结果集
    FROM orders
    GROUP BY region
)
SELECT region, total_sales
FROM regional_sales  -- 引用CTE
WHERE total_sales > 1000000;

二、底层执行机制:为什么性能不同?

2.1 CTE的物化特性与执行流程

CTE的关键是物化(Materialized):执行时会先将CTE的结果写入临时表,再供主查询使用。这个过程类似“先把中间结果存到一张临时表,再查这张表”。

示例:CTE的执行计划(EXPLAIN ANALYZE)

EXPLAIN ANALYZE
WITH cte AS (
    SELECT * FROM large_table WHERE category = 'A'
)
SELECT * FROM cte t1 JOIN cte t2 ON t1.id = t2.parent_id;

执行计划结果

CTE Scan on cte t1  -- 扫描CTE的临时表
CTE Scan on cte t2  -- 再次扫描同一临时表
CTE cte
  ->  Seq Scan on large_table  -- CTE的实际执行逻辑
        Filter: (category = 'A')

说明:CTE只执行一次(Seq Scan on large_table),生成的临时表被两次引用,避免了重复计算,但增加了临时表的I/O开销

2.2 子查询的优化融合机制

子查询的优势在于优化器融合:PostgreSQL会尝试将子查询逻辑合并到主查询计划中,比如将非相关子查询转换为JOIN,或对相关子查询使用LATERAL JOIN优化。

示例:子查询的优化结果 对于非相关子查询:

SELECT * FROM orders WHERE customer_id IN (SELECT id FROM customers WHERE country = 'China');

优化器会将其转换为JOIN

SELECT o.* FROM orders o JOIN customers c ON o.customer_id = c.id WHERE c.country = 'China';

这样避免了子查询的独立执行,直接利用JOIN的高效性。

三、性能差异的关键场景分析

3.1 物化带来的双刃剑:I/O vs 重复计算

CTE的物化是把“双刃剑”:

  • 优势:多次引用同一CTE时,避免重复计算(比如上面的两次JOIN);
  • 劣势:生成临时表会增加I/O开销,尤其是当CTE结果集很大时。

实战测试(100万行数据)

-- CTE版本:物化临时表
WITH cte AS (SELECT * FROM events WHERE event_time > NOW() - INTERVAL '1 day')
SELECT user_id, COUNT(*) FROM cte GROUP BY user_id;

-- 子查询版本:优化器融合
SELECT user_id, COUNT(*) 
FROM (SELECT * FROM events WHERE event_time > NOW() - INTERVAL '1 day') AS sub
GROUP BY user_id;

性能结果

方案执行时间内存使用说明
CTE850ms45MB物化临时表,I/O开销
子查询420ms12MB索引下推,无临时表

3.2 索引利用:CTE的“黑盒”限制vs子查询的谓词下推

CTE是黑盒:主查询的条件无法传递到CTE内部,导致索引无法被有效利用;而子查询的条件会被“下推”到内部,直接命中索引。

示例:索引失效的CTE

-- 创建索引(order_date)
CREATE INDEX idx_orders_date ON orders(order_date);

-- CTE版本:无法利用customer_id索引
WITH recent_orders AS (
    SELECT * FROM orders WHERE order_date > '2023-01-01'
)
SELECT * FROM recent_orders WHERE customer_id = 100;  -- 全表扫描recent_orders

-- 子查询版本:利用(customer_id, order_date)复合索引
SELECT * 
FROM (SELECT * FROM orders WHERE order_date > '2023-01-01') AS sub
WHERE customer_id = 100;  -- 索引扫描orders

说明:子查询的条件customer_id = 100会被下推到orders表的查询中,直接使用复合索引;而CTE的recent_orders是临时表,没有customer_id索引,只能全表扫描。

3.3 递归查询:CTE的独占场景

递归查询(比如“查找所有下级”“路径遍历”)是CTE的独占场景,子查询无法实现。

示例:递归CTE查询组织层级

WITH RECURSIVE subordinates AS (
    -- 锚点成员:初始上级(manager_id=100)
    SELECT employee_id, name, manager_id FROM employees WHERE manager_id = 100
    UNION ALL
    -- 递归成员:连接下级(e.manager_id = s.employee_id)
    SELECT e.employee_id, e.name, e.manager_id FROM employees e
    JOIN subordinates s ON s.employee_id = e.manager_id
)
SELECT * FROM subordinates;

说明:递归CTE通过UNION ALL连接“锚点成员”(初始查询)和“递归成员”(下级查询),直到没有新结果为止。子查询无法实现这种层级迭代。

四、实战案例:从代码到性能对比

4.1 案例一:多层聚合查询

需求:计算每个地区销售额前10的产品。

CTE实现

WITH regional_products AS (
    SELECT region, product_id, SUM(quantity*price) AS sales FROM orders GROUP BY region, product_id
),
ranked_products AS (
    SELECT region, product_id, sales,
           RANK() OVER (PARTITION BY region ORDER BY sales DESC) AS rank
    FROM regional_products
)
SELECT region, product_id, sales FROM ranked_products WHERE rank <=10;

子查询实现

SELECT region, product_id, sales FROM (
    SELECT region, product_id, sales,
           RANK() OVER (PARTITION BY region ORDER BY sales DESC) AS rank
    FROM (
        SELECT region, product_id, SUM(quantity*price) AS sales FROM orders GROUP BY region, product_id
    ) AS agg
) AS ranked WHERE rank <=10;

性能对比(1GB数据集)

指标CTE方案子查询方案
执行时间2.4s1.7s
临时文件大小180MB0MB
共享缓存使用45%68%

结论:子查询的优化融合(将三层查询合并为单次聚合)避免了CTE的临时表I/O,性能更优。

4.2 案例二:多维度关联分析

需求:关联用户行为数据(events)和交易数据(orders),计算每个用户的行为次数和总消费。

CTE实现

WITH user_events AS (
    SELECT user_id, COUNT(*) AS event_count FROM events WHERE event_date BETWEEN '2023-01-01' AND '2023-01-31' GROUP BY user_id
),
user_orders AS (
    SELECT user_id, SUM(amount) AS total_spent FROM orders WHERE order_date BETWEEN '2023-01-01' AND '2023-01-31' GROUP BY user_id
)
SELECT u.user_id, e.event_count, o.total_spent FROM users u
LEFT JOIN user_events e ON u.user_id = e.user_id
LEFT JOIN user_orders o ON u.user_id = o.user_id;

子查询实现

SELECT u.user_id,
       (SELECT COUNT(*) FROM events e WHERE e.user_id = u.user_id AND e.event_date BETWEEN '2023-01-01' AND '2023-01-31') AS event_count,
       (SELECT SUM(amount) FROM orders o WHERE o.user_id = u.user_id AND o.order_date BETWEEN '2023-01-01' AND '2023-01-31') AS total_spent
FROM users u;

性能对比

  • users表较小时(<1000行):子查询更优(避免CTE的临时表);
  • users表较大时(>10000行):CTE更优(避免子查询的重复扫描)。

五、决策指南:何时选CTE,何时选子查询?

5.1 优先选CTE的场景

场景类型原因示例
递归查询子查询无法实现组织层级、路径遍历
多次引用同一结果避免重复计算同一CTE被JOIN多次
复杂逻辑分解提高代码可读性多步骤数据清洗
查询调试分步验证中间结果检查CTE的输出是否正确

5.2 优先选子查询的场景

场景类型原因示例
小结果集过滤避免CTE的物化开销维度表(如customers)过滤
索引利用允许谓词下推范围查询+条件过滤
简单逻辑减少优化限制单层嵌套查询
LIMIT分页提前终止执行(如Top N)查找每个用户的最新订单

六、高级优化技巧:突破性能瓶颈

6.1 CTE的物化控制:NOT MATERIALIZED

PostgreSQL 12+支持NOT MATERIALIZED选项,让CTE不生成临时表,允许优化器将CTE逻辑融合到主查询中。

示例

WITH cte AS NOT MATERIALIZED (
    SELECT * FROM large_table WHERE category = 'A'
)
SELECT * FROM cte WHERE id = 100;  -- 优化器会将条件下推到large_table

说明:NOT MATERIALIZED适合CTE结果集大,但主查询有过滤条件的场景,避免临时表的I/O开销。

6.2 子查询的LATERAL JOIN优化

对于相关子查询(依赖外部表的字段),可以用LATERAL JOIN替代,提高性能。

示例:查找每个用户的最新订单

-- 相关子查询(性能差)
SELECT u.name, (SELECT amount FROM orders WHERE user_id=u.id ORDER BY order_date DESC LIMIT 1) AS latest_amount
FROM users u;

-- LATERAL JOIN优化(性能优)
SELECT u.name, o.amount FROM users u
CROSS JOIN LATERAL (
    SELECT amount FROM orders WHERE user_id=u.id ORDER BY order_date DESC LIMIT 1
) AS o;

说明:LATERAL JOIN允许子查询引用外部表(u.id),且优化器会为每个用户高效查找最新订单。

七、PostgreSQL版本对性能的影响

不同版本的优化能力差异很大,建议使用12+版本以获得更好的CTE和子查询支持:

版本CTE优化子查询优化
9.x强制物化有限优化
12支持NOT MATERIALIZED子查询内联增强
15并行递归CTE谓词下推增强

课后Quiz:巩固你的理解

  1. 以下哪种场景必须使用CTE?
    A. 单层嵌套查询 B. 递归路径查询 C. 小结果集过滤 D. 索引利用
    答案:B。解析:递归查询需要RECURSIVE关键字,子查询无法实现。

  2. PostgreSQL 12+中,如何让CTE不生成临时表?
    答案:使用WITH cte_name AS NOT MATERIALIZED (...)。解析:NOT MATERIALIZED允许优化器融合CTE逻辑到主查询。

  3. 子查询相比CTE更易利用索引的原因是?
    答案:子查询参与整体优化,允许谓词下推;CTE是“黑盒”,外部条件无法传递到内部。

常见报错与解决

1. ERROR: recursive query without RECURSIVE keyword

原因:递归CTE忘记写RECURSIVE关键字。
解决:在WITH后添加RECURSIVE,如WITH RECURSIVE subordinates AS (...)
预防:写递归CTE时检查是否包含RECURSIVE

2. ERROR: relation "cte_name" does not exist

原因:CTE的引用顺序错误(比如在定义前引用)。
解决:按顺序定义CTE,先定义的CTE可以被后定义的引用,如WITH cte1 AS (...), cte2 AS (SELECT * FROM cte1 ...)
预防:先定义基础CTE,再定义依赖它的CTE。

3. ERROR: subquery in FROM cannot refer to other relations of same query level

原因:FROM子句中的子查询引用了同一层级的表(如SELECT * FROM (SELECT * FROM t1 WHERE id=t2.id) AS sub, t2)。
解决:使用LATERAL JOIN,如SELECT * FROM t2 CROSS JOIN LATERAL (SELECT * FROM t1 WHERE id=t2.id) AS sub
预防:FROM子句中的子查询如需引用外部表,使用LATERAL JOIN

参考链接

  1. PostgreSQL官方文档:www.postgresql.org/docs/17/que…
往期文章归档
  • PostgreSQL选Join策略有啥小九九?Nested Loop/Merge/Hash谁是它的菜? - cmdragon's Blog
  • PostgreSQL新手SQL总翻车?这7个性能陷阱你踩过没? - cmdragon's Blog
  • PostgreSQL索引选B-Tree还是GiST?“瑞士军刀”和“多面手”的差别你居然还不知道? - cmdragon's Blog
  • 想知道数据库怎么给查询“算成本选路线”?EXPLAIN能帮你看明白? - cmdragon's Blog
  • PostgreSQL处理SQL居然像做蛋糕?解析到执行的4步里藏着多少查询优化的小心机? - cmdragon's Blog
  • PostgreSQL备份不是复制文件?物理vs逻辑咋选?误删还能精准恢复到1分钟前? - cmdragon's Blog
  • 转账不翻车、并发不干扰,PostgreSQL的ACID特性到底有啥魔法? - cmdragon's Blog
  • 银行转账不白扣钱、电商下单不超卖,PostgreSQL事务的诀窍是啥? - cmdragon's Blog
  • PostgreSQL里的PL/pgSQL到底是啥?能让SQL从“说目标”变“讲步骤”? - cmdragon's Blog
  • PostgreSQL视图不存数据?那它怎么简化查询还能递归生成序列和控制权限? - cmdragon's Blog
  • PostgreSQL索引这么玩,才能让你的查询真的“飞”起来? - cmdragon's Blog
  • PostgreSQL的表关系和约束,咋帮你搞定用户订单不混乱、学生选课不重复? - cmdragon's Blog
  • PostgreSQL查询的筛子、排序、聚合、分组?你会用它们搞定数据吗? - cmdragon's Blog
  • PostgreSQL数据类型怎么选才高效不踩坑? - cmdragon's Blog
  • 想解锁PostgreSQL查询从基础到进阶的核心知识点?你都get了吗? - cmdragon's Blog
  • PostgreSQL DELETE居然有这些操作?返回数据、连表删你试过没? - cmdragon's Blog
  • PostgreSQL UPDATE语句怎么玩?从改邮箱到批量更新的避坑技巧你都会吗? - cmdragon's Blog
  • PostgreSQL插入数据还在逐条敲?批量、冲突处理、返回自增ID的技巧你会吗? - cmdragon's Blog
  • PostgreSQL的“仓库-房间-货架”游戏,你能建出电商数据库和表吗? - cmdragon's Blog
  • PostgreSQL 17安装总翻车?Windows/macOS/Linux避坑指南帮你搞定? - cmdragon's Blog
  • 能当关系型数据库还能玩对象特性,能拆复杂查询还能自动管库存,PostgreSQL凭什么这么香? - cmdragon's Blog
  • 给接口加新字段又不搞崩老客户端?FastAPI的多版本API靠哪三招实现? - cmdragon's Blog
  • 流量突增要搞崩FastAPI?熔断测试是怎么防系统雪崩的? - cmdragon's Blog
  • FastAPI秒杀库存总变负数?Redis分布式锁能帮你守住底线吗 - cmdragon's Blog
  • FastAPI的CI流水线怎么自动测端点,还能让Allure报告美到犯规? - cmdragon's Blog
  • 如何用GitHub Actions为FastAPI项目打造自动化测试流水线? - cmdragon's Blog
  • 如何用Git Hook和CI流水线为FastAPI项目保驾护航? - cmdragon's Blog
  • FastAPI如何用契约测试确保API的「菜单」与「菜品」一致?
  • 为什么TDD能让你的FastAPI开发飞起来? - cmdragon's Blog
  • 如何用FastAPI玩转多模块测试与异步任务,让代码不再“闹脾气”? - cmdragon's Blog
  • 如何在FastAPI中玩转“时光倒流”的数据库事务回滚测试?
  • 如何在FastAPI中优雅地模拟多模块集成测试? - cmdragon's Blog
  • 多环境配置切换机制能否让开发与生产无缝衔接? - cmdragon's Blog
  • 如何在 FastAPI 中巧妙覆盖依赖注入并拦截第三方服务调用? - cmdragon's Blog
  • 为什么你的单元测试需要Mock数据库才能飞起来? - cmdragon's Blog
  • 如何在FastAPI中巧妙隔离依赖项,让单元测试不再头疼? - cmdragon's Blog
  • 如何在FastAPI中巧妙隔离依赖项,让单元测试不再头疼? - cmdragon's Blog
  • 测试覆盖率不够高?这些技巧让你的FastAPI测试无懈可击! - cmdragon's Blog
  • 为什么你的FastAPI测试覆盖率总是低得让人想哭? - cmdragon's Blog
  • 如何让FastAPI测试不再成为你的噩梦? - cmdragon's Blog
  • FastAPI测试环境配置的秘诀,你真的掌握了吗? - cmdragon's Blog
  • 全链路追踪如何让FastAPI微服务架构的每个请求都无所遁形? - cmdragon's Blog
  • 如何在API高并发中玩转资源隔离与限流策略? - cmdragon's Blog
  • 任务分片执行模式如何让你的FastAPI性能飙升? - cmdragon's Blog
  • 冷热任务分离:是提升Web性能的终极秘籍还是技术噱头? - cmdragon's Blog
免费好用的热门在线工具
  • Mermaid 在线编辑器 - 应用商店 | By cmdragon
  • 数学求解计算器 - 应用商店 | By cmdragon
  • 智能提词器 - 应用商店 | By cmdragon
  • 魔法简历 - 应用商店 | By cmdragon
  • Image Puzzle Tool - 图片拼图工具 | By cmdragon
  • 字幕下载工具 - 应用商店 | By cmdragon
  • 歌词生成工具 - 应用商店 | By cmdragon
  • 网盘资源聚合搜索 - 应用商店 | By cmdragon
  • ASCII字符画生成器 - 应用商店 | By cmdragon
  • JSON Web Tokens 工具 - 应用商店 | By cmdragon
  • Bcrypt 密码工具 - 应用商店 | By cmdragon
  • GIF 合成器 - 应用商店 | By cmdragon
  • GIF 分解器 - 应用商店 | By cmdragon
  • 文本隐写术 - 应用商店 | By cmdragon
  • CMDragon 在线工具 - 高级AI工具箱与开发者套件 | 免费好用的在线工具
  • 应用商店 - 发现1000+提升效率与开发的AI工具和实用程序 | 免费好用的在线工具
  • CMDragon 更新日志 - 最新更新、功能与改进 | 免费好用的在线工具
  • 支持我们 - 成为赞助者 | 免费好用的在线工具
  • AI文本生成图像 - 应用商店 | 免费好用的在线工具
  • 临时邮箱 - 应用商店 | 免费好用的在线工具
  • 二维码解析器 - 应用商店 | 免费好用的在线工具
  • 文本转思维导图 - 应用商店 | 免费好用的在线工具
  • 正则表达式可视化工具 - 应用商店 | 免费好用的在线工具
  • 文件隐写工具 - 应用商店 | 免费好用的在线工具
  • IPTV 频道探索器 - 应用商店 | 免费好用的在线工具
  • 快传 - 应用商店 | 免费好用的在线工具
  • 随机抽奖工具 - 应用商店 | 免费好用的在线工具
  • 动漫场景查找器 - 应用商店 | 免费好用的在线工具
  • 时间工具箱 - 应用商店 | 免费好用的在线工具
  • 网速测试 - 应用商店 | 免费好用的在线工具
  • AI 智能抠图工具 - 应用商店 | 免费好用的在线工具
  • 背景替换工具 - 应用商店 | 免费好用的在线工具
  • 艺术二维码生成器 - 应用商店 | 免费好用的在线工具
  • Open Graph 元标签生成器 - 应用商店 | 免费好用的在线工具
  • 图像对比工具 - 应用商店 | 免费好用的在线工具
  • 图片压缩专业版 - 应用商店 | 免费好用的在线工具
  • 密码生成器 - 应用商店 | 免费好用的在线工具
  • SVG优化器 - 应用商店 | 免费好用的在线工具
  • 调色板生成器 - 应用商店 | 免费好用的在线工具
  • 在线节拍器 - 应用商店 | 免费好用的在线工具
  • IP归属地查询 - 应用商店 | 免费好用的在线工具
  • CSS网格布局生成器 - 应用商店 | 免费好用的在线工具
  • 邮箱验证工具 - 应用商店 | 免费好用的在线工具
  • 书法练习字帖 - 应用商店 | 免费好用的在线工具
  • 金融计算器套件 - 应用商店 | 免费好用的在线工具
  • 中国亲戚关系计算器 - 应用商店 | 免费好用的在线工具
  • Protocol Buffer 工具箱 - 应用商店 | 免费好用的在线工具
  • IP归属地查询 - 应用商店 | 免费好用的在线工具
  • 图片无损放大 - 应用商店 | 免费好用的在线工具
  • 文本比较工具 - 应用商店 | 免费好用的在线工具
  • IP批量查询工具 - 应用商店 | 免费好用的在线工具
  • 域名查询工具 - 应用商店 | 免费好用的在线工具
  • DNS工具箱 - 应用商店 | 免费好用的在线工具
  • 网站图标生成器 - 应用商店 | 免费好用的在线工具
  • XML Sitemap
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]