Mysql生成任意指定两时间范围内的日期列表

本文介绍四种在SQL中生成指定日期范围内的所有日期的方法,包括创建存储过程、使用变量控制生成连续日期、利用现有表操作及直接传入起始和结束时间生成日期。每种方法都附有实例代码,适合不同业务需求。

前言:工作当中有一个场景是数据库存了一些数据,但是日期是零碎的,只有当用户进行了相应日期的操作才会有对应记录的生成。在做所有的数据统计可视化的时候,需要看到的是每一天的日期,此时就需要提供一段时间范围内的所有日期。

在网上查找资料,看到的有三种方法,总结如下:

一、创建存储过程(即创建一个临时的日历表。我是这样理解的),

例一:

以下是我从别人的博客抄过来的例子,这个例子是将日期范围写在创建的逻辑当中,感兴趣的伙伴可以试一下执行,当执行了调用的语句可以查看自己数据库中是否多了一张表。

例二:

这是另外一个创建存储过程,不同于以上的例子,这个可以直接在调用的时候给时间范围。对于日期范围需要和用户交互,需要灵活更改的,这个方法更加适用

DELIMITER $$
DROP PROCEDURE IF EXISTS create_calendar $$
CREATE PROCEDURE create_calendar (s_date DATE, e_date DATE)
BEGIN
 
	SET @createSql = 'CREATE TABLE IF NOT EXISTS calendar (
                      `date` date NOT NULL,
		       UNIQUE KEY `unique_date` (`date`) USING BTREE
                   )ENGINE=InnoDB DEFAULT CHARSET=utf8'; 
	prepare stmt from @createSql; 
	execute stmt; 
 
	WHILE s_date <= e_date DO
		INSERT IGNORE INTO calendar VALUES (DATE(s_date)) ;
		SET s_date = s_date + INTERVAL 1 DAY ;
	END WHILE ; 
 
END$$
DELIMITER ;
 
CALL create_calendar ('2018-03-01', '2018-12-30');

总结:使用存储过程的优点网上随便一查就能查到,以上这两种方式都能生成一张临时表,里面存放着你自己指定的时间范围内的所有日期。根据自己业务需求,可以选取其中一种方法,生成临时表,再与你的数据进行各种操作得出某段时间范围内日期齐全的数据。


二、(变量控制)指定数据条数,生成连续的数字或日期

例一:

SELECT DATE_FORMAT(DATE_SUB(NOW(), INTERVAL xc DAY), '%Y-%m-%d') as date
FROM ( 
			SELECT @xi:=@xi+1 as xc from 
			(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) xc1, 
			(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) xc2,  
			(SELECT @xi:=0) xc0 
) xcxc

 

结果如下所示:

 

 

以上代码的作用就是生成当日前25(包括当月)个日的日期。具体解释如下

查询日期的详解

 

 

总结:在如上的例子当中,涉及到的知识点是变量,DATE_SUB(),DATE_FORMAT().使用以上方法的好处就是不用创建存储过程,也不涉及到任何表。缺点就是数据的条数控制并不灵活,不能和用户之间形成互动,即不能自定义日期区间,只能控制数据条数。

三、(利用现有的表做操作)

要求是这个表中的数据足够庞大,好处是不涉及存储过程,不涉及变量,且能自定义日期区间。

select 
@num:=@num+1,
date_format(adddate(DATE_SUB('2019-01-17',INTERVAL 1 DAY), INTERVAL @num DAY),'%Y-%m-%d') as date
from order_info,(select @num:=0) t 
where adddate('2019-01-17', INTERVAL @num DAY) <= DATE_FORMAT(CURDATE(),'%Y-%m-%d')
order by date;

执行的结果是:

这个是直接把开始时间写成一个固定值的,当然,这个开始时间也类似于一个参数,是可传的,比如我不知道当前表中最开始的时间,那么,我们就只能通过查询出来之后再从当天开始:

select 
@num:=@num+1,
date_format(adddate(DATE_SUB((SELECT create_time FROM order_info ORDER BY create_time LIMIT 0,1),INTERVAL 1 DAY), INTERVAL @num DAY),'%Y-%m-%d') as date
from order_info,(select @num:=0) t 
where adddate((SELECT create_time FROM order_info ORDER BY create_time LIMIT 0,1), INTERVAL @num DAY) <= (SELECT MAX(create_time) FROM order_info WHERE is_delete =0)
order by date;

这里的起止时间,都是查询当前表中最大和最小的日期,结果如果:

 

 

这些sql的具体解释,如下:

 

还有一种类似的查询方式

SELECT
	@date := DATE_ADD( @date, INTERVAL + 1 DAY ) days 
FROM
	( SELECT @date := DATE_ADD( '2020-08-20', INTERVAL - 1 DAY ) FROM gas_day_schedule_card ) time 
WHERE
	to_days( @date ) < to_days( '2020-10-05' );

展示结果:

 

 

总结:这个是我选择的方法,因为业务需要,我不能用创建存储过程的方法,我主要是做的数据的统计,一些没有产生数据的日期,也需要显示出来,再加上还需要统计别的一些信息,所以我选择的最后这种利用现有的表来查询,再去做自己需要的一些查询。缺点就是必须要有一张现成的表,而且在这么久的长时间使用中,发现可能会出现就算有表,但是获取的时间不全。

 

四、不用现有的表进行操作,直接传入起始时间和结束时间,进行生成每一天数据

SELECT DATE_FORMAT( date_add(concat('2020-08-20'), interval(help_topic_id) DAY),'%Y-%m-%d') date
		FROM mysql.help_topic
		WHERE help_topic_id  <=  timestampdiff(DAY,concat('2020-08-20'),concat('2020-10-05'))

最终的结果是如下图:

总结:这个比较好的就是,不用现有数据库存在任何表,也不用创建存储过程什么的;Mysql没有递归查询,是如何实现指定日期查询的:mysql.help_topic 这时候就可以使用mysql自带的这个表来实现。(算是取巧的方法,暂时没找到更好的方法)


写到这里,也就差不多,以上的几种方法,也是我百度来的,可以根据自己的具体需求选择具体的方法。

 

 

 

