使用postgresql版本> 10,使用内置的generate_series
函数生成日期序列时遇到一个问题。从本质上讲,它不正确地符合day of the month
。
我有许多不同的频率(由用户提供),需要在给定的开始日期和结束日期之间进行计算。开始日期可以是任何日期,也可以是一个月中的任何一天。如以下输出所示,当将monthly
之类的频率与2018-01-31
或2018-01-30
的开始日期结合使用时,会产生问题。
我创建了一个解决方案,并希望将其发布在这里供其他人使用,因为我找不到其他解决方案。
但是,在进行一些测试后,我发现与(内置)generate_series
相比,在(大范围)日期范围上使用时,我的解决方案具有与内置select generate_series(date '2018-01-31',
date '2018-05-31',
interval '1 month')::date
as frequency;
不同的性能。有谁知道如何改进它?
TL; DR :如果可能会避免循环,因为它们会影响性能,请滚动至底部以改进实施。
frequency
------------
2018-01-31
2018-02-28
2018-03-28
2018-04-28
2018-05-28
生成:
28 due to the month of februari
从输出中可以看出,不遵守月份中的某天并将其截断为沿途遇到的最短日期,在这种情况下为create or replace function generate_date_series(
startsOn date,
endsOn date,
frequency interval)
returns setof date as $$
declare
intervalOn date := startsOn;
count int := 1;
begin
while intervalOn <= endsOn loop
return next intervalOn;
intervalOn := startsOn + (count * frequency);
count := count + 1;
end loop;
return;
end;
$$ language plpgsql immutable;
select generate_date_series(date '2018-01-31',
date '2018-05-31',
interval '1 month')
as frequency;
。
由于这个问题,我创建了一个自定义函数:
frequency
------------
2018-01-31
2018-02-28
2018-03-31
2018-04-30
2018-05-31
生成:
generate_series
无论提供什么日期范围,内置select generate_series(date '1900-01-01',
date '10000-5-31',
interval '1 month')::date
as frequency;
的以下性能平均为 2毫秒:
generate_date_series
自定义函数select generate_date_series(date '1900-01-01',
date '10000-5-31',
interval '1 month')::date
as frequency;
在以下方面的平均表现为 120ms :
generate_date_series
实际上,此类范围永远不会发生,因此这不是问题。对于大多数查询,自定义generate_date_series
将获得相同的性能。虽然,我确实想知道是什么导致了差异。
无论提供什么范围,内置函数为何都能平均保持 2ms 的原因?
是否有更好的方法来实现性能generate_series
与内置create or replace function generate_date_series(startsOn date, endsOn date, frequency interval)
returns setof date as $$
select (startsOn + (frequency * count))::date
from (
select (row_number() over ()) - 1 as count
from generate_series(startsOn, endsOn, frequency)
) series
$$ language sql immutable;
一样好?
(来自@eurotrash的答案)
generate_date_series
通过改进的实现,select generate_date_series(date '1900-01-01',
date '10000-5-31',
interval '1 month')::date
as frequency;
函数在以下方面的平均性能为 45ms :
generate_series
@eurotrash提供的实现平均给了我 80毫秒,我认为这是由于两次调用{{1}}函数造成的。
答案 0 :(得分:3)
为什么您的函数运行缓慢:您使用变量和(更重要的是)使用循环。循环很慢。变量还意味着对这些变量的读取和写入。
CREATE OR REPLACE FUNCTION generate_date_series_2(starts_on DATE, ends_on DATE, frequency INTERVAL)
RETURNS SETOF DATE AS
$BODY$
SELECT (starts_on + (frequency * g))::DATE
FROM generate_series(0, (SELECT COUNT(*)::INTEGER - 1 FROM generate_series(starts_on, ends_on, frequency))) g;
$BODY$
LANGUAGE SQL IMMUTABLE;
该概念与您的plpgsql函数基本相同,但是通过单个查询而不是循环。唯一的问题是确定需要进行多少次迭代(即generate_series的第二个参数)。可悲的是,除了为日期调用generate_series并使用其计数之外,我想不出一种更好的方法来获取所需的间隔数。当然,如果您知道间隔只会是某个值,那么就有可能进行优化;但是此版本可以处理任何间隔值。
在我的系统上,它比纯generate_series慢50%,比plpgsql版本慢400%。
答案 1 :(得分:1)
修订的解决方案
这在7秒内为我提供了97,212行(每行约0.7ms),并且还支持leap-years
,其中2月为29天:
SELECT t.day_of_month
FROM (
SELECT ds.day_of_month
, date_part('day', ds.day_of_month) AS day
, date_part('day', ((day_of_month - date_part('day', ds.day_of_month)::INT + 1) + INTERVAL '1' MONTH) - INTERVAL '1' DAY) AS eom
FROM (
SELECT generate_series( date '1900-01-01',
date '10000-12-31',
INTERVAL '1 day')::DATE as day_of_month
) AS ds
) AS t
--> REMEMBER to change the day at both places below (eg. 31)
WHERE t.day = 31 OR (t.day = t.eom AND t.day < 31)
输出数据:
答案 2 :(得分:1)
您可以使用date_trunc
并在generate_series
的输出中添加一个月,效果应该几乎相似。
SELECT
(date_trunc('month', dt) + INTERVAL '1 MONTH - 1 day') ::DATE AS frequency
FROM
generate_series(
DATE '2018-01-31', DATE '2018-05-31',
interval '1 MONTH'
) AS dt
测试
knayak=# select generate_series(date '2018-01-31',
knayak(# date '2018-05-31',
knayak(# interval '1 month')::date
knayak-# as frequency;
frequency
------------
2018-01-31
2018-02-28
2018-03-28
2018-04-28
2018-05-28
(5 rows)
Time: 0.303 ms
knayak=#
knayak=#
knayak=# SELECT
knayak-# (date_trunc('month', dt) + INTERVAL '1 MONTH - 1 day' ):: DATE AS frequency
knayak-# FROM
knayak-# generate_series(
knayak(# DATE '2018-01-31', DATE '2018-05-31',
knayak(# interval '1 MONTH'
knayak(# ) AS dt
knayak-# ;
frequency
------------
2018-01-31
2018-02-28
2018-03-31
2018-04-30
2018-05-31
(5 rows)
Time: 0.425 ms
答案 3 :(得分:0)
简单的解决方案:
SELECT '2000-01-31'::DATE + ('1 MONTH'::INTERVAL)*x FROM generate_series(0,100) x;
缺点:
由于 generate_series() 参数是整数,所以需要计算。
巨大优势:
generate_series() 在其参数为整数时向优化器提供正确的行数估计值,但当其参数为日期和间隔时,它不够智能:
这非常重要,尤其是当您使用它来构建一个巨大的系列时。使用日期参数将始终返回默认的 1000 行估计值,这可能会导致优化器执行灾难性计划。
CREATE UNLOGGED TABLE foo( id SERIAL PRIMARY KEY, dt TIMESTAMP NOT NULL );
INSERT INTO foo (dt) SELECT '2000-01-01'::TIMESTAMP + ('1 SECOND'::INTERVAL)*x FROM generate_series(1,1000000) x;
CREATE INDEX foo_dt ON foo(dt);
VACUUM ANALYZE foo;
EXPLAIN ANALYZE
WITH d AS (SELECT '2000-01-01'::TIMESTAMP + ('10 SECOND'::INTERVAL)*x dt FROM generate_series(1,100000) x)
SELECT * FROM foo JOIN d USING (dt);
Hash Join (cost=27906.00..30656.00 rows=100000 width=12) (actual time=191.020..237.268 rows=100000 loops=1)
Hash Cond: (('2000-01-01 00:00:00'::timestamp without time zone + ('00:00:10'::interval * (x.x)::double precision)) = foo.dt)
-> Function Scan on generate_series x (cost=0.00..1000.00 rows=100000 width=4) (actual time=7.070..11.096 rows=100000 loops=1)
CORRECT ESTIMATE -------------------------------------------------^
-> Hash (cost=15406.00..15406.00 rows=1000000 width=12) (actual time=181.844..181.845 rows=1000000 loops=1)
Buckets: 1048576 Batches: 1 Memory Usage: 51161kB
-> Seq Scan on foo (cost=0.00..15406.00 rows=1000000 width=12) (actual time=0.009..64.702 rows=1000000 loops=1)
EXPLAIN ANALYZE
WITH d AS (SELECT generate_series('2000-01-01'::TIMESTAMP, '2000-01-12 13:46:40'::TIMESTAMP, '10 SECOND'::INTERVAL) dt)
SELECT * FROM foo JOIN d USING (dt);
Nested Loop (cost=0.42..7515.52 rows=1000 width=12) (actual time=0.050..139.251 rows=100000 loops=1)
-> ProjectSet (cost=0.00..5.02 rows=1000 width=8) (actual time=0.006..5.493 rows=100001 loops=1)
WRONG ESTIMATE ----------------------^
-> Result (cost=0.00..0.01 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=1)
-> Index Scan using foo_dt on foo (cost=0.42..7.49 rows=1 width=12) (actual time=0.001..0.001 rows=1 loops=100001)
Index Cond: (dt = (generate_series('2000-01-01 00:00:00'::timestamp without time zone, '2000-01-12 13:46:40'::timestamp without time zone, '00:00:10'::interval)))
根据正确的估计,它使用哈希值,这是正确的做法。由于错误的,太低的估计,它使用嵌套循环索引扫描代替。如果星星对齐得恰到好处,那就是每页一个随机 IO。