探索PostgreSQL 14新特性--SEARCH和CYCLE

PG14的SEARCH和CYCLE新功能大大简化了递归查询的方式,本文给出一些基于旅行计划的示例。

创建数据库

本文示例基于任何PG14或更新版本的数据库。使用Aiven分支的服务及CLI。下面是创建数据库的CLI命令:

avn service create holidays-pg \--service-type pg \--plan hobbyist \--cloud google-europe-west3 \-c pg_version=14

上述在google-europe-west3区创建一个PG14(-c pg_version=14)的服务,名为holidays-pg,并使用最小的hobbyist计划。足够我们测试。使用avn service versions命令检查提供工具的版本。

需要等待几分钟以上PG14服务启动。可以通过以下方式关注服务创建任务的进度:

avn service wait holidays-pg

使用下面命令连接到holidays-pg的服务:

avn service cli holidays-pg

创建数据集

我们想要穿越欧洲,在预算范围内参观一些注意城市。看看这是之前讨论的背包问题的变体?使用类似的策略可以解决明显不同的问题总是很有趣。

为存储我们想要访问的城市,创建一个cities表,并用我们选择的位置填充。

create table cities(city_id int primary key,city_name varchar);insert into cities values (0, 'Rome'),(1, 'London'),(2, 'Paris'),(3, 'Helsinki'),(4, 'Barcelona');

我们如何在城市之间旅行?很简单,前往一个旅行预订网站,找到连接以及每次旅行的费用。通常会返回这样的图表:

为了在PG中表示上述信息,创建一个trips表存储出发地city_a_id和目的地city_b_id,以及欧洲旅行费用price_in_eur。

create table trips(trip_id int primary key,city_a_id int references cities(city_id),city_b_id int references cities(city_id),price_in_eur int);
insert into trips values(1, 0, 1, 200),(2, 0, 2, 250),(3, 0, 3, 150),(4, 1, 0, 120),(5, 1, 3, 350),(6, 1, 4, 450),(7, 2, 0, 170),(8, 2, 3, 320),(9, 3, 0, 50),(10, 3, 4, 120),(11, 4, 0, 30),(12, 4, 1, 500);

Trips表包含所有可用的路径以及相关成本。例如,id为2的路径从Rome(city_id:0)到Paris(city_id:2),成本为250欧元。

计划行程

我们的旅程需要从某个地方开始,既然知道条条道路通罗马,可以选择eternal city作为起点。为了查看可以去哪里旅行,可以在cities和trips表做join。

selectsrc.city_name,dst.city_name,trips.price_in_eur
from cities srcjoin trips on src.city_id = trips.city_a_idjoin cities dst on trips.city_b_id = dst.city_id
where src.city_name='Rome';

结果显示上图相同信息:可以达到London、Paris和Helsinki:

city_name | city_name | price_in_eur
-----------+-----------+--------------
Rome      | London    |          200
Rome      | Paris     |          250
Rome      | Helsinki  |          150
(3 rows)

为旅程添加更多节点

好了,下一步去哪里?我们可以利用递归查询的强大功能来遍历所有可能的组合。有无限多钱,我们就可以永远环游世界。用数据库术语来翻译,意味着递归查询可能会查询无限循环。为了模仿现实生活并使我们免于无限循环,让我们设定一个800欧元的总体预算来支付我们所有旅行。

可以像这样编写旅程的递归查询:

with recursive trip_journey(city_id,trip_id,total_price_in_eur,journey_stops)
as (selectcity_id as city_id,null::int as trip_id,0 price_in_eur,ARRAY[city_name] as journey_namefrom citieswhere city_id=0UNION ALLselecttrips.city_b_id,trips.trip_id,tj.total_price_in_eur + trips.price_in_eur,tj.journey_stops || city_b.city_namefrom trip_journey tj join trips on tj.city_id = trips.city_a_idjoin cities city_a on trips.city_a_id = city_a.city_idjoin cities city_b on trips.city_b_id = city_b.city_idwhere tj.total_price_in_eur + trips.price_in_eur < 800)
select * from trip_journey;

让我们分解下。第一部分陈述起点:我们要从Rome(city_id=0)开始。如果不旅行那么trip_id是null,总成本是0.

selectcity_id as city_id,null::int as trip_id,0 price_in_eur,ARRAY[city_name] as journey_name
from cities
where city_id=0

