news 2026/4/22 5:14:44

PostgreSQL 实战:一文掌握如何优雅的进行递归查询?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PostgreSQL 实战:一文掌握如何优雅的进行递归查询?

文章目录

    • 一、递归查询基础:CTE 与 `WITH RECURSIVE`
      • 1.1 什么是 CTE(Common Table Expression)?
      • 1.2 递归 CTE 的基本结构
      • 1.3 递归查询的建议
    • 二、经典场景实战:组织架构查询
      • 2.1 查询“技术部”及其所有子部门(向下递归)
      • 2.2 查询“后端组”的完整上级路径(向上递归)
    • 三、高级技巧:控制递归深度与防环
      • 3.1 限制递归深度(防止无限循环)
      • 3.2 检测并避免循环引用(图结构必备)
      • 3.3 反向应用:扁平数据转树形 JSON
    • 四、实战案例:商品分类树
      • 4.1 场景:电商商品分类(多级类目)
      • 4.2 查询“电子产品”下所有叶子类目(带完整路径)
      • 4.3 Python + SQLAlchemy 实战
    • 五、性能优化:索引与执行计划
      • 5.1 必建索引
      • 5.2 查看执行计划
      • 5.3 大数据量优化建议
    • 六、替代方案对比:何时不用递归?
      • 6.1 物化路径(Materialized Path)
      • 6.2 闭包表(Closure Table)
    • 七、常见陷阱与避坑指南
      • 陷阱 1:忘记 `WHERE` 条件导致无限循环
      • 陷阱 2:使用 `UNION` 而非 `UNION ALL`
      • 陷阱 3:在递归部分使用聚合函数

在实际开发中,我们经常会遇到树形结构图结构的数据需求,比如:

  • 组织架构(部门 → 子部门)
  • 商品分类(一级类目 → 二级类目 → …)
  • 评论回复(评论 → 回复 → 回复的回复)
  • 权限继承(角色 → 子角色)
  • 路径查找(最短路径、依赖关系)

这些场景的核心问题是:如何高效查询具有层级/递归关系的数据?

PostgreSQL 提供了强大的WITH RECURSIVE(公共表表达式递归)功能,是处理此类问题的标准 SQL 解决方案。本文将从基础到实战,手把手教你掌握递归查询的精髓。


一、递归查询基础:CTE 与WITH RECURSIVE

1.1 什么是 CTE(Common Table Expression)?

CTE 是一种临时结果集,可被主查询引用,语法如下:

WITHcte_nameAS(-- 查询语句)SELECT*FROMcte_name;

优点:提升 SQL 可读性、避免重复子查询、支持递归

1.2 递归 CTE 的基本结构

WITHRECURSIVE cte_nameAS(-- 1. 初始查询(锚点成员 Anchor Member)SELECT...FROMtableWHERE...UNION[ALL]-- 2. 递归查询(递归成员 Recursive Member)SELECT...FROMtable,cte_nameWHERE...)SELECT*FROMcte_name;

核心三要素:

部分作用注意事项
初始查询定义递归起点(如根节点)必须能终止递归
UNION [ALL]合并结果集UNION去重,UNION ALL保留重复(性能更高)
递归查询引用自身 CTE,向下/向上遍历必须有连接条件,避免无限循环

1.3 递归查询的建议

场景推荐方案
标准树形查询(上下级)WITH RECURSIVE+UNION ALL
防循环记录访问路径ARRAY[id]+!= ALL(path)
限制深度添加depth字段 +WHERE depth < N
高性能读物化路径 / 闭包表(写少读多)
返回树形 JSON自底向上聚合 +jsonb_build_object
Python 集成直接执行原生 SQL(SQLAlchemy 支持 CTE)

💡终极建议
“90% 的树形查询,一个精心设计的WITH RECURSIVE就够了。”
只有在性能成为瓶颈时,才考虑物化路径等复杂模型。


二、经典场景实战:组织架构查询

假设有一张部门表departments

CREATETABLEdepartments(idSERIALPRIMARYKEY,nameVARCHAR(100)NOTNULL,parent_idINTEGERREFERENCESdepartments(id));-- 插入示例数据INSERTINTOdepartments(name,parent_id)VALUES('总公司',NULL),('技术部',1),('产品部',1),('前端组',2),('后端组',2),('iOS组',2),('设计组',3);

