如果您不关心解释和细节,请使用下面的“黑魔法版”。
到目前为止,其他答案中提供的所有查询都使用不可搜索的条件进行操作——它们不能使用索引,并且必须为基表中的每一行计算一个表达式以查找匹配的行。与小桌子无关紧要。大桌子很重要。
给定以下简单表格:
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 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);
年份的范围是从表格中自动推导出来的 - 从而最大限度地减少生成的年份。
如果存在差距,您可以更进一步,提取现有年份的列表。
有效性共同取决于日期的分布。几年每行都有很多行,这使得这个解决方案更有用。多年来,每行很少,因此它的用处不大。
简单的 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)
实现。它必须允许它在以下基本多列表达式索引中使用:IMMUTABLE
date_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 次点击。
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;
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.黑魔法版”。
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;
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;
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 ,我的“黑魔法版”都是最快的。而且调用起来非常简单。
更新后的版本(基准测试之后)要快一些。
对于现实生活中的表,索引将产生更大的差异。更多的列使表更大,顺序扫描更昂贵,而索引大小保持不变。