PostgreSQL生成日期系列(性能)

时间:2018-11-10 13:55:42

标签: sql postgresql dateinterval generate-series

使用postgresql版本> 10,使用内置的generate_series函数生成日期序列时遇到一个问题。从本质上讲,它不正确地符合day of the month

我有许多不同的频率(由用户提供),需要在给定的开始日期和结束日期之间进行计算。开始日期可以是任何日期,也可以是一个月中的任何一天。如以下输出所示,当将monthly之类的频率与2018-01-312018-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}}函数造成的。

4 个答案:

答案 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)

结果输出: 请确保您更改红色的两者的日期。 Performance Output

输出数据:

Data Output

答案 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 

Demo

测试

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。