Obtenga una lista de fechas entre dos fechas

91

El uso de funciones estándar de mysql es una forma de escribir una consulta que devolverá una lista de días entre dos fechas.

por ejemplo, dado 2009-01-01 y 2009-01-13, devolvería una tabla de una columna con los valores:

 2009-01-01 
 2009-01-02 
 2009-01-03
 2009-01-04 
 2009-01-05
 2009-01-06
 2009-01-07
 2009-01-08 
 2009-01-09
 2009-01-10
 2009-01-11
 2009-01-12
 2009-01-13

Editar: Parece que no lo he tenido claro. Quiero GENERAR esta lista. Tengo valores almacenados en la base de datos (por fecha y hora) pero quiero que se agreguen en una combinación externa izquierda a una lista de fechas como se muestra arriba (espero que el lado derecho de esta combinación sea nulo durante algunos días y manejaré esto ).

Gilgad
fuente
2
Creo que la mejor solución se describe en la respuesta stackoverflow.com/a/2157776/466677
Marek Gregor
1
Posible duplicado de generar días desde el rango de fechas
Cees Timmerman

Respuestas:

68

Usaría este procedimiento almacenado para generar los intervalos que necesita en la tabla temporal llamada time_intervals , luego UNIRSE y agregar su tabla de datos con la tabla temporal time_intervals .

El procedimiento puede generar intervalos de todos los tipos diferentes que ve especificados en él:

call make_intervals('2009-01-01 00:00:00','2009-01-10 00:00:00',1,'DAY')
.
select * from time_intervals  
.
interval_start      interval_end        
------------------- ------------------- 
2009-01-01 00:00:00 2009-01-01 23:59:59 
2009-01-02 00:00:00 2009-01-02 23:59:59 
2009-01-03 00:00:00 2009-01-03 23:59:59 
2009-01-04 00:00:00 2009-01-04 23:59:59 
2009-01-05 00:00:00 2009-01-05 23:59:59 
2009-01-06 00:00:00 2009-01-06 23:59:59 
2009-01-07 00:00:00 2009-01-07 23:59:59 
2009-01-08 00:00:00 2009-01-08 23:59:59 
2009-01-09 00:00:00 2009-01-09 23:59:59 
.
call make_intervals('2009-01-01 00:00:00','2009-01-01 02:00:00',10,'MINUTE')
. 
select * from time_intervals
.  
interval_start      interval_end        
------------------- ------------------- 
2009-01-01 00:00:00 2009-01-01 00:09:59 
2009-01-01 00:10:00 2009-01-01 00:19:59 
2009-01-01 00:20:00 2009-01-01 00:29:59 
2009-01-01 00:30:00 2009-01-01 00:39:59 
2009-01-01 00:40:00 2009-01-01 00:49:59 
2009-01-01 00:50:00 2009-01-01 00:59:59 
2009-01-01 01:00:00 2009-01-01 01:09:59 
2009-01-01 01:10:00 2009-01-01 01:19:59 
2009-01-01 01:20:00 2009-01-01 01:29:59 
2009-01-01 01:30:00 2009-01-01 01:39:59 
2009-01-01 01:40:00 2009-01-01 01:49:59 
2009-01-01 01:50:00 2009-01-01 01:59:59 
.
I specified an interval_start and interval_end so you can aggregate the 
data timestamps with a "between interval_start and interval_end" type of JOIN.
.
Code for the proc:
.
-- drop procedure make_intervals
.
CREATE PROCEDURE make_intervals(startdate timestamp, enddate timestamp, intval integer, unitval varchar(10))
BEGIN
-- *************************************************************************
-- Procedure: make_intervals()
--    Author: Ron Savage
--      Date: 02/03/2009
--
-- Description:
-- This procedure creates a temporary table named time_intervals with the
-- interval_start and interval_end fields specifed from the startdate and
-- enddate arguments, at intervals of intval (unitval) size.
-- *************************************************************************
   declare thisDate timestamp;
   declare nextDate timestamp;
   set thisDate = startdate;

   -- *************************************************************************
   -- Drop / create the temp table
   -- *************************************************************************
   drop temporary table if exists time_intervals;
   create temporary table if not exists time_intervals
      (
      interval_start timestamp,
      interval_end timestamp
      );

   -- *************************************************************************
   -- Loop through the startdate adding each intval interval until enddate
   -- *************************************************************************
   repeat
      select
         case unitval
            when 'MICROSECOND' then timestampadd(MICROSECOND, intval, thisDate)
            when 'SECOND'      then timestampadd(SECOND, intval, thisDate)
            when 'MINUTE'      then timestampadd(MINUTE, intval, thisDate)
            when 'HOUR'        then timestampadd(HOUR, intval, thisDate)
            when 'DAY'         then timestampadd(DAY, intval, thisDate)
            when 'WEEK'        then timestampadd(WEEK, intval, thisDate)
            when 'MONTH'       then timestampadd(MONTH, intval, thisDate)
            when 'QUARTER'     then timestampadd(QUARTER, intval, thisDate)
            when 'YEAR'        then timestampadd(YEAR, intval, thisDate)
         end into nextDate;

      insert into time_intervals select thisDate, timestampadd(MICROSECOND, -1, nextDate);
      set thisDate = nextDate;
   until thisDate >= enddate
   end repeat;

 END;

