Obtenha uma lista de datas entre duas datas

91

Usando funções padrão do mysql, existe uma maneira de escrever uma consulta que retornará uma lista de dias entre duas datas.

por exemplo, dado 01-01-2009 e 13/01/2009, ele retornaria uma tabela de uma coluna com os 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

Edit: Parece que não fui claro. Eu quero GERAR esta lista. Tenho valores armazenados no banco de dados (por data e hora), mas quero que eles sejam agregados em uma junção externa esquerda a uma lista de datas como acima (estou esperando nulo do lado direito de algumas dessas junções por alguns dias e vou cuidar disso )

Gilgad
fonte
2
Acho que a melhor solução está descrita na resposta stackoverflow.com/a/2157776/466677
Marek Gregor
1
Possível duplicata de gerar dias a partir do intervalo de datas
Cees Timmerman

Respostas:

68

Eu usaria esse procedimento armazenado para gerar os intervalos de que você precisa na tabela temporária chamada time_intervals , então JOIN e agregar sua tabela de dados com a tabela temporária time_intervals .

O procedimento pode gerar intervalos de todos os diferentes tipos que você vê especificados nele:

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;

Cenário de dados de exemplo semelhante na parte inferior desta postagem , em que construí uma função semelhante para o SQL Server.

Ron Savage
fonte
4
Eu estava pessoalmente esperando por algo semelhante às sequências geradas no PostgreSQL.
Phillip Whelan de
Se você estiver gerando os dados uma vez, pode até usar tabelas permanentes e usar este script para preencher as datas ausentes.
Bimal Poudel de
Tive que alterar o delimitador antes da instrução do procedimento de criação para fazê-la funcionar por meio do phpMyAdmin (para evitar erro de sintaxe nas instruções de declaração) [código] DECLARAR // [/ código]
Defkon1
Esta solução é muito poderosa e flexível, inadequadamente em mariaDB Estou recebendo o erro # 1067 - Valor padrão inválido para 'interval_end' ao executar a chamada make_intervals ('2009-01-01 00:00:00', '2009-01-10 00: 00: 00 ', 1,' DAY '); Estou na versão 5.7.28
shelbypereira
28

Para MSSQL, você pode usar isso. É MUITO rápido.

Você pode resumir isso em uma função com valor de tabela ou proc armazenado e analisar as datas de início e término como variáveis.

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
Richard
fonte
2
Qual é a utilidade de OPTION (MAXRECURSION 0) aqui?
Shiva
14

Tivemos um problema semelhante com os relatórios do BIRT, pois queríamos relatar os dias que não tinham dados. Como não havia entradas para essas datas, a solução mais fácil para nós foi criar uma tabela simples que armazenasse todas as datas e usá-la para obter intervalos ou junção para obter valores zero para essa data.

Temos um trabalho que é executado todos os meses para garantir que a tabela seja preenchida em 5 anos no futuro. A tabela é criada assim:

create table all_dates (
    dt date primary key
);

Sem dúvida, existem maneiras mágicas e complicadas de fazer isso com diferentes SGBDs, mas sempre optamos pela solução mais simples. Os requisitos de armazenamento da tabela são mínimos e isso torna as consultas muito mais simples e portáteis. Esse tipo de solução quase sempre é melhor do ponto de vista do desempenho, pois não requer cálculos por linha nos dados.

A outra opção (e já usamos isso antes) é garantir que haja uma entrada na tabela para cada data. Nós varremos a tabela periodicamente e adicionamos zero entradas para datas e / ou horas que não existiam. Isso pode não ser uma opção no seu caso, depende dos dados armazenados.

Se você realmente acha que é um incômodo manter oall_dates tabela preenchida, um procedimento armazenado é o caminho a seguir, que retornará um conjunto de dados contendo essas datas. Isso quase certamente será mais lento, pois você terá que calcular o intervalo toda vez que for chamado, em vez de apenas extrair dados pré-calculados de uma tabela.

Mas, para ser honesto, você poderia preencher a tabela por 1000 anos sem problemas sérios de armazenamento de dados - 365.000 datas de 16 bytes (por exemplo) mais um índice que duplica a data mais 20% de sobrecarga para segurança, eu estimaria aproximadamente em cerca de 14M [365.000 * 16 * 2 * 1,2 = 14.016.000 bytes]), uma tabela minúscula no esquema das coisas.

paxdiablo
fonte
12

Você pode usar as variáveis ​​de usuário do MySQL assim:

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 é -1 porque você adiciona a ele na primeira vez que o usa. Além disso, você não pode usar "HAVING date_sequence" porque isso faz com que a variável do usuário seja incrementada duas vezes para cada linha.

Andrew Vit
fonte
8

Tirando uma ideia dessa resposta, você pode configurar uma tabela de 0 a 9 e usá-la para gerar sua lista de datas.

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

Isso permitirá que você gere uma lista de até 1000 datas. Se precisar aumentar, você pode adicionar outra junção cruzada à consulta interna.

Logan5
fonte
Esta solução funciona muito melhor. Mas tem um problema com UNIX_TIMESTAMP () - fornece resultados como 1231185600.000000; a parte dos milissegundos após o ponto decimal; enquanto - SELECT UNIX_TIMESTAMP (ADDDATE ('2009-01-01', 0)) AS date; NÃO resulta nessa parte.
Bimal Poudel
3

