暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

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

yanzongshuaiDBA 2021-12-20
567

探索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
    (101200),
    (202250),
    (303150),
    (410120),
    (513350),
    (614450),
    (720170),
    (823320),
    (93050),
    (1034120),
    (114030),
(12, 4, 1, 500);

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

计划行程

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

select
    src.city_name,
    dst.city_name,
    trips.price_in_eur
from cities src
    join trips on src.city_id = trips.city_a_id
    join 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 (
    select
        city_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
    UNION ALL
    select
        trips.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
    )
select * from trip_journey;

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

select
    city_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
select
    trips.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 (
    select
        city_id as city_id,
        null::int,
        0 price_in_eur,
        ARRAY[city_name] as journey_name,
        ARRAY[0::intas journey_price,
        0 return_price
    from cities
    where city_id=0
    UNION ALL
    select
        trips.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_eur
    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
    join trips return_trips on trips.city_b_id = return_trips.city_a_id and return_trips.city_b_id = 0
    where 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


文章转载自yanzongshuaiDBA,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论