Ottieni un elenco di date tra due date


91

Utilizzando le funzioni standard di mysql c'è un modo per scrivere una query che restituirà un elenco di giorni tra due date.

ad esempio, dato il 2009-01-01 e il 2009-01-13 restituirebbe una tabella di una colonna con i valori:

 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

Modifica: sembra che non sia stato chiaro. Voglio GENERARE questo elenco. Ho dei valori memorizzati nel database (per datetime) ma voglio che siano aggregati su un join esterno sinistro a un elenco di date come sopra (mi aspetto null dal lato destro di alcuni di questo join per alcuni giorni e lo gestirò ).


2
Penso che la soluzione migliore sia descritta nella risposta stackoverflow.com/a/2157776/466677
Marek Gregor

1
Possibile duplicato dei giorni
Cees Timmerman

Risposte:


68

Vorrei utilizzare questa procedura memorizzata per generare gli intervalli necessari nella tabella temporanea denominata time_intervals , quindi UNISCITI e aggregare la tabella dati con la tabella temp time_intervals .

La procedura può generare intervalli di tutti i diversi tipi che vedi specificati in essa:

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;

Scenario di dati di esempio simile nella parte inferiore di questo post , in cui ho creato una funzione simile per SQL Server.


4
Personalmente speravo in qualcosa di simile alle sequenze generate in PostgreSQL.
Phillip Whelan

Se stai generando i dati una volta, puoi anche utilizzare tabelle permanenti e utilizzare questo script per riempire le date mancanti.
Bimal Poudel

Ho dovuto cambiare il delimitatore prima dell'istruzione della procedura di creazione per farlo funzionare tramite phpMyAdmin (per evitare errori di sintassi sulle dichiarazioni di dichiarazione) [code] DECLARE // [/ code]
Defkon1

Questa soluzione è molto potente e flessibile, sfortunatamente su mariaDB ricevo l'errore # 1067 - Valore predefinito non valido per 'interval_end' durante l'esecuzione della chiamata make_intervals ('2009-01-01 00:00:00', '2009-01-10 00: 00: 00 ', 1,' GIORNO '); Sono sulla versione 5.7.28
shelbypereira

28

Per MSSQL puoi usare questo. È MOLTO veloce.

Puoi racchiuderlo in una funzione con valori di tabella o in un processo memorizzato e analizzare le date di inizio e fine come variabili.

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

2
Qual è l'uso di OPTION (MAXRECURSION 0) qui?
Shiva

14

Abbiamo avuto un problema simile con i rapporti BIRT in quanto volevamo segnalare quei giorni che non avevano dati. Poiché non c'erano voci per quelle date, la soluzione più semplice per noi era creare una semplice tabella che memorizzasse tutte le date e usarla per ottenere intervalli o unirsi per ottenere valori zero per quella data.

Abbiamo un lavoro che viene eseguito ogni mese per garantire che la tabella venga popolata entro 5 anni nel futuro. La tabella viene creata così:

create table all_dates (
    dt date primary key
);

Senza dubbio ci sono modi magici e complicati per farlo con diversi DBMS, ma optiamo sempre per la soluzione più semplice. I requisiti di archiviazione per la tabella sono minimi e rende le query molto più semplici e portabili. Questo tipo di soluzione è quasi sempre migliore dal punto di vista delle prestazioni poiché non richiede calcoli per riga sui dati.

L'altra opzione (e l'abbiamo usata in precedenza) è assicurarsi che ci sia una voce nella tabella per ogni data. Abbiamo spazzato periodicamente la tabella e aggiunto zero voci per date e / o orari che non esistevano. Questa potrebbe non essere un'opzione nel tuo caso, dipende dai dati memorizzati.

Se pensi davvero che sia una seccatura mantenere la all_datestabella popolata, una stored procedure è la strada da percorrere che restituirà un set di dati contenente quelle date. Questo sarà quasi certamente più lento poiché devi calcolare l'intervallo ogni volta che viene chiamato piuttosto che estrarre dati precalcolati da una tabella.

Ma, ad essere onesti, potresti popolare la tabella per 1000 anni senza gravi problemi di archiviazione dei dati: 365.000 date a 16 byte (ad esempio) più un indice che duplica la data più il 20% di overhead per sicurezza, stimerei approssimativamente a circa 14 M [365.000 * 16 * 2 * 1,2 = 14.016.000 byte]), una minuscola tabella nello schema delle cose.


12

Puoi usare le variabili utente di MySQL in questo modo:

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 perché lo aggiungi la prima volta che lo usi. Inoltre, non è possibile utilizzare "HAVING date_sequence" perché ciò fa aumentare la variabile utente due volte per ogni riga.


8

Prendendo in prestito un'idea da questa risposta, puoi impostare una tabella da 0 a 9 e usarla per generare il tuo elenco di date.

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

Ciò ti consentirà di generare un elenco fino a 1000 date. Se è necessario aumentare le dimensioni, è possibile aggiungere un altro cross join alla query interna.


Questa soluzione funziona molto meglio. Ma ha un problema con UNIX_TIMESTAMP () - fornisce risultati come 1231185600.000000; la parte dei millisecondi dopo il punto decimale; while - SELEZIONA UNIX_TIMESTAMP (ADDDATE ('2009-01-01', 0)) AS date; NON risulta quella parte.
Bimal Poudel

3

Per l'accesso (o qualsiasi linguaggio SQL)

  1. Crea una tabella che abbia 2 campi, chiameremo questa tabella tempRunDates:
    --Fields fromDatee toDate
    --Quindi inserisci solo 1 record, che ha la data di inizio e la data di fine.

  2. Crea un'altra tabella: - Time_Day_Ref
    Importa un elenco di date (creare un elenco in Excel è facile) in questa tabella.
    - Il nome del campo nel mio caso è Greg_Dt, per la data gregoriana
    - Ho creato la mia lista dal 1 ° gennaio 2009 al 1 ° gennaio 2020.

  3. Esegui la query:

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

