SQL Server时间粒度系列----第2节日期、周时间粒度详解
学习和使用过MySQL的博友,大都知道MySQL提供了很多针对日期和时间的函数,提供了获取不同时间粒度上的功能。相对而然SQL Server提供的有关日期和时间函数不太多的,但是其提供的功能也是蛮强大的。还是让我们继续话说MySQL的日期和时间函数,发现了TO_DAYS和FROM_DAYS这一对函数:TO_DAYS将一个日期转换为一个从1开始的整数(注意(摘自mysql官网文档):Given a date date, returns a day number (the number of days since year 0).),FROM_DAYS将一个从1开始的整数(更严格的说从366开始,因为1-365得到的结果值为NULL)转换为一个日期(摘自mysql官网文档):Given a day number N, returns a DATE value.)。
数字和日期很显然让我想起序列这个概念。那什么是序列呢?序列就是一个连续的任意相邻两个间隔单位值相等的集合(很想数学上的等差数列的定义),比如自然数集合(非负整数集合)(0,1,2,3,……正无穷)、整数集合(……,-3,-2,-1,0,1,2,3,……)、有符号(负)整数集合(……,-3,-2,-1)、无符号(非负)整数集合(自然数集合)、日期区间从"0001-01-01"‘到“9999-12-31“间隔单位值相差1天的日期集合、当然也可以是时间序列相邻间隔单位小时等等。间隔单位值也可以是2、5等等任意的一个整数值(对于数字序列来说的),也可以1天,12小时,480分钟等等(对于日期和时间序列来说的)。
从序列的定义来分析,数字序列和日期序列都具有共同的特性:间隔单位值相等。MySQL提供的这一对将整数和日期相互转换的函数,也是基于一个基准日期的实现。MySQL的函数TO_DAYS的基准日期是”0000-01-01“的,该函数返回值还是从整数1开始计数的。
提起MySQL提供的基准日期,SQL Server引擎也提供一个默认的基准日期,那就是”1900-01-01“,下面我们来看看如何使用这个基准日期。
SQL Server很显然没有提供向MySQL中那样的将整数和日期相互转换的一对函数,但是我们了解了日期序列的特性,再结合SQL Server提供的基准日期,我们很容易实现类似的一对功能函数。在提供SQL Server版本的整数和日期相互转换的一对函数实现前,我们还是讲解日期这个时间粒度。
日期这个时间粒度就是一个表示年月日的值。SQL Server 提供的日期和时间的数据类型包括:date(3字节)、smalldatetime(4字节),time(5字节)、datetime(8字节),datetime2(8字节)和datetimeoffset(10字节),其中除smalldatetime和datetime以外的其他日期和时间类型是从SQL Server 2008提供的。为了保证我们的实现方案可以在SQL Server 2005+环境运行,我们只能针对性地选择smalldatetime和datetime这两个日期和时间数据类型。从这两个日期和时间数据类的日期部分范围(不包括时间部分)来看,smalldatetime的日期范围区间是[1900-01-01,2079-06-06],datetime的日期部分范围区间是[1753-01-01,9999-12-31]。目前我们处在21世界的,新开发的应用或项目中使用到的历史数据也很难是19世纪以前的。基于这因素和基准日期”1900-01-01“的考虑,我们要满足的日期部分范围区间是[1900-01-01,9999-12-31],这个范围区间也是我们要确定的日期时间粒度的范围区间。
日期时间粒度的范围区间找到了,只需要将该区间中的每一个日期和一个整数值进行一映射对应,也就是日期和整数间的相互转换,基于2664600(该范围区间获得的总天数:((9999-1900) + 1)*366),使用4字节整数(以下简称int)完全满足存储要求的。我们也看到了smalldatetime和其对应的int都是4字节,就存储空间而然将字段列的数据类型设置为smalldatetime和int任意一个都是没有问题的,但是存储了smalldatetime的日期部分的范围显然不能存储2079-06-07以后的日期时间值的,另外SQl Server引擎内部使用了两个2字节的整数存储,第一个 2 字节存储 1900 年 1 月 1 日后的天数。另外一个 2 字节存储午夜后经过的分钟数,从其存储原理我们知道该数据类型的值从文件页(数据页、索引页等等)中提取到得到smalldatetime值的过程是要经过一系列转换(比如分别将两个2字节的整数转换为日期和时间两部分,再将两部分串联等等),相比较一个int数据类型的值,从文件页提取到得到其值就没有向smalldate那样复杂的转换操作。基于smallldatetime和int在存储范围区间和转换复杂度这两个方面的比较,我们在设计表字段列时如果遇到只存储日期时间粒度的时间值(只包含年月日的值,即日期部分值)时,可以设计为int数据类型。
SQL Server实现的日期和整数相互转换的功能函数对,T-SQL代码如下:
1 IF OBJECT_ID(N'dbo.ufn_Days', 'FN') IS NOT NULL 2 BEGIN 3 DROP FUNCTION dbo.ufn_Days; 4 END 5 GO 6 7 --================================== 8 -- 功能: 获得指定日期时间基于基准日期的总天数(一个整数值) 9 -- 说明: 如果指定的日期时间为NULL或者小于基准日期“1900-01-01”时,则其值默认基准日期 10 -- 结果值为非负整数,从0开始计数。 11 -- 作者: XXX 12 -- 创建: yyyy-MM-dd 13 -- 修改: yyyy-MM-dd XXX 修改内容描述 14 -- 调用: SET @intDays = dbo.ufn_Days('2008-01-14') 15 --================================== 16 CREATE FUNCTION dbo.ufn_Days 17 ( 18 @dtmDate DATETIME -- 指定的日期时间 19 ) RETURNS INT 20 --$Encode$-- 21 AS 22 BEGIN 23 IF @dtmDate IS NULL OR @dtmDate < '1900-01-01' 24 BEGIN 25 SET @dtmDate = '1900-01-01'; 26 END 27 28 -- datepart参数也可以为dd或d 29 RETURN DATEDIFF(DAY, '1900-01-01', @dtmDate) 30 END 31 GO 32 33 IF OBJECT_ID(N'dbo.ufn_Days2Date', 'FN') IS NOT NULL 34 BEGIN 35 DROP FUNCTION dbo.ufn_Days2Date; 36 END 37 GO 38 39 --================================== 40 -- 功能: 获得一个整数值基于基准日期对应的日期 41 -- 说明: 如果指定的整数值为NULL或为负整数时,则其值默认为0; 42 -- 如果指定的整数值大于“9999-12-31”对应的整数值时,则其值默认设置为“9999-12-31”对应的整数值 43 -- 结果值为从基准日期开始计数的日期 44 -- 作者: XXX 45 -- 创建: yyyy-MM-dd 46 -- 修改: yyyy-MM-dd XXX 修改内容描述 47 -- 调用: SET @dtmDate = dbo.ufn_Days2Date(39459) --'2008-01-14' 48 --================================== 49 CREATE FUNCTION dbo.ufn_Days2Date 50 ( 51 @intDays INT -- 指定的整数值 52 ) RETURNS DATETIME 53 --$Encode$-- 54 AS 55 BEGIN 56 IF @intDays IS NULL OR @intDays < 0 57 BEGIN 58 SET @intDays = 0; 59 END 60 61 DECLARE @intMaxDays AS INT; 62 SET @intMaxDays = dbo.ufn_Days('9999-12-31'); 63 64 IF @intDays >= @intMaxDays 65 BEGIN 66 SET @intDays = @intMaxDays; 67 END 68 69 -- datepart参数也可以为dd或d 70 RETURN DATEADD(DAY, @intDays, '1900-01-01'); 71 END 72 GO
以上功能函数对的测试T-SQL代码如下:
1 SELECT dbo.ufn_Days(NULL) AS 'NULL值对应的整数值', dbo.ufn_Days('1899-12-31') AS '小于1900-01-01对应的整数值', dbo.ufn_Days('1900-01-01') AS '1900-01-01对应的整数值', dbo.ufn_Days('2016-01-07') AS '2016-01-07对应的整数值', dbo.ufn_Days('9999-12-31') AS '9999-12-31对应的整数值'; 2 SELECT dbo.ufn_Days2Date(NULL) AS 'NULL值对应的日期', dbo.ufn_Days2Date(-1) AS '小于0对应的日期', dbo.ufn_Days2Date(0) AS '0对应的日期', dbo.ufn_Days2Date(42374) AS '42374对应的日期', dbo.ufn_Days2Date(2958463) AS '2958463对应的整数值', dbo.ufn_Days2Date(2958464) AS '大于2958463对应的整数值'; 3 GO
执行后的查询结果如下图:
SQL Server周有关时间粒度可以表述为以下问题:一个日期属于一年的第几周和一个日期属于当前所在周的周几。一个日期是否是工作日这个要根据是否上班来确定的,不然简单的根据周一到周五是工作日,周六和周日是休息日来判断的。第一个问题很用以通过datepart(weekday, @dtmDateTime)来解决的,第二个问题则通过datename(weekday, @dtmDateTime)来得到结果的,不过结果值的展现形式要依赖默认语言的。
将以上两个问题的解决封装在SQL Server标量函数中,其T-SQL代码如下:
1 IF OBJECT_ID(N'dbo.ufn_WeekOfYear', 'FN') IS NOT NULL 2 BEGIN 3 DROP FUNCTION dbo.ufn_WeekOfYear; 4 END 5 GO 6 7 --================================== 8 -- 功能: 获取指定日期属于当前第几周 9 -- 说明: 一年最多1-53周 10 -- 作者: XXX 11 -- 创建: yyyy-MM-dd 12 -- 修改: yyyy-MM-dd XXX 修改内容描述 13 -- 调用: SELECT dbo.ufn_WeekOfYear('2016-01-07'); 14 --================================== 15 CREATE FUNCTION dbo.ufn_WeekOfYear 16 ( 17 @dtmDate DATETIME 18 ) RETURNS TINYINT 19 --$Encode$-- 20 AS 21 BEGIN 22 -- datepart参数也可以为wk, ww 23 RETURN CAST(DATEPART(WEEK, @dtmDate) AS TINYINT) 24 END 25 GO 26 27 IF OBJECT_ID(N'dbo.ufn_WeekdayNameOfWeek', 'FN') IS NOT NULL 28 BEGIN 29 DROP FUNCTION dbo.ufn_WeekdayNameOfWeek; 30 END 31 GO 32 33 --================================== 34 -- 功能: 获取指定日期属于当前周周几的名称 35 -- 说明: 结果值的展示形式会以来默认设置语言 36 -- 作者: XXX 37 -- 创建: yyyy-MM-dd 38 -- 修改: yyyy-MM-dd XXX 修改内容描述 39 -- 调用: SELECT dbo.ufn_WeekOfYear('2016-01-07'); 40 --================================== 41 CREATE FUNCTION dbo.ufn_WeekdayNameOfWeek 42 ( 43 @dtmDate DATETIME 44 ) RETURNS NVARCHAR(20) 45 --$Encode$-- 46 AS 47 BEGIN 48 -- datepart参数也可以为dw 49 RETURN DATENAME(WEEKDAY, @dtmDate); 50 END 51 GO 52
测试以上函数效果的T-SQL代码如下:
SET LANGUAGE N'us_english'; SELECT @@LANGUAGE, dbo.ufn_WeekOfYear('2017-02-01'), dbo.ufn_WeekdayNameOfWeek('2017-02-01') GO SET LANGUAGE N'简体中文'; SELECT @@LANGUAGE, dbo.ufn_WeekOfYear('2017-02-01'), dbo.ufn_WeekdayNameOfWeek('2017-02-01') GO
执行后的查询结果如下图:
注意:以上使用了SET LANGUAGE和@@LANGUAGE,更多阅读配置函数。
继续补充增加不论@@DATEFISRT以任何一天作为一周的开始,都要准确获得指定日期隶属当前周的周几的功能,其实现的T-SQL代码如下:
1 IF OBJECT_ID(N'dbo.ufn_WeekdayOfWeek', 'FN') IS NOT NULL 2 BEGIN 3 DROP FUNCTION dbo.ufn_WeekdayOfWeek; 4 END 5 GO 6 7 --================================== 8 -- 功能: 获取指定日期隶属当前周周几 9 -- 说明: 结果值从1到7,分别对应从周一到周日,该值与@@DATEFISRT配置函数值保持一致 10 -- 作者: XXX 11 -- 创建: yyyy-MM-dd 12 -- 修改: yyyy-MM-dd XXX 修改内容描述 13 -- 调用: SELECT dbo.ufn_WeekdayOfWeek('2017-01-07') -- 4(表示星期四) 14 --================================== 15 CREATE FUNCTION dbo.ufn_WeekdayOfWeek 16 ( 17 @dtmDate DATETIME -- 指定的日期时间 18 ) RETURNS TINYINT 19 --$Encode$-- 20 BEGIN 21 DECLARE 22 @tintDateFirst AS TINYINT, 23 @tintWeekDayIndexID AS TINYINT, 24 @tintSum AS TINYINT; 25 26 SELECT 27 @tintDateFirst = @@DATEFIRST, 28 @tintWeekDayIndexID = DATEPART(WEEKDAY, @dtmDate), 29 @tintSum = @tintDateFirst + @tintWeekDayIndexID; 30 31 RETURN (CASE WHEN @tintSum >= 9 THEN @tintSum - 8 WHEN @tintSum = 8 THEN 7 ELSE @tintSum - 1 END); 32 END 33 GO
测试其功能的T-SQL代码如下:
1 DECLARE @tintLoopID AS TINYINT; 2 SET @tintLoopID = 1; 3 4 DECLARE @dtmDate AS DATETIME; 5 SET @dtmDate = '2016-01-07'; 6 7 SELECT @dtmDate AS 'date', DATENAME(WEEKDAY, @dtmDate) AS 'WeekdayName'; 8 WHILE @tintLoopID <= 7 9 BEGIN 10 SET DATEFIRST @tintLoopID; 11 12 SELECT @@DATEFIRST AS 'Start Day Of Week[1=星期一、2=星期二、3=星期三,……,7=星期日]', dbo.ufn_WeekdayOfWeek(@dtmDate) AS 'Nth Of Week1=星期一、2=星期二、3=星期三,……,7=星期日]' 13 14 SET @tintLoopID = @tintLoopID + 1; 15 END 16 GO 17
执行后的查询结果下图:
本文我们了解到MySQL提供的将日期和整数相互转换的功能函数对,还了解了序列的概念,又提供了SQL Server将日期和整数相互转换的实现,最后也实现了一个日期隶属当年的第几周以及其隶属当前周周几名称的标量函数。(我中间一直在尝试通过datepart(weekday, @dtmDateTime)过去指定日期所在本周的索引值从1开始计数到7,不过这个要根据@@datefirst配置函数来设置一周的第一天为周几,周一到周日分别对应1到7,前者的值与后者的值相关,但是不会时刻保持相同,例如美国默认周日是一周的第一天,也是@@datefirst为7(通过SET DATEFIRST N来设置),如果通过datepart(weekday, @dtmDateTime)获得的结果值为2,改日期在其隶属周围星期一。目前还没有实现,中间花费了不少的时间,博文针对这个问题的解决提供一个实现。博友如果更好的思路,请提出宝贵的建议。)