Obtenez une liste de dates entre deux dates

91

En utilisant les fonctions standard de mysql, il existe un moyen d'écrire une requête qui retournera une liste de jours entre deux dates.

Par exemple, étant donné le 2009-01-01 et le 2009-01-13, il renverrait une table à une colonne avec les valeurs:

 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: Il semble que je n'ai pas été clair. Je veux GÉNÉRER cette liste. J'ai des valeurs stockées dans la base de données (par date / heure) mais je veux qu'elles soient agrégées sur une jointure externe gauche à une liste de dates comme ci-dessus (j'attends null du côté droit de certaines de cette jointure pendant quelques jours et je vais gérer cela ).

Gilgad
la source
2
Je pense que la meilleure solution est décrite dans la réponse stackoverflow.com/a/2157776/466677
Marek Gregor
1
Reproduction possible des jours
Cees Timmerman

Réponses:

68

J'utiliserais cette procédure stockée pour générer les intervalles dont vous avez besoin dans la table temporaire nommée time_intervals , puis JOIN et agréger votre table de données avec la table temp time_intervals .

La procédure peut générer des intervalles de tous les types différents que vous y voyez spécifiés:

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;

Exemple de scénario de données similaire au bas de cet article , où j'ai créé une fonction similaire pour SQL Server.

Ron Savage
la source
4
J'espérais personnellement quelque chose de similaire aux séquences générées dans PostgreSQL.
Phillip Whelan
Si vous générez les données une fois, vous pouvez même utiliser des tables permanentes et utiliser ce script pour remplir les dates manquantes.
Bimal Poudel
J'ai dû changer le délimiteur avant l'instruction de procédure de création pour qu'elle fonctionne via phpMyAdmin (pour éviter les erreurs de syntaxe sur les instructions de déclaration) [code] DECLARE // [/ code]
Defkon1
Cette solution est très puissante et flexible, malheureusement sur mariaDB j'obtiens l'erreur # 1067 - Valeur par défaut invalide pour 'interval_end' lors de l'exécution de l'appel make_intervals ('2009-01-01 00:00:00', '2009-01-10 00: 00: 00 ', 1,' JOUR '); Je suis sur la version 5.7.28
shelbypereira
28

Pour MSSQL, vous pouvez utiliser ceci. C'est TRÈS rapide.

Vous pouvez encapsuler cela dans une fonction de table ou un processus stocké et analyser les dates de début et de fin en tant que 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
Richard
la source
2
À quoi sert OPTION (MAXRECURSION 0) ici?
Shiva du
14

Nous avons eu un problème similaire avec les rapports BIRT en ce sens que nous voulions signaler les jours qui n'avaient pas de données. Comme il n'y avait pas d'entrées pour ces dates, la solution la plus simple pour nous était de créer une table simple qui stockait toutes les dates et de l'utiliser pour obtenir des plages ou une jointure pour obtenir des valeurs nulles pour cette date.

Nous avons un travail qui se déroule tous les mois pour nous assurer que le tableau est rempli 5 ans dans le futur. La table est créée ainsi:

create table all_dates (
    dt date primary key
);

Il existe sans aucun doute des moyens magiques et délicats de le faire avec différents SGBD, mais nous optons toujours pour la solution la plus simple. Les exigences de stockage pour la table sont minimes et cela rend les requêtes tellement plus simples et portables. Ce type de solution est presque toujours meilleur du point de vue des performances, car il ne nécessite pas de calculs par ligne sur les données.

L'autre option (et nous l'avons déjà utilisée) est de s'assurer qu'il y a une entrée dans le tableau pour chaque date. Nous avons balayé le tableau périodiquement et ajouté zéro entrée pour les dates et / ou heures qui n'existaient pas. Cela peut ne pas être une option dans votre cas, cela dépend des données stockées.

Si vous pensez vraiment qu'il est difficile de maintenir la all_datestable remplie, une procédure stockée est la voie à suivre qui renverra un ensemble de données contenant ces dates. Ce sera certainement plus lent car vous devez calculer la plage à chaque fois qu'elle est appelée plutôt que de simplement extraire des données précalculées d'une table.

