генерировать дни из диапазона дат - PullRequest
128 голосов
/ 28 января 2010

Я хотел бы выполнить запрос как

select ... as days where `date` is between '2010-01-20' and '2010-01-24'

И возвращать данные как:

days
----------
2010-01-20
2010-01-21
2010-01-22
2010-01-23
2010-01-24

Ответы [ 28 ]

1 голос
/ 23 августа 2014

Как указывалось (или, по крайней мере, упоминалось) во многих замечательных ответах, которые уже даны, эта проблема легко решается, если у вас есть набор чисел для работы.* Ниже приведен T-SQL, но это просто моя конкретная реализация общих концепций, уже упомянутых здесь и в Интернете в целом.Преобразовать код в выбранный вами диалект должно быть относительно просто.

Как? Рассмотрим этот запрос:

SELECT DATEADD(d, N, '0001-01-22')
FROM Numbers -- A table containing the numbers 0 through N
WHERE N <= 5;

Выше приведен диапазон дат 1 /22/0001 - 27 января 0001 года и очень тривиально.В приведенном выше запросе есть 2 ключевых элемента информации: дата начала из 0001-01-22 и смещение из 5.Если мы объединим эти две части информации, то у нас, очевидно, будет дата окончания.Таким образом, с учетом двух дат генерация диапазона может быть разбита следующим образом:

  • Найти разницу между двумя данными датами (смещение), просто:

    -- Returns 125 SELECT ABS(DATEDIFF(d, '2014-08-22', '2014-12-25'))

    Использование ABS() гарантирует, что порядок дат не имеет значения.

  • Генерирует ограниченный набор чисел, также легко:

    -- Returns the numbers 0-2 SELECT N = ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) - 1 FROM(SELECT 'A' AS S UNION ALL SELECT 'A' UNION ALL SELECT 'A')

    Обратите внимание, что нам на самом деле все равно, что мы выбираем FROM здесь.Нам просто нужен набор для работы, чтобы мы посчитали количество строк в нем.Я лично использую TVF, некоторые используют CTE, другие вместо этого используют таблицу чисел, вы понимаете.Я выступаю за использование наиболее эффективного решения, которое вы также понимаете.

Объединение этих двух методов решит нашу проблему:

DECLARE @date1 DATE = '9001-11-21';
DECLARE @date2 DATE = '9001-11-23';

SELECT D = DATEADD(d, N, @date1)
FROM (
    SELECT N = ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) - 1
    FROM (SELECT 'A' AS S UNION ALL SELECT 'A' UNION ALL SELECT 'A') S
) Numbers
WHERE N <= ABS(DATEDIFF(d, @date1, @date2));

Приведенный выше пример - ужасный код, но демонстрирует, как все объединяется.

Больше удовольствия

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

Следующая функция требует ~ 16 мс времени ЦП для возврата максимального диапазона65536 дат.

CREATE FUNCTION dbo.GenerateRangeDate (   
    @date1 DATE,   
    @date2 DATE   
)   
RETURNS TABLE
WITH SCHEMABINDING   
AS   
RETURN (
    SELECT D = DATEADD(d, N + 32768, CASE WHEN @date1 <= @date2 THEN @date1 ELSE @date2 END)
    FROM dbo.GenerateRangeSmallInt(-32768, ABS(DATEDIFF(d, @date1, @date2)) - 32768)
);

GO