2.1 查询“技术部”及其所有子部门(向下递归)

WITHRECURSIVE dept_treeAS(-- 锚点:找到“技术部”SELECTid,name,parent_id,0ASlevelFROMdepartmentsWHEREname='技术部'UNIONALL-- 递归:找子部门SELECTd.id,d.name,d.parent_id,dt.level+1FROMdepartments dINNERJOINdept_tree dtONd.parent_id=dt.id)SELECTLPAD('',level*4,' ')||nameAShierarchy,-- 缩进显示层级id,parent_id,levelFROMdept_treeORDERBYlevel;

输出结果:

hierarchy | id | parent_id | level -----------------|----|-----------|------ 技术部 | 2 | 1 | 0 前端组 | 4 | 2 | 1 后端组 | 5 | 2 | 1 iOS组 | 6 | 2 | 1

技巧:LPAD('', level * 4, ' ')生成缩进,直观展示树形结构


2.2 查询“后端组”的完整上级路径(向上递归)

WITHRECURSIVE dept_pathAS(-- 锚点:从“后端组”开始SELECTid,name,parent_id,0ASlevelFROMdepartmentsWHEREname='后端组'UNIONALL-- 递归:找父部门SELECTd.id,d.name,d.parent_id,dp.level+1FROMdepartments dINNERJOINdept_path dpONd.id=dp.parent_idWHEREdp.parent_idISNOTNULL-- 避免 NULL 连接)SELECTREPEAT(' → ',level)||nameASpath_from_rootFROMdept_pathORDERBYlevelDESC;-- 从根到当前节点

输出结果:

path_from_root --------------------------- 总公司 → 技术部 → 后端组

三、高级技巧:控制递归深度与防环

3.1 限制递归深度(防止无限循环)

WITHRECURSIVE dept_limitedAS(SELECTid,name,parent_id,1ASdepthFROMdepartmentsWHEREparent_idISNULL-- 从根开始UNIONALLSELECTd.id,d.name,d.parent_id,dl.depth+1FROMdepartments dINNERJOINdept_limited dlONd.parent_id=dl.idWHEREdl.depth<3-- 最多查3层)SELECT*FROMdept_limited;

3.2 检测并避免循环引用(图结构必备)

如果数据存在循环(如 A→B→C→A),递归会无限进行。解决方案:记录访问路径

WITHRECURSIVE graph_traversalAS(-- 锚点SELECTid,name,parent_id,ARRAY[id]ASpath,-- 记录已访问节点1ASdepthFROMdepartmentsWHEREname='技术部'UNIONALL-- 递归SELECTd.id,d.name,d.parent_id,gt.path||d.id,-- 追加当前节点gt.depth+1FROMdepartments dINNERJOINgraph_traversal gtONd.parent_id=gt.idWHEREd.id!=ALL(gt.path)-- 关键:当前节点不在已访问路径中ANDgt.depth<10-- 安全兜底)SELECT*FROMgraph_traversal;

d.id != ALL(gt.path)确保不重复访问节点,彻底解决循环问题

3.3 反向应用:扁平数据转树形 JSON

PostgreSQL 支持将递归结果直接转为嵌套 JSON,适合 API 返回。如使用jsonb_build_object构建树

WITHRECURSIVE treeAS(-- 叶子节点(无子节点)SELECTid,name,parent_id,jsonb_build_object('id',id,'name',name,'children','[]'::jsonb)ASnodeFROMcategories c1WHERENOTEXISTS(SELECT1FROMcategories c2WHEREc2.parent_id=c1.id)UNIONALL-- 非叶子节点(聚合子节点)SELECTp.id,p.name,p.parent_id,jsonb_build_object('id',p.id,'name',p.name,'children',jsonb_agg(t.node))ASnodeFROMcategories pINNERJOINtree tONt.parent_id=p.idGROUPBYp.id,p.name,p.parent_id)SELECTnodeFROMtreeWHEREparent_idISNULL;-- 返回根节点

输出 JSON:

{"id":1,"name":"电子产品","children":[{"id":2,"name":"手机","children":[{"id":3,"name":"iPhone","children":[]},{"id":4,"name":"华为","children":[]}]},{"id":5,"name":"电脑","children":[{"id":6,"name":"笔记本","children":[]}]}]}

💡 此方法利用自底向上聚合,天然避免循环,但要求数据为严格树形(无环)


四、实战案例:商品分类树

4.1 场景:电商商品分类(多级类目)

CREATETABLEcategories(idSERIALPRIMARYKEY,nameVARCHAR(100)NOTNULL,parent_idINTEGERREFERENCEScategories(id),is_leafBOOLEANDEFAULTfalse-- 是否叶子节点);-- 插入数据INSERTINTOcategories(name,parent_id,is_leaf)VALUES('电子产品',NULL,false),('手机',1,false),('iPhone',2,true),('华为',2,true),('电脑',1,false),('笔记本',5,true);

4.2 查询“电子产品”下所有叶子类目(带完整路径)

WITHRECURSIVE category_treeAS(-- 锚点:根类目SELECTid,name,parent_id,name::TEXTASfull_path,-- 路径字符串1ASlevelFROMcategoriesWHEREname='电子产品'UNIONALL-- 递归:拼接路径SELECTc.id,c.name,c.parent_id,ct.full_path||' > '||c.name,-- 路径拼接ct.level+1FROMcategories cINNERJOINcategory_tree ctONc.parent_id=ct.id)SELECTfull_path,id,levelFROMcategory_treeWHEREis_leaf=true;-- 只查叶子节点

输出:

full_path | id | level ----------------------------|----|------ 电子产品 > 手机 > iPhone | 3 | 3 电子产品 > 手机 > 华为 | 4 | 3 电子产品 > 电脑 > 笔记本 | 6 | 3

4.3 Python + SQLAlchemy 实战

在 Python 中使用递归查询:

fromsqlalchemyimporttextfromsqlalchemy.ormimportsessionmakerdefget_dept_tree(session,root_name):query=text(""" WITH RECURSIVE dept_tree AS ( SELECT id, name, parent_id, 0 AS level FROM departments WHERE name = :root_name UNION ALL SELECT d.id, d.name, d.parent_id, dt.level + 1 FROM departments d INNER JOIN dept_tree dt ON d.parent_id = dt.id ) SELECT * FROM dept_tree ORDER BY level; """)result=session.execute(query,{"root_name":root_name})returnresult.fetchall()# 使用withSession()assession:tree=get_dept_tree(session,"技术部")forrowintree:print(f"{' '*row.level}{row.name}")

五、性能优化:索引与执行计划

5.1 必建索引

-- 对 parent_id 建索引(递归连接的关键)CREATEINDEXidx_departments_parent_idONdepartments(parent_id);-- 如果常按 name 查询根节点CREATEINDEXidx_departments_nameONdepartments(name);

5.2 查看执行计划

EXPLAIN(ANALYZE,BUFFERS)WITHRECURSIVE...;-- 你的递归查询