Mais, pour être honnête, vous pouvez remplir le tableau pendant 1000 ans sans aucun problème sérieux de stockage de données - 365000 dates de 16 octets (par exemple) plus un index dupliquant la date plus 20% de frais généraux pour la sécurité, j'estimerais approximativement à environ 14M [365 000 * 16 * 2 * 1,2 = 14 016 000 octets]), une table minuscule dans le schéma des choses.

paxdiablo
la source
12

Vous pouvez utiliser les variables utilisateur de MySQL comme ceci:

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 vaut -1 car vous y ajoutez la première fois que vous l'utilisez. De plus, vous ne pouvez pas utiliser "HAVING date_sequence" car cela fait que la variable utilisateur s'incrémente deux fois pour chaque ligne.

Andrew Vit
la source
8

En empruntant une idée à cette réponse, vous pouvez configurer une table avec 0 à 9 et l'utiliser pour générer votre liste de dates.

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

Cela vous permettra de générer une liste de jusqu'à 1000 dates. Si vous avez besoin d'augmenter votre taille, vous pouvez ajouter une autre jointure croisée à la requête interne.

Logan5
la source
Cette solution fonctionne beaucoup mieux. Mais il a un problème avec UNIX_TIMESTAMP () - il donne des résultats comme 1231185600.000000; la partie en millisecondes après la virgule décimale; tandis que - SELECT UNIX_TIMESTAMP (ADDDATE ('2009-01-01', 0)) AS date; ne résulte PAS de cette partie.
Bimal Poudel
3

Pour Access (ou tout autre langage SQL)

  1. Créez une table qui a 2 champs, nous appellerons cette table tempRunDates:
    --Fields fromDateet toDate
    --Ensuite, insérez seulement 1 enregistrement, qui a la date de début et la date de fin.

  2. Créez une autre table: - Time_Day_Ref
    Importez une liste de dates (créer une liste dans Excel est facile) dans cette table.
    - Le nom du champ dans mon cas est Greg_Dt, pour la date grégorienne
    - J'ai créé ma liste du 1er janvier 2009 au 1er janvier 2020.

  3. Exécutez la requête:

    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!

Nathan Wood
la source
2

En règle générale, on utiliserait une table de nombres auxiliaires que vous gardez habituellement à cette fin, avec quelques variations à ce sujet:

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'ai vu des variations avec des fonctions table, etc.

Vous pouvez également conserver une liste permanente des dates. Nous avons cela dans notre entrepôt de données ainsi qu'une liste d'heures de la journée.

Cade Roux
la source
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
la source
2

Solution élégante utilisant la nouvelle fonctionnalité récursive (Common Table Expressions) dans MariaDB> = 10.3 et 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;

Ce qui précède renvoie un tableau des dates entre '2019-01-01' et '2019-04-30'.

Brad
la source
"sélectionner '2019-01-01' comme dt" qu'est-ce que ce sélecteur? Est-il possible de sélectionner simplement un champ from et to?
Miguel Stevens le
1

Nous l'avons utilisé dans notre système HRMS, vous le trouverez 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
Anas
la source
1

Cette solution fonctionne avec MySQL 5.0
Créer une table - mytable.
Le schéma n'a pas d'importance. Ce qui compte, c'est le nombre de lignes qu'il contient.
Ainsi, vous ne pouvez conserver qu'une seule colonne de type INT avec 10 lignes, valeurs - 1 à 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';

Limitation: le nombre maximum de dates renvoyées par la requête ci-dessus sera
(rows in mytable)*(rows in mytable) = 10*10 = 100.

Vous pouvez augmenter cette plage en modifiant la partie du formulaire en SQL:
from mytable x, mytable y, mytable z
So, the range be 10*10*10 =1000and so on.

Vihang Patil
la source
0

Créez une procédure stockée qui prend deux paramètres a_begin et a_end. Créez une table temporaire à l'intérieur de celle-ci appelée t, déclarez une variable d, affectez a_begin à d et exécutez une WHILEboucle INSERTd dans t et appelez la ADDDATEfonction pour incrémenter la valeur d. Enfin SELECT * FROM t.