CREATE FUNCTION dbo.GenerateRangeSmallInt (
    @num1 SMALLINT = -32768
  , @num2 SMALLINT = 32767
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN (
    WITH Numbers(N) AS (
        SELECT N FROM(VALUES
            (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 16
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 32
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 48
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 64
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 80
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 96
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 112
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 128
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 144
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 160
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 176
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 192
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 208
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 224
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 240
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 256
        ) V (N)
    )
    SELECT TOP(ABS(CAST(@num1 AS INT) - CAST(@num2 AS INT)) + 1)
           N = ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) + CASE WHEN @num1 <= @num2 THEN @num1 ELSE @num2 END - 1
    FROM Numbers A
       , Numbers B
);
1 голос
/ 29 ноября 2018

Вы хотите получить диапазон дат.

В вашем примере вы хотели бы получить даты между '2010-01-20' и '2010-01-24'

возможное решение:

 select date_add('2010-01-20', interval row day) from
 ( 
    SELECT @row := @row + 1 as row FROM 
    (select 0 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 6 union all select 7 union all select 8 union all select 9) t,
    (select 0 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 6 union all select 7 union all select 8 union all select 9) t2, 
    (select 0 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 6 union all select 7 union all select 8 union all select 9) t3, 
    (select 0 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 6 union all select 7 union all select 8 union all select 9) t4, 
    (SELECT @row:=-1) r
 ) sequence
 where date_add('2010-01-20', interval row day) <= '2010-01-24'

Объяснение

MySQL имеет функцию date_add , поэтому

select date_add('2010-01-20', interval 1 day)

даст вам

2010-01-21

Функция datediff позволит вам часто знать, что вам придется повторять это

select datediff('2010-01-24', '2010-01-20')

, который возвращает

 4

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

Наиболее одобренный ответ здесь основывается на подходе, аналогичном https://stackoverflow.com/a/2652051/1497139:

SELECT @row := @row + 1 as row FROM 
(select 0 union all select 1 union all select 3 union all select 4 union all select 5 union all select 6 union all select 6 union all select 7 union all select 8 union all select 9) t,
(select 0 union all select 1 union all select 3 union all select 4 union all select 5 union all select 6 union all select 6 union all select 7 union all select 8 union all select 9) t2, 
(select 0 union all select 1 union all select 3 union all select 4 union all select 5 union all select 6 union all select 6 union all select 7 union all select 8 union all select 9) t3, 
(select 0 union all select 1 union all select 3 union all select 4 union all select 5 union all select 6 union all select 6 union all select 7 union all select 8 union all select 9) t4, 
(SELECT @row:=0) r
limit 4

, что приведет к

row
1.0
2.0
3.0
4.0

Теперь строки можно использовать для создания списка дат с заданной датой начала. Чтобы включить дату начала, мы начинаем со строки -1;

select date_add('2010-01-20', interval row day) from
 ( 
    SELECT @row := @row + 1 as row FROM 
    (select 0 union all select 1 union all select 3 union all select 4 union all select 5 union all select 6 union all select 6 union all select 7 union all select 8 union all select 9) t,
    (select 0 union all select 1 union all select 3 union all select 4 union all select 5 union all select 6 union all select 6 union all select 7 union all select 8 union all select 9) t2, 
    (select 0 union all select 1 union all select 3 union all select 4 union all select 5 union all select 6 union all select 6 union all select 7 union all select 8 union all select 9) t3, 
    (select 0 union all select 1 union all select 3 union all select 4 union all select 5 union all select 6 union all select 6 union all select 7 union all select 8 union all select 9) t4, 
    (SELECT @row:=-1) r
 ) sequence
 where date_add('2010-01-20', interval row day) <= '2010-01-24'
1 голос
/ 27 октября 2012

Создание дат между двумя полями дат

Если вы знакомы с запросом SQL CTE, то это решение поможет вам решить ваш вопрос

Вот пример

У нас есть даты в одной таблице

Название таблицы: «дата теста»

STARTDATE   ENDDATE
10/24/2012  10/24/2012
10/27/2012  10/29/2012
10/30/2012  10/30/2012

Требуется результат:

STARTDATE
10/24/2012
10/27/2012
10/28/2012
10/29/2012
10/30/2012

Решение:

WITH CTE AS
  (SELECT DISTINCT convert(varchar(10),StartTime, 101) AS StartTime,
                   datediff(dd,StartTime, endTime) AS diff
   FROM dbo.testdate
   UNION ALL SELECT StartTime,
                    diff - 1 AS diff
   FROM CTE
   WHERE diff<> 0)
SELECT DISTINCT DateAdd(dd,diff, StartTime) AS StartTime
FROM CTE

Объяснение: CTE Рекурсивное объяснение запроса

  • Первая часть запроса:

    SELECT DISTINCT convert(varchar(10), StartTime, 101) AS StartTime, datediff(dd, StartTime, endTime) AS diff FROM dbo.testdate

    Пояснение: первый столбец - «startdate», второй столбец - разница начала и конца. дата в днях и будет считаться столбцом «diff»

  • Вторая часть запроса:

    UNION ALL SELECT StartTime, diff-1 AS diff FROM CTE WHERE diff<>0

    Объяснение: Union all будет наследовать результат вышеупомянутого запроса, пока результат не станет нулевым, Таким образом, результат «StartTime» наследуется от сгенерированного запроса CTE, а от diff, уменьшается - 1, поэтому он выглядит как 3, 2 и 1 до 0

Например

STARTDATE   DIFF
10/24/2012  0
10/27/2012  0
10/27/2012  1
10/27/2012  2
10/30/2012  0

Спецификация результата

STARTDATE       Specification
10/24/2012  --> From Record 1
10/27/2012  --> From Record 2
10/27/2012  --> From Record 2
10/27/2012  --> From Record 2
10/30/2012  --> From Record 3
  • 3-я часть запроса

    SELECT DISTINCT DateAdd(dd,diff, StartTime) AS StartTime FROM CTE

    Это добавит день «diff» в «startdate», поэтому результат должен быть таким, как показано ниже

Результат

STARTDATE
10/24/2012
10/27/2012
10/28/2012
10/29/2012
10/30/2012
1 голос
/ 30 августа 2017

Хорошая идея - генерировать эти даты на лету.Тем не менее, я не чувствую себя комфортно делать это с довольно большим диапазоном, поэтому я получил следующее решение:

  1. Создал таблицу «DatesNumbers», в которой будут храниться числа, используемые для расчета дат:

CREATE TABLE DatesNumbers ( i MEDIUMINT NOT NULL, PRIMARY KEY (i) ) COMMENT='Used by Dates view' ;

Заполнение таблицы с использованием вышеуказанных методов с номерами от -59999 до 40000. Этот диапазон даст мне даты от 59999 дней (~ 164 лет) до 40000 дней (109 лет) вперед:

INSERT INTO DatesNumbers SELECT a.i + (10 * b.i) + (100 * c.i) + (1000 * d.i) + (10000 * e.i) - 59999 AS i FROM (SELECT 0 AS i 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) AS a , (SELECT 0 AS i 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) AS b , (SELECT 0 AS i 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) AS c , (SELECT 0 AS i 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) AS d , (SELECT 0 AS i 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) AS e ;

Создано представление «Даты»:

SELECT i , CURRENT_DATE() + INTERVAL i DAY AS Date FROM DatesNumbers

Вот и все.

  • (+) Легко читаемые запросы
  • (+) Нет на лету чисел поколений
  • (+) Дает даты в прошлом и будущем, и НЕТ СОЮЗА для этого, как в этом посте .
  • (+) Даты «Только в прошлом» или «Только в будущем» могут быть отфильтрованы с использованием WHERE i < 0 или WHERE i > 0 (PK)
  • (-) «временной» таблицы& view используется
1 голос
/ 21 января 2016

Короче, чем принятый ответ, та же идея:

(SELECT TRIM('2016-01-05' + INTERVAL a + b DAY) date
FROM
(SELECT 0 a UNION SELECT 1 a UNION SELECT 2 UNION SELECT 3
UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7
UNION SELECT 8 UNION SELECT 9 ) d,
(SELECT 0 b UNION SELECT 10 UNION SELECT 20
UNION SELECT 30 UNION SELECT 40) m
WHERE '2016-01-05' + INTERVAL a + b DAY  <=  '2016-01-21')
1 голос
/ 06 июня 2017

Для тех, кто хочет использовать это как сохраненное представление (MySQL не поддерживает вложенные операторы select в представлениях):

create view zero_to_nine as
    select 0 as n 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;

create view date_range as
    select curdate() - INTERVAL (a.n + (10 * b.n) + (100 * c.n)) DAY as date
    from zero_to_nine as a
    cross join zero_to_nine as b
    cross join zero_to_nine as c;

Вы можете сделать

select * from date_range

чтобы получить

date
---
2017-06-06
2017-06-05
2017-06-04
2017-06-03
2017-06-02
...
1 голос
/ 29 января 2010

если вам когда-нибудь понадобится больше пары дней, вам нужен стол.

Создать диапазон дат в mysql

тогда

select from days.day, count(mytable.field) as fields from days left join mytable on day=date where date between x and y;
0 голосов
/ 28 ноября 2018

Еще одно решение для mysql 8.0.1 и mariadb 10.2.2 с использованием рекурсивных общих табличных выражений:

with recursive dates as (
    select '2010-01-20' as date
    union all
    select date + interval 1 day from dates where date < '2010-01-24'
)
select * from dates;
0 голосов
/ 29 октября 2013

Для Oracle мое решение:

select trunc(sysdate-dayincrement, 'DD') 
  from dual, (select level as dayincrement 
                from dual connect by level <= 30)

Системная дата может быть изменена на определенную дату, а номер уровня может быть изменен для увеличения числа дат.

0 голосов
/ 14 июля 2016

улучшено с выходным днем ​​и присоединением к пользовательскому праздничному столу Microsoft MSSQL 2012 для таблицы дат PowerPivot https://gist.github.com/josy1024/cb1487d66d9e0ccbd420bc4a23b6e90e

with [dates] as (
    select convert(datetime, '2016-01-01') as [date] --start
    union all
    select dateadd(day, 1, [date])
    from [dates]
    where [date] < '2018-01-01' --end
)
select [date]
, DATEPART (dw,[date]) as Wochentag
, (select holidayname from holidaytable 
where holidaytable.hdate = [date]) 
as Feiertag
from [dates]
where [date] between '2016-01-01' and '2016-31-12'
option (maxrecursion 0)
...