Отримайте список дат між двома датами


91

За допомогою стандартних функцій mysql є спосіб написати запит, який поверне список днів між двома датами.

наприклад, з урахуванням 01-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

Редагувати: Здається, я не зрозумів. Я хочу СТВОРИТИ цей список. У мене є значення, що зберігаються в базі даних (за датою і часом), але хочу, щоб вони були агреговані на лівому зовнішньому об’єднанні до списку дат, як зазначено вище (я очікую нульове значення з правого боку цього об’єднання протягом декількох днів, і це буде обробляти ).


2
Я думаю, найкраще рішення описано у відповіді stackoverflow.com/a/2157776/466677
Марек Грегор,

Відповіді:


68

Я б використав цю збережену процедуру для створення необхідних інтервалів у тимчасовій таблиці з ім'ям 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.


4
Я особисто сподівався на щось подібне до згенерованих послідовностей у PostgreSQL.
Філіп Вілан

Якщо ви генеруєте дані один раз, ви можете навіть використовувати постійні таблиці і використовувати цей скрипт, щоб заповнити відсутні дати.
Bimal Poudel

Мені довелося змінити роздільник перед оператором створення процедури, щоб він працював через phpMyAdmin (щоб уникнути синтаксичної помилки в операторах оголошення) [код] DECLARE // [/ code]
Defkon1

Це рішення є дуже потужним та гнучким, на жаль, на mariaDB. Я отримую помилку # 1067 - Недійсне значення за замовчуванням для 'interval_end' під час запуску виклику make_intervals ('2009-01-01 00:00:00', '2009-01-10 00: 00: 00 ', 1,' ДЕНЬ '); Я на версії 5.7.28
shelbypereira

28

Для MSSQL ви можете використовувати це. Це ДУЖЕ швидко.

Ви можете обернути це у табличну функцію або зберегти proc та проаналізувати дати початку та кінця як змінні.

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
Яка користь від OPTION (MAXRECURSION 0) тут?
Шива

14

У нас була подібна проблема зі звітами 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 байт]), мінімальна таблиця на схемі речей.


12

Ви можете використовувати користувацькі змінні 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", оскільки це робить змінну користувача двічі збільшеною для кожного рядка.


8

Запозичивши ідею з цієї відповіді, ви можете створити таблицю з 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 дат. Якщо вам потрібно збільшити розмір, ви можете додати до внутрішнього запиту ще одне поперечне з’єднання.


Це рішення працює набагато краще. Але у нього проблема з UNIX_TIMESTAMP () - вона дає результати, такі як 1231185600.000000; мілісекундна частина після десяткової коми; в той час як - SELECT UNIX_TIMESTAMP (ADDDATE ('2009-01-01', 0)) AS date; НЕ приводить цю частину.
Bimal Poudel

3

Для доступу (або будь-якої мови SQL)

  1. Створіть одну таблицю, яка має 2 поля, ми будемо називати цю таблицю tempRunDates: -
    Поля 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;

Легко!


2

Зазвичай можна використовувати таблицю допоміжних чисел, яку ви зазвичай тримаєте саме для цієї мети, з деякими варіаціями щодо цього:

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

Я бачив варіації табличних функцій тощо.

Ви також можете зберегти постійний список дат. У нас це є у нашому сховищі даних, а також перелік періодів доби.



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

Елегантне рішення з використанням нових функцій рекурсивного (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 ''.


"вибрати" 2019-01-01 "як dt" що це за селектор? Чи можна просто вибрати поле від і до?
Мігель Стівенс,

1

Ми використали це в нашій системі управління персоналом, і ви знайдете це за корисне

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

Це рішення працює з 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
Отже, діапазон be 10*10*10 =1000тощо.


0

Створіть збережену процедуру, яка приймає два параметри a_begin і a_end. Створіть в ній тимчасову таблицю під назвою t, оголосіть змінну d, призначте a_begin d і запустіть WHILEцикл INSERTd в t і виклик ADDDATEфункції для збільшення значення d. Нарешті SELECT * FROM t.


Насправді я вважаю, що постійна таблиця була б кращою для швидкості виконання (має мінімальні вимоги до зберігання). Швидше заздалегідь обчислити дати в таблиці та витягнути їх, коли це потрібно, ніж створити діапазони кожного разу, коли вони вам потрібні.
paxdiablo

@Pax, це залежить, чи варто набране поліпшення швидкості обслуговування таблиці. Наприклад, якщо генерація звіту займає 3 секунди, а запуск циклу WHILE для генерації дат займає 0,001 секунди, я стверджую, що приріст продуктивності ігнорується. Кнут: Передчасна оптимізація - корінь усього зла.
Євген Йокота,

@ eed3si9n, передчасна оптимізація, так, не вся оптимізація :-) Якщо ваш приклад був правильним, тоді так, я б погодився з вами, але це слід виміряти напевно. Table maint зник, якщо ви генеруєте його за 1000 років (одноразова вартість), як я вже запропонував, але ці секунди додадуться.
paxdiablo

0

Я б використав щось подібне до цього:

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таблиця змінних містить усі дати, збільшені по днях між цими двома датами, готові приєднатися до вашого змісту.


Привіт, я намагаюся використати код і вставити вихідні дані у таблицю, яку я визначив DateTest із заповненням дати стовпця типу DATETIME ... але якщо не вдасться ... чи можете ви надати код, який буде працювати з цим, будь ласка? Примітка: Використання SQL Server тут Дякую :)
sys_debug

0

Я борюся з цим вже досить давно. Оскільки це перший хіт у 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, але він повернув би лише один окремий рядок.


1
Цікавий підхід. Але .. чи не по суті це те, що запропонував Віханг вище;)?
Лі

0
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. Роблячи це, ви можете отримати всі дати між заданими інтервалами.

вихід:
25.02.2011
26.02.2011
27.02.2011


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

ця процедура буде вставляти всі дати з початку року до теперішнього часу, просто підставляючи дні "початку" і "кінця", і ви готові до роботи!


Як на лінію IF a=365 THENвпливають високосні роки?
Стюарт

Крім того, яке значення має число 150в рядку 9? Цей код - "відповідь" без особливих пояснень.
Стюарт

0

Мені потрібен був список із усіма місяцями між 2 датами для статистики. 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

0

Ви можете використовувати це

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

я використовую 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)

зараз, wee побачить вставлені записи всередині.

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)

сподіваюся, це допоможе багатьом.


Цей SQL, без будь-якої причини, без жодної причини, чудово працює і працює.
ArifMustafa
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.