Получить список дат между двумя датами

Используя стандартные функции mysql, можно написать запрос, который будет возвращать список дней между двумя датами.

например, учитывая 2009-01-01 и 2009-01-13, он вернет таблицу из одного столбца со значениями:

 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

Изменить: кажется, я не был ясен. Я хочу создать этот список. У меня есть значения, хранящиеся в базе данных (по дате и времени), но я хочу, чтобы они агрегировались при левом внешнем соединении со списком дат, как указано выше (я ожидаю нулевое значение в правой части части этого соединения в течение нескольких дней и обработаю это).

24 ответа

Решение

Я бы использовал эту хранимую процедуру, чтобы сгенерировать необходимые интервалы во временную таблицу с именем time_intervals, затем JOIN и объединить вашу таблицу данных с временной таблицей time_intervals.

Процедура может генерировать интервалы всех различных типов, которые вы видите указанными в ней:

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;

Подобный пример сценария данных в нижней части этого поста, где я построил аналогичную функцию для SQL Server.

Для MSSQL вы можете использовать это. Это очень быстро.

Вы можете обернуть это в табличную функцию или в сохраненную процедуру и проанализировать начальную и конечную даты как переменные.

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

Вы можете использовать пользовательские переменные MySQL следующим образом:

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, потому что вы добавляете его в первый раз, когда используете его. Кроме того, вы не можете использовать "HAVING date_sequence", потому что это приводит к увеличению пользовательской переменной в два раза для каждой строки.

У нас была похожая проблема с отчетами BIRT в том, что мы хотели сообщать в те дни, когда данных не было. Поскольку для этих дат не было записей, самым простым решением для нас было создать простую таблицу, в которой хранятся все даты, и использовать ее для получения диапазонов или объединить, чтобы получить нулевые значения для этой даты.

У нас есть работа, которая выполняется каждый месяц, чтобы обеспечить заполнение таблицы на 5 лет в будущем. Таблица создана таким образом:

create table all_dates (
    dt date primary key
);

Без сомнения, существуют магические хитрые способы сделать это с разными СУБД, но мы всегда выбираем самое простое решение. Требования к хранилищу для таблицы минимальны, и это делает запросы намного более простыми и переносимыми. Такое решение почти всегда лучше с точки зрения производительности, поскольку не требует вычислений для каждой строки данных.

Другой вариант (и мы использовали это раньше) - обеспечить наличие записи в таблице для каждой даты. Мы периодически просматривали таблицу и добавляли ноль записей для дат и / или времени, которых не было. Это может быть не вариант в вашем случае, это зависит от хранимых данных.

Если вы действительно думаете, что это сложно all_dates Заполненная таблица, хранимой процедурой является путь, который вернет набор данных, содержащий эти даты. Это почти наверняка будет медленнее, поскольку вы должны вычислять диапазон каждый раз, когда он вызывается, а не просто извлекать предварительно рассчитанные данные из таблицы.

Но, если честно, вы могли бы заполнить таблицу на 1000 лет без каких-либо серьезных проблем с хранением данных - 365 000 16-байтовых (например) дат плюс индекс, дублирующий дату, плюс 20% накладных расходов на безопасность, я бы примерно оценил в около 14 млн. [365 000 * 16 * 2 * 1,2 = 14 016 000 байт]), минимальная таблица в схеме вещей.

Элегантное решение, использующее новую рекурсивную (Common Table Expressions) функциональность в MariaDB >= 10.3 и 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;

Выше приведена таблица дат между "2019-01-01" и "2019-04-30".

Заимствуя идею из этого ответа, вы можете настроить таблицу с 0 по 9 и использовать ее для составления списка дат.

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

Это позволит вам создать список до 1000 дат. Если вам нужно увеличить размер, вы можете добавить еще одно перекрестное соединение к внутреннему запросу.

Для доступа (или любого языка SQL)

  1. Создайте одну таблицу, которая имеет 2 поля, мы назовем эту таблицу tempRunDates:
    --Fields fromDate а также toDate
    - Затем вставьте только 1 запись, которая имеет дату начала и дату окончания.

  2. Создать другую таблицу: Time_Day_Ref
    - Импортировать список дат (сделать список в Excel легко) в эту таблицу.
    - Имя поля в моем случае Greg_Dt, для григорианской даты
    - Я сделал свой список с 1 января 2009 года по 1 января 2020 года.

  3. Запустите запрос:

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

Легко!

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

Обычно можно использовать таблицу вспомогательных чисел, которую вы обычно держите для этой цели, с некоторыми вариациями:

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

Я видел варианты с табличными функциями и т. Д.

Вы также можете сохранить постоянный список дат. У нас есть это в нашем хранилище данных, а также список времени суток.

Вы можете использовать это

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;

Как найти даты между двумя указанными датами в SQL-сервере, объясняется на http://ektaraval.blogspot.com/2010/09/writing-recursive-query-to-find-out-all.html

Мы использовали это в нашей системе HRMS, вы найдете это полезным

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 до последнего дня того же месяца. Вы можете изменить диапазон дат, играя со значениями @e `INTERVAL`.

      SET @s := CAST('2021-12-01' AS DATE); -- start at 2021-12-01
SET @e := @s + INTERVAL 1 MONTH - INTERVAL 1 DAY; -- ends at 2021-12-31

WITH RECURSIVE d_range AS
(
       SELECT @s AS gdate -- generated dates
       UNION ALL
       SELECT gdate + INTERVAL 1 day
       FROM   d_range
       WHERE  gdate < @e
)
SELECT *
FROM   d_range;

Результат:

      | gdate       |
+------------+
| 2021-12-01 |
| 2021-12-02 |
| 2021-12-03 |
| 2021-12-04 |
| 2021-12-05 |
| 2021-12-06 |
| 2021-12-07 |
| 2021-12-08 |
| 2021-12-09 |
| 2021-12-10 |
| 2021-12-11 |
| 2021-12-12 |
| 2021-12-13 |
| 2021-12-14 |
| 2021-12-15 |
| 2021-12-16 |
| 2021-12-17 |
| 2021-12-18 |
| 2021-12-19 |
| 2021-12-20 |
| 2021-12-21 |
| 2021-12-22 |
| 2021-12-23 |
| 2021-12-24 |
| 2021-12-25 |
| 2021-12-26 |
| 2021-12-27 |
| 2021-12-28 |
| 2021-12-29 |
| 2021-12-30 |
| 2021-12-31 |
+------------+
-> 31 rows in set (0.037 sec)

Примечание. В этом запросе используется РЕКУРСИЯ, потому что он довольно медленный. 31 rows in set (0.037 sec), при выборе более длинных диапазонов дат он будет работать еще медленнее.

Это решение работает с MySQL 5.0
Создать таблицу - mytable,
Схема не материальная. Что имеет значение, так это количество строк в нем.
Таким образом, вы можете хранить только один столбец типа INT с 10 строками, значения - от 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';

Ограничение: максимальное количество дат, возвращаемых по вышеуказанному запросу, будет
(rows in mytable)*(rows in mytable) = 10*10 = 100.

Вы можете увеличить этот диапазон, изменив часть формы в sql:
из mytable x,mytable y, mytable z
Итак, диапазон будет 10*10*10 =1000 и так далее.

Я хотел бы использовать что-то похожее на это:

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

Тогда @HOLDER Таблица переменных содержит все даты, увеличенные по дням между этими двумя датами, готовые присоединиться к вашему сердцу.

Мне нужен был список со всеми месяцами между двумя датами для статистики. 2 даты - начало и конец подписки. Таким образом, список показывает все месяцы и количество подписок в месяц.

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
select * from table_name where col_Date between '2011/02/25' AND DATEADD(s,-1,DATEADD(d,1,'2011/02/27'))

Здесь сначала добавьте день к текущей конечной дате, это будет 2011-02-28 00:00:00, затем вычтите одну секунду, чтобы сделать конечную дату 2011-02-27 23:59:59. Делая это, вы можете получить все даты между заданными интервалами.

выход:
2011/02/25
2011/02/26
2011/02/27

Портативное решение

      -- Generate 
