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

标签

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


背景

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

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

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

例子

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

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
时间: 2024-12-31 09:07:05

震精 - PostgreSQL 递归查询 - 树型数据按路径分组输出的相关文章

震精 - PostgreSQL decimal64 decimal128 高效率数值 类型扩展

标签 PostgreSQL , decimal64 , decimal128 , float4 , float8 , numeric 背景 PostgreSQL内置的数值类型包括 整型.浮点.整型序列."无限"精度数值 Name Storage Size Description Range smallint 2 bytes small-range integer -32768 to +32767 integer 4 bytes typical choice for integer -2

震精 - PostgreSQL 10.0 preview 性能增强 - WARM提升一倍性能

标签 PostgreSQL , 10.0 , WARM , 写放大 , 索引写放大 背景 目前,PostgreSQL的MVCC是多版本来实现的,当更新数据时,产生新的版本.(社区正在着手增加基于回滚段的存储引擎) 由于索引存储的是KEY+CTID(行号),当tuple的新版本与旧版本不在同一个数据块(BLOCK)的时候,索引也要随之变化,当新版本在同一个块里面时,则发生HOT UPDATE,索引的值不需要更新,但是因为产生了一条新的记录,所以也需要插入一条索引item,垃圾回收时,将其回收,因此

震精 - PostgreSQL 单机3.9 万亿/天(计数器、序列、自增)

标签 PostgreSQL , 计数器 , 序列 , 自增值 背景 数据库中,自增序列是常见的需求,例如计数,主键,唯一值,或者自动生成的流水号等等. 因此序列这个功能就应运而生,序列的功能在很多商业数据库中都支持需求,PostgreSQL当然也支持,而且更好用. 在PostgreSQL中可以创建多个序列,设置序列的起始值,步长,缓存大小,是否轮回等. postgres=# \h create sequence Command: CREATE SEQUENCE Description: defi

《程序设计解题策略》——第1章 利用树型数据关系的解题策略 1.1 利用划分树求解整数区间内第k大的值

第1章 利用树型数据关系的解题策略 树是一个具有层次结构的集合,一种限制前件数且没有回路的连通图.在现实生活和程序设计的竞赛试题中,许多问题的数据关系呈树型结构,因此有关树的概念.原理.操作方法和一些由树的数据结构支持的算法,一直受到编程者的重视,被广泛应用于解题过程.在本章里,我们将介绍利用树型数据关系解题的七种策略: 1) 利用划分树求解整数区间内第k大值. 2) 利用最小生成树及其扩展形式(最优比率生成树.最小k度生成树.次小生成树)计算有权连通无向图中边权和满足限制条件的最优生成树. 3

java-[求助]使用JAVA实现以树型结构显示文件路径

问题描述 [求助]使用JAVA实现以树型结构显示文件路径 用JAVA实现 以树型结构显示文件路径 一.功能要求: 使用树控件显示本机的文件夹与文件的信息,并可实现显示和隐藏树节点的操作. 二.设计要求: 信息树的创建和监听窗体激活事件获取本计算机的磁盘列表,窗体激活后的图形用户界面如下: 解决方案 http://download.csdn.net/download/talent_marquis/312569

震精 - 数据库还能这样玩 - 三十六计 (下)

PostgreSQL 三十六计 - 下 25. 数据库端编程,处理复杂业务逻辑. 在传统企业.电商.运营商等涉及用户交互.或者多个系统交互的业务场景中,通常一个事务涉及到很复杂的业务逻辑,需要保证数据的一致性,同时还需要与数据库多次交互. 比如银行开户,涉及的业务系统多,逻辑复杂.在传统企业中,通常也使用商业数据库的过程函数,实现此类复杂的逻辑. PostgreSQL的数据库过程函数支持的语言非常丰富,比如plpgsql(可与Oracle pl/sql功能比肩),另外还支持语言的扩展,编程语言可

震精 - 数据库还能这样玩 - 三十六计 (中)

PostgreSQL 三十六计 - 中 13. 金融风控.公安刑侦.社会关系.人脉分析 等业务场景,高效实现图式数据搜索. 利用PostgreSQL函数编程,异步消息,复杂JOIN等手段,解决高效的图式数据查询需求. 1. 猎头挖人 作为IT人士或者猎头.HR,对Linkedin一定不陌生,领英网实际上就是一个维护人际关系的网站. 通过搜索你的一度人脉,可以找到与你直接相关的人,搜索2度人脉,可以搜索到与你间接相关的人. 当然你还可以继续搜索N度人脉,不过那些和你可能就不那么相关了. 如果你知道

PostgreSQL 递归查询一例

问答中,一位网友的问题: one等于上一个one加上现在的money,语句怎么写? 在PostgreSQL中,可以使用递归查询满足以上业务场景的需求: 需要用到递归查询. postgres=# create table m(id serial primary key,money int, one int); CREATE TABLE postgres=# insert into m(money,one) values (0,2000),(85,0),(100,0),(19,0),(21,0);

MySQL递归查询树状表的子节点、父节点具体实现_Mysql

简介:mysql5.0.94版本,该版本以及较高级的版本(5.5.6等等)尚未支持循环递归查询,和sqlserver.oracle相比,mysql难于在树状表中层层遍历的子节点.本程序重点参考了下面的资料,写了两个sql存储过程,子节点查询算是照搬了,父节点查询是逆思维弄的. 表结构和表数据就不公示了,查询的表user_role,主键是id,每条记录有parentid字段(对应该记录的父节点,当然,一个父节点自然会有一个以上的子节点嘛) 复制代码 代码如下: CREATE FUNCTION `g