Ако не ви интересуват обясненията и подробностите, използвайте „Версия на черната магия“ по-долу.
Всички запитвания, представени в други отговори до момента, работят с условия, които не подлежат на 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 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 цигулка за игра с.
3. Версия на черна магия
Актуализирано през 2016 г. за премахване на „генерирана колона“, която би блокирала H.O.T. актуализации; по-проста и по-бърза функция.
Актуализирано 2018 г. за изчисляване на MMDD с IMMUTABLE
изрази, които позволяват вграждане на функцията.
Създайте проста 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
или с избор от дадени години. Прочетете тук. Почти без допълнителни разходи за индекса. date
се вписва в 4-те байта, които иначе биха били загубени при подреждане поради подравняване на данните. Прочетете тук.
Освен това, тъй като и двете колони на индекса препращат към една и съща колона на таблицата, няма недостатък по отношение на H.O.T. актуализации. Прочетете тук.
Една таблична функция 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();
Обадете се за 7 дни, започващи '2014-08-23':
SELECT * FROM f_anniversary(date '2014-08-23', 7);
SQL Fiddle сравняване на EXPLAIN ANALYZE
.
29 февруари
Когато се занимавате с годишнини или „рождени дни“, трябва да определите как да се справите със специалния случай „29 февруари“ през високосна.
При тестване за диапазони от дати, Feb 29
обикновено се включва автоматично, дори ако текущата година не е високосна . Диапазонът от дни се разширява с 1 ретроактивно, когато обхваща този ден.
От друга страна, ако текущата година е високосна и искате да търсите 15 дни, може да получите резултати за 14 дни във високосна, ако данните ви са от невисокосни години.
Да речем, Боб е роден на 29 февруари:
Моята заявка 1. и 2. включва 29 февруари само през високосна. Боб има рожден ден само на всеки ~ 4 години.
Моята заявка 3 включва 29 февруари в диапазона. Боб има рожден ден всяка година.
Няма магическо решение. Трябва да дефинирате какво искате за всеки случай.
Тест
За да потвърдя тезата си, проведох обширен тест с всички представени решения. Адаптих всяка една от заявките към дадената таблица и да дам идентични резултати без ORDER BY
.
Добрата новина:всички те са правилни и дават същия резултат - с изключение на заявката на Гордън, която имаше синтактични грешки, и заявката на @wildplasser, която се проваля, когато годината приключи (лесна за коригиране).
Вмъкнете 108 000 реда със произволни дати от 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 - 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;
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. Black magic версия" по-горе.
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 - 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;
Опростено, за да върне същото като всички останали:
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.
Резултати
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 --!!
Всички други заявки изпълняват същото със или без индекс, тъй като използват не-sargable изрази.
Заключение
-
Досега заявката на @Daniel беше най-бързата.
-
Подходът на @wildplassers (пренаписан) също се представя приемливо.
-
Версията на @Catcall е нещо като моя обратен подход. Производителността бързо излиза извън контрол с по-големи таблици.
Пренаписаната версия обаче се представя доста добре. Изразът, който използвам е нещо като по-опростена версия наthis_years_birthday()
на @wildplassser функция. -
Моята "проста версия" е по-бърза дори без индекс , защото се нуждае от по-малко изчисления.
-
С индекс "разширената версия" е толкова бърза, колкото и "простата версия", защото
min()
иmax()
станете много евтино с индекс. И двете са значително по-бързи от останалите, които не могат да използват индекса. -
Моята "черна магическа версия" е най-бърза със или без индекс . И е много лесен за обаждане.
-
С реална таблица синдекс ще направи още по-голямо разлика. Повече колони правят таблицата по-голяма, а последователното сканиране по-скъпо, докато размерът на индекса остава същият.