震精 - PostgreSQL 递归查询 - 树型数据按路径分组输出

本文涉及的产品
云原生数据库 PolarDB 分布式版,标准版 2核8GB
RDS PostgreSQL Serverless,0.5-4RCU 50GB 3个月
推荐场景:
对影评进行热评分析
云数据库 RDS SQL Server,基础系列 2核4GB
简介:

标签

PostgreSQL , 递归查询 , with recursive , 路径分组 , 路径递归


背景

PostgreSQL的递归查询可以解决很多树形结构,路径查询等问题。

结合PostgreSQL plpgsql编程,可以实现更加复杂的问题,比如接下来这个CASE。

pic

用户要求输出每个末端路径涉及的所有的记录。

例子

创建测试表,插入路径数据.

create table test(id serial, path text, info text);  
insert into test (path)  values ('0'),('0.1'),('0.1.3'),('0.1.4'),('0.1.3.7'),('0.1.3.8'),('0.1.4.9'),('0.1.4.10'),('0.2'),('0.2.5'),('0.2.6'),('0.2.5.11'),('0.2.5.12');  
  
pipeline=# select * from test;  
 id |   path   | info   
----+----------+------  
  1 | 0        |   
  2 | 0.1      |   
  3 | 0.1.3    |   
  4 | 0.1.4    |   
  5 | 0.1.3.7  |   
  6 | 0.1.3.8  |   
  7 | 0.1.4.9  |   
  8 | 0.1.4.10 |   
  9 | 0.2      |   
 10 | 0.2.5    |   
 11 | 0.2.6    |   
 12 | 0.2.5.11 |   
 13 | 0.2.5.12 |   
(13 rows)  

需求

每个末端,以及所有到达末端的所有记录,进行分组

组1

0  
0.1  
0.1.3  
0.1.3.7  

组2

0  
0.1  
0.1.3  
0.1.3.8  

组3

0  
0.1  
0.1.4  
0.1.4.9  

组4

0  
0.1  
0.1.4  
0.1.4.10  

组5

0  
0.2  
0.2.5  
0.2.5.11  

组6

0  
0.2  
0.2.5  
0.2.5.12  

组7

0  
0.2  
0.2.6  

将以上数据分组输出,输出时新增两个字段,组编号,以及记录本身在组内按路径顺序的编号

例如:

组1

组ID,路径ID,原有记录信息  
1, 1, 0  
1, 2, 0.1  
1, 3, 0.1.3  
1, 4, 0.1.3.7  

组2

组ID,路径ID,原有记录信息  
2, 1, 0  
2, 2, 0.1  
2, 3, 0.1.3  
2, 4, 0.1.3.8  

其他略

思路

1. 查询出末端路径

2. 根据末端路径,使用PostgreSQL递归语句找出每个末端对应的所有路径记录

找出末端路径

首先,需要用到ltree插件,目的是检索出末端路径。(当然不使用也可以,用它可以简化我们的QUERY)。

创建ltree插件

create extension ltree;  

创建gist索引

CREATE INDEX path_gist_idx ON test USING GIST ((path::ltree));  

使用自关联,可以查询出末端记录

pipeline=# select t1.path from test t1 where not exists (select 1 from test t2 where t1.path::ltree @> t2.path::ltree and t1.path<>t2.path) order by 1;  
   path     
----------  
 0.1.3.7  
 0.1.3.8  
 0.1.4.10  
 0.1.4.9  
 0.2.5.11  
 0.2.5.12  
 0.2.6  
(7 rows)  
  
pipeline=# explain select t1.path from test t1 where not exists (select 1 from test t2 where t1.path::ltree @> t2.path::ltree and t1.path<>t2.path);  
                                     QUERY PLAN                                       
