94

使用标准 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

编辑:看来我还不清楚。我想生成这个列表。我有存储在数据库中的值(按日期时间),但希望它们在左外连接上聚合到上面的日期列表(我希望这些连接的右侧有几天为空,并将处理这个)。

4

22 回答 22

69

我将使用此存储过程将您需要的时间间隔生成到名为time_intervals的临时表中,然后将您的数据表与临时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 构建了一个类似的函数。

于 2009-02-04T06:02:57.100 回答
30

对于 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

编辑 2021/01(V 博士): 我喜欢这个解决方案并使它适用于 mySQL V8。这是代码,将其包装到一个过程中:

DELIMITER //

CREATE PROCEDURE dates_between (IN from_date DATETIME,
                               IN to_date DATETIME) BEGIN
    WITH RECURSIVE dates(Date) AS
    (
        SELECT from_date as Date
        UNION ALL
        SELECT DATE_ADD(Date, INTERVAL 1 day) FROM dates WHERE Date < to_date
    )
    SELECT DATE(Date) FROM dates;
END//

DELIMITER ;
于 2011-11-18T14:20:51.810 回答
14

您可以像这样使用 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”,因为这会使用户变量每行增加两次。

于 2009-02-04T05:10:05.397 回答
14

我们在 BIRT 报告中遇到了类似的问题,因为我们想报告那些没有数据的日子。由于这些日期没有条目,对我们来说最简单的解决方案是创建一个简单的表来存储所有日期并使用它来获取范围或连接以获取该日期的零值。

我们有一个每月运行的工作,以确保该表在未来 5 年内填充。该表是这样创建的:

create table all_dates (
    dt date primary key
);

毫无疑问,使用不同的 DBMS 有一些神奇的棘手方法可以做到这一点,但我们总是选择最简单的解决方案。表的存储需求很小,它使查询变得更加简单和可移植。从性能的角度来看,这种解决方案几乎总是更好,因为它不需要对数据进行每行计算。

另一个选项(我们之前使用过)是确保每个日期的表中都有一个条目。我们定期扫描表格并为不存在的日期和/或时间添加零条目。在您的情况下,这可能不是一个选项,它取决于存储的数据。

如果您真的认为保留all_dates表格很麻烦,那么可以使用存储过程来返回包含这些日期的数据集。这几乎肯定会更慢,因为您必须在每次调用它时计算范围,而不是仅仅从表中提取预先计算的数据。

但是,老实说,您可以将表格填充 1000 年而不会出现任何严重的数据存储问题 - 365,000 个 16 字节(例如)日期加上复制日期的索引加上 20% 的安全开销,我粗略估计为大约 14M [365,000 * 16 * 2 * 1.2 = 14,016,000 字节]),这是事物计划中的一个小表格。

于 2009-02-04T05:19:16.253 回答
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 个日期的列表。如果需要更大,可以在内部查询中添加另一个交叉连接。

于 2009-02-04T05:48:19.237 回答
5

在 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-02-05T16:27:37.520 回答
3

对于 Access(或任何 SQL 语言)

  1. 创建一个包含 2 个字段的表,我们将调用此表tempRunDates:--
    FieldsfromDate和--然后toDate
    仅插入 1 条记录,该记录具有开始日期和结束日期。

  2. 创建另一个表:Time_Day_Ref
    --将日期列表(在 excel 中制作列表很容易)导入到此表中。
    -- 在我的例子中,字段名称是Greg_Dt, 表示公历日期
    -- 我从 2009 年 1 月 1 日到 2020 年 1 月 1 日列出了我的列表。

  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;
    

简单的!

于 2011-08-23T17:46:36.437 回答
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

我已经看到了表值函数等的变化。

您还可以保留永久的日期列表。我们的数据仓库中有它以及一天中的时间列表。

于 2009-02-04T06:12:40.343 回答
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
于 2012-01-24T21:42:28.607 回答
1

我们在 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
于 2011-06-08T23:05:39.677 回答
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中的form part来增加这个范围:
from mytable x,mytable y, mytable z
所以,范围是10*10*10 =1000等等。

于 2011-08-04T06:42:39.667 回答
1

你可以用这个

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;
于 2020-08-05T15:45:31.947 回答
1

没有临时表 | 无程序 | 无功能

下面的示例显示一个月的日期范围 - 从同一个月的第 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)在选择较长的日期范围时会变得更慢。

于 2021-07-17T16:07:57.243 回答
0

创建一个带有两个参数 a_begin 和 a_end 的存储过程。在其中创建一个名为 t 的临时表,声明一个变量 d,将 a_begin 分配给 d,然后运行一个WHILE循环INSERTd 到 t 中并调用ADDDATE函数来增加值 d。最后SELECT * FROM t

于 2009-02-04T04:46:01.390 回答
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变量表保存这两个日期之间按天递增的所有日期,随时可以加入。

于 2010-08-19T10:49:34.827 回答
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,但它只会返回一行。

于 2012-04-06T17:58:21.027 回答
0
select * from table_name where col_Date between '2011/02/25' AND DATEADD(s,-1,DATEADD(d,1,'2011/02/27'))

在这里,首先给当前的 endDate 添加一天,它将是 2011-02-28 00:00:00,然后减去一秒以使结束日期为 2011-02-27 23:59:59。通过这样做,您可以获得给定间隔之间的所有日期。

产量:
2011/02/25
2011/02/26
2011/02/27

于 2013-08-15T16:27:29.677 回答
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 $$

这个程序将插入从年初到现在的所有日期,只需替换“开始”和“结束”的日子,你就可以开始了!

于 2014-07-10T20:33:17.953 回答
0

我需要一个包含两个日期之间所有月份的列表来进行统计。这两个日期是订阅的开始日期和结束日期。 因此,该列表显示了所有月份和每月的订阅量。

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
于 2016-08-31T09:11:47.250 回答
0

没有功能,没有程序

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;

UNIONS 定义可供选择的可用日期的长度和范围。因此,请始终确保使其记录足够多。

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

基于本文
测试并按预期工作

于 2021-07-17T15:35:02.010 回答
0

改进@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;
于 2021-09-08T21:52:41.400 回答
-3

我在用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)

希望这会帮助很多人。

于 2018-04-19T02:11:27.080 回答