上下文

以下是我的问题的简化版本

我们有一个名为positions的表,它存储了一个项目在几个容器中的移动.

每条记录都包含

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

在两个连续的记录之间可能存在"时间间隔".也就是说,物品在上午10点之前一直放在集装箱A里,然后下午4点出现在集装箱B里,中间没有任何东西.

以下是一个示例数据集

ID container date_from date_to
1 A 2023-10-01T00:00:00 2023-10-01T10:00:00
2 A 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 A 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_from个位置而言,从前一位置的date_to开始的特定时间阈值内.

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

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

  • container=A=
  • date_from从我压扁的5个位置中的第一个
  • 从5个位置中的最后一个位置减go date_to

我编写的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

性能问题

上面的查询是正确的,它做了我期望它做的事情.然而,当提取子查询with_first_to_squash时出现性能问题.如果我将查询切碎到with_first_to_squash之前,则性能将呈指数级提高.

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

  • 首先,我们将那些已经标记为"to_Squash"并且是此类位置中的第一个的位置标记为"first_to_Squash"(即,前一个位置尚未标记为"to_Squash"):这是通过内联子查询(在with_marked_first_to_squash的定义中)完成的
  • 其次,我们只 Select 不会被挤压的位置(即,每个相邻子序列中的最后一个),并对每个位置运行一个子查询,该查询"返回"到过go 标记为"First_to_Squash"的第一个位置:一旦找到该位置,我们使用它来检索date_from

在我删除第二个子查询的那一刻,事情变得非常迅速.

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

推荐答案

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

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

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;

工作fiddle

Sql相关问答推荐

如何在PostgreSQL中同时为id s列表执行多个update语句?'

在数据分区内执行确定

SUM(条件)在Oracle?

如何用3个(半)固定位置建模团队,并有效地搜索相同/不同的团队?

在甲骨文中查找前一个星期一的S日期

Oracle SQL-将结果列在单行中

如何设计一个调用嵌套函数并仅在所有被调用的嵌套函数都提交时才提交的事务,例如,如果一个子函数失败则中止?

从结果SQL查询中排除空值

如何将不同层次的产品组和规格组合到最深一层?

Haystack针相交-在元素最多的Haystack中查找集合

MySQL中的递归查询邻接表深度优先?

SQL 根据前一天的最大值计算每天的值数

SQL Server - 判断 ids 层次 struct 中的整数 (id)

SQL for Smarties 类型问题:从表中 Select 记录,并对某些值进行分组

如何创建一个递归计数器来查找一个元素有多少父级和子级?

多行状态下的分组查询判断状态

使用 PL/PGSQL 函数 Select 返回多条记录

字符串从更改到表列和查询中的一行的转换错误

创建一个将层次 struct 级别放入列中的查询

BigQuery 错误:SELECT 列表表达式引用 esthetician.LICENSE_TYPE,它既未在 [49:8] 分组也未聚合