pgsql 数据结构设计样例-文件夹方案设计
文件夹方案设计
概述
应用与文件夹属于树形结构关系
-
文件夹可嵌套子文件夹
-
文件夹下可包含
应用或子文件夹 -
应用可以不归属于任意文件夹
方案对比
路径枚举 (Path Enumeration / LTREE)
在文件夹表中增加一个 path 字段,存储从根节点到当前节点的完整路径(如 1.2.5)。PostgreSQL 官方提供了一个强大的扩展插件 ltree。
表结构: path ltree 索引字段。
查询是否可删: 检查是否存在 path <@ '1.2.4'(表示路径以 1.2.4 开头的文件夹)关联的应用。
优点: 查询极其简单,无需递归,性能极高。
缺点: 移动文件夹(修改父级)时,需要递归更新所有子节点的 path。
闭包表 (Closure Table)
创建一张独立的关联表 folder_relations,记录树中所有节点之间的祖先-后代关系(不仅仅是父子,而是跨级记录)。
表结构: ancestor_id, descendant_id, depth。
查询是否可删: 只需要一条简单的 Join:SELECT 1 FROM apps a JOIN folder_relations r ON a.folder_id = r.descendant_id WHERE r.ancestor_id = 4 LIMIT 1。
优点: 彻底消除递归,在处理极深、极广的树时性能最稳。
缺点: 存储开销大($N$ 个节点可能产生 $N^2$ 条关系记录),维护逻辑复杂。
嵌套集合 (Nested Sets)
通过两个数字 lft (左值) 和 rgt (右值) 标记节点的范围。子节点的范围一定在父节点的范围内。
表结构: lft INTEGER, rgt INTEGER。
查询是否可删: 查找 folder_id 对应的应用,其文件夹的 lft 必须在目标文件夹的 [lft, rgt] 之间。
优点: 极其擅长查询整个子树。
缺点: 插入或移动节点会导致全表大量数据的 lft/rgt 重新计算,写性能很差。
方案横评
| 维度 | 递归 CTE (邻接表) | 路径枚举 (LTREE) | 闭包表 (Closure) | 嵌套集合 (Nested Sets) |
|---|---|---|---|---|
| 查询子树性能 | 中(依赖树深度) | 高 | 极高 | 极高 |
| 判定可删(短路) | 较好 | 极好 | 极好 | 极好 |
| 增删改性能 | 极高(只改父ID) | 中(需更新子路径) | 低(需维护关联表) | 极低(需重算全表) |
| 存储开销 | 极小 | 中 | 高 | 小 |
| 实现复杂度 | 简单(纯 SQL) | 中(需插件支持) | 复杂(需触发器/业务逻辑) | 极复杂 |
| 应用场景 | 通用型/频繁重命名 | 目录树/文件系统 | 深度社交关系/超大规模树 | 极少变动的分类目录 |
结论
拟定采用 PostgreSQL 的递归 CTE(WITH RECURSIVE)实现树形结构的查询
文件夹查询
-
需要返回文件夹是否可删除标志字段给前端,如:can_delete
-
查询文件夹下所有子孙节点(含应用 / 子文件夹)
-
查询
应用所属的完整路径
查询限制
-
应用和文件夹是单独的界面(独立的接口,各自翻页)或者混合展示(各自查询 page_size 条,根据更新时间倒排组合应用和文件夹数据,记录应用和文件夹的更新时间,翻页时根据本次查询得到应用和文件夹的更新时间或者 None 进行查询,寻找早于此次应用和文件夹的更新时间的数据) -
返回的数据按照更新时间倒序
-
条数限制:暂定 100
文件夹更新
仅在新增或者删除应用/文件夹时同步更新当前文件夹的更新时间
本期设计不联动更新上级文件夹更新时间
文件夹删除
仅允许删除不含应用的空文件夹,允许含有空文件夹嵌套
应用文件夹方案设计
简易 demo
数据构造
新建数据库 test ,然后建表进行验证
外键的使用需要权衡,考虑高并发场景不建议使用外键
PostgreSQL 外键的默认行为是 RESTRICT,具体表现为:
-
ON DELETE RESTRICT:如果尝试删除被引用的文件夹(folders 表中的记录),但该文件夹仍被子文件夹(parent_id 关联)或应用(apps 表的 folder_id 关联)引用,数据库会直接拒绝删除操作,并抛出外键约束冲突错误。 -
ON UPDATE RESTRICT:如果尝试修改被引用的 folders.id(主键),但该 ID 仍被其他记录引用,同样会被拒绝。
```Plain Text – 1. 文件夹表:自关联结构 CREATE TABLE folders ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, parent_id INTEGER REFERENCES folders(id), created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP );
– 2. 应用表:关联文件夹 CREATE TABLE apps ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, folder_id INTEGER REFERENCES folders(id), created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP );
– 为常用查询路径建立索引 CREATE INDEX idx_folders_parent ON folders(parent_id); CREATE INDEX idx_apps_folder ON apps(folder_id);
```Plain Text
-- 插入文件夹(5级嵌套)
INSERT INTO folders (id, name, parent_id) VALUES
(1, '研发部', NULL), -- Level 1
(2, '中间件组', 1), -- Level 2
(3, '数据库组', 2), -- Level 3
(4, 'PostgreSQL小组', 3), -- Level 4
(5, '插件开发支队', 4), -- Level 5 (包含应用:不可删)
(6, '备份脚本支队', 4), -- Level 5 (空文件夹:可删)
(7, '废弃归档区', NULL), -- Level 1 (空文件夹:可删)
(8, '嵌套空目录A', 7), -- Level 2
(9, '嵌套空目录B', 8); -- Level 3
-- 插入应用
INSERT INTO apps (name, folder_id) VALUES
('看板应用', NULL), -- 不属于任何文件夹
('监控系统', 1), -- 研发部下的应用
('Pg_Stat_Tool', 5), -- 5层深处的应用
('Test_Runner', 5), -- 5层深处的应用(用于测试同级重名约束)
('video', 4),
('music', 4);
查询语句
查询当前文件夹下包含的应用和文件夹
查询树形应用和文件夹
递归查询指定文件夹下的所有子文件夹
并关联查出这些文件夹下的所有应用
```Plain Text WITH RECURSIVE sub_tree AS ( – 初始节点:指定查询的文件夹 SELECT id, name, parent_id, 1 as level FROM folders WHERE id = 1 – 假设查询【研发部】
UNION ALL
-- 递归下钻
SELECT f.id, f.name, f.parent_id, st.level + 1
FROM folders f
INNER JOIN sub_tree st ON f.parent_id = st.id ) -- 汇总结果:包含子文件夹信息和它们关联的应用 SELECT 'folder' as type, id, name, parent_id, level FROM sub_tree UNION ALL SELECT 'app' as type, a.id, a.name, a.folder_id, st.level FROM apps a JOIN sub_tree st ON a.folder_id = st.id; ```
判断文件夹是否可删
查询指定文件夹 id 是否存在应用,返回文件夹是否可删除标志
知识库同理
短路判定:文件夹是否可删除
只要递归搜索到该文件夹或其任意深度的子文件夹中存在 1条应用记录
立即停止递归返回
Plain Text
SELECT NOT EXISTS (
-- 外层 EXISTS 保证了只要内层返回一行,整个查询立即停止
SELECT 1
FROM (
WITH RECURSIVE folder_tree AS (
-- 只查询 ID,内存占用极小
SELECT id FROM folders WHERE id = 4
UNION ALL
SELECT f.id FROM folders f
JOIN folder_tree ft ON f.parent_id = ft.id
)
SELECT 1 FROM folder_tree ft
-- 核心:将应用检查放在这里
-- 数据库会针对每一个查到的 ft.id 去 apps 表索引里探测
WHERE EXISTS (SELECT 1 FROM apps a WHERE a.folder_id = ft.id)
-- 一旦找到一个有应用的文件夹,立即返回 1 给外层 EXISTS
LIMIT 1
) search_result
) AS is_deletable;
参考文档
- https://www.postgresql.org/docs/15/queries-with.html