然后我们开始添加行程,使用递归部分,将先前定义的trip_journey与trips表join以发现所有可能的目的地和相关成本。

UNION ALL
selecttrips.city_b_id,trips.trip_id,tj.total_price_in_eur + trips.price_in_eur,tj.journey_stops || city_b.city_name
from trip_journey tj join trips on tj.city_id = trips.city_a_id
join cities city_a on trips.city_a_id = city_a.city_id
join cities city_b on trips.city_b_id = city_b.city_id
where tj.total_price_in_eur + trips.price_in_eur < 800

将city_b.city_name添加到journey_stops中,然后计算总代价:tj.total_price_in_eur + trips.price_in_eur。最后,我们在where条件中限制总成本800。这个查询检索89行,从Rome开始直到{Rome,Helsinki,Rome,Helsinki,Rome,Helsinki,Barcelona,Rome}:

city_id | trip_id | total_price_in_eur |                          journey_stops
---------+---------+--------------------+-----------------------------------------------------------------0 |         |                  0 | {Rome}1 |       1 |                200 | {Rome,London}2 |       2 |                250 | {Rome,Paris}3 |       3 |                150 | {Rome,Helsinki}0 |       4 |                320 | {Rome,London,Rome}3 |       5 |                550 | {Rome,London,Helsinki}
....4 |      10 |                770 | {Rome,Helsinki,Rome,Helsinki,Barcelona,Rome,Helsinki,Barcelona}0 |      11 |                700 | {Rome,Helsinki,Rome,Helsinki,Rome,Helsinki,Barcelona,Rome}
(89 rows)

使用SEARCH选项定义探索路径

上面的89行很好地总结了我们可以采取的所有可能的路径。但是该数据集是怎么排序的?在PG14中,SEARCH选项提供了一种新的方法定义递归查询:

1) 基于stops的个数进行排序,可以使用BREADTH选项。我们会首先看到0 stops的trips然后1依次向后。

2) 如果想基于trip路径进行排序,可以使用DEPTH选项。我们将看到旅程在每一步扩展,例如{Rome}-> {Rome->London} -> {Rome->London->Helsinki}直到找到旅程最大深度,然后探索树的连续分支。

为了在我们的数据集上提供一个示例,只需要将最后一条select *from trip_journey语句替换以下内容:

SEARCH BREADTH FIRST BY city_id SET ordercol
select * from trip_journey order by ordercol limit 15;

我们将查询限制为仅返回前15行(limit 15),这可以节省计算。因为我们不会扫描整个组合,但仍使我们能够演示该功能。因为我们正在使用BREADTH选项,所以结果集按stops排序:

city_id | trip_id | total_price_in_eur |         journey_stops          | ordercol
---------+---------+--------------------+--------------------------------+----------0 |         |                  0 | {Rome}                         | (0,0)1 |       1 |                200 | {Rome,London}                  | (1,1)2 |       2 |                250 | {Rome,Paris}                   | (1,2)3 |       3 |                150 | {Rome,Helsinki}                | (1,3)0 |       4 |                320 | {Rome,London,Rome}             | (2,0)0 |       9 |                200 | {Rome,Helsinki,Rome}           | (2,0)0 |       7 |                420 | {Rome,Paris,Rome}              | (2,0)3 |       5 |                550 | {Rome,London,Helsinki}         | (2,3)3 |       8 |                570 | {Rome,Paris,Helsinki}          | (2,3)4 |       6 |                650 | {Rome,London,Barcelona}        | (2,4)4 |      10 |                270 | {Rome,Helsinki,Barcelona}      | (2,4)0 |       9 |                600 | {Rome,London,Helsinki,Rome}    | (3,0)0 |      11 |                300 | {Rome,Helsinki,Barcelona,Rome} | (3,0)0 |       9 |                620 | {Rome,Paris,Helsinki,Rome}     | (3,0)0 |      11 |                680 | {Rome,London,Barcelona,Rome}   | (3,0)
(15 rows)

该ordercol列包含一个元组(A,B),其中第一项代表级别,第二项代表最新的city_id。例如(2,0)说明旅行包括2此行程,并以Rome(city_id=0)结束。stops列包含相同信息{Rome,Paris,Rome}。

如果使用DEPTH替换BREADTH,可以得到以trip路径排序的前15个trip。

