16

我正在尝试选择在接下来的 14 天内有周年纪念日的日期。如何根据不包括年份的日期进行选择?我尝试过类似以下的方法。

SELECT * FROM events
WHERE EXTRACT(month FROM "date") = 3
AND EXTRACT(day FROM "date") < EXTRACT(day FROM "date") + 14

这个问题是几个月换行。
我宁愿做这样的事情,但我不知道如何忽略这一年。

SELECT * FROM events
WHERE (date > '2013-03-01' AND date < '2013-04-01')

如何在 Postgres 中完成这种日期数学?

4

8 回答 8

44

如果您不关心解释和细节,请使用下面的“黑魔法版”

到目前为止,其他答案中提供的所有查询都使用不可搜索的条件进行操作——它们不能使用索引,并且必须为基表中的每一行计算一个表达式以查找匹配的行。与小桌子无关紧要。大桌子重要。

给定以下简单表格:

CREATE TABLE event (
  event_id   serial PRIMARY KEY
, event_date date
);

询问

下面的版本 1. 和 2. 可以使用以下形式的简单索引:

CREATE INDEX event_event_date_idx ON event(event_date);

但是如果没有 index ,以下所有解决方案都更快

1.简单版

SELECT *
FROM  (
   SELECT ((current_date + d) - interval '1 year' * y)::date AS event_date
   FROM       generate_series( 0,  14) d
   CROSS JOIN generate_series(13, 113) y
   ) x
JOIN  event USING (event_date);

子查询从两次调用x中计算给定年份范围内的所有可能日期。选择是通过最终的简单连接完成的。CROSS JOINgenerate_series()

2.进阶版

WITH val AS (
   SELECT extract(year FROM age(current_date + 14, min(event_date)))::int AS max_y
        , extract(year FROM age(current_date,      max(event_date)))::int AS min_y
   FROM   event
   )
SELECT e.*
FROM  (
   SELECT ((current_date + d.d) - interval '1 year' * y.y)::date AS event_date
   FROM   generate_series(0, 14) d
        ,(SELECT generate_series(min_y, max_y) AS y FROM val) y
   ) x
JOIN  event e USING (event_date);

年份的范围是从表格中自动推导出来的 - 从而最大限度地减少生成的年份。
如果存在差距,您可以更进一步,提取现有年份的列表。

有效性共同取决于日期的分布。几年每行都有很多行,这使得这个解决方案更有用。多年来,每行很少,因此它的用处不大。

简单的 SQL Fiddle玩。

3.黑魔法版

2016 年更新删除了一个“生成的列”,它会阻止 HOT 更新;更简单,更快捷的功能。
更新 2018 以使用IMMUTABLE表达式计算 MMDD 以允许函数内联。

创建一个简单的 SQL 函数来integer从模式中计算一个'MMDD'

CREATE FUNCTION f_mmdd(date) RETURNS int LANGUAGE sql IMMUTABLE AS
'SELECT (EXTRACT(month FROM $1) * 100 + EXTRACT(day FROM $1))::int';

起初我有to_char(time, 'MMDD'),但切换到上面的表达式,这在 Postgres 9.6 和 10 的新测试中证明是最快的:

db<>在这里摆弄

它允许函数内联,因为在内部使用函数EXTRACT (xyz FROM date)实现。它必须允许它在以下基本多列表达式索引中使用:IMMUTABLEdate_part(text, date)IMMUTABLE

CREATE INDEX event_mmdd_event_date_idx ON event(f_mmdd(event_date), event_date);

多列有多种原因:
可以帮助ORDER BY或从给定年份中进行选择。在这里阅读。索引几乎没有额外成本。Adate适合 4 个字节,否则会因数据对齐而丢失填充。在这里阅读。
此外,由于两个索引列都引用同一个表列,因此在HOT更新方面没有缺点。在这里阅读。

一个 PL/pgSQL 表函数来统治它们

分叉到两个查询之一以涵盖年初:

CREATE OR REPLACE FUNCTION f_anniversary(date = current_date, int = 14)
  RETURNS SETOF event AS
$func$
DECLARE
   d  int := f_mmdd($1);
   d1 int := f_mmdd($1 + $2 - 1);  -- fix off-by-1 from upper bound