Para Access (ou qualquer linguagem SQL)

  1. Crie uma tabela que tenha 2 campos, chamaremos esta tabela tempRunDates:
    --Campos fromDatee - Em toDate
    seguida, insira apenas 1 registro, que tem a data de início e a data de término.

  2. Crie outra tabela: Time_Day_Ref
    --Importe uma lista de datas (fazer lista no excel é fácil) para esta tabela.
    --O nome do campo no meu caso é Greg_Dt, para data gregoriana
    --fiz minha lista de 1 ° de janeiro de 2009 a 1 ° de janeiro de 2020.

  3. Execute a 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
fonte
2

Normalmente, alguém usaria uma tabela auxiliar de números que você normalmente mantém apenas para este propósito, com algumas variações sobre isto:

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

Já vi variações com funções com valor de tabela, etc.

Você também pode manter uma lista permanente de datas. Temos isso em nosso data warehouse, bem como uma lista de horários do dia.

Cade Roux
fonte
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
fonte
2

Solução elegante usando a nova funcionalidade recursiva (Common Table Expressions) em MariaDB> = 10.3 e 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;

O texto acima retorna uma tabela de datas entre '01/01/2019' e '30/04/2019'.

Brad
fonte
"select '2019-01-01' as dt" o que é este seletor? É possível selecionar apenas um campo de e para?
Miguel Stevens
1

Usamos isso em nosso sistema HRMS, você vai achar ú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
fonte
1

Esta solução está funcionando com MySQL 5.0
Criar uma tabela - mytable.
O esquema não é material. O que importa é o número de linhas nele.
Portanto, você pode manter apenas uma coluna do tipo INT com 10 linhas, 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';

Limitação: O número máximo de datas retornadas pela consulta acima será
(rows in mytable)*(rows in mytable) = 10*10 = 100.

Você pode aumentar este intervalo alterando a parte do formulário em sql:
from mytable x, mytable y, mytable z
Então, o intervalo be 10*10*10 =1000e assim por diante.

Vihang Patil
fonte
0

Crie um procedimento armazenado que receba dois parâmetros a_begin e a_end. Crie uma tabela temporária dentro dela chamada t, declare uma variável d, atribua a_begin a d e execute um WHILElooping INSERTd em t e chame a ADDDATEfunção para incrementar o valor d. Finalmente SELECT * FROM t.

Eugene Yokota
fonte
Na verdade, acredito que uma tabela permanente seria melhor para a velocidade de execução (tem requisitos mínimos de armazenamento). É mais rápido pré-calcular as datas em uma tabela e extrair quando necessário do que criar intervalos sempre que precisar deles.
paxdiablo
@Pax, depende se a melhoria de velocidade obtida vale a pena a manutenção da mesa. Por exemplo, se a geração do relatório leva 3 segundos e a execução do loop WHILE para gerar datas leva 0,001 segundos, eu diria que o ganho de desempenho é ignorável. Knuth: A otimização prematura é a raiz de todos os males.
Eugene Yokota
@ eed3si9n, otimização prematura, sim, nem toda otimização :-) Se seu exemplo estivesse correto, então sim, eu concordo com você, mas deve ser medido com certeza. A manutenção da tabela acaba se você gerá-la 1000 anos (um custo único) como sugeri, mas esses segundos vão se somar.
paxdiablo
0

Eu usaria algo semelhante a este:

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

Em seguida, a @HOLDERtabela Variável contém todas as datas incrementadas por dia entre essas duas datas, prontas para serem unidas quando quiser.

Tom 'Blue' Piddock
fonte
Olá, Estou tentando usar o código e inserir a saída em uma tabela que defini DateTest com a coluna datefill do tipo DATETIME ... mas falhando ... você poderia fornecer o código que funcionaria com isso, por favor? Nota: Usando o SQL Server aqui, obrigado :)
sys_debug
0

Eu tenho lutado com isso há um bom tempo. Já que este é o primeiro hit no Google quando busquei a solução, deixe-me postar onde cheguei até agora.

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

Substitua [yourTable]por uma tabela de seu banco de dados. O truque é que o número de linhas na tabela que você seleciona deve ser> = o número de datas que você deseja retornar. Tentei usar o marcador de posição DUAL da tabela, mas ele retornaria apenas uma única linha.

campeão
fonte
1
Abordagem interessante. Mas .. não é essencialmente isso que Vihang sugeriu acima;)?
Leigh
0
select * from table_name where col_Date between '2011/02/25' AND DATEADD(s,-1,DATEADD(d,1,'2011/02/27'))

Aqui, primeiro adicione um dia à data final atual, será 2011-02-28 00:00:00, então você subtrairá um segundo para definir a data final 2011-02-27 23:59:59. Ao fazer isso, você pode obter todas as datas entre os intervalos fornecidos.

saída:
2011/02/25
2011/02/26
2011/02/27

Chandra Prakash
fonte
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 procedimento irá inserir todas as datas desde o início do ano até agora, apenas substitua os dias de "início" e "fim" e está pronto para começar!

Julio Marins
fonte
Como a linha é IF a=365 THENafetada por anos bissextos?
Stewart,
Além disso, qual é o significado do número 150na linha 9? Este código é uma "resposta padrão" sem muita explicação real.
Stewart
0

Eu precisava de uma lista com todos os meses entre 2 datas para estatísticas. As 2 datas são a data de início e de término de uma assinatura. Assim, a lista mostra todos os meses e a quantidade de assinaturas por mês.

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
Uau
fonte
0

Você pode usar isso

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
fonte
-2

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

Agora vamos resolver isso de uma maneira simples.

Eu criei uma tabela chamada "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)

agora, veremos os registros inseridos 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)

e aqui nossa consulta para buscar registros dentro de duas datas em vez dessas datas.

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 isso ajude muitos.

ArifMustafa
fonte
Em votação negativa, sem qualquer razão, este SQL está bom e em cenário de trabalho.
ArifMustafa