SQLserver, Oracle 限制层数 递归查询和反向查询父记录

本文介绍了在SQL Server和Oracle中如何使用递归查询获取节点的所有子节点或父节点,并通过实例展示了递归删除操作及一个复杂的销售表返利计算案例。

以前使用Oracle,觉得它的递归查询很好用,就研究了一下SqlServer,发现它也支持在Sql里递归查询
举例说明:
SqlServer2008版本的Sql如下:
比如一个表,有id和pId字段,id是主键,pid表示它的上级节点,表结构和数据:
CREATE TABLE [aaa](
[id] [int] NULL,
[pid] [int] NULL,
[name] [nchar](10)
)
GO
INSERT INTO aaa VALUES(1,0,'a')
INSERT INTO aaa VALUES(2,0,'b')
INSERT INTO aaa VALUES(3,1,'c')
INSERT INTO aaa VALUES(4,1,'d')
INSERT INTO aaa VALUES(5,2,'e')
INSERT INTO aaa VALUES(6,3,'f')
INSERT INTO aaa VALUES(7,3,'g')
INSERT INTO aaa VALUES(8,4,'h')
GO

--下面的Sql是查询出1结点的所有子结点
with my1 as(select * from aaa where id = 1
union all select aaa.* from my1, aaa where my1.id = aaa.pid
)
select * from my1 --结果包含1这条记录,如果不想包含,可以在最后加上:where id <> 1

--下面的Sql是查询出8结点的所有父结点
with my1 as(select * from aaa where id = 8
union all select aaa.* from my1, aaa where my1.pid = aaa.id
)
select * from my1;

--下面是递归删除1结点和所有子结点的语句:
with my1 as(select * from aaa where id = 1
union all select aaa.* from my1, aaa where my1.id = aaa.pid
)
delete from aaa where exists (select id from my1 where my1.id = aaa.id)

Oracle版本的Sql如下:
比如一个表,有id和pId字段,id是主键,pid表示它的上级节点,表结构和数据请参考SqlServer2005的,Sql如下:
--下面的Sql是查询出1结点的所有子结点
SELECT * FROM aaa
START WITH id = 1
CONNECT BY pid = PRIOR id

--下面的Sql是查询出8结点的所有父结点
SELECT * FROM aaa
START WITH id = 8
CONNECT BY PRIOR pid = id

今天帮别人做了一个有点意思的sql,也是用递归实现,具体如下:
假设有个销售表如下:
CREATE TABLE [tb](
[qj] [int] NULL, -- 月份,本测试假设从1月份开始,并且数据都是连续的月份,中间没有隔断
[je] [int] NULL, -- 本月销售实际金额
[rwe] [int] NULL, -- 本月销售任务额
[fld] [float] NULL -- 本月金额大于任务额时的返利点,返利额为je*fld
) ON [PRIMARY]
现在要求计算每个月的返利金额,规则如下:
1月份销售金额大于任务额 返利额=金额*返利点
2月份销售金额大于任务额 返利额=(金额-1月份返利额)*返利点
3月份销售金额大于任务额 返利额=(金额-1,2月份返利额)*返利点
以后月份依次类推,销售额小于任务额时,返利为0
具体的Sql如下:
WITH my1 AS (
SELECT *,
CASE
WHEN je > rwe THEN (je * fld)
ELSE 0
END fle,
CAST(0 AS FLOAT) tmp
FROM tb
WHERE qj = 1
UNION ALL
SELECT tb.*,
CASE
WHEN tb.je > tb.rwe THEN (tb.je - my1.fle -my1.tmp)
* tb.fld
ELSE 0
END fle,
my1.fle + my1.tmp tmp -- 用于累加前面月份的返利
FROM my1,
tb
WHERE tb.qj = my1.qj + 1
)
SELECT *
FROM my1

 

SQLserver2008使用表达式递归查询

--由父项递归下级
with cte(id,parentid,text)
as
(--父项
select id,parentid,text from treeview where parentid = 450
union all
--递归结果集中的下级
select t.id,t.parentid,t.text from treeview as t
inner join cte as c on t.parentid = c.id
)
select id,parentid,text from cte