city_id | trip_id | total_price_in_eur |                    journey_stops                    |           ordercol
---------+---------+--------------------+-----------------------------------------------------+-------------------------------0 |         |                  0 | {Rome}                                              | {(0)}1 |       1 |                200 | {Rome,London}                                       | {(0),(1)}0 |       4 |                320 | {Rome,London,Rome}                                  | {(0),(1),(0)}1 |       1 |                520 | {Rome,London,Rome,London}                           | {(0),(1),(0),(1)}0 |       4 |                640 | {Rome,London,Rome,London,Rome}                      | {(0),(1),(0),(1),(0)}3 |       3 |                790 | {Rome,London,Rome,London,Rome,Helsinki}             | {(0),(1),(0),(1),(0),(3)}2 |       2 |                570 | {Rome,London,Rome,Paris}                            | {(0),(1),(0),(2)}0 |       7 |                740 | {Rome,London,Rome,Paris,Rome}                       | {(0),(1),(0),(2),(0)}3 |       3 |                470 | {Rome,London,Rome,Helsinki}                         | {(0),(1),(0),(3)}0 |       9 |                520 | {Rome,London,Rome,Helsinki,Rome}                    | {(0),(1),(0),(3),(0)}1 |       1 |                720 | {Rome,London,Rome,Helsinki,Rome,London}             | {(0),(1),(0),(3),(0),(1)}2 |       2 |                770 | {Rome,London,Rome,Helsinki,Rome,Paris}              | {(0),(1),(0),(3),(0),(2)}3 |       3 |                670 | {Rome,London,Rome,Helsinki,Rome,Helsinki}           | {(0),(1),(0),(3),(0),(3)}0 |       9 |                720 | {Rome,London,Rome,Helsinki,Rome,Helsinki,Rome}      | {(0),(1),(0),(3),(0),(3),(0)}4 |      10 |                790 | {Rome,London,Rome,Helsinki,Rome,Helsinki,Barcelona} | {(0),(1),(0),(3),(0),(3),(4)}
(15 rows)

ordercol列包含city_id的拼接列表,例如{(0),(1),(0),(2)}表示,我们经过Rome->London->Rome->Paris。返回行的顺序依赖于ordercol。

通过CYCLE选项避免循环

Rome->London->Rome->Paris的旅途是不是很美好?你可能不喜欢多次经过一个城市。循环时一种非常低效的旅行方式,我们应该尽可能避免。PG14的CYCLE选项提供了一种跳过他们的方法。

在原始递归查询中,使用下面语句替换select * from trip_journey:

CYCLE city_id SET is_cycle USING journey_ids
select * from trip_journey where is_cycle=false;

以上为递归查询添加了几列:

1) journey_ids以数组ARRAY形式包含city_id的序列

2) is_cycle通过检查当前city_id是否已经在journey_ids列中来标记循环。

过滤is_cycle=false后的查询结果提供了可以支付得起的无循环旅程列表:

city_id | trip_id | total_price_in_eur |          journey_stops           | is_cycle |    journey_ids
---------+---------+--------------------+----------------------------------+----------+-------------------0 |         |                  0 | {Rome}                           | f        | {(0)}1 |       1 |                200 | {Rome,London}                    | f        | {(0),(1)}2 |       2 |                250 | {Rome,Paris}                     | f        | {(0),(2)}3 |       3 |                150 | {Rome,Helsinki}                  | f        | {(0),(3)}3 |       5 |                550 | {Rome,London,Helsinki}           | f        | {(0),(1),(3)}4 |       6 |                650 | {Rome,London,Barcelona}          | f        | {(0),(1),(4)}3 |       8 |                570 | {Rome,Paris,Helsinki}            | f        | {(0),(2),(3)}4 |      10 |                270 | {Rome,Helsinki,Barcelona}        | f        | {(0),(3),(4)}4 |      10 |                690 | {Rome,Paris,Helsinki,Barcelona}  | f        | {(0),(2),(3),(4)}4 |      10 |                670 | {Rome,London,Helsinki,Barcelona} | f        | {(0),(1),(3),(4)}1 |      12 |                770 | {Rome,Helsinki,Barcelona,London} | f        | {(0),(3),(4),(1)}
(11 rows)