关键观察点:

  • 是否使用了Index Scan(而非Seq Scan
  • 递归深度是否合理
  • 内存使用(Buffers

5.3 大数据量优化建议

问题解决方案
递归太深(>100层)限制depth < N,业务上通常不需要过深层级
数据量大(百万级)分页查询(先查ID再关联)、物化路径(见下文)
频繁查询使用物化路径(Materialized Path)闭包表(Closure Table)

六、替代方案对比:何时不用递归?

虽然WITH RECURSIVE很强大,但在某些场景下,其他模型更高效

6.1 物化路径(Materialized Path)

在每条记录中存储完整路径:

ALTERTABLEcategoriesADDCOLUMNpathTEXT;-- 如 "/1/2/3/"-- 查询“手机”下所有子类目SELECT*FROMcategoriesWHEREpathLIKE'/1/2/%';

✅ 优点:查询极快(走索引)
❌ 缺点:移动节点时需更新大量 path

6.2 闭包表(Closure Table)

额外建一张表存储所有祖先-后代关系:

CREATETABLEcategory_closure(ancestor_idINT,descendant_idINT,depthINT);-- 查询“手机”(id=2)的所有后代SELECTc.*FROMcategories cJOINcategory_closure clONc.id=cl.descendant_idWHEREcl.ancestor_id=2;

✅ 优点:查询快,支持任意深度
❌ 缺点:写操作复杂,存储空间大

📌选择建议

  • 读多写少 + 深度固定→ 物化路径
  • 频繁查询全路径→ 闭包表
  • 通用场景 + 中小数据量WITH RECURSIVE

七、常见陷阱与避坑指南

陷阱 1:忘记WHERE条件导致无限循环

-- 错误:缺少终止条件SELECT...FROMtable,cteWHEREtable.parent_id=cte.id-- 如果存在循环引用,永远停不下来!

✅ 解决:始终加上depth < N或路径检测

陷阱 2:使用UNION而非UNION ALL

  • UNION会去重,但递归中通常不需要(父子ID唯一)
  • 性能损失高达 30%+

✅ 解决:除非明确需要去重,否则用UNION ALL

陷阱 3:在递归部分使用聚合函数

-- 错误:递归成员不能包含聚合SELECT...,COUNT(*)FROM...JOINcte...

✅ 解决:先递归,再在外层聚合

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/17 17:41:01

爬虫部署:从零到一讲述 Supervisor 的详细使用

更多内容请见: 《爬虫和逆向教程》 - 专栏介绍和目录 文章目录 一、为什么需要 Supervisor? 1.1 爬虫运行的典型痛点 1.2 Supervisor 的核心价值 1.3 替代方案对比 1.4 Supervisor 部署建议 二、安装与基础配置 2.1 安装 Supervisor 2.2 主配置文件结构 2.3 启动 Supervisor …

作者头像 李华
网站建设 2026/4/15 9:46:44

2026年降AI工具TOP5:从90%降到10%实测有效

2026年降AI工具TOP5&#xff1a;从90%降到10%实测有效 TL;DR&#xff08;太长不看&#xff09;&#xff1a;实测20多款降AI工具后&#xff0c;筛选出TOP5真正能把AI率从90%降到10%以下的。第一名嘎嘎降AI达标率99.26%&#xff0c;价格仅4.8元&#xff1b;第二名比话降AI不达标全…

作者头像 李华
网站建设 2026/4/11 2:27:13

2026毕业生降AI攻略:从初稿到定稿全流程

2026毕业生降AI攻略&#xff1a;从初稿到定稿全流程 TL;DR&#xff08;太长不看&#xff09;&#xff1a;2026年毕业论文降AI分4个阶段&#xff1a;初稿完成后先检测AI率→用嘎嘎降AI或比话降AI处理→人工校对专业术语→定稿前再测确认达标。建议答辩前一个月开始准备&#xff…

作者头像 李华
网站建设 2026/4/15 15:18:26

QHeaderView表头和QTablView+QSS的应用

一、QHeaderView在看QTableView之前&#xff0c;先看看QHeaderView由于表头分水平和垂直方向&#xff0c;只是方向上的不同而已&#xff0c;文中的示例都将使用水平方向的表头。函数功能1.hideSection隐藏一列内容2.showSection显示一列内容&#xff0c;和上面的正好凑一对。 看…

作者头像 李华
网站建设 2026/4/20 18:36:23

手把手教学:如何用科哥镜像搭建个人抠图小工具

手把手教学&#xff1a;如何用科哥镜像搭建个人抠图小工具 1. 为什么你需要一个自己的抠图工具 你有没有遇到过这些情况&#xff1f; 想给朋友圈头像换背景&#xff0c;但 Photoshop 太重、美图秀秀又抠不干净发丝&#xff1b;做电商上架商品&#xff0c;每天要处理20张产品…

作者头像 李华
网站建设 2026/4/18 8:58:51

麦橘超然科研伦理审查:生成内容合规性部署建议

麦橘超然科研伦理审查&#xff1a;生成内容合规性部署建议 1. 为什么图像生成需要“科研伦理审查”这道关&#xff1f; 很多人第一次听说“麦橘超然”时&#xff0c;第一反应是&#xff1a;又一个画得挺好的 Flux 模型&#xff1f;点开界面输入“一只穿西装的柴犬”&#xff…

作者头像 李华