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


12

我有一个表(在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');

现在,我想为给定的日期和每种类型计算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 |
+-------+------------+----+

我想出了两种解决方案,一是与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

另一个带有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

我想知道是否有更好的方法编写此查询?以及如何包含对数为零的日期类型?

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

没有索引。准确地说,这是子查询的结果,但是我想将问题限制为一个问题,所以它更笼统。


如果表中的范围不重叠或不重叠,该怎么办。例如,如果您有一个(kind,start,end)=的范围,(1,2018-01-01,2018-01-15)并且(1,2018-01-20,2018-01-25)在确定有多少个重叠日期时是否要考虑这一点?
埃文·卡罗尔

我也很困惑,为什么你的桌子很小?为什么不是2018-01-31或者2018-01-30或者2018-01-29在它时,第1范围具有所有的人?
埃文·卡罗尔

@EvanCarroll中的日期generate_series是外部参数-它们不一定涵盖dates_ranges表中的所有范围。关于第一个问题,我想我不理解-输入中的行dates_ranges是独立的,我不想确定重叠。
BartekCh '18

Answers:


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;

但这并不比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;

-并且我必须使用overlaps运算符!请注意,interval '1 day'由于重叠运算符认为右侧的时间段是打开的,因此必须在右侧添加(这很合逻辑,因为通常将日期视为具有午夜时间成分的时间戳)。


很好,我不知道generate_series可以那样使用。经过几次测试后,我得到了以下观察结果。您的查询在选择的范围长度上确实可以很好地扩展-retret在3年和10年之间几乎没有区别。但是对于较短的时间段(1年),我的解决方案速度更快-我猜测原因是其中存在一些非常长的范围dates_ranges(例如2010-2100),这减慢了您的查询速度。限制start_dateend_date内部查询应该会有所帮助。我需要再进行一些测试。
BartekCh '18

6

以及如何包含对数为零的日期类型?

建立一个包含所有组合的网格,然后 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;

也应该尽快。

LEFT JOIN LATERAL ... on true起初有,但是子查询中有一个聚合c,所以我们总是得到一行并且也可以使用CROSS JOIN。性能无差异。

如果您有一个包含所有相关种类的表,请使用该表而不是使用subquery生成列表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;

至于SELECT列表中的返回集合的函数-我读过它是不可取的,但是,如果只有一个这样的函数,它看起来工作得很好。如果我确定只有一个,那会出问题吗?
BartekCh

@BartekCh:SELECT列表中的单个SRF 可以正常工作。也许添加评论以警告不要添加另一个。或将其移至FROM列表,以从较早版本的Postgres开始。为什么要冒并发症的风险?(这也是标准的SQL,不会混淆来自其他RDBMS的人员。)
Erwin Brandstetter

1

使用daterange类型

PostgreSQL有一个daterange。使用它非常简单。从您的样本数据开始,我们开始使用表上的类型。

BEGIN;
  ALTER TABLE dates_ranges ADD COLUMN myrange daterange;
  UPDATE dates_ranges
    SET myrange = daterange(start_date, end_date, '[]');
  ALTER TABLE dates_ranges
    DROP COLUMN start_date,
    DROP COLUMN end_date;
COMMIT;

-- Now you can create GIST index on it...
CREATE INDEX ON dates_ranges USING gist (myrange);

TABLE dates_ranges;
 kind |         myrange         
------+-------------------------
    1 | [2018-01-01,2018-02-01)
    1 | [2018-01-01,2018-01-06)
    1 | [2018-01-03,2018-01-07)
    2 | [2018-01-01,2018-01-02)
    2 | [2018-01-01,2018-01-03)
    3 | [2018-01-02,2018-01-09)
    3 | [2018-01-05,2018-01-11)
(7 rows)

我想针对给定的日期和每种类型,计算dates_ranges中每个日期落入的行数。

现在要查询它,我们可以逆向执行该过程,并生成一个日期序列,但这是查询本身可以使用containment(@>)运算符通过一个索引来检查日期是否在范围内的功能。

请注意,我们使用timestamp without time zone(停止DST危险)

SELECT d1.kind, day::date, count(d2.kind)
FROM dates_ranges AS d1
CROSS JOIN LATERAL generate_series(
  lower(myrange)::timestamp without time zone,
  upper(myrange)::timestamp without time zone,
  '1 day'
) AS gs(day)
INNER JOIN dates_ranges AS d2
  ON d2.myrange @> day::date
GROUP BY d1.kind, day;

这是该指数的逐日重叠项。

作为附带的好处,使用daterange类型,您可以停止插入与其他区域重叠的区域EXCLUDE CONSTRAINT


您的查询出了点问题,看来它多次计数了行,JOIN我猜这太多了。
BartekCh

@BartekCh不,您有重叠的行,可以通过删除重叠的范围(建议)或使用count(DISTINCT kind)
Evan Carroll

但是我想要重叠的行。例如,实物1日期2018-01-01位于的前两行之内dates_ranges,但您的查询给出8
BartekCh

还是使用count(DISTINCT kind)DISTINCT那里添加了关键字?
埃文·卡罗尔

不幸的是,使用DISTINCT关键字后,它仍然无法正常工作。它为每个日期计算不同的种类,但是我想为每个日期计算每种种类的所有行。
BartekCh
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.