避免循环后,可以比较旅程:{Rome,Helsinki,Barcelona,London} 和{Rome,London,Helsinki,Barcelona}包含相同城市,但是第一个便宜100欧元。

回国

每次旅行都有一个让您很高兴的回家时刻,但若检查上面的旅行,由于避免了循环,因此我们不可能再回到Rome。

为实现这一点,在原始查询中,需要考虑与trips表的额外join。将返回成本添加到每个旅程中,可以用下面查询:

with recursive trip_journey(city_id,trip_id,total_price_in_eur,journey_stops,journey_prices,return_price)
as (selectcity_id as city_id,null::int,0 price_in_eur,ARRAY[city_name] as journey_name,ARRAY[0::int] as journey_price,0 return_pricefrom citieswhere city_id=0UNION ALLselecttrips.city_b_id,trips.trip_id,tj.total_price_in_eur + trips.price_in_eur,tj.journey_stops || city_b.city_name,tj.journey_prices || trips.price_in_eur,return_trips.price_in_eurfrom trip_journey tj join trips on tj.city_id = trips.city_a_idjoin cities city_a on trips.city_a_id = city_a.city_idjoin cities city_b on trips.city_b_id = city_b.city_idjoin trips return_trips on trips.city_b_id = return_trips.city_a_id and return_trips.city_b_id = 0where tj.total_price_in_eur + trips.price_in_eur + return_trips.price_in_eur < 800) CYCLE city_id SET is_cycle USING journey_ids
select * from trip_journey where is_cycle=false;

join trips return_trips on trips.city_b_id = return_trips.city_a_id and return_trips.city_b_id = 0部分确保我们前往Rome的一个回程。tj.total_price_in_eur + trips.price_in_eur + return_trips.price_in_eur < 800语句进行预算检查。

结果显示了所有10条可能的旅程,其中包括到Rome的回程:

city_id | trip_id | total_price_in_eur |          journey_stops           | journey_prices  | return_price | is_cycle |    journey_ids
---------+---------+--------------------+----------------------------------+-----------------+--------------+----------+-------------------0 |         |                  0 | {Rome}                           | {0}             |            0 | f        | {(0)}1 |       1 |                200 | {Rome,London}                    | {0,200}         |          120 | f        | {(0),(1)}2 |       2 |                250 | {Rome,Paris}                     | {0,250}         |          170 | f        | {(0),(2)}3 |       3 |                150 | {Rome,Helsinki}                  | {0,150}         |           50 | f        | {(0),(3)}3 |       5 |                550 | {Rome,London,Helsinki}           | {0,200,350}     |           50 | f        | {(0),(1),(3)}4 |       6 |                650 | {Rome,London,Barcelona}          | {0,200,450}     |           30 | f        | {(0),(1),(4)}3 |       8 |                570 | {Rome,Paris,Helsinki}            | {0,250,320}     |           50 | f        | {(0),(2),(3)}4 |      10 |                270 | {Rome,Helsinki,Barcelona}        | {0,150,120}     |           30 | f        | {(0),(3),(4)}4 |      10 |                690 | {Rome,Paris,Helsinki,Barcelona}  | {0,250,320,120} |           30 | f        | {(0),(2),(3),(4)}4 |      10 |                670 | {Rome,London,Helsinki,Barcelona} | {0,200,350,120} |           30 | f        | {(0),(1),(3),(4)}
(10 rows)

Wrapping up

SEARCH和CYCLE选项提供了一种新的、更优雅的方式来定义递归查询行为。可以在手册中查找使用方法。

原文

https://aiven.io/blog/explore-the-new-search-and-cycle-features-in-postgresql-14