------------------------------------------------------------------------------------  
 Nested Loop Anti Join  (cost=0.14..5.91 rows=13 width=32)  
   ->  Seq Scan on test t1  (cost=0.00..1.13 rows=13 width=32)  
   ->  Index Scan using path_gist_idx on test t2  (cost=0.14..0.34 rows=1 width=32)  
         Index Cond: ((t1.path)::ltree @> (path)::ltree)  
         Filter: (t1.path <> path)  
(5 rows)  

递归,输出每个末端路径为分组的数据

例如'0.2.5.12',可以这样输出

with recursive tmp as (  
  select * from test where path='0.2.5.12'  
  union all  
  select test.* from test join tmp on (test.path = regexp_replace( (case when strpos(tmp.path, '.')=0 then null else tmp.path end) , '\.[\d]+$'::text , ''))   
)  
select * from tmp;  
  
 id |   path   | info   
----+----------+------  
 13 | 0.2.5.12 |   
 10 | 0.2.5    |   
  9 | 0.2      |   
  1 | 0        |   
(4 rows)  

我们可以写一个online code,看看效果

do language plpgsql $$  
declare  
  v_path text;  
  rec record;  
  v_gid int :=1;  
begin  
  for v_path in select t1.path from test t1 where not exists (select 1 from test t2 where t1.path::ltree @> t2.path::ltree and t1.path<>t2.path) order by 1  
  loop  
    for rec in with recursive tmp as (  
      select v_gid as gid, 1 as lid,* from test where test.path=v_path  
      union all  
      select tmp.gid, tmp.lid+1, test.* from test join tmp on (test.path = regexp_replace( (case when strpos(tmp.path, '.')=0 then null else tmp.path end) , '\.[\d]+$'::text , ''))   
    )  
    select * from tmp  
    loop  
      raise notice '%', rec;  
    end loop;  
    v_gid := v_gid+1;  
  end loop;  
end;  
$$;  
  
  
NOTICE:  (1,1,5,0.1.3.7,)  
NOTICE:  (1,2,3,0.1.3,)  
NOTICE:  (1,3,2,0.1,)  
NOTICE:  (1,4,1,0,)  
NOTICE:  (2,1,6,0.1.3.8,)  
NOTICE:  (2,2,3,0.1.3,)  
NOTICE:  (2,3,2,0.1,)  
NOTICE:  (2,4,1,0,)  
NOTICE:  (3,1,8,0.1.4.10,)  
NOTICE:  (3,2,4,0.1.4,)  
NOTICE:  (3,3,2,0.1,)  
NOTICE:  (3,4,1,0,)  
NOTICE:  (4,1,7,0.1.4.9,)  
NOTICE:  (4,2,4,0.1.4,)  
NOTICE:  (4,3,2,0.1,)  
NOTICE:  (4,4,1,0,)  
NOTICE:  (5,1,12,0.2.5.11,)  
NOTICE:  (5,2,10,0.2.5,)  
NOTICE:  (5,3,9,0.2,)  
NOTICE:  (5,4,1,0,)  
NOTICE:  (6,1,13,0.2.5.12,)  
NOTICE:  (6,2,10,0.2.5,)  
NOTICE:  (6,3,9,0.2,)  
NOTICE:  (6,4,1,0,)  
NOTICE:  (7,1,11,0.2.6,)  
NOTICE:  (7,2,9,0.2,)  
NOTICE:  (7,3,1,0,)  
DO  
Time: 2.813 ms  

为了方便使用,可以写成函数

create or replace function get_test() returns setof record as $$  
declare  
  v_path text;  
  rec record;  
  v_gid int :=1;  
begin  
  for v_path in select t1.path from test t1 where not exists (select 1 from test t2 where t1.path::ltree @> t2.path::ltree and t1.path<>t2.path) order by 1  
  loop  
    for rec in with recursive tmp as (  
      select v_gid as gid, 1 as lid,* from test where test.path=v_path  
      union all  
      select tmp.gid, tmp.lid+1, test.* from test join tmp on (test.path = regexp_replace( (case when strpos(tmp.path, '.')=0 then null else tmp.path end) , '\.[\d]+$'::text , ''))   
    )  
    select * from tmp  
    loop  
      return next rec;  
    end loop;  
    v_gid := v_gid+1;  
  end loop;  