BEGIN
   IF d1 > d THEN
      RETURN QUERY
      SELECT *
      FROM   event e
      WHERE  f_mmdd(e.event_date) BETWEEN d AND d1
      ORDER  BY f_mmdd(e.event_date), e.event_date;

   ELSE  -- wrap around end of year
      RETURN QUERY
      SELECT *
      FROM   event e
      WHERE  f_mmdd(e.event_date) >= d OR
             f_mmdd(e.event_date) <= d1
      ORDER  BY (f_mmdd(e.event_date) >= d) DESC, f_mmdd(e.event_date), event_date;
      -- chronological across turn of the year
   END IF;
END
$func$  LANGUAGE plpgsql;

使用默认值调用:从“今天”开始的 14 天:

SELECT * FROM f_anniversary();

呼吁从“2014-08-23”开始的 7 天:

SELECT * FROM f_anniversary(date '2014-08-23', 7);

SQL Fiddle比较EXPLAIN ANALYZE

2月29日

在处理周年纪念日或“生日”时,您需要定义闰年特殊情况“2 月 29 日”的处理方式。

在测试日期范围时,Feb 29通常会自动包含在内,即使当前年份不是闰年。当涵盖这一天时,日期范围追溯延长 1 天。
另一方面,如果当前年份是闰年,并且您想要查找 15 天,那么如果您的数据来自非闰年,您最终可能会在闰年获得 14 天的结果。

假设 Bob 出生于 2 月 29 日:
我的查询 1. 和 2. 仅在闰年包括 2 月 29 日。Bob 每 4 年才过一次生日。
我的查询 3. 在范围内包括 2 月 29 日。鲍勃每年过生日。

没有神奇的解决方案。您必须为每种情况定义您想要的内容。

测试

为了证实我的观点,我对所有提出的解决方案进行了广泛的测试。我将每个查询调整到给定的表,并在没有ORDER BY.

好消息:所有这些都是正确的并且产生相同的结果 - 除了 Gordon 的查询有语法错误,以及 @wildplasser 的查询在年份结束时失败(易于修复)。

插入 108000 行随机日期为 20 世纪的行,这类似于在世人(13 岁或以上)的表格。

INSERT INTO  event (event_date)
SELECT '2000-1-1'::date - (random() * 36525)::int
FROM   generate_series (1, 108000);

删除 ~ 8 % 以创建一些死元组并使表格更加“真实”。

DELETE FROM event WHERE random() < 0.08;
ANALYZE event;

我的测试用例有 99289 行,4012 次点击。

C -猫叫

WITH anniversaries as (
   SELECT event_id, event_date
         ,(event_date + (n || ' years')::interval)::date anniversary
   FROM   event, generate_series(13, 113) n
   )
SELECT event_id, event_date -- count(*)   --
FROM   anniversaries
WHERE  anniversary BETWEEN current_date AND current_date + interval '14' day;

C1 - Catcall 的想法被改写

除了小的优化之外,主要的区别是只添加确切的年份 date_trunc('year', age(current_date + 14, event_date))来获得今年的周年纪念日,这完全避免了对 CTE 的需要:

SELECT event_id, event_date
FROM   event
WHERE (event_date + date_trunc('year', age(current_date + 14, event_date)))::date
       BETWEEN current_date AND current_date + 14;

D -丹尼尔

SELECT *   -- count(*)   -- 
FROM   event
WHERE  extract(month FROM age(current_date + 14, event_date))  = 0
AND    extract(day   FROM age(current_date + 14, event_date)) <= 14;

E1 - 欧文 1

参见上面的“1. 简单版”。

E2 - 欧文 2

参见上面的“2. 高级版”。

E3 - 欧文 3

见上文“3.黑魔法版”。

G -戈登

SELECT * -- count(*)   
FROM  (SELECT *, to_char(event_date, 'MM-DD') AS mmdd FROM event) e
WHERE  to_date(to_char(now(), 'YYYY') || '-'
                 || (CASE WHEN mmdd = '02-29' THEN '02-28' ELSE mmdd END)
              ,'YYYY-MM-DD') BETWEEN date(now()) and date(now()) + 14;

H - a_horse_with_no_name

WITH upcoming as (
   SELECT event_id, event_date
         ,CASE 
            WHEN date_trunc('year', age(event_date)) = age(event_date)
                 THEN current_date
            ELSE cast(event_date + ((extract(year FROM age(event_date)) + 1)
                      * interval '1' year) AS date) 
          END AS next_event
   FROM event
   )
SELECT event_id, event_date
FROM   upcoming
WHERE  next_event - current_date  <= 14;

W -野生动物

CREATE OR REPLACE FUNCTION this_years_birthday(_dut date) RETURNS date AS
$func$
DECLARE
    ret date;
BEGIN
    ret :=
    date_trunc( 'year' , current_timestamp)
        + (date_trunc( 'day' , _dut)
         - date_trunc( 'year' , _dut));
    RETURN ret;
END
$func$ LANGUAGE plpgsql;

简化为与其他所有返回相同:

SELECT *
FROM   event e
WHERE  this_years_birthday( e.event_date::date )
        BETWEEN current_date
        AND     current_date + '2weeks'::interval;

W1 - wildplasser 的查询重写

以上内容存在许多低效的细节(超出了这篇已经相当大的帖子的范围)。重写的版本快得多:

CREATE OR REPLACE FUNCTION this_years_birthday(_dut INOUT date) AS
$func$
SELECT (date_trunc('year', now()) + ($1 - date_trunc('year', $1)))::date
$func$ LANGUAGE sql;

SELECT *
FROM   event e
WHERE  this_years_birthday(e.event_date)
        BETWEEN current_date
        AND    (current_date + 14);

测试结果

我在 PostgreSQL 9.1.7 上使用临时表运行了这个测试。EXPLAIN ANALYZE结果以 5 的最好成绩收集。

结果

无索引
C:总运行时间:76714.723 毫秒
C1:总运行时间:307.987 ms -- !
D:总运行时间:325.549 毫秒
E1:总运行时间:    253.671 ms -- !
E2:总运行时间:484.698 ms -- min() & max() 没有索引很昂贵
E3:总运行时间:    213.805 ms -- !
G:总运行时间:984.788 毫秒
H:总运行时间:977.297 毫秒
W:总运行时间:2668.092 毫秒
W1:总运行时间:596.849 ms -- !

使用索引
E1:总运行时间:     37.939 ms --!! 
E2:总运行时间:     38.097 毫秒 --!!

使用表达式
E3 上的索引:总运行时间:     11.837 ms --!!

所有其他查询在有或没有索引的情况下都执行相同的操作,因为它们使用非 sargable表达式。

结论

  • 到目前为止,@Daniel 的查询是最快的。

  • @wildplassers(重写)方法也可以接受。

  • @Catcall 的版本类似于我的相反方法。使用更大的桌子时,性能很快就会失控。
    不过,重写后的版本表现相当不错。我使用的表达式类似于@wildplassserthis_years_birthday()函数的更简单版本。

  • 即使没有 index ,我的“简单版本”也更快,因为它需要更少的计算。

  • min()使用索引,“高级版”与“简单版”差不多快,因为使用索引max()变得非常便宜。两者都比其他无法使用索引的速度快得多。

  • 无论有没有 index ,我的“黑魔法版”都是最快的。而且调用起来非常简单。
    更新后的版本(基准测试之后)要快一些。

  • 对于现实生活中的表,索引将产生更大的差异。更多的列使表更大,顺序扫描更昂贵,而索引大小保持不变。

于 2013-03-02T21:40:21.690 回答
7

我相信以下测试适用于所有情况,假设列名为anniv_date

select * from events
where extract(month from age(current_date+interval '14 days', anniv_date))=0
  and extract(day from age(current_date+interval '14 days', anniv_date)) <= 14

作为跨越一年(以及一个月)时如何工作的示例,假设周年日期是2009-01-04并且运行测试的日期是2012-12-29

我们要考虑介于2012-12-292013-01-12(14 天)之间的任何日期

age('2013-01-12'::date, '2009-01-04'::date)4 years 8 days

extract(month...)从 this is0extract(days...)is 8,它比14它匹配的要低。

于 2013-03-02T12:53:41.437 回答
3

这个怎么样?

select *
from events e
where to_char(e."date", 'MM-DD') between to_char(now(), 'MM-DD') and 
                                         to_char(date(now())+14, 'MM-DD')

您可以将比较作为字符串进行。

考虑到年终,我们将转换回日期:

select *
from events e
where to_date(to_char(now(), 'YYYY')||'-'||to_char(e."date", 'MM-DD'), 'YYYY-MM-DD')
           between date(now()) and date(now())+14

您确实需要在 2 月 29 日稍作调整。我可能会建议:

select *
from (select e.*,
             to_char(e."date", 'MM-DD') as MMDD
      from events
     ) e
where to_date(to_char(now(), 'YYYY')||'-'||(case when MMDD = '02-29' then '02-28' else MMDD), 'YYYY-MM-DD')
           between date(now()) and date(now())+14
于 2013-03-02T01:15:56.953 回答
2

为方便起见,我创建了两个函数来生成当年的(预期或过去的)生日和即将到来的生日。

CREATE OR REPLACE FUNCTION this_years_birthday( _dut DATE) RETURNS DATE AS
$func$

DECLARE
        ret DATE;
BEGIN
        ret =
        date_trunc( 'year' , current_timestamp)
        + (date_trunc( 'day' , _dut)
          - date_trunc( 'year' , _dut)
          )
        ;
        RETURN ret;
END;
$func$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION next_birthday( _dut DATE) RETURNS DATE AS
$func$

DECLARE
        ret DATE;
BEGIN
        ret =
        date_trunc( 'year' , current_timestamp)
        + (date_trunc( 'day' , _dut)
          - date_trunc( 'year' , _dut)
          )
        ;
        IF (ret < date_trunc( 'day' , current_timestamp))
           THEN ret = ret + '1year'::interval; END IF;
        RETURN ret;
END;
$func$ LANGUAGE plpgsql;

      --
      -- call the function
      --
SELECT date_trunc( 'day' , t.topic_date) AS the_date
        , this_years_birthday( t.topic_date::date ) AS the_day
        , next_birthday( t.topic_date::date ) AS next_day
FROM topic t
WHERE this_years_birthday( t.topic_date::date )
        BETWEEN  current_date
        AND  current_date + '2weeks':: interval
        ;

注意:需要演员表,因为我只有可用的时间戳。

于 2013-03-02T13:21:43.757 回答
1

您可以生成一个虚拟的周年纪念表,并从中进行选择。

with anniversaries as (
  select event_date, 
         (event_date + (n || ' years')::interval)::date anniversary
  from events, generate_series(1,10) n
)
select event_date, anniversary
from anniversaries
where anniversary between current_date and current_date + interval '14' day
order by event_date, anniversary

调用generate_series(1,10)具有为每个 event_date 生成 10 周年纪念日的效果。我不会在生产中使用文字值 10 。相反,我要么计算在子查询中使用的正确年数,要么使用像 100 这样的大字面量。

您需要调整 WHERE 子句以适合您的应用程序。

如果虚拟表存在性能问题(当“事件”中有很多行时),请将公用表表达式替换为具有相同结构的基表。将周年纪念日存储在基表中会使它们的值显而易见(尤其是对于例如 2 月 29 日的周年纪念日),并且对此类表的查询可以使用索引。在我的桌面上仅使用上面的 SELECT 语句查询一百万行的周年纪念表需要 25 毫秒。

于 2013-03-02T01:32:09.427 回答
1

这也应该在年底处理环绕:

with upcoming as (
  select name, 
         event_date,
         case 
           when date_trunc('year', age(event_date)) = age(event_date) then current_date
           else cast(event_date + ((extract(year from age(event_date)) + 1) * interval '1' year) as date) 
         end as next_event
  from events
)
select name, 
       next_event, 
       next_event - current_date as days_until_next
from upcoming
order by next_event - current_date 

您可以过滤表达式next_event - current_date以应用“未来 14 天”

case ...仅当您将“今天”的事件也视为“即将发生”的事件时才需要。否则,可以将其简化为elsecase 语句的一部分。

请注意,我将列“重命名”"date"event_date. 主要是因为保留字不应该用作标识符,还因为date它是一个糟糕的列名。它不会告诉你任何关于它存储什么的信息。

于 2013-03-02T13:49:10.907 回答
0

我找到了一种方法。

SELECT EXTRACT(DAYS FROM age('1999-04-10', '2003-05-12')), 
       EXTRACT(MONTHS FROM age('1999-04-10', '2003-05-12'));
 date_part | date_part 
-----------+-----------
        -2 |        -1

然后我可以检查月份是否为 0 并且天数是否小于 14。

如果您有更优雅的解决方案,请发布。我将把这个问题留一会儿。

于 2013-03-02T01:02:47.603 回答
0

我不使用 postgresql,所以我用谷歌搜索了它的日期函数并发现了这个: http ://www.postgresql.org/docs/current/static/functions-datetime.html

如果我没看错的话,在接下来的 14 天内寻找事件就像这样简单:

 where mydatefield >= current_date
 and mydatefield < current_date + integer '14'

当然,我可能没有正确阅读它。

于 2013-03-02T01:30:49.027 回答