Facile!


2

In genere si usa una tabella di numeri ausiliari che di solito tieni in giro solo per questo scopo con alcune variazioni su questo:

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

Ho visto variazioni con funzioni con valori di tabella, ecc.

Puoi anche mantenere un elenco permanente di date. Lo abbiamo nel nostro data warehouse, oltre a un elenco di momenti della giornata.



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

2

Elegante soluzione che utilizza la nuova funzionalità ricorsiva (Common Table Expressions) in 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;

Quanto sopra restituisce una tabella di date tra "2019-01-01" e "2019-04-30".


"seleziona '2019-01-01' come dt" che cos'è questo selettore? È possibile selezionare solo un campo da e a?
Miguel Stevens

1

Lo abbiamo utilizzato nel nostro sistema HRMS, lo troverai utile

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

1

Questa soluzione funziona con MySQL 5.0
Crea una tabella - mytable.
Lo schema non ha importanza. Ciò che conta è il numero di righe in esso.
Quindi, puoi mantenere solo una colonna di tipo INT con 10 righe, valori da 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';

Limitazione: il numero massimo di date restituite dalla query precedente sarà
(rows in mytable)*(rows in mytable) = 10*10 = 100.

È possibile aumentare questo intervallo modificando la parte del modulo in sql:
da mytable x, mytable y, mytable z
Quindi, l'intervallo be 10*10*10 =1000e così via.


0

Creare una procedura memorizzata che accetta due parametri a_begin e a_end. Crea una tabella temporanea al suo interno chiamata t, dichiara una variabile d, assegna a_begin a d, ed esegui un WHILEciclo INSERTd in te chiamando la ADDDATEfunzione per incrementare il valore d. Infine SELECT * FROM t.


In realtà credo che una tabella permanente sarebbe migliore per la velocità di esecuzione (ha requisiti di archiviazione minimi). È più veloce pre-calcolare le date in una tabella ed estrarle quando necessario, piuttosto che creare intervalli ogni volta che ne hai bisogno.
paxdiablo

@Pax, dipende se il miglioramento della velocità ottenuto vale la manutenzione del tavolo. Ad esempio, se la generazione del report richiede 3 secondi e l'esecuzione del ciclo WHILE per generare le date richiede 0,001 secondi, direi che il miglioramento delle prestazioni è ignorabile. Knuth: L'ottimizzazione prematura è la radice di tutti i mali.
Eugene Yokota

@ eed3si9n, ottimizzazione prematura, sì, non tutte le ottimizzazioni :-) Se il tuo esempio fosse corretto, allora sì, sarei d'accordo con te, ma dovrebbe essere misurato per certezza. La manutenzione della tabella è sparita se la generi 1000 anni (un costo una tantum) come ho suggerito, ma quei secondi si sommeranno.
paxdiablo

0

Userei qualcosa di simile a questo:

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

Quindi la @HOLDERtabella Variabile contiene tutte le date incrementate di giorno tra quelle due date, pronte per unirsi al contenuto del tuo cuore.


Ciao, sto cercando di utilizzare il codice e inserire l'output in una tabella che ho definito DateTest con la colonna datefill di tipo DATETIME ... ma non riuscendo ... potresti fornire il codice che funzionerebbe con quello per favore? Nota: utilizzando SQL Server qui Grazie :)
sys_debug

0

Ho combattuto con questo per un bel po '. Poiché questo è il primo successo su Google quando ho cercato la soluzione, lasciami pubblicare dove sono arrivato finora.

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

Sostituisci [yourTable]con una tabella dal tuo database. Il trucco è che il numero di righe nella tabella selezionata deve essere> = il numero di date che desideri vengano restituite. Ho provato a utilizzare il segnaposto della tabella DUAL, ma restituiva solo una singola riga.


1
Approccio interessante. Ma .. non è essenzialmente quello che Vihang ha suggerito sopra;)?
Leigh

0
select * from table_name where col_Date between '2011/02/25' AND DATEADD(s,-1,DATEADD(d,1,'2011/02/27'))

Qui, prima aggiungi un giorno alla data di fine corrente, sarà 2011-02-28 00:00:00, quindi sottrai un secondo per ottenere la data di fine 2011-02-27 23:59:59. In questo modo, puoi ottenere tutte le date tra gli intervalli indicati.

uscita:
2011/02/25
2011/02/26
2011/02/27


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

questa procedura inserirà tutte le date dall'inizio dell'anno ad oggi, basta sostituire i giorni di "inizio" e "fine" e sei pronto per partire!


In che modo la linea viene IF a=365 THENinfluenzata dagli anni bisestili?
Stewart

Inoltre, qual è il significato del numero 150sulla riga 9? Questo codice è una "risposta pat" senza molte spiegazioni reali.
Stewart

0

Avevo bisogno di un elenco con tutti i mesi tra 2 date per le statistiche. Le 2 date sono l'inizio e la data di fine di un abbonamento. Quindi l'elenco mostra tutti i mesi e la quantità di abbonamenti mensili.

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

0

Puoi usarlo

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;


-2

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

Ora lo risolveremo in modo semplice.

Ho creato una tabella denominata "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)

ora, vedremo i record inseriti all'interno.

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 qui la nostra query per recuperare i record entro due date anziché quelle date.

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)

spero che questo possa aiutare molti.


Downvoted, senza alcun motivo, questo SQL va bene e in uno scenario di lavoro.
ArifMustafa
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.