PostgreSQL - 按距离阈值压缩相邻行的性能

PostgreSQL - performance of squashing adjacent rows by a distance threshold

提问人:Adriano di Lauro 提问时间:10/31/2023 更新时间:10/31/2023 访问量:25

问:

背景

这是我问题的简化版本

我们得到了一个名为 的表,它存储了项目在多个容器中的移动。positions

每条记录包含

  • 容器的名称(我们称之为container)
  • 两个 datetime 属性分别称为 和 ,它们包含项目进入和离开容器的时间戳date_fromdate_to

在两个连续的记录之间可能存在“时间间隔”。也就是说,物品在容器 A 内直到上午 10 点,然后在下午 4 点出现在容器 B 中,中间没有任何东西。

下面是一个示例数据集

编号 container date_from date_to
1 一个 2023-10-01T00:00:00 2023-10-01T10:00:00
2 一个 2023-10-03T09:00:00 2023-10-03T11:00:00
3 B 2023-10-04T02:00:00 2023-10-04T03:00:00
4 C 2023-10-04T06:00:00 2023-10-04T08:00:00
5 C 2023-10-05T00:00:00 2023-10-06T10:00:00
6 一个 2023-10-06T11:00:00 2023-10-06T20:00:00
7 C 2023-10-06T21:00:00 2023-10-07T10:00:00

要求

我需要压扁所有连续的相邻位置

  1. 位于同一容器中(项目永远不会在子序列上离开该容器)
  2. 并且彼此“足够接近”:即对于第二个位置中的哪个位置在前一个位置的某个时间阈值内。date_fromdate_to

对于我压缩的每个子序列,我需要取 的第一个值和 的最后一个值 ,并将它们放在同一个结果行中。date_fromdate_to

例如,如果容器 A 内有 5 条连续记录,并且根据规则,它们足够接近可以被压扁,那么我压扁这些位置的最后一行将具有

  • container= 一个
  • date_from取自我压扁的 5 个位置中的第一个
  • date_to取自 5 个位置中的最后一个位置

我编写的 PostgreSQL 查询

    WITH with_next_position AS (
      SELECT
        id,
        container,
        date_from,
        date_to,
        (
          SELECT subquery.id
          FROM positions subquery
          WHERE subquery.date_from > base.date_from
          ORDER BY subquery.date_from ASC
          LIMIT 1
        ) AS next_position_id

      FROM positions
    ),

    with_time_lapse AS (
      SELECT
        with_next_position.date_from AS date_from,
        with_next_position.date_to AS date_from,
        with_next_position.container AS container,
        CASE
        WHEN join_table.date_from IS NOT NULL
          THEN EXTRACT(EPOCH FROM (join_table.date_from - with_next_position.date_to))
        ELSE
          NULL
        END AS time_lapse,
        join_table.marina_id AS next_container

      FROM
        with_next_position
        FULL OUTER JOIN with_next_position join_table ON join_table.id = with_next_position.next_position_id

      WHERE
        with_next_position.container IS NOT NULL
    ),

    with_marked_to_squash AS (
      SELECT
        date_from,
        date_to,
        container,
        CASE
        WHEN next_container = container AND time_lapse <= 10000000 # This is where I put the threshold
          THEN TRUE
        ELSE
          FALSE
        END AS to_squash

      FROM with_time_lapse
    )

    with_marked_first_to_squash AS (
      SELECT
        date_from,
        date_to,
        container,
        CASE
        WHEN to_squash
          THEN (
            SELECT CASE WHEN to_squash THEN FALSE ELSE TRUE END
            FROM with_marked_to_squash subquery
            WHERE subquery.date_from < with_marked_to_squash.date_from
            ORDER BY subquery.date_from DESC
            LIMIT 1
          )
        ELSE
          FALSE
        END AS first_to_squash

      FROM with_marked_to_squash
    ),

    with_first_to_squash AS (
      SELECT
        date_from,
        date_to,
        container,
        (
          SELECT subquery.date_from
          FROM with_marked_first_to_squash subquery
          WHERE subquery.date_from < with_marked_first_to_squash.date_from AND first_to_squash IS TRUE
          ORDER BY subquery.date_from DESC
          LIMIT 1
        ) AS first_date_in_position

      FROM with_marked_first_to_squash

      WHERE to_squash IS FALSE
    )

    SELECT
      COALESCE(first_date_in_position, date_from) AS date_from,
      date_to,
      container
      EXTRACT(EPOCH FROM (date_to - COALESCE(first_date_in_position, date_from))) AS time_spent

    FROM with_first_to_squash

    ORDER BY date_from

性能问题

上面的查询是正确的,它做了我期望它做的事情。但是,提取子查询时会出现性能问题。如果我将查询切到 BEFORE ,性能会呈指数级提高。with_first_to_squashwith_first_to_squash

我认为性能问题的原因是,通过连续运行和 ,我使数据库引擎通过两个嵌套循环:with_marked_first_to_squashwith_first_to_squash

  • 首先,我们将那些已经标记为“to_squash”且是同类中的第一个位置标记为“first_to_squash”(即前一个位置未标记为“to_squash”):这是通过内联子查询完成的(在with_marked_first_to_squash)
  • 其次,我们只选择不会被压扁的位置(即每个相邻子序列中的最后一个位置),对于每个位置,我们运行一个子查询,该子查询“返回”,直到过去第一个被标记为“first_to_squash”的位置:一旦找到该位置,我们就用它来检索date_from

在我删除第二个子查询的那一刻,事情变得飞快。

我确信有一个解决方案可以允许从子序列中的第一个位置提取,可能涉及分区,但我不熟悉分区及其语法。有没有人可以给我提示?date_from

SQL PostgreSQL 数据库-性能 分区

评论

0赞 The Impaler 10/31/2023
A 和 B 可以交错出现,但仍然需要被压制吗?这对索引有影响。

答:

1赞 Mike Organek 10/31/2023 #1

我怀疑您列表中的子查询是扼杀您性能的原因。select

请尝试以下窗口函数解决方案来解决您的间隙和孤岛问题,因为它只需要排序一次:

with squashes as (
  select *,
         case
           when     container = lag(container) over w
                and date_from - lag(date_to) over w <= interval '5 days' then false 
           else true
         end as keep_me
    from positions
  window w as (order by date_from)
), islands as (
  select *, sum(keep_me::int) over (order by date_from) as group_num
    from squashes
)
select container, min(date_from) as date_from, max(date_to) as date_to
  from islands
 group by group_num, container
 order by group_num;

工作小提琴

评论

0赞 The Impaler 10/31/2023
也许-->?window w as (order by date_from)window w as (partition by container order by date_from)
0赞 Mike Organek 10/31/2023
@TheImpaler 我没有添加一个,因为提供的示例数据似乎是针对一个项目或 SKU 或其他什么。如果我按容器分区,那么这将取消按日期排序。partition by
1赞 Adriano di Lauro 10/31/2023
这很有效,谢谢!我不知道这是一个众所周知的 SQL 问题,我只是在谷歌上搜索了“gaps-and-islands”并找到了很多文章