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 JOIN
和GROUP 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 行(但它可能会显着增长)。
没有索引。确切地说,在我的情况下,它是子查询的结果,但我想将问题限制在一个问题上,因此它更通用。
以及如何包含 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)
如果“缺少零”没问题,以下查询也适用:
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'
到右侧,因为重叠运算符认为时间段在右侧开放(这是相当合乎逻辑的,因为日期通常被认为是具有午夜时间部分的时间戳)。