sql >> Base de Datos >  >> RDS >> PostgreSQL

¿Cómo haces matemáticas de fechas que ignoran el año?

Si no le gustan las explicaciones y los detalles, use la "versión de magia negra" a continuación.

Todas las consultas presentadas en otras respuestas hasta ahora operan con condiciones que no se pueden sargable - no pueden usar un índice y tienen que calcular una expresión para cada fila en la tabla base para encontrar filas coincidentes. No importa mucho con mesas pequeñas. Importa (mucho ) con mesas grandes.

Dada la siguiente tabla simple:

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

Consulta

Las versiones 1 y 2 a continuación pueden usar un índice simple de la forma:

CREATE INDEX event_event_date_idx ON event(event_date);

Pero todas las siguientes soluciones son aún más rápidas sin índice .

1. Versión sencilla

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

Subconsulta x calcula todas las fechas posibles en un rango dado de años a partir de un CROSS JOIN de dos generate_series() llamadas La selección se realiza con la unión simple final.

2. Versión avanzada

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

El rango de años se deduce de la tabla automáticamente, minimizando así los años generados.
Usted podría ir un paso más allá y destilar una lista de años existentes si hay lagunas.

La eficacia depende de la distribución de las fechas. Pocos años con muchas filas cada uno hacen que esta solución sea más útil. Muchos años con pocas filas cada uno lo hacen menos útil.

Violín SQL simple para jugar.

3. Versión de magia negra

Actualizado en 2016 para eliminar una "columna generada", que bloquearía H.O.T. actualizaciones; función más simple y rápida.
Actualizado en 2018 para calcular MMDD con IMMUTABLE expresiones para permitir funciones en línea.

Cree una función SQL simple para calcular un integer del patrón 'MMDD' :

CREATE FUNCTION f_mmdd(date) RETURNS int LANGUAGE sql IMMUTABLE AS
'SELECT (EXTRACT(month FROM $1) * 100 + EXTRACT(day FROM $1))::int';

Tuve to_char(time, 'MMDD') al principio, pero cambió a la expresión anterior que demostró ser la más rápida en nuevas pruebas en Postgres 9.6 y 10:

db<>violín aquí

Permite la función en línea porque EXTRACT (xyz FROM date) se implementa con el IMMUTABLE función date_part(text, date) internamente. Y tiene que ser IMMUTABLE para permitir su uso en el siguiente índice de expresión multicolumna esencial:

CREATE INDEX event_mmdd_event_date_idx ON event(f_mmdd(event_date), event_date);

Multicolumna por varias razones:
Puede ayudar con ORDER BY o con la selección de años dados. Leer aquí. Casi sin costo adicional para el índice. Una date cabe en los 4 bytes que, de otro modo, se perderían debido al relleno debido a la alineación de datos. Lea aquí.
Además, dado que ambas columnas de índice hacen referencia a la misma columna de la tabla, no hay inconveniente con respecto a H.O.T. actualizaciones Leer aquí.

Una función de tabla PL/pgSQL para gobernarlos a todos

Bifurcación a una de las dos consultas para cubrir el cambio de año:

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;

Llamar utilizando los valores predeterminados:14 días a partir de "hoy":

SELECT * FROM f_anniversary();

Llame durante 7 días a partir del '2014-08-23':

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

Violín SQL comparando EXPLAIN ANALYZE .

29 de febrero

Cuando se trata de aniversarios o "cumpleaños", debe definir cómo tratar el caso especial "29 de febrero" en años bisiestos.

Al probar rangos de fechas, Feb 29 normalmente se incluye automáticamente, incluso si el año actual no es un año bisiesto . El rango de días se amplía en 1 retroactivamente cuando cubre este día.
Por otro lado, si el año actual es un año bisiesto y desea buscar 15 días, puede terminar obteniendo resultados para 14 días en años bisiestos si sus datos son de años no bisiestos.

Digamos que Bob nació el 29 de febrero:
Mi consulta 1. y 2. incluyen el 29 de febrero solo en años bisiestos. Bob cumple años solo cada ~ 4 años.
Mi consulta 3. incluye el 29 de febrero en el rango. Bob cumple años todos los años.

No existe una solución mágica. Tienes que definir lo que quieres para cada caso.

Prueba

Para corroborar mi punto, realicé una prueba exhaustiva con todas las soluciones presentadas. Adapté cada una de las consultas a la tabla dada y obtuve resultados idénticos sin ORDER BY .

La buena noticia:todos ellos son correctos y produce el mismo resultado, excepto por la consulta de Gordon que tenía errores de sintaxis y la consulta de @wildplasser que falla cuando termina el año (fácil de arreglar).

Inserte 108000 filas con fechas aleatorias del siglo XX, que es similar a una tabla de personas vivas (13 años o más).

INSERT INTO  event (event_date)
SELECT '2000-1-1'::date - (random() * 36525)::int
FROM   generate_series (1, 108000);

Elimine ~ 8 % para crear algunas tuplas muertas y hacer que la tabla sea más "real".

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

Mi caso de prueba tenía 99289 filas, 4012 resultados.

C - Silbido

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 - La idea de Catcall reescrita

Aparte de las optimizaciones menores, la principal diferencia es agregar solo la cantidad exacta de años date_trunc('year', age(current_date + 14, event_date)) para obtener el aniversario de este año, lo que evita la necesidad de un CTE por completo:

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

Ver "1. Versión simple" arriba.

E2 - Erwin 2

Consulte "2. Versión avanzada" más arriba.

E3 - Erwin 3

Consulte "3. Versión de magia negra" más arriba.

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_caballo_sin_nombre

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 - salvaje

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;

Simplificado para devolver lo mismo que todos los demás:

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

W1 - consulta de wildplasser reescrita

Lo anterior adolece de una serie de detalles ineficientes (más allá del alcance de esta publicación ya considerable). La versión reescrita es mucho más rápido:

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

Resultados de la prueba

Ejecuté esta prueba con una tabla temporal en PostgreSQL 9.1.7. Los resultados se recopilaron con EXPLAIN ANALYZE , al mejor de 5.

Resultados

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  --!!

Todas las demás consultas funcionan igual con o sin índice porque usan no sargable expresiones.

Conclusión

  • Hasta ahora, la consulta de @Daniel fue la más rápida.

  • El enfoque de @wildplassers (reescrito) también funciona aceptablemente.

  • La versión de @ Catcall es algo así como el enfoque inverso al mío. El rendimiento se sale de control rápidamente con tablas más grandes.
    Sin embargo, la versión reescrita funciona bastante bien. La expresión que uso es algo así como una versión más simple de this_years_birthday() de @wildplassser función.

  • Mi "versión simple" es más rápida incluso sin índice , porque necesita menos cálculos.

  • Con índice, la "versión avanzada" es tan rápida como la "versión simple", porque min() y max() volverse muy barato con un índice. Ambos son sustancialmente más rápidos que el resto que no puede usar el índice.

  • Mi "versión de magia negra" es la más rápida con o sin índice . Y es muy fácil de llamar.

  • Con una tabla de la vida real un índice hará aún mejor diferencia. Más columnas hacen que la tabla sea más grande y que el escaneo secuencial sea más costoso, mientras que el tamaño del índice permanece igual.