Postgresql generate date series (performance)
使用postgresql版本> 10时,我在使用内置
我有许多不同的频率(由用户提供),需要在给定的开始和结束日期之间进行计算。开始日期可以是任何日期,因此也可以是该月的任何一天。当具有诸如
我创建了一个解决方案,并希望在此发布此内容供其他人使用,因为我找不到任何其他解决方案。
但是,经过一些测试后,我发现在(荒谬的)大日期范围内使用时,我的解决方案与内置
TL; DR:如果可能的话,避免循环,因为它们是性能损失,滚动到底部以改进实现。
内置输出
1 2 3 4 | SELECT generate_series(DATE '2018-01-31', DATE '2018-05-31', INTERVAL '1 month')::DATE AS frequency; |
产生:
1 2 3 4 5 6 7 | frequency ------------ 2018-01-31 2018-02-28 2018-03-28 2018-04-28 2018-05-28 |
从输出中可以看出,该月中的某一天未得到遵守并被截断为沿途遇到的最小日期,在这种情况下:
预期产出
由于这个问题,我创建了一个自定义函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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; |
产生:
1 2 3 4 5 6 7 | frequency ------------ 2018-01-31 2018-02-28 2018-03-31 2018-04-30 2018-05-31 |
性能比较
无论提供什么日期范围,内置
1 2 3 4 | SELECT generate_series(DATE '1900-01-01', DATE '10000-5-31', INTERVAL '1 month')::DATE AS frequency; |
而自定义函数
1 2 3 4 | SELECT generate_date_series(DATE '1900-01-01', DATE '10000-5-31', INTERVAL '1 month')::DATE AS frequency; |
题
实际上,这样的范围永远不会发生,因此它不是问题。对于大多数查询,自定义
无论提供什么范围,内置功能是否能够平均达到2ms的恒定性能?
有没有更好的方法来实现
改进实现没有循环
(来自@eurotrash的答案)
1 2 3 4 5 6 7 8 | 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; |
通过改进的实现,
1 2 3 4 | SELECT generate_date_series(DATE '1900-01-01', DATE '10000-5-31', INTERVAL '1 month')::DATE AS frequency; |
@eurotrash提供的实现平均给我80ms,我假设是由于两次调用
为什么你的函数很慢:你使用变量和(更重要的是)循环。循环很慢。变量还意味着从这些变量读取和写入。
1 2 3 4 5 6 7 | 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 2 3 4 5 6 7 | 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 |
演示
测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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 |
修订后的解决方案
这在7秒内给出了97,212行(每行大约0.7ms)并且还支持
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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) |
结果输出:
请确保您在同一天更改RED号码。
输出数据: