你怎么做数学忽略年份?

时间:2013-03-02 00:56:39

标签: sql postgresql date datetime indexing

我正在尝试选择在接下来的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中完成这种日期数学?

8 个答案:

答案 0 :(得分:37)

如果您不在乎解释和详细信息,请使用下面的&#34; Black magic版本&#34;

到目前为止提供的所有查询都使用not sargable条件 - 它们不能使用索引,必须为基表中的每一行计算表达式以查找匹配的行。对于小桌子,这并不重要。但是,对于大表,这很重要 很多

给出以下简单表:

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

查询

版本1.和2.可以使用表单的简单索引:

CREATE INDEX event_event_date_idx ON event(event_date);

但是以下解决方案甚至更快,没有索引

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()WITH val AS ( SELECT extract(year FROM age(now()::date + 14, min(event_date)))::int AS max_y , extract(year FROM age(now()::date, max(event_date)))::int AS min_y FROM event ) SELECT e.* -- count(*) -- FROM ( SELECT ((current_date + d) - interval '1y' * y.y)::date AS event_date FROM generate_series(0, 14) AS d) d ,(SELECT generate_series(min_y, max_y) AS y FROM val) y ) x JOIN event e USING (event_date); 个调用中给定年份范围内的所有可能日期。选择是通过简单的等连接完成的。

2。高级版

IMMUTABLE

自动从表格中推断出年份范围 - 从而最大限度地减少生成的年份 如果你的年限有差距,你甚至可以更进一步,提炼现有年份的清单。

有效性取决于日期的分布。几年中有许多行使我的解决方案更有用。多年的行数很少,每个行都没那么有用。

Simple SQL Fiddle可以玩。

3。黑魔法版

更新2016以删除不必要的&#34;生成的列&#34;,这将阻止H.O.T更新,并使用更简单,更快速的功能。
更新了2018以使用integer表达式计算MMDD以允许函数内联。

创建一个简单的SQL函数来从模式'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')

我最初使用EXTRACT (xyz FROM date),但切换到上面的表达式,结果证明在Postgres 9.6和10的新测试中速度最快:

db&lt;&gt;小提琴here

它仍然允许function inlining,因为它只使用IMMUTABLE - 内部使用date_part(text, date)函数CREATE INDEX event_mmdd_event_date_idx ON event(f_mmdd(event_date), event_date); 实现。

在多列表达式索引中必须使用IMMUTABLE

ORDER BY

多列有多种原因: 可以帮助date或从给定年份中选择。阅读here。几乎没有额外的索引成本。 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 due to including 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; 适合4个字节,否则由于数据对齐而会丢失填充。阅读here
此外,由于两个索引列都引用相同的表列,因此 H.O.T。更新没有任何缺陷。阅读here

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

分叉到两个查询中的一个来覆盖今年的转折。

SELECT * FROM f_anniversary();

使用默认设置:14天开始&#34;今天&#34;:

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

开始7天开始&#39; 2014-08-23&#39;:

EXPLAIN ANALYZE

SQL Fiddle比较Feb 29

2月29日

在处理周年纪念日或生日时,您需要定义如何在闰年2月29日处理特殊情况。

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

说,鲍勃出生于2月29日:
我的查询1.和2.仅包括2月29日的闰年。鲍勃每隔约4年就过生日 我的查询3.包括2月29日的范围。鲍勃每年都过生日。

没有神奇的解决方案。你必须为每个案例定义你想要的东西。

测试

为了证实我的观点,我对所有提出的解决方案进行了广泛的测试。我将每个查询调整到给定的表,并在没有INSERT INTO event (event_date) SELECT '2000-1-1'::date - (random() * 36525)::int FROM generate_series (1, 108000); 的情况下生成相同的结果。

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

插入具有20世纪随机日期的108000行,类似于生活人员表(13岁或以上)。

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

删除~8%以创建一些死元组并使表更多&#34;现实生活&#34;。

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;

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

C - Catcall

date_trunc('year', age(current_date + 14, event_date))

C1 - 改变了Catcall的想法

除了次要的优化之外,主要区别在于只添加年度的确切年数 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; 才能获得今年的周年纪念日,这样就完全无需CTE :

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;

D - Daniel

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;

E1 - Erwin 1

参见&#34; 1。简单版本&#34;上方。

E2 - Erwin 2

参见&#34; 2。高级版&#34;上方。

<3> E3 - Erwin 3

参见&#34; 3。黑魔法版&#34;上方。

G - Gordon

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;

H - a_horse_with_no_name

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;

W - wildplasser

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

简化以返回与所有其他人相同的内容:

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);

W1 - 重写了wildplasser的查询

以上内容存在一些效率低下的细节(超出了这个已经很大的帖子的范围)。重写的版本很多更快:

EXPLAIN ANALYZE

测试结果

我在PostgreSQL 9.1.7上使用临时表运行此测试。 结果以this_years_birthday()收集,最好为5。

结果

Without index
C:  Total runtime: 76714.723 ms
C1: Total runtime: 307.987 ms   -- !
D:  Total runtime: 325.549 ms
E1: Total runtime: 253.671 ms  -- !
E2: Total runtime: 484.698 ms   -- min() & max() expensive without index
E3: Total runtime: 213.805 ms  -- !
G:  Total runtime: 984.788 ms
H:  Total runtime: 977.297 ms
W:  Total runtime: 2668.092 ms
W1: Total runtime: 596.849 ms   -- !

With index
E1: Total runtime: 37.939 ms   --!!
E2: Total runtime: 38.097 ms   --!!

With index on expression
E3: Total runtime: 11.837 ms   --!!

所有其他查询在使用或不使用索引时执行相同操作,因为它们使用不可搜索的表达式。

Conclusio

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

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

  • @Catcall的版本就像我的反向方法。使用更大的桌子,性能会迅速失控 尽管如此,重写的版本表现还算不错。我使用的表达式类似于@ wildplassser&{39} min()函数的简单版本。

  • 我的简单版本&#34;更快即使没有索引,因为它需要更少的计算。

  • 使用索引,&#34;高级版本&#34;与&#34;简单版本&#34;一样快,因为max()和{{1}}使用索引变得非常便宜。两者都比不能使用指数的其他人快得多。

  • 我的&#34;黑魔法版&#34;有或没有索引是最快的。而且非常很容易打电话 更新后的版本(在基准测试之后)有点快。

  • 使用真实生活表,索引会使更大差异。更多列使表更大,顺序扫描更昂贵,而索引大小保持不变。

答案 1 :(得分: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...)来自0extract(days...)8,低于14,因此匹配。

答案 2 :(得分:2)

这个怎么样?

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

答案 3 :(得分: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
        ;

注意:需要强制转换,因为我只有时间戳可用。

答案 4 :(得分: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语句查询50万行的周年纪念日需要25毫秒的桌面。

答案 5 :(得分: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 ...只有在您将“今天”的事件视为“即将发生”时才是必要的。否则,可以将其缩减为案例陈述的else部分。

请注意,我已将“{1}}列”重命名为"date"。主要是因为保留字不应该用作标识符,而且因为event_date是一个糟糕的列名。它没有告诉你它存储的内容。

答案 6 :(得分: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。

如果您有更优雅的解决方案,请发布。我会暂时搁置这个问题。

答案 7 :(得分:0)

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

如果我读得正确,在接下来的14天内查找事件就像这样简单:

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

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