WITH 
tdates AS (
  WITH   t19 AS (
          SELECT  0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union 
          select 7     union select 8 union select 9 )    
   SELECT  adddate('1970-01-01',t4.i*10000 + t3.i*1000 + t2.i*100 + t1.i*10 + t0.i) datei 
   FROM  t19 AS t0, t19 AS t1, t19 AS t2, t19 AS t3,  t19 AS t4 
)

select * from tdates
where datei between '2012-02-10' and '2012-02-15'

Я боролся с этим довольно давно. Так как это первый хит в Google, когда я искал решение, позвольте мне публиковать, где я до сих пор.

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

замещать [yourTable] с таблицей из вашей базы данных. Хитрость в том, что количество строк в выбранной вами таблице должно быть>= количество дат, которые вы хотите вернуть. Я попытался использовать заполнитель таблицы DUAL, но он вернул бы только одну строку.

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

эта процедура вставит все даты с начала года по настоящее время, просто замените дни "начало" и "конец", и вы готовы к работе!

Нет функции, нет процедур

      SET @s := '2020-01-01';
SET @e := @s + INTERVAL 1 YEAR - INTERVAL 1 DAY; -- set end date to select

SELECT CAST((DATE(@s)+INTERVAL (H+T+U) DAY) AS DATE) d -- generate a list of 400 days starting from @s date so @e can not be more then @s + 
FROM ( SELECT 0 H
    UNION ALL SELECT 100 UNION ALL SELECT 200 UNION ALL SELECT 300
  ) H CROSS JOIN ( SELECT 0 T
    UNION ALL SELECT  10 UNION ALL SELECT  20 UNION ALL SELECT  30
    UNION ALL SELECT  40 UNION ALL SELECT  50 UNION ALL SELECT  60
    UNION ALL SELECT  70 UNION ALL SELECT  80 UNION ALL SELECT  90
  ) T CROSS JOIN ( SELECT 0 U
    UNION ALL SELECT   1 UNION ALL SELECT   2 UNION ALL SELECT   3
    UNION ALL SELECT   4 UNION ALL SELECT   5 UNION ALL SELECT   6
    UNION ALL SELECT   7 UNION ALL SELECT   8 UNION ALL SELECT   9
  ) U
 -- generated lenght of date list can be calculated as fallow 1 * H(3 + 1) * T(9 + 1) * U(9 + 1) = 400
 -- it is crucial to preserve the numbering convention as 1, 2 ... 20, 30..., 100, 200, ... to retrieve chronological date selection
 -- add more UNION 400, 500, 600, ... H(6 + 1) if you want to select from more than 700 days!
WHERE
  (DATE(@s+INTERVAL (H+T+U) DAY)) <= DATE((@e))
ORDER BY d;

СОЮЗЫ определяют длину и диапазон доступных дат для выбора. Так что всегда следите за тем, чтобы журнал был достаточным.

      SELECT 1 * (3 + 1) * (9 + 1) * (9 + 1); -- 400 days starting from from @s

На основе этой статьи
Протестировано и работает должным образом

Улучшая ответ @ Brad , я использовал CTE с курсором:

      DECLARE cursorPeriod CURSOR FOR SELECT * FROM (
   WITH RECURSIVE period as
        (
             SELECT '2019-01-01' as dt
             UNION
             SELECT DATE_ADD(period.dt, INTERVAL 1 DAY) FROM period WHERE DATE_ADD(period.dt, INTERVAL 1 DAY) <= '2019-04-30'
        )
   SELECT DATE(dt) FROM period ORDER BY dt ) AS derived_table;

DECLARE CONTINUE HANDLER FOR NOT FOUND SET exit_loop = TRUE;

Создайте хранимую процедуру, которая принимает два параметра a_begin и a_end. Создайте временную таблицу с именем t, объявите переменную d, присвойте a_begin d и запустите WHILE петля INSERTвходя в т и звоня ADDDATE функция для увеличения значения d. в заключение SELECT * FROM t,

Я использую Server version: 5.7.11-log MySQL Community Server (GPL)

Теперь мы решим это простым способом.

Я создал таблицу с именем "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)

Теперь мы увидим вставленные записи.

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)

и здесь наш запрос для получения записей в течение двух дат, а не этих дат.

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)

надеюсь, что это поможет многим.

Другие вопросы по тегам