Si vous ne vous souciez pas des explications et des détails, utilisez la "version magie noire" ci-dessous.
Toutes les requêtes présentées dans d'autres réponses jusqu'à présent fonctionnent avec des conditions qui ne sont pas sargables - ils ne peuvent pas utiliser d'index et doivent calculer une expression pour chaque ligne de la table de base pour trouver les lignes correspondantes. Peu importe avec de petites tables. Ça compte (beaucoup ) avec de grandes tables.
Étant donné le tableau simple suivant :
CREATE TABLE event (
event_id serial PRIMARY KEY
, event_date date
);
Requête
Les versions 1. et 2. ci-dessous peuvent utiliser un simple index de la forme :
CREATE INDEX event_event_date_idx ON event(event_date);
Mais toutes les solutions suivantes sont encore plus rapides sans index .
1. Version simplifiée
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);
Sous-requête x
calcule toutes les dates possibles sur une plage d'années donnée à partir d'un CROSS JOIN
de deux generate_series()
appels. La sélection se fait avec la jointure simple finale.
2. Version avancée
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);
La plage d'années est automatiquement déduite du tableau - minimisant ainsi les années générées.
Vous pourriez aller plus loin et distiller une liste des années existantes s'il y a des lacunes.
L'efficacité co-dépend de la répartition des dates. Quelques années avec de nombreuses lignes chacune rendent cette solution plus utile. De nombreuses années avec peu de lignes chacune le rendent moins utile.
Violon SQL simple jouer avec.
3. Version magie noire
Mise à jour en 2016 pour supprimer une "colonne générée", qui bloquerait H.O.T. mises à jour; fonction plus simple et plus rapide.
Mise à jour 2018 pour calculer MMDD avec IMMUTABLE
expressions pour permettre l'inlining de la fonction.
Créer une fonction SQL simple pour calculer un integer
du motif 'MMDD'
:
CREATE FUNCTION f_mmdd(date) RETURNS int LANGUAGE sql IMMUTABLE AS
'SELECT (EXTRACT(month FROM $1) * 100 + EXTRACT(day FROM $1))::int';
J'avais to_char(time, 'MMDD')
au début, mais est passé à l'expression ci-dessus qui s'est avérée la plus rapide lors de nouveaux tests sur Postgres 9.6 et 10 :
db<>jouez ici
Il permet l'inlining de la fonction car EXTRACT (xyz FROM date)
est implémenté avec le IMMUTABLE
fonction date_part(text, date)
intérieurement. Et il doit être IMMUTABLE
pour permettre son utilisation dans l'index d'expression multicolonne essentiel suivant :
CREATE INDEX event_mmdd_event_date_idx ON event(f_mmdd(event_date), event_date);
Multicolonne pour un certain nombre de raisons :
Peut aider avec ORDER BY
ou en sélectionnant parmi des années données. Lisez ici. À presque aucun coût supplémentaire pour l'index. Une date
correspond aux 4 octets qui seraient autrement perdus à cause du remplissage en raison de l'alignement des données. Lire ici.
De plus, puisque les deux colonnes d'index référencent la même colonne de table, aucun inconvénient en ce qui concerne H.O.T. mises à jour. Lisez ici.
Une fonction de table PL/pgSQL pour les gouverner toutes
Bifurquez vers l'une des deux requêtes pour couvrir le tournant de l'année :
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;
Appeler en utilisant les valeurs par défaut :14 jours commençant "aujourd'hui" :
SELECT * FROM f_anniversary();
Appelez pendant 7 jours à partir du 23/08/2014 :
SELECT * FROM f_anniversary(date '2014-08-23', 7);
Violon SQL comparant EXPLAIN ANALYZE
.
29 février
Lorsque vous traitez des anniversaires ou des "anniversaires", vous devez définir comment traiter le cas spécial "29 février" dans les années bissextiles.
Lors du test des plages de dates, Feb 29
est généralement inclus automatiquement, même si l'année en cours n'est pas une année bissextile . La plage de jours est allongée de 1 rétroactivement lorsqu'elle couvre ce jour.
En revanche, si l'année en cours est une année bissextile et que vous souhaitez rechercher 15 jours, vous risquez d'obtenir des résultats pour 14 jours. jours dans les années bissextiles si vos données proviennent d'années non bissextiles.
Supposons que Bob soit né le 29 février :
Ma requête 1. et 2. n'inclut le 29 février que dans les années bissextiles. Bob n'a d'anniversaire que tous les ~ 4 ans.
Ma requête 3. inclut le 29 février dans la plage. Bob fête son anniversaire chaque année.
Il n'y a pas de solution magique. Vous devez définir ce que vous voulez pour chaque cas.
Tester
Pour étayer mon propos, j'ai effectué un test approfondi avec toutes les solutions présentées. J'ai adapté chacune des requêtes à la table donnée et pour obtenir des résultats identiques sans ORDER BY
.
La bonne nouvelle :elles sont toutes correctes et donnent le même résultat - à l'exception de la requête de Gordon qui comportait des erreurs de syntaxe et de la requête de @wildplasser qui échoue à la fin de l'année (facile à corriger).
Insérez 108 000 lignes avec des dates aléatoires du 20e siècle, ce qui est similaire à un tableau de personnes vivantes (13 ans ou plus).
INSERT INTO event (event_date)
SELECT '2000-1-1'::date - (random() * 36525)::int
FROM generate_series (1, 108000);
Supprimez ~ 8 % pour créer des tuples morts et rendre le tableau plus "réel".
DELETE FROM event WHERE random() < 0.08;
ANALYZE event;
Mon cas de test avait 99289 lignes, 4012 hits.
C - Appeau
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 - L'idée de Catcall réécrite
Mis à part les optimisations mineures, la principale différence est d'ajouter uniquement le nombre exact d'années date_trunc('year', age(current_date + 14, event_date))
pour obtenir l'anniversaire de cette année, ce qui évite complètement le besoin d'un 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 - Daniel
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 - Erwin 1
Voir "1. Version simplifiée" ci-dessus.
E2 - Erwin 2
Voir "2. Version avancée" ci-dessus.
E3 - Erwin 3
Voir "3. Version magie noire" ci-dessus.
G-Gordon
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 - un_cheval_sans_nom
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 - sauvage
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;
Simplifié pour retourner le même que tous les autres :
SELECT *
FROM event e
WHERE this_years_birthday( e.event_date::date )
BETWEEN current_date
AND current_date + '2weeks'::interval;
W1 - requête de wildplasser réécrite
Ce qui précède souffre d'un certain nombre de détails inefficaces (au-delà de la portée de cet article déjà important). La version réécrite est beaucoup plus rapide :
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);
Résultats des tests
J'ai exécuté ce test avec une table temporaire sur PostgreSQL 9.1.7. Les résultats ont été recueillis avec EXPLAIN ANALYZE
, meilleur des 5.
Résultats
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 --!!
Toutes les autres requêtes fonctionnent de la même manière avec ou sans index car elles utilisent non-sargable expressions.
Conclusion
-
Jusqu'à présent, la requête de @Daniel a été la plus rapide.
-
L'approche @wildplassers (réécrite) fonctionne également de manière acceptable.
-
La version de @ Catcall ressemble à l'approche inverse de la mienne. Les performances deviennent rapidement incontrôlables avec des tables plus grandes.
La version réécrite fonctionne cependant plutôt bien. L'expression que j'utilise est quelque chose comme une version plus simple dethis_years_birthday()
de @wildplassser fonction. -
Ma "version simple" est plus rapide même sans index , car il nécessite moins de calculs.
-
Avec index, la "version avancée" est à peu près aussi rapide que la "version simple", car
min()
etmax()
devenir très pas cher avec un index. Les deux sont nettement plus rapides que les autres qui ne peuvent pas utiliser l'index. -
Ma "version magie noire" est la plus rapide avec ou sans index . Et c'est très simple à appeler.
-
Avec une table de la vie réelle un index rendra encore plus grand différence. Plus de colonnes rendent la table plus grande et l'analyse séquentielle plus coûteuse, tandis que la taille de l'index reste la même.