18人参与 • 2026-01-26 • MsSqlserver
在实际开发中,我们经常会遇到树形结构或图结构的数据需求,比如:
这些场景的核心问题是:如何高效查询具有层级/递归关系的数据?
postgresql 提供了强大的 with recursive(公共表表达式递归) 功能,是处理此类问题的标准 sql 解决方案。本文将从基础到实战,手把手教你掌握递归查询的精髓。
cte 是一种临时结果集,可被主查询引用,语法如下:
with cte_name as (
-- 查询语句
)
select * from cte_name;
优点:提升 sql 可读性、避免重复子查询、支持递归
with recursive cte_name as (
-- 1. 初始查询(锚点成员 anchor member)
select ... from table where ...
union [all]
-- 2. 递归查询(递归成员 recursive member)
select ... from table, cte_name where ...
)
select * from cte_name;
核心三要素:
| 部分 | 作用 | 注意事项 |
|---|---|---|
| 初始查询 | 定义递归起点(如根节点) | 必须能终止递归 |
| union [all] | 合并结果集 | union 去重,union all 保留重复(性能更高) |
| 递归查询 | 引用自身 cte,向下/向上遍历 | 必须有连接条件,避免无限循环 |
| 场景 | 推荐方案 |
|---|---|
| 标准树形查询(上下级) | with recursive + union all |
| 防循环 | 记录访问路径 array[id] + != all(path) |
| 限制深度 | 添加 depth 字段 + where depth < n |
| 高性能读 | 物化路径 / 闭包表(写少读多) |
| 返回树形 json | 自底向上聚合 + jsonb_build_object |
| python 集成 | 直接执行原生 sql(sqlalchemy 支持 cte) |
终极建议:
“90% 的树形查询,一个精心设计的 with recursive 就够了。”
只有在性能成为瓶颈时,才考虑物化路径等复杂模型。
假设有一张部门表 departments:
create table departments (
id serial primary key,
name varchar(100) not null,
parent_id integer references departments(id)
);
-- 插入示例数据
insert into departments (name, parent_id) values
('总公司', null),
('技术部', 1),
('产品部', 1),
('前端组', 2),
('后端组', 2),
('ios组', 2),
('设计组', 3);
with recursive dept_tree as (
-- 锚点:找到“技术部”
select id, name, parent_id, 0 as level
from departments
where 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
lpad('', level * 4, ' ') || name as hierarchy, -- 缩进显示层级
id, parent_id, level
from dept_tree
order by level;
输出结果:
hierarchy | id | parent_id | level
-----------------|----|-----------|------
技术部 | 2 | 1 | 0
前端组 | 4 | 2 | 1
后端组 | 5 | 2 | 1
ios组 | 6 | 2 | 1
技巧:lpad('', level * 4, ' ') 生成缩进,直观展示树形结构
with recursive dept_path as (
-- 锚点:从“后端组”开始
select id, name, parent_id, 0 as level
from departments
where name = '后端组'
union all
-- 递归:找父部门
select d.id, d.name, d.parent_id, dp.level + 1
from departments d
inner join dept_path dp on d.id = dp.parent_id
where dp.parent_id is not null -- 避免 null 连接
)
select
repeat(' → ', level) || name as path_from_root
from dept_path
order by level desc; -- 从根到当前节点
输出结果:
path_from_root --------------------------- 总公司 → 技术部 → 后端组
with recursive dept_limited as (
select id, name, parent_id, 1 as depth
from departments
where parent_id is null -- 从根开始
union all
select d.id, d.name, d.parent_id, dl.depth + 1
from departments d
inner join dept_limited dl on d.parent_id = dl.id
where dl.depth < 3 -- 最多查3层
)
select * from dept_limited;
如果数据存在循环(如 a→b→c→a),递归会无限进行。解决方案:记录访问路径。
with recursive graph_traversal as (
-- 锚点
select
id,
name,
parent_id,
array[id] as path, -- 记录已访问节点
1 as depth
from departments
where name = '技术部'
union all
-- 递归
select
d.id,
d.name,
d.parent_id,
gt.path || d.id, -- 追加当前节点
gt.depth + 1
from departments d
inner join graph_traversal gt on d.parent_id = gt.id
where
d.id != all(gt.path) -- 关键:当前节点不在已访问路径中
and gt.depth < 10 -- 安全兜底
)
select * from graph_traversal;
d.id != all(gt.path) 确保不重复访问节点,彻底解决循环问题
postgresql 支持将递归结果直接转为 嵌套 json,适合 api 返回。如使用 jsonb_build_object 构建树
with recursive tree as (
-- 叶子节点(无子节点)
select
id,
name,
parent_id,
jsonb_build_object('id', id, 'name', name, 'children', '[]'::jsonb) as node
from categories c1
where not exists (
select 1 from categories c2 where c2.parent_id = c1.id
)
union all
-- 非叶子节点(聚合子节点)
select
p.id,
p.name,
p.parent_id,
jsonb_build_object(
'id', p.id,
'name', p.name,
'children', jsonb_agg(t.node)
) as node
from categories p
inner join tree t on t.parent_id = p.id
group by p.id, p.name, p.parent_id
)
select node
from tree
where parent_id is null; -- 返回根节点
输出 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": []}
]
}
]
}
此方法利用 自底向上聚合,天然避免循环,但要求数据为严格树形(无环)
create table categories (
id serial primary key,
name varchar(100) not null,
parent_id integer references categories(id),
is_leaf boolean default false -- 是否叶子节点
);
-- 插入数据
insert into categories (name, parent_id, is_leaf) values
('电子产品', null, false),
('手机', 1, false),
('iphone', 2, true),
('华为', 2, true),
('电脑', 1, false),
('笔记本', 5, true);
with recursive category_tree as (
-- 锚点:根类目
select
id,
name,
parent_id,
name::text as full_path, -- 路径字符串
1 as level
from categories
where name = '电子产品'
union all
-- 递归:拼接路径
select
c.id,
c.name,
c.parent_id,
ct.full_path || ' > ' || c.name, -- 路径拼接
ct.level + 1
from categories c
inner join category_tree ct on c.parent_id = ct.id
)
select
full_path,
id,
level
from category_tree
where is_leaf = true; -- 只查叶子节点
输出:
full_path | id | level ----------------------------|----|------ 电子产品 > 手机 > iphone | 3 | 3 电子产品 > 手机 > 华为 | 4 | 3 电子产品 > 电脑 > 笔记本 | 6 | 3
在 python 中使用递归查询:
from sqlalchemy import text
from sqlalchemy.orm import sessionmaker
def get_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})
return result.fetchall()
# 使用
with session() as session:
tree = get_dept_tree(session, "技术部")
for row in tree:
print(f"{' ' * row.level}{row.name}")
-- 对 parent_id 建索引(递归连接的关键) create index idx_departments_parent_id on departments(parent_id); -- 如果常按 name 查询根节点 create index idx_departments_name on departments(name);
explain (analyze, buffers) with recursive ... ; -- 你的递归查询
关键观察点:
index scan(而非 seq scan)buffers)| 问题 | 解决方案 |
|---|---|
| 递归太深(>100层) | 限制 depth < n,业务上通常不需要过深层级 |
| 数据量大(百万级) | 分页查询(先查id再关联)、物化路径(见下文) |
| 频繁查询 | 使用 物化路径(materialized path) 或 闭包表(closure table) |
虽然 with recursive 很强大,但在某些场景下,其他模型更高效:
在每条记录中存储完整路径:
alter table categories add column path text; -- 如 "/1/2/3/" -- 查询“手机”下所有子类目 select * from categories where path like '/1/2/%';
✅ 优点:查询极快(走索引)
❌ 缺点:移动节点时需更新大量 path
额外建一张表存储所有祖先-后代关系:
create table category_closure (
ancestor_id int,
descendant_id int,
depth int
);
-- 查询“手机”(id=2)的所有后代
select c.*
from categories c
join category_closure cl on c.id = cl.descendant_id
where cl.ancestor_id = 2;
✅ 优点:查询快,支持任意深度
❌ 缺点:写操作复杂,存储空间大
选择建议:
- 读多写少 + 深度固定 → 物化路径
- 频繁查询全路径 → 闭包表
- 通用场景 + 中小数据量 → with recursive
-- 错误:缺少终止条件 select ... from table, cte where table.parent_id = cte.id -- 如果存在循环引用,永远停不下来!
✅ 解决:始终加上 depth < n 或路径检测
union 会去重,但递归中通常不需要(父子id唯一)✅ 解决:除非明确需要去重,否则用 union all
-- 错误:递归成员不能包含聚合 select ..., count(*) from ... join cte ...
✅ 解决:先递归,再在外层聚合
以上就是postgresql优雅的进行递归查询的实战指南的详细内容,更多关于postgresql进行递归查询的资料请关注代码网其它相关文章!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论