end;  
$$ language plpgsql strict;  

最终的输出结果如下

pipeline=# select * from get_test() as (gid int, lid int, id int, path text, info text);  
 gid | lid | id |   path   | info   
-----+-----+----+----------+------  
   1 |   1 |  5 | 0.1.3.7  |   
   1 |   2 |  3 | 0.1.3    |   
   1 |   3 |  2 | 0.1      |   
   1 |   4 |  1 | 0        |   
   2 |   1 |  6 | 0.1.3.8  |   
   2 |   2 |  3 | 0.1.3    |   
   2 |   3 |  2 | 0.1      |   
   2 |   4 |  1 | 0        |   
   3 |   1 |  8 | 0.1.4.10 |   
   3 |   2 |  4 | 0.1.4    |   
   3 |   3 |  2 | 0.1      |   
   3 |   4 |  1 | 0        |   
   4 |   1 |  7 | 0.1.4.9  |   
   4 |   2 |  4 | 0.1.4    |   
   4 |   3 |  2 | 0.1      |   
   4 |   4 |  1 | 0        |   
   5 |   1 | 12 | 0.2.5.11 |   
   5 |   2 | 10 | 0.2.5    |   
   5 |   3 |  9 | 0.2      |   
   5 |   4 |  1 | 0        |   
   6 |   1 | 13 | 0.2.5.12 |   
   6 |   2 | 10 | 0.2.5    |   
   6 |   3 |  9 | 0.2      |   
   6 |   4 |  1 | 0        |   
   7 |   1 | 11 | 0.2.6    |   
   7 |   2 |  9 | 0.2      |   
   7 |   3 |  1 | 0        |   
(27 rows)  
  
Time: 1.495 ms  

PostgreSQL的功能相当多,你GET到了吗?

小知识点

1. ltree是PostgreSQL的树形结构类型,详见

https://www.postgresql.org/docs/9.6/static/ltree.html

本文用到了它的路径包含与否的判断操作符

Operator Returns Description
ltree @> ltree boolean is left argument an ancestor of right (or equal)?
ltree <@ ltree boolean is left argument a descendant of right (or equal)?

2. PostgreSQL中的规则表达式匹配代替函数regexp_replace

regexp_replace( string , '\.[\d]+$'::text , '')  
  
截断末尾的 '.数字' , 取上一路径  

3. PostgreSQL中用于查询字符出现位置的函数strpos

strpos(tmp.path, '.') = 0 表示这个字符串中没有'.'  

4. 实际上ltree也支持取上一路径的功能,参考如下函数库