探索PostgreSQL 14新特性--SEARCH和CYCLE相关推荐

  1. Flink 1.14 新特性预览

    简介: 一文了解 Flink 1.14 版本新特性及最新进展 本文由社区志愿者陈政羽整理,内容源自阿里巴巴技术专家宋辛童 (五藏) 在 8 月 7 日线上 Flink Meetup 分享的<Fl ...

  2. 【Flink】Flink Flink 1.14 新特性预览

    1.概述 转载:Flink 1.14 新特性预览 简介: 一文了解 Flink 1.14 版本新特性及最新进展 本文由社区志愿者陈政羽整理,内容源自阿里巴巴技术专家宋辛童 (五藏) 在 8 月 7 日 ...

  3. PostgreSQL 15 新特性解读 | 墨天轮优质文章合集

    5月19日,PostgreSQL 全球开发组宣布 PostgreSQL 15 的第一个 beta 版本,这一新版本在开发者体验.性能表现等方面都有提升.为了帮助大家更快速了解到PostgreSQL 1 ...

  4. PostgreSQL 11 新特性之 PL/pgSQL 增强

    文章目录 PostgreSQL 11 增加了一个新的编程对象,存储过程(PROCEDURE).它与存储函数类似,但是没有返回值.存储过程还支持事务,参考文章"PostgreSQL 11 新特 ...

  5. Apache Flink Meetup 8.7 深圳站,企业实践 + 1.14 新特性预览

    时隔 10 个月,Apache Flink Meetup 又来到了深圳这个与 Flink 契合度很高的城市.正如改革和创新是深圳的标签,Apache Flink 给技术开发者们带来的,也是摒弃旧的 & ...

  6. 七、Java 14 新特性

    七.Java 14 新特性 Java 14 已如期于 2020 年 3 月 17 日正式发布,此次更新是继半年前 Java 13 这大版本发布之后的又一次常规版本更新,即便在全球疫情如此严峻形势下,依 ...

  7. 技术前沿资讯-Apache Flink 1.14 新特性介绍

    一.简介 1.14 新版本原本规划有 35 个比较重要的新特性以及优化工作,目前已经有 26 个工作完成:5 个任务不确定是否能准时完成:另外 4 个特性由于时间或者本身设计上的原因,会放到后续版本完 ...

  8. JDK 5、6、7、8、9、10、11、12、13、14 新特性汇总

    JDK5 新特性 自动拆装箱 Foreach 静态导入 可变参数 Var args 枚举 格式化输出 泛型 ProcessBuilder 内省 线程并发库(JUC) 监控和管理虚拟机 元数据 JDK6 ...

  9. JDK 12 ,JDK 13 , JDK 14 新特性 详解

    近年来,甲骨文做出了一些突破性的决定. 他们包括新的半年发布模式与预览功能和更短的发布和反馈周期的新功能. 许可模式也发生了变化,Oracle JDK 不再免费提供. 这加剧了竞争,因此您现在可以从包 ...

最新文章

  1. 通俗易懂地解决中文乱码问题(2) --- 分析解决Mysql插入移动端表情符报错 ‘incorrect string value: '\xF0......
  2. 【转】OneDrive开发入门
  3. net start mysql 发生系统错误 5。 拒绝访问。的解决方法
  4. php curl跨域cookie_PHP curl模拟文件上传(接口请求实现跨域文件中转)
  5. php composer使用过程
  6. 华为5500v3多路径linux6,CentOS7 DM-Multipath+HUAWEI OceanStor存储多路径配置
  7. (转)投资AI的核心标准是场景和数据
  8. 让所有版本的IE卡死的HTML代码
  9. OPPO R17在哪里打开usb调试模式的完美流程
  10. HDU - 1573 X问题
  11. mysql将数字月份替换成文月份_mysql必知必会--使用数据处理函数
  12. 青龙面板+傻妞+JD(自用)
  13. 小屏幕android电视,手机屏幕还能投屏到电视?教你4种方法,1秒钟小屏变大屏
  14. CIPT备考心得分享-下一个考过的就是你
  15. HBuilder发行App(Android和ios)
  16. 【Java基础】(八)方法(即C等语言中的函数)
  17. 把图片转换成视频-ffmpeg
  18. 软件测试 | 白盒的测试方法
  19. mm_cas登入失败
  20. 优衣库46万客户信息遭泄露,这些年,你的信息还被谁泄露过?

热门文章

  1. nw.js node-webkit系列(22).nw执行过程及如何获取.nw真实路径
  2. c语言四舍五入保留小数
  3. Vmware Linux虚拟机硬盘扩容
  4. 关于海康威视在chrome 浏览器的问题
  5. 5-24 藏尾诗 (20分)
  6. 计算机网络技术 英语 文章,计算机网络故障期刊文章参考文献 计算机网络故障英语参考文献哪里找...
  7. 2022年6月份报告合辑 附下载
  8. JAVA校验时间的大小,并确定是否为当天
  9. 怎么准备年终总结谈话
  10. 显示器点距 测试软件,液晶显示器的点距是怎么计算的