你怎么做数学忽略年份?

And*_*bbs 14 sql postgresql indexing datetime date

我想在接下来的14天里选择一个周年纪念日.如何根据不包括年份的日期进行选择?我尝试过类似下面的内容.

SELECT * FROM events
WHERE EXTRACT(month FROM "date") = 3
AND EXTRACT(day FROM "date") < EXTRACT(day FROM "date") + 14
Run Code Online (Sandbox Code Playgroud)

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

SELECT * FROM events
WHERE (date > '2013-03-01' AND date < '2013-04-01')
Run Code Online (Sandbox Code Playgroud)

我怎样才能在Postgres中完成这种日期数学运算?

Erw*_*ter 39

如果您不在乎解释和细节,请使用下面的"黑魔法版".

到目前为止呈现的所有查询都使用不可搜索的条件- 它们不能使用索引,并且必须为基表中的每一行计算表达式以查找匹配的行.对于小桌子,这并不重要.随着大表,但是,这个重要很多.

给出以下简单表:

CREATE TABLE event (
  event_id   serial PRIMARY KEY
, event_date date
);
Run Code Online (Sandbox Code Playgroud)

询问

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

CREATE INDEX event_event_date_idx ON event(event_date);
Run Code Online (Sandbox Code Playgroud)

但是如果没有索引,以下解决方案会更快.

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);
Run Code Online (Sandbox Code Playgroud)

子查询xCROSS JOIN两次generate_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);
Run Code Online (Sandbox Code Playgroud)

从表中自动推导出年份范围 - 从而最大限度地减少生成的年份.
可能甚至走一步和提炼现有的年份列表,如果你在你的年范围有差距.

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

简单的SQL小提琴玩.

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';
Run Code Online (Sandbox Code Playgroud)

to_char(time, 'MMDD')刚开始使用,但切换到上面的表达式,结果证明在Postgres 9.6和10的新测试中速度最快:

db <> 在这里小提琴

它仍然允许函数内联,因为它只使用EXTRACT (xyz FROM date)- 内部IMMUTABLE函数实现date_part(text, date).

IMMUTABLE必须用于多列表达式索引:

CREATE INDEX event_mmdd_event_date_idx ON event(f_mmdd(event_date), event_date);
Run Code Online (Sandbox Code Playgroud)

多列的一些原因:可以帮助ORDER BY或与给定的年中选择.阅读这里.几乎没有额外的索引成本.A date适合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;
Run Code Online (Sandbox Code Playgroud)

使用默认值呼叫:从"今天"开始14天:

SELECT * FROM f_anniversary();
Run Code Online (Sandbox Code Playgroud)

从'2014-08-23'开始,请致电7天:

SELECT * FROM f_anniversary(date '2014-08-23', 7);
Run Code Online (Sandbox Code Playgroud)

SQL小提琴比较EXPLAIN ANALYZE.

2月29日

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

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

说,Bob出生于2月29日:
我的查询1.和2.仅包括2月29日的闰年.鲍勃每隔约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);
Run Code Online (Sandbox Code Playgroud)

删除~8%以创建一些死元组并使表更"真实".

DELETE FROM event WHERE random() < 0.08;
ANALYZE event;
Run Code Online (Sandbox Code Playgroud)

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

C - Catcall

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;
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

W - wildplasser

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;
Run Code Online (Sandbox Code Playgroud)

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

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

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);
Run Code Online (Sandbox Code Playgroud)

检测结果

我在PostgreSQL 9.1.7上使用临时表运行此测试.收集结果EXPLAIN ANALYZE,最好是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 this_years_birthday()函数的简单版本.

  • 即使没有索引,我的"简单版本"也更快,因为它需要更少的计算.

  • 随着指数的"高级版"是一样快的"简易版",因为min()max()变得非常便宜的带有索引.两者都比不能使用指数的其他人快得多.

  • 无论是否有索引,我的"黑魔法版本"都是最快的.而且打电话非常简单.
    更新后的版本(在基准测试之后)有点快.

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

  • @ErwinBrandstetter令人难以置信的广泛答案.+1你应该得到比+1更多的东西.:) (6认同)

Dan*_*ité 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
Run Code Online (Sandbox Code Playgroud)

作为跨越一年(以及一个月)的工作方式的一个例子,假设一个周年日期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它匹配.