---------------------------------------------------------------------------------------------------------------------------------------------

--由子级递归父项
with cte(id,parentid,text)
as
(--下级父项
select id,parentid,text from treeview where id = 450
union all
--递归结果集中的父项
select t.id,t.parentid,t.text from treeview as t
inner join cte as c on t.id = c.parentid
)
select id,parentid,text from cte

<think>我们正在讨论树形结构(通过pidid构建)的高效查询问题。根据用户的问题,重点在于多层嵌套树形结构的高效查询方法。结合之前的讨论(使用邻接表模式)引用的分区优化思想,我们可以从数据库设计程序设计两个层面来回答。 核心挑战:邻接表模式在查询多层嵌套结构时通常需要递归查询,而递归查询在数据量大时可能效率较低。因此,我们需要采用优化策略。 参考引用: [1] 提到了分区设计可以提高性能,特别是当数据量增大时,索引可能效率降低,分区可以提升大范围扫描的效率。 [2] 强调了分区设计的原则:减少I/O冲突,均衡负载,分散热点数据,保证关键数据的快速访问。 因此,我们可以结合分区索引,以及一些冗余字段或特殊结构来优化。 以下为具体方案: 一、数据库层面优化 1. **使用递归查询(CTE)**:适用于支持递归的数据库(如PostgreSQL, SQL Server, MySQL 8.0+)。但递归深度大时性能会下降。 ```sql WITH RECURSIVE cte AS ( SELECT * FROM sys_dict WHERE id = ? -- 起始节点 UNION ALL SELECT d.* FROM sys_dict d INNER JOIN cte ON d.pid = cte.id ) SELECT * FROM cte; ``` 2. **路径枚举法**:在表中增加一个`path`字段(如:存储从根节点到当前节点的ID路径,用分隔符连接,例如:/1/3/5/)。这样查询某个节点的所有子节点可以通过`path LIKE '/1/3/5/%'`实现,避免了递归。但更新节点时需要维护路径。 - 示例查询:查找节点5的所有后代 ```sql SELECT * FROM sys_dict WHERE path LIKE '/1/3/5/%'; ``` 3. **闭包表**:创建一张额外的表,存储节点之间的所有祖先-后代关系(包括直接间接关系)。虽然占用空间大,但查询极其高效。 - 闭包表结构: ```sql CREATE TABLE tree_closure ( ancestor BIGINT NOT NULL, descendant BIGINT NOT NULL, depth INT NOT NULL, -- 可选:记录深度 PRIMARY KEY (ancestor, descendant) ); ``` - 查询节点5的所有后代: ```sql SELECT d.* FROM sys_dict d JOIN tree_closure t ON d.id = t.descendant WHERE t.ancestor = 5; ``` 4. **使用分区表**:根据引用[1][2]的思想,我们可以按照树的层(如根节点、第一层子节点等)或按照字典类型进行分区。这样可以将不同层的节点分散到不同分区,提高查询并行性。例如: - 按`level`字段(节点深度)分区:将同一层的节点放在同一个分区,这样在查询某一层节点时只需扫描一个分区。 - 注意:分区键的选择要均衡,避免热点。同时,分区后对于跨分区的递归查询可能没有直接帮助,但结合路径枚举或闭包表,分区可以提升扫描效率。 5. **索引优化**: - 为`pid`字段建立索引(邻接表模式必须)。 - 如果使用路径枚举,为`path`字段建立索引(如前缀索引)。 - 闭包表中主键已经是(ancestor,descendant),查询效率高。 二、程序层面优化 1. **一次加载+内存构建**:如果整个树结构可以放入内存,可以在程序启动时全量加载,在内存中构建树(例如使用Map结构存储节点,再通过节点ID构建树)。后续查询直接在内存中进行,效率极高。 - 更新时需同步内存数据库(可通过消息队列或定时刷新)。 2. **缓存策略**:对频繁查询的子树进行缓存(如Redis缓存子树结构),减少数据库访问。 三、综合方案建议 根据数据量访问频率选择: - 数据量小(万以下):邻接表+递归查询+内存缓存。 - 数据量大(百万)且查询频繁:闭包表或路径枚举法,并配合分区索引。 - 更新频繁:邻接表+路径枚举(更新时维护路径,但维护成本比闭包表低)。 四、性能对比 | 方法 | 查询效率 | 更新效率 | 空间占用 | 实现复杂度 | |--------------|----------|----------|----------|------------| | 邻接表+递归 | 低 | 高 | 低 | 低 | | 路径枚举 | 高 | 中 | 中 | 中 | | 闭包表 | 极高 | 低 | 高 | 高 | 五、示例:路径枚举法的实现 1. 修改表结构: ```sql ALTER TABLE sys_dict ADD COLUMN path VARCHAR(1000) COMMENT '路径, 格式: /根节点ID/.../节点ID/当前节点ID/'; -- 示例: 节点5的节点是3,3的节点是1,则path为 '/1/3/5/' ``` 2. 查询节点ID为5的所有子节点(包括所有后代): ```sql SELECT * FROM sys_dict WHERE path LIKE CONCAT((SELECT path FROM sys_dict WHERE id=5), '%'); ``` 3. 查询节点5到根节点的路径: ```sql -- 利用path字段直接拆分即可(或反向查询) SELECT * FROM sys_dict WHERE id IN (SELECT * FROM unnest(string_to_array(TRIM(LEADING '/' FROM (SELECT path FROM sys_dict WHERE id=5)), '/'))) -- 或者更简单:根据path的ID顺序直接解析(在程序中处理) ``` 4. 更新节点位置(如移动子树)时,需要更新该节点及其所有后代的path。可以通过触发器或程序实现。 六、分区设计示例(按层分区) 假设我们有一个`level`字段表示节点深度(根节点为1),我们可以按`level`进行范围分区: ```sql -- 假设最大10层,每层一个分区(实际可按层段分区) CREATE TABLE sys_dict ( id BIGINT PRIMARY KEY, pid BIGINT DEFAULT 0, dict_type VARCHAR(50) NOT NULL, dict_value VARCHAR(100) NOT NULL, level INT NOT NULL, path VARCHAR(1000) ) PARTITION BY RANGE (level); CREATE TABLE sys_dict_level1 PARTITION OF sys_dict FOR VALUES FROM (1) TO (2); CREATE TABLE sys_dict_level2 PARTITION OF sys_dict FOR VALUES FROM (2) TO (3); -- ... 创建更多分区 ``` 这样,当查询特定层的节点时,可以只扫描一个分区。但注意,跨层查询(如递归)可能涉及多个分区,此时分区的好处可能不明显。因此,分区更适合按层过滤的场景。 总结:高效查询多层嵌套树形结构,推荐使用路径枚举法或闭包表,并结合索引分区。对于实时性要求高且数据量大的场景,闭包表是最优选择;对于更新较多而查询模式相对固定的场景,路径枚举法更合适。 最后,需要根据实际业务场景(读多写少还是写频繁)进行权衡。 参考文献: [^1]: 分区提高性能 [^2]: 分区设计原则</think>### 高效查询多层嵌套树形结构的优化方案 在具有 `pid`-`id` 结构的树形数据中,高效查询需结合 **数据库设计**、**索引策略** **查询算法** 进行优化。以下是关键解决方案: --- #### 一、核心优化策略 1. **路径枚举法(Path Enumeration)** - **设计**:添加 `path` 字段存储从根节点到当前节点的路径(如 `/100/101/103`) - **查询示例**: ```sql -- 查询节点103的所有祖先 SELECT * FROM sys_dict WHERE id IN (SELECT unnest(string_to_array(trim('/100/101/103', '/'), '/'))); -- 查询节点101的所有后代 SELECT * FROM sys_dict WHERE path LIKE '/100/101/%'; ``` - **优势**:避免递归查询,复杂度 $O(1)$ - **代价**:更新节点时需维护路径(可通过触发器实现)[^1] 2. **闭包表(Closure Table)** - **设计**:新建关系表存储所有祖先-后代关系 ```sql CREATE TABLE dict_closure ( ancestor BIGINT NOT NULL, -- 祖先ID descendant BIGINT NOT NULL, -- 后代ID depth INT NOT NULL, -- 层距离 PRIMARY KEY (ancestor, descendant) ); ``` - **查询示例**: ```sql -- 查询节点101的所有后代(含自身) SELECT d.* FROM sys_dict d JOIN dict_closure c ON d.id = c.descendant WHERE c.ancestor = 101; ``` - **优势**:查询效率 $O(1)$,避免递归 - **代价**:空间占用增加,写入时需更新关系表[^1] --- #### 二、分区与索引优化 1. **层分区(Level Partitioning)** - 按节点深度 `level` 字段分区,将同层数据集中存储: ```sql CREATE TABLE sys_dict (...) PARTITION BY RANGE (level); CREATE TABLE dict_level1 PARTITION OF sys_dict FOR VALUES FROM (1) TO (2); CREATE TABLE dict_level2 PARTITION OF sys_dict FOR VALUES FROM (2) TO (3); ``` - **优势**: - 查询特定层时减少 I/O(如 `WHERE level=3` 仅扫描 `dict_level3` 分区) - 均衡磁盘负载,避免热点[^1][^2] 2. **组合索引策略** - 针对高频查询场景建立索引: ```sql -- 路径枚举查询优化 CREATE INDEX idx_path_gist ON sys_dict USING GIST (path gist_ltree_ops); -- 闭包表查询优化 CREATE INDEX idx_closure_desc ON dict_closure(descendant); CREATE INDEX idx_closure_anc ON dict_closure(ancestor); ``` --- #### 三、程序层优化 1. **内存缓存树结构** - 一次性加载全量数据到内存,构建 `Map<id, Node>` `Map<pid, List<Node>>` - 查询后代节点时直接内存遍历,复杂度 $O(n)$ 2. **批量递归查询(CTE优化)** ```sql -- MySQL 8.0+/PostgreSQL递归CTE WITH RECURSIVE cte AS ( SELECT * FROM sys_dict WHERE id = 101 -- 起点 UNION ALL SELECT child.* FROM sys_dict child JOIN cte ON child.pid = cte.id ) SELECT * FROM cte; ``` - **优化点**: - 添加 `WHERE level < 5` 限制递归深度 - 使用 `MATERIALIZED` 避免重复计算(PostgreSQL 12+) --- #### 四、性能对比与选型建议 | **方案** | 查询速度 | 写入速度 | 空间占用 | 适用场景 | |------------------|----------|----------|----------|------------------------| | 路径枚举 | ★★★★ | ★★ | ★★ | 读多写少,层固定 | | 闭包表 | ★★★★★ | ★ | ★★★ | 高频深度查询 | | 递归CTE | ★★ | ★★★★ | ★ | 层浅(<5层) | | 内存缓存 | ★★★★★ | ★★ | - | 数据量小(<10万) | > **选型指南**: > - 百万数据:**闭包表 + 分区** > - 频繁更新:**路径枚举 + 触发器** > - 实时性要求高:**内存缓存 + 增量同步** --- #### 五、分区设计原则 1. **分区键选择** - 优先按 `level` 分区(层分布均匀) - 次选按 `dict_type`(字典类型)分区 2. **避免热点** ```sql -- 将根节点单独分区(高频访问) CREATE TABLE dict_root PARTITION OF sys_dict FOR VALUES FROM (1) TO (2); ``` 3. **并行查询优化** ```sql SET max_parallel_workers_per_gather = 8; -- PostgreSQL SELECT /*+ PARALLEL(d 4) */ ... -- Oracle ``` > 通过分区将 I/O 分散到多个磁盘,提升并发查询能力[^2]。 --- ### 总结 高效查询树形结构的核心是 **空间换时间**: 1. **小数据量**:内存缓存 + 递归CTE 2. **大数据量**:闭包表/路径枚举 + 层分区 3. **高并发**:分区分散 I/O + 组合索引 在写入频繁的场景,建议采用 **路径枚举 + 异步更新任务** 平衡读写性能[^1]。实际部署前需通过 `EXPLAIN ANALYZE` 验证执行计划。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值