Escenario de datos de ejemplo similar al final de esta publicación , donde construí una función similar para SQL Server.

Ron Savage
fuente
4
Personalmente, esperaba algo similar a las secuencias generadas en PostgreSQL.
Phillip Whelan
Si está generando los datos una vez, incluso puede usar tablas permanentes y usar este script para completar las fechas que faltan.
Bimal Poudel
Tuve que cambiar el delimitador antes de la declaración de procedimiento de creación para que funcione a través de phpMyAdmin (para evitar errores de sintaxis en declaraciones de declaración) [código] DECLARE // [/ código]
Defkon1
Esta solución es muy poderosa y flexible, desafortunadamente en mariaDB Recibo el error # 1067 - Valor predeterminado no válido para 'interval_end' al ejecutar la llamada make_intervals ('2009-01-01 00:00:00', '2009-01-10 00: 00: 00 ', 1,' DÍA '); Estoy en la versión 5.7.28
shelbypereira
28

Para MSSQL, puede usar esto. Es MUY rápido.

Puede resumir esto en una función con valores de tabla o un proceso almacenado y analizar las fechas de inicio y finalización como variables.

DECLARE @startDate DATETIME
DECLARE @endDate DATETIME

SET @startDate = '2011-01-01'
SET @endDate = '2011-01-31';

WITH dates(Date) AS 
(
    SELECT @startdate as Date
    UNION ALL
    SELECT DATEADD(d,1,[Date])
    FROM dates 
    WHERE DATE < @enddate
)

SELECT Date
FROM dates
OPTION (MAXRECURSION 0)
GO
Ricardo
fuente
2
¿Cuál es el uso de OPTION (MAXRECURSION 0) aquí?
Shiva
14

Tuvimos un problema similar con los informes BIRT en el sentido de que queríamos informar sobre aquellos días que no tenían datos. Como no había entradas para esas fechas, la solución más fácil para nosotros fue crear una tabla simple que almacenara todas las fechas y usarla para obtener rangos o unirse para obtener valores cero para esa fecha.

Tenemos un trabajo que se ejecuta todos los meses para garantizar que la tabla se llene dentro de cinco años. La tabla se crea así:

create table all_dates (
    dt date primary key
);

Sin duda, hay formas mágicas y complicadas de hacer esto con diferentes DBMS, pero siempre optamos por la solución más simple. Los requisitos de almacenamiento de la tabla son mínimos y hace que las consultas sean mucho más sencillas y portátiles. Este tipo de solución es casi siempre mejor desde el punto de vista del rendimiento, ya que no requiere cálculos por fila de los datos.

