【问题标题】:How do I calculate overlapping date ranges duration (more than 2) with PostgreSQL?如何使用 PostgreSQL 计算重叠日期范围持续时间(超过 2 个)?
【发布时间】:2017-02-23 09:45:02
【问题描述】:

我正在尝试计算许多日期范围的全局持续时间。

在我的数据库中,我有候选人和经验。

一个候选人可能有很多经验,一个经验有一个开始日期,也可能有一个结束日期。

体验日期范围可以重叠,我卡在这里,如何计算持续时间?

这就是我加入模型的方式:

我想通过查询经验和技能来检索候选人列表: 我有 2 个输入,范围和技能名称。例如,我希望所有候选人都通过经验和经验技能拥有“Ruby”技能,并且经验全球持续时间为 5 年。

编辑

目前的解决方案:

      SELECT * FROM (
            WITH cte AS (
                SELECT
                  experiences.candidate_id AS candidate_id,
                  experiences.id AS e_id,
                  experiences.start_at AS start_at,
                  experiences.end_at AS end_at,
                  LAG(experiences.start_at, 1, start_at)
                  OVER (PARTITION BY experiences.candidate_id ORDER BY experiences.start_at) AS prev_start_at,
                  LAG(experiences.end_at, 1, start_at)
                  OVER (PARTITION BY experiences.candidate_id ORDER BY experiences.start_at) AS prev_end_at,
                  LEAD(experiences.start_at)
                  OVER (PARTITION BY experiences.candidate_id ORDER BY experiences.start_at) AS next_start_at,
                  LEAD(experiences.end_at, 1, current_date)
                  OVER (PARTITION BY experiences.candidate_id ORDER BY experiences.start_at) AS next_end_at
                FROM experiences
                  INNER JOIN experiences_skills ON experiences_skills.experience_id = experiences.id
                  INNER JOIN skills ON skills.id = experiences_skills.skill_id
                WHERE skills.name = 'Ruby'
            )
            SELECT
              SUM(CASE
                  WHEN (cte.prev_end_at > cte.end_at AND cte.prev_end_at < cte.next_start_at)
                    THEN cte.prev_end_at
                  WHEN (cte.prev_end_at > cte.end_at AND cte.prev_end_at > cte.next_start_at)
                    THEN cte.next_start_at
                  WHEN cte.end_at > cte.next_start_at
                    THEN cte.next_start_at
                  ELSE cte.end_at
                  END
                  -
                  cte.start_at
              ) AS duration_day,
              candidates.*
            FROM cte
              INNER JOIN candidates ON candidates.id = cte.candidate_id
            GROUP BY candidates.id
          ) AS candidates
      WHERE duration_day > 0 AND duration_day < 1000';

【问题讨论】:

  • 样本数据和期望的结果会有所帮助
  • 好的,我会在 5 分钟内添加这个
  • 所以,如果我做对了,候选人可以在同一技能上拥有多个经验,这些经验可能(或可能不)重叠并且您想要这些经验长度的总和(没有重叠部分)? -- 因此,在您的示例中,如果候选人有 2 种 Ruby 技能的经验:f.ex。 2010-20132011-2014 并且您查询 5 年 的全球持续时间,则该候选人不符合条件,因为(没有重叠)他/她在 2010 年至 2014 年期间拥有该技能的经验(只有 4 年)?
  • 另外,如果体验没有end_at,我假设你会用current_timestamp 计算(或者current_date 也许?end_at 的确切类型是什么?)。开放范围没有多大意义,因为它的长度是无限的(它总是大于查询的有限全局持续时间)。
  • 没错,让我发布我目前的解决方案。查看我的编辑

标签: sql ruby-on-rails database postgresql


【解决方案1】:

如果没有defining your own aggregate,我认为你无法解决这个问题,例如:

CREATE OR REPLACE FUNCTION range_array_merge(s anyarray, v anynonarray)
    RETURNS anyarray
    LANGUAGE SQL
    IMMUTABLE
AS $func$
  WITH RECURSIVE arrays(r) AS (
      SELECT s || v
    UNION ALL
      SELECT array_agg(DISTINCT u)
      FROM   (SELECT a + b u
              FROM   arrays,
                     unnest(r) a
              JOIN   unnest(r) b ON a <> b AND a && b) u
      HAVING COUNT(u) > 0
  ),
  ranges(r) AS (
      SELECT unnest(r)
      FROM   arrays
  )
  SELECT    array_agg(DISTINCT r.r)
  FROM      ranges r
  LEFT JOIN ranges c ON c.r <> r.r AND c.r @> r.r
  WHERE     c.r IS NULL