Function Return Type Description Example Result
subltree(ltree, int start, int end) ltree subpath of ltree from position start to position end-1 (counting from 0) subltree('Top.Child1.Child2',1,2) Child1
subpath(ltree, int offset, int len) ltree subpath of ltree starting at position offset, length len. If offset is negative, subpath starts that far from the end of the path. If len is negative, leaves that many labels off the end of the path. subpath('Top.Child1.Child2',0,2) Top.Child1
subpath(ltree, int offset) ltree subpath of ltree starting at position offset, extending to end of path. If offset is negative, subpath starts that far from the end of the path. subpath('Top.Child1.Child2',1) Child1.Child2
nlevel(ltree) integer number of labels in path nlevel('Top.Child1.Child2') 3
index(ltree a, ltree b) integer position of first occurrence of b in a; -1 if not found index('0.1.2.3.5.4.5.6.8.5.6.8','5.6') 6
index(ltree a, ltree b, int offset) integer position of first occurrence of b in a, searching starting at offset; negative offset means start -offset labels from the end of the path index('0.1.2.3.5.4.5.6.8.5.6.8','5.6',-4) 9
text2ltree(text) ltree cast text to ltree - -
ltree2text(ltree) text cast ltree to text - -
lca(ltree, ltree, ...) ltree lowest common ancestor, i.e., longest common prefix of paths (up to 8 arguments supported) lca('1.2.2.3','1.2.3.4.5.6') 1.2
lca(ltree[]) ltree lowest common ancestor, i.e., longest common prefix of paths lca(array['1.2.2.3'::ltree,'1.2.3']) 1.2
相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
相关文章
|
1月前
|
SQL 关系型数据库 数据库
PostgreSQL性能飙升的秘密:这几个调优技巧让你的数据库查询速度翻倍!
【10月更文挑战第25天】本文介绍了几种有效提升 PostgreSQL 数据库查询效率的方法,包括索引优化、查询优化、配置优化和硬件优化。通过合理设计索引、编写高效 SQL 查询、调整配置参数和选择合适硬件,可以显著提高数据库性能。
166 1
|
6月前
|
消息中间件 Java 关系型数据库
实时计算 Flink版操作报错合集之从 PostgreSQL 读取数据并写入 Kafka 时,遇到 "initial slot snapshot too large" 的错误,该怎么办
在使用实时计算Flink版过程中,可能会遇到各种错误,了解这些错误的原因及解决方法对于高效排错至关重要。针对具体问题,查看Flink的日志是关键,它们通常会提供更详细的错误信息和堆栈跟踪,有助于定位问题。此外,Flink社区文档和官方论坛也是寻求帮助的好去处。以下是一些常见的操作报错及其可能的原因与解决策略。
1014 0
|
4月前
|
缓存 关系型数据库 数据库
PostgreSQL 查询性能
【8月更文挑战第5天】PostgreSQL 查询性能
82 8
|
4月前
|
SQL 关系型数据库 MySQL
SQL Server、MySQL、PostgreSQL:主流数据库SQL语法异同比较——深入探讨数据类型、分页查询、表创建与数据插入、函数和索引等关键语法差异,为跨数据库开发提供实用指导
【8月更文挑战第31天】SQL Server、MySQL和PostgreSQL是当今最流行的关系型数据库管理系统,均使用SQL作为查询语言,但在语法和功能实现上存在差异。本文将比较它们在数据类型、分页查询、创建和插入数据以及函数和索引等方面的异同,帮助开发者更好地理解和使用这些数据库。尽管它们共用SQL语言,但每个系统都有独特的语法规则,了解这些差异有助于提升开发效率和项目成功率。
418 0
|
4月前
|
SQL 关系型数据库 HIVE
实时计算 Flink版产品使用问题之如何将PostgreSQL数据实时入库Hive并实现断点续传
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
5月前
|
Java 关系型数据库 API
使用Spring Boot和PostgreSQL构建高级查询
使用Spring Boot和PostgreSQL构建高级查询
|
4月前
|
开发框架 关系型数据库 数据库
在 PostgreSQL 中,解决图片二进制数据,由于bytea_output参数问题导致显示不正常的问题。
在 PostgreSQL 中,解决图片二进制数据,由于bytea_output参数问题导致显示不正常的问题。
|
6月前
|
关系型数据库 5G PostgreSQL
postgreSQL 导出数据、导入
postgreSQL 导出数据、导入
57 1
|
SQL Cloud Native 关系型数据库
ADBPG(AnalyticDB for PostgreSQL)是阿里云提供的一种云原生的大数据分析型数据库
ADBPG(AnalyticDB for PostgreSQL)是阿里云提供的一种云原生的大数据分析型数据库
1282 1
|
数据可视化 关系型数据库 MySQL
将 PostgreSQL 迁移到 MySQL 数据库
将 PostgreSQL 迁移到 MySQL 数据库
1762 2

相关产品

  • 云原生数据库 PolarDB
  • 云数据库 RDS PostgreSQL 版