La otra opción (y la hemos usado antes) es asegurarnos de que haya una entrada en la tabla para cada fecha. Barrimos la tabla periódicamente y agregamos cero entradas para fechas y / o horas que no existían. Esto puede no ser una opción en su caso, depende de los datos almacenados.

Si realmente cree que es una molestia mantener la all_datestabla llena, un procedimiento almacenado es el camino a seguir que devolverá un conjunto de datos que contiene esas fechas. Es casi seguro que esto será más lento, ya que debe calcular el rango cada vez que se llama en lugar de simplemente extraer datos precalculados de una tabla.

Pero, para ser honesto, podría completar la tabla durante 1000 años sin ningún problema grave de almacenamiento de datos: 365,000 fechas de 16 bytes (por ejemplo) más un índice que duplica la fecha más un 20% de sobrecarga por seguridad, estimaría aproximadamente en aproximadamente 14M [365,000 * 16 * 2 * 1.2 = 14,016,000 bytes]), una tabla minúscula en el esquema de las cosas.

paxdiablo
fuente
12

Puede usar las variables de usuario de MySQL de esta manera:

SET @num = -1;
SELECT DATE_ADD( '2009-01-01', interval @num := @num+1 day) AS date_sequence, 
your_table.* FROM your_table
WHERE your_table.other_column IS NOT NULL
HAVING DATE_ADD('2009-01-01', interval @num day) <= '2009-01-13'

@num es -1 porque lo agrega la primera vez que lo usa. Además, no puede usar "HAVING date_sequence" porque eso hace que la variable de usuario se incremente dos veces para cada fila.

Andrew Vit
fuente
8

Tomando prestada una idea de esta respuesta, puede configurar una tabla con 0 a 9 y usarla para generar su lista de fechas.

CREATE TABLE num (i int);
INSERT INTO num (i) VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9);

select adddate('2009-01-01', numlist.id) as `date` from
(SELECT n1.i + n10.i*10 + n100.i*100 AS id
   FROM num n1 cross join num as n10 cross join num as n100) as numlist
where adddate('2009-01-01', numlist.id) <= '2009-01-13';

Esto le permitirá generar una lista de hasta 1000 fechas. Si necesita agrandar, puede agregar otra combinación cruzada a la consulta interna.

Logan5
fuente
Esta solución funciona mucho mejor. Pero tiene un problema con UNIX_TIMESTAMP () - da resultados como 1231185600.000000; la parte de milisegundos después del punto decimal; while - SELECCIONAR UNIX_TIMESTAMP (ADDDATE ('2009-01-01', 0)) AS date; NO da como resultado esa parte.
Bimal Poudel
3

Para Access (o cualquier lenguaje SQL)

  1. Cree una tabla que tenga 2 campos, llamaremos a esta tabla tempRunDates:
    --Campos fromDatey - toDate
    Luego inserte solo 1 registro, que tenga la fecha de inicio y la fecha de finalización.

  2. Cree otra tabla: Time_Day_Ref
    --Importe una lista de fechas (hacer una lista en Excel es fácil) en esta tabla.
    --El nombre del campo en mi caso es Greg_Dt, para Gregoriano Fecha
    --Hice mi lista desde el 1 de enero de 2009 hasta el 1 de enero de 2020.

  3. Ejecute la consulta:

    SELECT Time_Day_Ref.GREG_DT
    FROM tempRunDates, Time_Day_Ref
    WHERE Time_Day_Ref.greg_dt>=tempRunDates.fromDate And greg_dt<=tempRunDates.toDate;

¡Fácil!

Nathan Wood
fuente
2

Por lo general, se usaría una tabla de números auxiliares que generalmente mantiene para este propósito con alguna variación en esto:

SELECT *
FROM (
    SELECT DATEADD(d, number - 1, '2009-01-01') AS dt
    FROM Numbers
    WHERE number BETWEEN 1 AND DATEDIFF(d, '2009-01-01', '2009-01-13') + 1
) AS DateRange
LEFT JOIN YourStuff
    ON DateRange.dt = YourStuff.DateColumn