$func$;

CREATE AGGREGATE range_array_merge_agg(anynonarray) (
  STYPE    = anyarray,
  SFUNC    = range_array_merge(anyarray, anynonarray),
  INITCOND = '{}'
);

这可以收集任何类型的范围并将它们累积到一个数组中,该数组只有单独的范围(重叠的范围合并在一起)。

有了这个,您的查询就像“简单”一样:

SELECT   e.candidate_id,
         SUM(upper(r.r) - lower(r.r) + 1) total_days
FROM     (SELECT   e.candidate_id,
                   range_array_merge_agg(daterange(e.start_at, COALESCE(e.end_at, current_date))) r
          FROM     experiences e
          JOIN     experiences_skills es ON es.experience_id = e.id
          WHERE    es.skill_id = 42 --> search for a specific skill
          GROUP BY e.candidate_id) e,
         unnest(e.r) r
GROUP BY e.candidate_id;

假设start_atend_at 的类型为date。使用 timestamp [with time zone] 类型时,事情会变得一团糟,但我怀疑您是否需要这种精度。

您可以简单地使用HAVING SUM(upper(r.r) - lower(r.r) + 1) &gt; 1000 过滤上面的查询。

http://rextester.com/DNSWS30622

编辑:搜索完整的候选行:

SELECT   c.*
FROM     (SELECT   e.candidate_id,
                   range_array_merge_agg(daterange(e.start_at, COALESCE(e.end_at, current_date))) r
          FROM     experiences e
          JOIN     experiences_skills es ON es.experience_id = e.id
          WHERE    es.skill_id = 42 --> search for a specific skill
          GROUP BY e.candidate_id) e,
         unnest(e.r) r
JOIN     candidates c ON e.candidate_id = c.id
GROUP BY c.id
HAVING   SUM(upper(r.r) - lower(r.r) + 1) > 1000; --> search for minimum number of total days

注意:如果您想知道为什么总和中是 + 1,原因很简单。 date '2017-01-01' - date '2017-01-01'0(零)。虽然我认为这是 1 天的体验(那一天正好是 2017-01-01)。所以这就是为什么总和需要+ 1。您也可以在 daterange 构造函数中表达这一点,例如:daterange(e.start_at, COALESCE(e.end_at, current_date)), '[]')。但由于date 是离散的,[2017-01-01,2017-01-02] 范围将被规范化为[2017-01-01,2017-01-03) 形式。这样就不需要将1 添加到总和中,因为规范化已经“扩展”了它的upper() 界限。

【讨论】:

  • 感谢您的完整回答!我会尽快尝试的。由于您的测试返回预期结果,我验证您的答案
  • 这是一个聪明的答案,但我无法用它收集候选人名单。您的查询只会向我发送带有持续时间的 ID,它允许我按持续时间过滤它,但我不确定如何获得候选人。* 不会让您的查询膨胀。
  • @TerryRaimondo 你可以将candidates 加入到外部查询ON candidates.id = e.candidate_id 中,使用GROUP BY candidates.id 而不是e.candidate_id(无论如何它们都是一样的)并且因为candidates.* 依赖于@987654348 @ 功能上(它是表的主键),您可以在外部查询中选择candidates.*。如果您只想过滤它,则不必选择total_days
  • 先生,您拯救了我的一天。愿你长寿。
  • 我更改了您的隐式/显式连接的顺序。我遇到了这个问题:)
【解决方案2】:

我找不到你的experience_skills 表的主键,因为experience_id 和skill_id 都是FK。

为避免此问题,您可以删除experience_skills 表并将候选人表的PK 作为FK 添加到experiences_skills 和技能表中,然后尝试以下查询:

select * from candidates c, exp e, skills s where c.candid = e.candid and c.candid = s.candid and (datediff(year,e.sdate, e.edate) = 1) and s.name = 'test'

下面是表结构的图片:

【讨论】:

  • 最好使用JOIN形式
猜你喜欢
  • 1970-01-01
  • 2023-01-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-06
  • 2018-03-03
相关资源
最近更新 更多