<think> 我们已知MySQL提供了`LAST_DAY()`函数来获取任意日期所在月份的最后一天。现在用户的需求是查询一个指定日期范围内(例如从'2023-01-01'到'2023-12-31')的每一个月的最后一天。 思路: 1. 首先需要生成指定日期范围内的所有日期(或者至少是每个月的代表日期),然后对每个日期应用`LAST_DAY()`函数,但这样会得到很多重复的最后一天(因为同一个月的不同日期都会返回该月的最后一天)。 2. 因此,更高效的做法是先生成这个日期范围内所有的月份,然后对每个月份取最后一天。 具体步骤: 方法一:使用日期区间生成一系列月份的第一天,然后通过`LAST_DAY()`得到该月的最后一天。 方法二:生成一个包含连续数字的辅助表(例如使用`mysql.help_topic`表或递归CTE(MySQL 8.0+)),然后通过日期计算得到每个月份。 下面分别给出种方法: ### 方法一:使用辅助表(如mysql.help_topic)生成月份序列(适用于MySQL 5.x和8.x) 假设我们有一个辅助表(如`mysql.help_topic`)来提供序列数字。我们可以通过计算起始日期和结束日期之间的月份数,然后生成每个月份的第一天,再取最后一天。 步骤: 1. 计算起始日期(start_date)到结束日期(end_date)之间相差的月份数(记为n)。 2. 生成0到n(包含n)的整数序列,每个整数代表从起始日期开始增加的月份数。 3. 对每个整数i,计算日期:`start_date + INTERVAL i MONTH`,然后取该日期所在月份的第一天(可以用`DATE_ADD(start_date, INTERVAL i MONTH)`,但注意这样得到的日期可能不是1号,所以我们可以先取这个日期的1号,然后再取最后一天?但是这样复杂了。实际上,我们可以直接取`LAST_DAY(start_date + INTERVAL i MONTH)`,但这样可能会超出范围?所以应该先生成每个月的第一天。 更准确的做法是:生成每个月的第一天,然后取这个第一天的最后一天(即该月的最后一天)。但是注意,如果我们从起始日期的第一天开始,那么最后一天不会超出范围。 具体SQL: 例如,查询从'2023-01-15'到'2023-12-20'之间的每月最后一天: ```sql SELECT LAST_DAY(DATE_ADD('2023-01-15', INTERVAL t.month_offset MONTH)) AS last_day_of_month FROM (SELECT @row := @row + 1 AS month_offset 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) AS a, (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) AS b, (SELECT @row := -1) AS init ) AS t WHERE DATE_ADD('2023-01-15', INTERVAL t.month_offset MONTH) <= '2023-12-20' ``` 但是这种方法需要生成足够多的序列,并且起始点设置为-1是为了包含起始日期所在的月份(因为第一个月偏移0个月就是起始日期所在月份,但我们的起始日期是1月15,我们想要1月的最后一天,所以偏移0个月取1月15然后取最后一天是1月31,正确)。但是这样生成的序列是从0开始到月份差(这里月份差是11个月,从0到11共12个月)。 但是上面的辅助表只能生成最多36个数字(6*6),如果时间跨度很长就不够。我们可以使用`mysql.help_topic`表(如果有权限且数据足够)或者用递归CTE(MySQL 8.0+)。 使用`mysql.help_topic`的版本: ```sql SELECT LAST_DAY(DATE_ADD('2023-01-15', INTERVAL t.month_offset MONTH)) AS last_day_of_month FROM (SELECT @row := @row + 1 AS month_offset FROM mysql.help_topic, (SELECT @row := -1) AS init WHERE @row < PERIOD_DIFF(DATE_FORMAT('2023-12-20', '%Y%m'), DATE_FORMAT('2023-01-15', '%Y%m')) ) AS t ``` 解释: 1. 使用变量`@row`生成一个从-1开始的序列,直到月份差(注意:`PERIOD_DIFF`返回日期之间的月份差,注意日期格式为'YYYYMM'或'YYMM',它返回的是整数,表示日期相差的月份数)。 2. 注意:`PERIOD_DIFF`计算的是日期之间相差的月份数,比如从202301到202312,相差11。但是我们希望生成的月份偏移量是从0到11(共12个月),所以我们设置`@row`从-1开始,这样当`@row`从-1增加到11时,条件`@row < 11`(因为`PERIOD_DIFF`返回11)会生成0到11(因为当`@row`=11时,条件不满足,所以最大是10?)。所以需要调整条件。 修正:我们想要月份偏移量从0到n(n为月份差),那么我们需要生成0到n的整数。注意,起始日期加上0个月就是第一个月,加上n个月就是最后一个月(如果结束日期大于等于这个日期的话)。所以月份数应该是:0,1,...,n,其中n=PERIOD_DIFF(end_date, start_date)?但是注意PERIOD_DIFF是取整的,比如2023-01-15和2023-12-20,PERIOD_DIFF('202312','202301')=11。 所以我们需要生成0到11的序列,共12个。因此,我们可以这样: ```sql SELECT LAST_DAY(DATE_ADD('2023-01-15', INTERVAL t.month_offset MONTH)) AS last_day_of_month FROM (SELECT @row := @row + 1 AS month_offset FROM mysql.help_topic CROSS JOIN (SELECT @row := -1) AS init WHERE @row < PERIOD_DIFF(DATE_FORMAT('2023-12-20', '%Y%m'), DATE_FORMAT('2023-01-15', '%Y%m')) ) AS t ``` 但是这样写,由于`mysql.help_topic`的行数可能很多,我们只需要12行,所以条件`@row < 11`会限制只生成12行(因为@row从-1开始,每次加1,直到10(因为10<11,所以会生成到10,然后下一次@row=11就不满足条件了))这样只有11个偏移量(0到10),缺少了11。所以应该改为: 我们可以让条件为`@row < PERIOD_DIFF(...)`,这样生成的偏移量就是0到`PERIOD_DIFF(...)-1`?不对,因为初始为-1,然后每次+1,所以第一行是0,第二行是1,直到第n行是n-1。所以如果我们想要0到n(n=11)的12个数,那么条件应该是`@row <= n`,但是n=11,所以条件应该是`@row < 12`?或者我们改变初始值和条件。 更简单的方法:我们直接计算需要生成的行数,即月份差+1(因为包含起始月份)。所以: ```sql SELECT LAST_DAY(DATE_ADD('2023-01-15', INTERVAL t.month_offset MONTH)) AS last_day_of_month FROM (SELECT @row := @row + 1 AS month_offset FROM mysql.help_topic, (SELECT @row := -1) AS init WHERE @row < (PERIOD_DIFF(DATE_FORMAT('2023-12-20', '%Y%m'), DATE_FORMAT('2023-01-15', '%Y%m'))) ) t ``` 但是这样只能生成0到11(因为初始-1,然后条件<11,所以生成0,1,...,10)共11个?我们需要12个(0到11)。所以应该让条件为`@row <= PERIOD_DIFF(...)`,但是这样写的话,当月份差为11时,我们需要生成0到11(共12个)。所以: 修改:初始值设为-1,然后条件`@row < (月份差)`,这样生成的是0到月份差-1。所以月份差应该是实际相差的月份数,而我们想要的是从0到月份差(包含)?实际上,我们想要的是从起始月份到结束月份的所有月份,所以月份数=结束月份-起始月份+1(在同一个年份里)。但是PERIOD_DIFF返回的是日期之间的整月数,比如202301和202301,返回0;202301和202302,返回1。所以如果我们从202301到202312,那么PERIOD_DIFF返回11(因为202312-202301=11)。但是实际上我们需要12个月(1月到12月)。所以行数应该是12=11+1。因此,我们生成0到11(共12个)偏移量。 因此,条件应该是`@row <= (PERIOD_DIFF(..., ...))`,但是初始为-1,那么当月份差为11时,我们需要生成12行,即条件应该是`@row < (PERIOD_DIFF(..., ...) + 1)`?这样初始-1,然后0,1,...,10,11,当@row=11时,条件11<12成立,然后下一行@row=12,条件12<12不成立,所以最后一行是11。所以: ```sql SELECT LAST_DAY(DATE_ADD('2023-01-15', INTERVAL t.month_offset MONTH)) AS last_day_of_month FROM (SELECT @row := @row + 1 AS month_offset FROM mysql.help_topic, (SELECT @row := -1) AS init WHERE @row < (PERIOD_DIFF(DATE_FORMAT('2023-12-20', '%Y%m'), DATE_FORMAT('2023-01-15', '%Y%m')) + 1) ) t ``` 这样,对于2023-01-15到2023-12-20,我们会生成偏移量0到11,共12个月份,每个月份的最后一天。 注意:如果起始日期是当月的最后一天,那么它会被包含吗?例如,起始日期是2023-01-31,那么偏移0个月得到2023-01-31,即最后一天。所以包含。 但是,如果我们的结束日期是2023-12-15,那么2023-12月的最后一天(2023-12-31)是否应该包含?因为我们的结束日期是12月15,而12月31日已经超过了结束日期。但用户要求的是“指定日期范围内每月最后一天”,如果最后一天在结束日期之前,则包含;如果最后一天在结束日期之后,则不包含。但是注意,我们生成的月份偏移量是从0到月份差(这里月份差=11,即0到11),那么12月的最后一天是2023-12-31,大于结束日期2023-12-15,所以我们应该在最后过滤掉大于结束日期的那些最后一天。 因此,我们可以在外层加上条件: ```sql SELECT last_day FROM ( SELECT LAST_DAY(DATE_ADD('2023-01-15', INTERVAL t.month_offset MONTH)) AS last_day FROM (SELECT @row := @row + 1 AS month_offset FROM mysql.help_topic, (SELECT @row := -1) AS init WHERE @row < (PERIOD_DIFF(DATE_FORMAT('2023-12-20', '%Y%m'), DATE_FORMAT('2023-01-15', '%Y%m')) + 1) ) t ) AS all_last_days WHERE last_day <= '2023-12-20' ``` 这样,我们就能确保只返回在结束日期之前的最后一天。 ### 方法二:使用递归CTE(MySQL 8.0及以上) 如果使用MySQL 8.0,我们可以使用递归CTE来生成日期序列。 ```sql WITH RECURSIVE DateRange AS ( SELECT LAST_DAY('2023-01-15') AS month_end -- 第一个日期:起始日期所在月份的最后一天 UNION ALL SELECT LAST_DAY(DATE_ADD(month_end, INTERVAL 1 MONTH)) -- 下个月的最后一天 FROM DateRange WHERE DATE_ADD(month_end, INTERVAL 1 MONTH) <= '2023-12-20' -- 条件:下个月的最后一天对应的月份(即下个月)的开始日期<=结束日期?注意:我们生成的是最后一天,但是条件应该是下个月的最后一天是否在结束日期之后?所以这里条件应该是:下个月的最后一天大于结束日期时停止,但是我们这里条件是用下个月的第一天(即DATE_ADD(month_end, INTERVAL 1 DAY))来加一个月?不对,我们直接加一个月得到下个月的最后一天,然后判断这个最后一天是否小于等于结束日期?不对,因为我们递归的条件是:只要下个月的最后一天对应的月份的开始日期(即下个月的第一天)小于等于结束日期,我们就继续?但是这样复杂。 -- 修正:我们实际上想要的是,在起始日期的最后一天之后,逐月增加,直到最后一天超过结束日期。所以我们可以这样: -- 递归条件:当前月份的最后一天加上1个月(得到下个月的最后一天)应该小于等于结束日期?不对,因为可能结束日期在一个月的中间。所以我们应该用:当前月份的最后一天小于结束日期,就继续递归?这样也不对,因为当前月份的最后一天已经小于结束日期,那么下个月的最后一天可能大于结束日期,我们还要生成下个月吗?但是用户要求的是每月最后一天,只要这个最后一天在指定日期范围内(小于等于结束日期)就包括。 -- 所以递归条件应该是:当前月份的最后一天加上1个月后得到的下一个月的最后一天,其日期必须小于等于结束日期?这样我们就能在递归中生成下一个月的最后一天。但是这样会导致最后一次递归生成的那个月最后一天可能大于结束日期。所以我们应该在最后一步过滤。 -- 因此,我们可以在递归中不加过滤,而是通过递归条件控制不要生成超过结束日期的月份。但是递归条件用:DATE_ADD(month_end, INTERVAL 1 MONTH) <= '2023-12-20' ?这样比较的是下个月的最后一天(因为LAST_DAY(DATE_ADD(month_end, INTERVAL 1 MONTH)))?但是我们在递归条件中还没有生成下个月的最后一天,所以不能直接用。 -- 改为:我们递归生成的是每个月的最后一天,那么递归的终止条件应该是:当前最后一天(即上一次生成的)小于结束日期,然后我们才需要生成下一个月的最后一天。但是注意,我们递归中已经生成了当前最后一天,下一个最后一天是当前最后一天加1个月。所以条件应该是:当前最后一天加1个月(即下一个月的第一天?)小于等于结束日期?不对,因为下一个月的最后一天可能大于结束日期,但是只要下个月的最后一天大于结束日期,我们就不需要生成了。所以我们可以用:当前最后一天(即已生成的)小于结束日期,那么我们就生成下一个月。但是这样会导致最后一个月即使最后一天大于结束日期,我们还是会生成(因为递归时,当前最后一天是上个月的最后一天,它小于结束日期,所以我们生成了这个月,但这个月最后一天大于结束日期)。因此,我们可以在递归后过滤。 -- 所以,我们可以在递归中不设置条件,而是设置一个最大递归深度(避免无限递归),然后在最外层过滤。 -- 或者,我们可以这样:递归条件为:DATE_ADD(month_end, INTERVAL 1 MONTH) <= 结束日期?这里我们比较的是日期,但是注意,结束日期可能是2023-12-20,而DATE_ADD(month_end, INTERVAL 1 MONTH)得到的是下个月的最后一天(例如2023-02-28加1个月得到2023-03-31),这个日期大于2023-12-20?不对,我们其实只需要判断下个月的第一天(即当前最后一天+1天)是否小于等于结束日期?不对。 -- 更简单:我们递归生成每个月的第一天,然后取最后一天,并且控制第一天在结束日期之前。 -- 因此,我们改变策略:生成每个月的第一天,然后取最后一天。递归生成每个月的第一天,直到超过结束日期。 -- 所以,递归CTE可以这样写: -- 但是题目要求最后一天,我们可以先生成月份的第一天,然后取最后一天。 -- 因此,我们重新写递归CTE: WITH RECURSIVE MonthStarts AS ( SELECT DATE_FORMAT('2023-01-15', '%Y-%m-01') AS month_start -- 起始日期所在月的第一天 UNION ALL SELECT DATE_ADD(month_start, INTERVAL 1 MONTH) FROM MonthStarts WHERE DATE_ADD(month_start, INTERVAL 1 MONTH) <= LAST_DAY('2023-12-20') -- 下个月的第一天必须小于等于结束日期所在月的最后一天?注意结束日期是2023-12-20,那么下个月的第一天直到2024-01-01,但是2024-01-01已经大于结束日期,所以条件应该是:DATE_ADD(month_start, INTERVAL 1 MONTH) <= '2023-12-20'?这样不行,因为12-20的下个月第一天是2024-01-01,大于12-20,所以不会生成12月。所以条件应该是:DATE_ADD(month_start, INTERVAL 1 MONTH) <= DATE_ADD(LAST_DAY('2023-12-20'), INTERVAL 1 DAY) ? 这样太复杂。 -- 其实,我们只需要生成在起始日期到结束日期之间的所有月份的第一天。条件为:下个月的第一天<=结束日期所在月份的下一个月的第一天?不对。 -- 简单条件:只要生成的那个月第一天<=结束日期,我们就继续生成下个月?但是这样会多生成结束日期之后的一个月(因为结束日期可能是某个月的最后一天,那么下一个月的第一天还是大于结束日期?)。所以我们应该用:当前月第一天<=结束日期,那么下个月第一天可能大于结束日期,但我们不需要。所以递归条件应该是:当前月第一天<=结束日期,然后我们继续生成下个月?这样会多生成一个月。 -- 所以,我们换一个条件:递归生成直到当前月第一天大于结束日期。那么递归条件就是:当前月第一天<=结束日期。这样,最后一次递归会生成一个大于结束日期的第一天,然后我们不要它。所以我们在最后结果中过滤掉大于结束日期的月份的最后一天。 ) 因此,我们采用先生成月份第一天,然后取最后一天,最后过滤掉大于结束日期的: ```sql WITH RECURSIVE MonthStarts AS ( SELECT DATE_FORMAT('2023-01-15', '%Y-%m-01') AS month_start -- 起始日期所在月的第一天 UNION ALL SELECT DATE_ADD(month_start, INTERVAL 1 MONTH) FROM MonthStarts WHERE DATE_ADD(month_start, INTERVAL 1 MONTH) <= '2023-12-20' -- 下个月的第一天<=结束日期?这样会生成结束日期所在月份的下一个月?所以条件应该是:DATE_ADD(month_start, INTERVAL 1 MONTH) <= LAST_DAY('2023-12-20')? 不对,因为LAST_DAY返回的是日期,而结束日期是2023-12-20,LAST_DAY('2023-12-20')='2023-12-31',所以下个月第一天是2024-01-01>2023-12-20,所以不会生成。那么结束日期所在月份的第一天(2023-12-01)已经生成,然后递归时,下个月第一天2024-01-01>2023-12-20,所以停止。所以这样会缺少2023年12月吗?不会,因为第一个月是2023-01-01,然后递归生成2023-02-01, ..., 直到2023-12-01(因为2023-12-01<=2023-12-20,所以会
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值