He visto variaciones con funciones con valores de tabla, etc.

También puede mantener una lista permanente de fechas. Tenemos eso en nuestro almacén de datos, así como una lista de horas del día.

Cade Roux
fuente
2
CREATE FUNCTION [dbo].[_DATES]
(
    @startDate DATETIME,
    @endDate DATETIME
)
RETURNS 
@DATES TABLE(
    DATE1 DATETIME
)
AS
BEGIN
    WHILE @startDate <= @endDate
    BEGIN 
        INSERT INTO @DATES (DATE1)
            SELECT @startDate   
    SELECT @startDate = DATEADD(d,1,@startDate) 
    END
RETURN
END
Todd Dickerson
fuente
2

Solución elegante que utiliza la nueva funcionalidad recursiva (Expresiones de tabla comunes) en MariaDB> = 10.3 y MySQL> = 8.0.

WITH RECURSIVE t as (
    select '2019-01-01' as dt
  UNION
    SELECT DATE_ADD(t.dt, INTERVAL 1 DAY) FROM t WHERE DATE_ADD(t.dt, INTERVAL 1 DAY) <= '2019-04-30'
)
select * FROM t;

Lo anterior devuelve una tabla de fechas entre '2019-01-01' y '2019-04-30'.

Puntilla
fuente
"seleccione '2019-01-01' como dt" ¿Qué es este selector? ¿Es posible seleccionar un campo desde y hasta?
Miguel Stevens
1

Usamos esto en nuestro sistema HRMS, lo encontrará útil

SELECT CAST(DAYNAME(daydate) as CHAR) as dayname,daydate
    FROM
    (select CAST((date_add('20110101', interval H.i*100 + T.i*10 + U.i day) )as DATE) as daydate
      from erp_integers as H
    cross
      join erp_integers as T
    cross
      join erp_integers as U
     where date_add('20110101', interval H.i*100 + T.i*10 + U.i day ) <= '20110228'
    order
        by daydate ASC
        )Days
Anas
fuente
1

Esta solución está trabajando con MySQL 5.0
Crear una tabla - mytable.
El esquema no es material. Lo que importa es el número de filas que contiene.
Por lo tanto, puede mantener solo una columna de tipo INT con 10 filas, valores: 1 a 10.

SQL:

set @tempDate=date('2011-07-01') - interval 1 day;
select
date(@tempDate := (date(@tempDate) + interval 1 day)) as theDate
from mytable x,mytable y
group by theDate
having theDate <= '2011-07-31';

Limitación: el número máximo de fechas devueltas por la consulta anterior será
(rows in mytable)*(rows in mytable) = 10*10 = 100.

Puede aumentar este rango cambiando la forma parte en sql:
de mytable x, mytable y, mytable z
Entonces, el rango be 10*10*10 =1000y así sucesivamente.

Vihang Patil
fuente
0

Cree un procedimiento almacenado que tome dos parámetros a_begin y a_end. Cree una tabla temporal dentro de ella llamada t, declare una variable d, asigne a_begin ad, y ejecute un WHILEbucle INSERTd en t y llame a la ADDDATEfunción para incrementar el valor d. Finalmente SELECT * FROM t.

Eugene Yokota
fuente
En realidad, creo que una tabla permanente sería mejor para la velocidad de ejecución (tiene requisitos mínimos de almacenamiento). Es más rápido calcular previamente las fechas en una tabla y extraerlas cuando sea necesario, que crear rangos cada vez que los necesite.
paxdiablo
@Pax, depende de si la mejora de velocidad obtenida vale la pena el mantenimiento de la mesa. Por ejemplo, si la generación del informe tarda 3 segundos y la ejecución del ciclo WHILE para generar fechas tarda 0,001 segundos, yo diría que la ganancia de rendimiento es ignorable. Knuth: La optimización prematura es la raíz de todos los males.
Eugene Yokota
@ eed3si9n, optimización prematura, sí, no todas las optimizaciones :-) Si su ejemplo fuera correcto, entonces sí, estaría de acuerdo con usted, pero debería medirse con certeza. El mantenimiento de la tabla desaparece si lo genera 1000 años después (un costo único) como sugerí, pero esos segundos se sumarán.
paxdiablo
0

Usaría algo similar a esto:

DECLARE @DATEFROM AS DATETIME
DECLARE @DATETO AS DATETIME
DECLARE @HOLDER TABLE(DATE DATETIME)

SET @DATEFROM = '2010-08-10'
SET @DATETO = '2010-09-11'

INSERT INTO
    @HOLDER
        (DATE)
VALUES
    (@DATEFROM)

WHILE @DATEFROM < @DATETO
BEGIN

    SELECT @DATEFROM = DATEADD(D, 1, @DATEFROM)
    INSERT 
    INTO
        @HOLDER
            (DATE)
    VALUES
        (@DATEFROM)
END

SELECT 
    DATE
FROM
    @HOLDER

Luego, la @HOLDERtabla Variable contiene todas las fechas incrementadas por día entre esas dos fechas, listas para unirse al contenido de su corazón.

Tom 'Blue' Piddock
fuente
Hola, estoy tratando de usar el código e insertar el resultado en una tabla que definí DateTest con la columna datefill del tipo DATETIME ... pero fallando ... ¿podrías proporcionar el código que funcionaría con eso, por favor? Nota: Uso de SQL Server aquí Gracias :)
sys_debug
0

He estado luchando con esto durante bastante tiempo. Dado que este es el primer resultado en Google cuando busqué la solución, permítanme publicar dónde he llegado hasta ahora.

SET @d := '2011-09-01';
SELECT @d AS d, cast( @d := DATE_ADD( @d , INTERVAL 1 DAY ) AS DATE ) AS new_d
  FROM [yourTable]
  WHERE @d <= '2012-05-01';

Reemplace [yourTable]con una tabla de su base de datos. El truco es que la cantidad de filas en la tabla que seleccione debe ser> = la cantidad de fechas que desea que se devuelvan. Intenté usar el marcador de posición de la tabla DUAL, pero solo devolvería una sola fila.

morder
fuente
1
Enfoque interesante. Pero ... ¿no es esto esencialmente lo que Vihang sugirió anteriormente;)?
Leigh
0
select * from table_name where col_Date between '2011/02/25' AND DATEADD(s,-1,DATEADD(d,1,'2011/02/27'))

Aquí, primero agregue un día a la fecha de finalización actual, será 2011-02-28 00:00:00, luego resta un segundo para hacer la fecha de finalización 2011-02-27 23:59:59. Al hacer esto, puede obtener todas las fechas entre los intervalos dados.

salida:
2011/02/25
2011/02/26
2011/02/27

Chandra Prakash
fuente
0
DELIMITER $$  
CREATE PROCEDURE popula_calendario_controle()
   BEGIN
      DECLARE a INT Default 0;
      DECLARE first_day_of_year DATE;
      set first_day_of_year = CONCAT(DATE_FORMAT(curdate(),'%Y'),'-01-01');
      one_by_one: LOOP
         IF dayofweek(adddate(first_day_of_year,a)) <> 1 THEN
            INSERT INTO calendario.controle VALUES(null,150,adddate(first_day_of_year,a),adddate(first_day_of_year,a),1);
         END IF;
         SET a=a+1;
         IF a=365 THEN
            LEAVE one_by_one;
         END IF;
      END LOOP one_by_one;
END $$

Este procedimiento insertará todas las fechas desde el comienzo del año hasta ahora, simplemente sustituya los días de "inicio" y "fin", ¡y estará listo para comenzar!