Eugene Yokota
la source
En fait, je pense qu'une table permanente serait meilleure pour la vitesse d'exécution (a des exigences de stockage minimales). Il est plus rapide de pré-calculer les dates dans une table et de les extraire si nécessaire, que de créer des plages chaque fois que vous en avez besoin.
paxdiablo le
@Pax, cela dépend si l'amélioration de la vitesse acquise vaut le maintien de la table. Par exemple, si la génération du rapport prend 3 secondes et que l'exécution de la boucle WHILE pour générer des dates prend 0,001 seconde, je dirais que le gain de performances est ignorable. Knuth: L'optimisation prématurée est la racine de tout mal.
Eugene Yokota le
@ eed3si9n, optimisation prématurée, oui, pas toute l'optimisation :-) Si votre exemple était correct alors oui, je suis d'accord avec vous, mais il devrait être mesuré avec certitude. La table maint est partie si vous la générez 1000 ans (un coût unique) comme je l'ai suggéré, mais ces secondes s'additionneront.
paxdiablo le
0

J'utiliserais quelque chose de similaire à ceci:

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

Ensuite, la @HOLDERtable Variable contient toutes les dates incrémentées par jour entre ces deux dates, prêtes à être jointes à votre guise.

Tom 'Blue' Piddock
la source
Bonjour, j'essaie d'utiliser le code et d'insérer la sortie dans une table J'ai défini DateTest avec la colonne datefill de type DATETIME ... mais en échec ... pourriez-vous fournir le code qui fonctionnerait avec cela s'il vous plaît? Remarque: Utilisation de SQL Server ici Merci :)
sys_debug
0

Je me bats avec ça depuis un bon moment. Comme il s'agit du premier succès sur Google lorsque j'ai cherché la solution, permettez-moi de poster où je suis arrivé jusqu'à présent.

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

Remplacez [yourTable]par une table de votre base de données. L'astuce est que le nombre de lignes dans le tableau que vous sélectionnez doit être> = le nombre de dates que vous souhaitez renvoyer. J'ai essayé d'utiliser l'espace réservé de table DUAL, mais il ne renverrait qu'une seule ligne.

champion
la source
1
Approche intéressante. Mais ... n'est-ce pas essentiellement ce que Vihang a suggéré ci-dessus;)?
Leigh
0
select * from table_name where col_Date between '2011/02/25' AND DATEADD(s,-1,DATEADD(d,1,'2011/02/27'))

Ici, ajoutez d'abord un jour à la date de fin actuelle, ce sera le 28/02/2011 00:00:00, puis vous soustrayez une seconde pour obtenir la date de fin 27/02/2011 23:59:59. En faisant cela, vous pouvez obtenir toutes les dates entre les intervalles donnés.

sortie:
2011/02/25
2011/02/26
2011/02/27

Chandra Prakash
la source
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 $$

cette procédure insérera toutes les dates du début de l'année jusqu'à maintenant, remplacez simplement les jours du «début» et de la «fin», et vous êtes prêt à partir!

Julio Marins
la source
Comment la ligne est-elle IF a=365 THENaffectée par les années bissextiles?
Stewart le
Aussi, quelle est la signification du nombre 150à la ligne 9? Ce code est une "réponse pat" sans grande explication réelle.
Stewart le
0

J'avais besoin d'une liste avec tous les mois entre 2 dates pour les statistiques. Les 2 dates sont les dates de début et de fin d'un abonnement. Ainsi, la liste montre tous les mois et le nombre d'abonnements par mois.

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
sensationnel
la source
0

Vous pouvez utiliser ceci

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
la source
Source: - shayanderson.com/mysql
Dixon
-2

j'utilise Server version: 5.7.11-log MySQL Community Server (GPL)

Maintenant, nous allons résoudre cela de manière simple.

J'ai créé une table nommée "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)

maintenant, nous verrons les enregistrements insérés à l'intérieur.

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)

et ici notre requête pour récupérer les enregistrements dans deux dates plutôt que ces dates.

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)

espérons que cela aiderait beaucoup d’entre eux.

ArifMustafa
la source
Downvoté, sans aucune raison, ce SQL est bien et dans un scénario de travail.
ArifMustafa