计算系列中每个日期有多少日期范围的最快方法

Bar*_*kCh 12 postgresql join functions postgresql-9.4

我有一个看起来像这样的表(在 PostgreSQL 9.4 中):

CREATE TABLE dates_ranges (kind int, start_date date, end_date date);
INSERT INTO dates_ranges VALUES 
    (1, '2018-01-01', '2018-01-31'),
    (1, '2018-01-01', '2018-01-05'),
    (1, '2018-01-03', '2018-01-06'),
    (2, '2018-01-01', '2018-01-01'),
    (2, '2018-01-01', '2018-01-02'),
    (3, '2018-01-02', '2018-01-08'),
    (3, '2018-01-05', '2018-01-10');
Run Code Online (Sandbox Code Playgroud)

现在我想计算给定日期和每种类型,计算dates_ranges每个日期有多少行。零可以省略。

想要的结果:

+-------+------------+----+
|  kind | as_of_date |  n |
+-------+------------+----+
|     1 | 2018-01-01 |  2 |
|     1 | 2018-01-02 |  2 |
|     1 | 2018-01-03 |  3 |
|     2 | 2018-01-01 |  2 |
|     2 | 2018-01-02 |  1 |
|     3 | 2018-01-02 |  1 |
|     3 | 2018-01-03 |  1 |
+-------+------------+----+
Run Code Online (Sandbox Code Playgroud)

我想出了两种解决方案,一种是LEFT JOINGROUP BY

SELECT
kind, as_of_date, COUNT(*) n
FROM
    (SELECT d::date AS as_of_date FROM generate_series('2018-01-01'::timestamp, '2018-01-03'::timestamp, '1 day') d) dates
LEFT JOIN
    dates_ranges ON dates.as_of_date BETWEEN start_date AND end_date
GROUP BY 1,2 ORDER BY 1,2
Run Code Online (Sandbox Code Playgroud)

和一个LATERAL,稍微快一点:

SELECT
    kind, as_of_date, n
FROM
    (SELECT d::date AS as_of_date FROM generate_series('2018-01-01'::timestamp, '2018-01-03'::timestamp, '1 day') d) dates,
LATERAL
    (SELECT kind, COUNT(*) AS n FROM dates_ranges WHERE dates.as_of_date BETWEEN start_date AND end_date GROUP BY kind) ss
ORDER BY kind, as_of_date
Run Code Online (Sandbox Code Playgroud)

我想知道写这个查询有什么更好的方法吗?以及如何包含 0 计数的日期类型对?

实际上有几种不同的类型,最长五年(1800 个日期),dates_ranges表中约 30k 行(但它可能会显着增长)。

没有索引。确切地说,在我的情况下,它是子查询的结果,但我想将问题限制在一个问题上,因此它更通用。

Erw*_*ter 6

以及如何包含 0 计数的日期类型对?

构建所有组合的网格,然后 LATERAL加入您的表格,如下所示:

SELECT k.kind, d.as_of_date, c.n
FROM  (SELECT DISTINCT kind FROM dates_ranges) k
CROSS  JOIN (
   SELECT d::date AS as_of_date
   FROM   generate_series(timestamp '2018-01-01', timestamp '2018-01-03', interval '1 day') d
   ) d
CROSS  JOIN LATERAL (
   SELECT count(*)::int AS n
   FROM   dates_ranges
   WHERE  kind = k.kind
   AND    d.as_of_date BETWEEN start_date AND end_date
   ) c
ORDER  BY k.kind, d.as_of_date;
Run Code Online (Sandbox Code Playgroud)

也应该尽可能快。

我一开始有LEFT JOIN LATERAL ... on true,但是子查询中有一个聚合c,所以我们总是得到一行并且也可以使用CROSS JOIN。性能上没有区别。

如果您有一个包含所有相关种类的表,请使用它而不是使用子查询生成列表k

强制转换integer为可选。否则你得到bigint

索引会有所帮助,尤其是(kind, start_date, end_date). 由于您是在子查询上构建的,因此可能会也可能不会实现。

在 10 之前的 Postgres 版本中,通常不建议generate_series()使用SELECT列表中的设置返回函数(除非您确切地知道自己在做什么)。看:

如果您有很多行很少或没有行的组合,则此等效形式可能会更快:

SELECT k.kind, d.as_of_date, count(dr.kind)::int AS n
FROM  (SELECT DISTINCT kind FROM dates_ranges) k
CROSS JOIN (
   SELECT d::date AS as_of_date
   FROM   generate_series(timestamp '2018-01-01', timestamp '2018-01-03', interval '1 day') d
   ) d
LEFT   JOIN dates_ranges dr ON dr.kind = k.kind
                           AND d.as_of_date BETWEEN dr.start_date AND dr.end_date
GROUP  BY 1, 2
ORDER  BY 1, 2;
Run Code Online (Sandbox Code Playgroud)


Col*_*art 4

如果“缺少零”没问题,以下查询也适用:

select *
from (
  select
    kind,
    generate_series(start_date, end_date, interval '1 day')::date as d,
    count(*)
  from dates_ranges
  group by 1, 2
) x
where d between date '2018-01-01' and date '2018-01-03'
order by 1, 2;
Run Code Online (Sandbox Code Playgroud)

但它并不比lateral小数据集的版本快。不过,它可能会更好地扩展,因为不需要连接,但上面的版本聚合了所有行,因此它可能会再次失败。

以下查询尝试通过删除任何不重叠的系列来避免不必要的工作:

select
  kind,
  generate_series(greatest(start_date, date '2018-01-01'), least(end_date, date '2018-01-03'), interval '1 day')::date as d,
  count(*)
from dates_ranges
where (start_date, end_date + interval '1 day') overlaps (date '2018-01-01', date '2018-01-03' + interval '1 day')
group by 1, 2
order by 1, 2;
Run Code Online (Sandbox Code Playgroud)

——我必须使用overlaps接线员!请注意,您必须添加interval '1 day'到右侧,因为重叠运算符认为时间段在右侧开放(这是相当合乎逻辑的,因为日期通常被认为是具有午夜时间部分的时间戳)。