Julio Marins
fuente
¿Cómo se IF a=365 THENve afectada la línea por los años bisiestos?
Stewart
Además, ¿cuál es el significado del número 150de la línea 9? Este código es una "respuesta sencilla" sin mucha explicación real.
Stewart
0

Necesitaba una lista con todos los meses entre 2 fechas para estadísticas. Las 2 fechas son la fecha de inicio y finalización de una suscripción. Entonces, la lista muestra todos los meses y la cantidad de suscripciones por mes.

MYSQL

CREATE PROCEDURE `get_amount_subscription_per_month`()
BEGIN
   -- Select the ultimate start and enddate from subscribers
   select @startdate := min(DATE_FORMAT(a.startdate, "%Y-%m-01")), 
          @enddate := max(DATE_FORMAT(a.enddate, "%Y-%m-01")) + interval 1 MONTH
   from subscription a;

   -- Tmp table with all months (dates), you can always format them with DATE_FORMAT) 
   DROP TABLE IF EXISTS tmp_months;
   create temporary table tmp_months (
      year_month date,
      PRIMARY KEY (year_month)
   );


   set @tempDate=@startdate;  #- interval 1 MONTH;

   -- Insert every month in tmp table
   WHILE @tempDate <= @enddate DO
     insert into tmp_months (year_month) values (@tempDate);
     set @tempDate = (date(@tempDate) + interval 1 MONTH);
   END WHILE;

   -- All months
   select year_month from tmp_months;

   -- If you want the amount of subscription per month else leave it out
   select mnd.year_month, sum(subscription.amount) as subscription_amount
   from tmp_months mnd
   LEFT JOIN subscription ON mnd.year_month >= DATE_FORMAT(subscription.startdate, "%Y-%m-01") and mnd.year_month <= DATE_FORMAT(subscription.enddate, "%Y-%m-01")
   GROUP BY mnd.year_month;

 END
Guau
fuente
0

Puedes usar esto

SELECT CAST(cal.date_list AS DATE) day_year
FROM (
  SELECT SUBDATE('2019-01-01', INTERVAL 1 YEAR) + INTERVAL xc DAY AS date_list
  FROM (
        SELECT @xi:=@xi+1 as xc from
        (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) xc1,
        (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) xc2,
        (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) xc3,
        (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) xc4,
        (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) xc5,
        (SELECT @xi:=-1) xc0
    ) xxc1
) cal
WHERE cal.date_list BETWEEN '2019-01-01' AND '2019-12-31'
ORDER BY cal.date_list DESC;
Dixon
fuente
Fuente: - shayanderson.com/mysql/…
Dixon
-2

estoy usando Server version: 5.7.11-log MySQL Community Server (GPL)

Ahora resolveremos esto de una manera sencilla.

He creado una tabla llamada "datetable"

mysql> describe datetable;
+---------+---------+------+-----+---------+-------+
| Field   | Type    | Null | Key | Default | Extra |
+---------+---------+------+-----+---------+-------+
| colid   | int(11) | NO   | PRI | NULL    |       |
| coldate | date    | YES  |     | NULL    |       |
+---------+---------+------+-----+---------+-------+
2 rows in set (0.00 sec)

ahora, veremos los registros insertados dentro.

mysql> select * from datetable;
+-------+------------+
| colid | coldate    |
+-------+------------+
|   101 | 2015-01-01 |
|   102 | 2015-05-01 |
|   103 | 2016-01-01 |
+-------+------------+
3 rows in set (0.00 sec)

y aquí nuestra consulta para obtener registros dentro de dos fechas en lugar de esas fechas.

mysql> select * from datetable where coldate > '2015-01-01' and coldate < '2016-01-01';
+-------+------------+
| colid | coldate    |
+-------+------------+
|   102 | 2015-05-01 |
+-------+------------+
1 row in set (0.00 sec)

Espero que esto ayude a muchos.

ArifMustafa
fuente
Votado en contra, sin ningún motivo, este SQL está bien y en un escenario de trabajo.
ArifMustafa