时间类型的终极指南(翻译)

本文的目是解释在sqlserver中,时间类型的运作原理,包括常见的一些错误和推荐的一些用法。适用于sqlserver 7.0,2000,2005和2008。

 

 

sqlserver 中 日期和时间类型

在sqlserver 2008以前,有两种类型用来处理日期和类型相关问题,在详细说明之前,先用表简要介绍一下(注意"sn”列)

Name sn 最小值 最大值 精度 存储大小
smalldatetime sdt 1900-01-1 00:00:00 2079-06-06 23:59:00 分钟 4字节
datetime dt 1753-01-01 00:00:00.000 9999-12-31 23:59:59.998 3.33毫秒 8字节

注意,没有单独的类型来存储日期和时间。上面的类型中都包括日期部分和时间部分。如果你单独的规定日期部分,则sqlserver储存的时间

为00:00:00.000,如果只规定了时间部分,则sqlserver存储日期为1900-01-01,谨记以上所说。

SELECT CAST('20041223' AS datetime)
-----------------------
2004-12-23 00:00:00.000

SELECT CAST('14:23:58' AS datetime)
-----------------------
1900-01-01 14:23:58.000

在sqlserver 2008中,引入几种新的涉及到日期和时间的类型
Name sn 最小值 最大值 精度 存储大小
datetime2 dt2 0001-01-01 00:00:00.0000000 9999-12-31 23:59:59.9999999 100ns 6-8字节
date d 001-01-01 9999-12-31 day 3字节
time t 00:00:00.0000000 23:59:59.9999999 100ns 3-5字节
datetimeoffset dto 0001-01-01 00:00:00.0000000 9999-12-31 23:59:59.9999999 100ns 8-10字节

 

如你所见,sqlserver 2008 最终引入单独的日期和时间类型 。从各种情况来看,datetime2是个更好的选择,它不比datetime占据更大的存储空间,

甚至更少。 对于包含时间部分的类型来说,能够设定秒钟的精确度,即小数点后数字的位数。例如 time(3) 能存储 14:23:12.567,

如果你输入14:23:12.5677,被舍入为14:23:12.568,此外datetimeoffset包括时区部分

 
  
Date 和 Time 类型
一种普遍的误解是sqlserver以某种可读的格式在内部存储日期和时间类型。但这不是事实,sqlserver以某种内部格式来存储这些值(如以整型来存储
datetime和smalldatetime)。但是,当我们插入值时,是以字符串的形式表现的,sqlserver知道以何种规则来解析它。记住sqlserver无论无何都
不会记住有关格式的一些信息
 
  
Date 和Time 输入格式
有多种格式可以运用在date/time/datetime上,有一些更好,更适用一些,通过阅读本章,我希望你能理解“更好,更适用”。
 QQ截图未命名 
  1.  
  2.  
  3.  

 

 

 

 

 

 

  

1.注意ANSI SQL 格式,它允许短线(-),反斜线(/)和点号(.)作为分隔符,这仅仅在ANSI SQL中定义的,非常的特殊

2.许多格式能单独应用在日期或时间部分,尽管这看起来有点奇怪。同时也非常笨拙的把'2008-08-25’转化为时间类型,把空字符串转化为时间类型

如下所示:(适用于sqlserver 2008)

SELECT CAST('' AS time)
SELECT CAST('2008-08-25' AS time)

两个结果都为(time 00:00:00)

3.ODBC格式化有点不同在于有一个标记(字符t,d,或者ts),用来显示或时间,或日期,或两者来正确选择不同的标记

4.ISO 8601 标准需要日期和时间部分

5.SET DATEFORMAT 继承SET LANGUAGE的相关设定,(在SET DATEFORMAT 中能够覆盖掉SET LANGUAGE中的设定),默认的语言设置与登录名

一一对应,后续也可以在sp_configure中更改

6.对日期部分和时间的格式化显示常常导致迷惑费解,因此新引入的类型如(date,datetime2,time),微软尽量使这些类型的格式化显示不依赖LANGUAGE

和DATEFORMAT设置,尽量与ANSI SQL保持一致,语言中立,对于字符串形式的日期类型,只要年份最先出现,那么sqlserver认为这是年(四位数不

是两位数),接下来依次为月份,天数,不管DATEFORMAT,LANGUAGE是如何设置的。

SET LANGUAGE British --uses dmy 适用sqlserver 2008
GO
SELECT CAST('02-23-1998 14:23:05' AS date) --Error
GO
SELECT CAST('2/23/1998 14:23:05' AS date) --Error
GO
SELECT CAST('1998-02-23 14:23:05' AS date) --Ok
GO
SELECT CAST('1998.02.23 14:23:05' AS date) --Ok
GO
SELECT CAST('1998/02/23 14:23:05' AS date) --Ok
GO 
前两个脚本失败,是因为年份不是第一个出现的,最后三个成功,恰是年份为第一个出现的。分隔符为(/ -  .  )都可以帮助文档里列出所有的
格式化显示信息,点击这儿浏览每个格式化显示的用法,切记:ODBC格式化时间表示时,不像其它用法显示的日期为‘1900-01-01’,而是显示
当前的日期。结论:

1.Alphabetic格式化用法是个非常糟糕的选择,因为SET LANGUAGE影响所有的日期和时间类型

2.smalldatetime和datetime非常的不幸,它们的显示方式常常取决于SET LANGUAGE和SET DATEFORMAT的设置(还包括登陆名对应的语言设置)

3.可能你认为这没有关系,既然依赖SET LANGUAGE,那我永远不要执行SET LANGUAGE好了,但是我们仔细想一想,DATEFORMAT的默认设置

继承于登陆配置。 想象一下,如果项目是在美国开发的,默认的DATEFORMAT 为m/d/y,但在德国安装,运行,bang,程序死掉了,明白不?

4.sqlserver 2008中引入新的DATE/TIME类型非常的友好,如果年份出现在最左边的话,根本不依赖SET DATEFORMAT的设置。

推荐的日期输入格式

在程序开发中,除非你正确设置了DATETIME 和LANGUAGE,否则你一定要配合使用format和type,尤其当它们依赖语言,文化特性时。同时意识到在存储

过程中这些设置会导致重编译可执行计划。我强烈推荐使用语言无关的格式化显示,使用无分隔符样式,而ISO 8601也是一种充分被使用的国际标准。

此时,sqlserver 2008已经发布,以后我可能转移到date,time,datetime2和datetimeoffset上去,到时使用ANSI SQL格式化标准来显示日期和时间,

但是我希望ANSI SQL能支持ISO 8601格式化标准,为了安全起见,我将使用无分隔符或者ISO 8601标准格式化日期和时间类型,即使在sqlserver2008 中。

警告和普遍的误解

再说一遍:在程序开发中,除非你正确设置了DATETIME 和LANGUAGE,否则你一定要配合使用format和type,尤其当它们依赖语言,文化特性时。

数字格式化(numeric format)能使用短线(-),句点(.),正斜线(/)作为分隔符。sqlserver解析日期类字符串规则并不依赖分隔符,一个普遍

的误解是:认为针对smalldatetime,datetime类型的ANSI SQL 格式化(format) 如1998-02-23是语言中立的,实际是错误的,它依赖于SETDATEFORMAT

和SET LANGUAGE设置

    SET LANGUAGE us_english --m/d/y
    SELECT CAST('2003-02-28' AS datetime)
    -----------------------
    2003-02-28 00:00:00.000
    
    SET LANGUAGE british  --d/m/y
    SELECT CAST('2003-02-28' AS datetime)
--The conversion of a char data type to a datetime data type resulted in an out-of-range datetime value.
注意:1.当日期类字符串中有四位数表示的年份时,系统能自动辨别,并且年份不受DATEFORMAT的影响,而月份和天数受到DATEFORM的影响。

         2.第一个脚本为2月28日,可以转化为日期类型,第二个脚本为28月2日,非法的日期,转化失败

         3.SET DATEFORMAT,SET LANGUAGE 影响输入部分的日期顺序

日期值的输出

当一个日期数值离开sqlserver时,是难以阅读的,客户端程序收到并格式化该值使之可以阅读的,一些程序和开发平台也可以设置格式化信息,但是这超出了

sqlserver的控制,你也可以使用某种格式返回DATETIME类型的值,这需要你把DATETIME类型转化为字符串,可以使用CONVERT函数,尤其是第三个

参数规定了日期的输出格式,相当好使好用。

SELECT CONVERT(char(8), CURRENT_TIMESTAMP, 112)
--------
20040312

SELECT CONVERT(char(19), CURRENT_TIMESTAMP, 120)
-------------------
2004-03-12 18:08:43

SELECT CONVERT(char(10), CURRENT_TIMESTAMP, 103)
----------
12/03/2004
查找DATETIME类型的值
如果我们使用包含日期和时间部分的smalldatetime,datetime,datetime2,datetimeoffset类型时,问题就出现了,请看下面的几组例子
CREATE TABLE #dts(c1 char(1), dt datetime)
INSERT INTO #dts (c1, dt) VALUES('a', '20040305 09:12:59')
INSERT INTO #dts (c1, dt) VALUES('b', '20040305 16:03:12')
INSERT INTO #dts (c1, dt) VALUES('c', '20040306 00:00:00')
INSERT INTO #dts (c1, dt) VALUES('d', '20040306 02:41:32')
INSERT INTO #dts (c1, dt) VALUES('e', '20040315 11:45:17')
INSERT INTO #dts (c1, dt) VALUES('f', '20040412 09:12:59')
INSERT INTO #dts (c1, dt) VALUES('g', '20040523 11:43:25') 
如果想要找到dt为2004-03-15的所有的行时,一个普遍的错误如下:
SELECT c1, dt FROM #dts WHERE dt = '20040305'

可惜,没有返回任何行,怎么呐?在WHERE子句后面有两种数据类型,一种为DATETIME,另一种为String,SqlServer根据数据类型的优先级

一种数据类型转化为另一种类型。DATETIME比字符串的优先级高,因此字符串被转换为DATETIME类型,当我们没有指明时间部分时,该值被转换为

2004-03-5 00:00:00,因此没有返回任何的数据行。好,我们换种方式。使用CONVERT函数把等号左边的列转换成字符串,然后与等号右边的字符串

做相等比较。

SELECT c1, dt FROM #dts WHERE CONVERT(char(8), dt, 112) = '20040305'

这会返回预想中的行,当仍然有问题。当我们使用CONVERT函数来计算某列的值时,SQLSERVER不会使用索引,这对性能造成损害。让我们试试BETWEEN

SELECT c1, dt FROM #dts WHERE dt BETWEEN '20040305' AND '20040306'

因为BETWEEN是包含两端的值,上面的脚本会返回dt= ‘2004-03-06 00:00:00’,我们需要排除这种情况:

SELECT c1, dt FROM #dts WHERE dt BETWEEN '20040305' AND '20040305 23:59:59.999'
棒极了?但仍然返回了dt=’2004-03-05 00:00:00’的行,我们需要仔细瞧瞧类型定义。出来了,DATETIME类型的精度为3.33ms,意味着
2004-03-05 23:59:59.999将向上舍入为 2004-03-06 00:00:00 ,这不是我所需要的,为解决这个上述问题,试试如下的方法:
SELECT c1, dt FROM #dts WHERE dt BETWEEN '20040305' AND '20040305 23:59:59.997'
尽管返回预料中的行数,当仍然不直观,难以阅读。假如dt的数据类型为smalldatetime,则舍入到分钟(29.998向下舍入到最接近的分钟,29.999向上
舍入到最接近的分钟) 。因此也不会产生预料中的行数。我们需要两种方法来分别处理datetime和smalldatetime类型。如果是smalldatetime类型,
可以使用如下的脚本:
SELECT c1, dt FROM #dts WHERE dt BETWEEN '20040305' AND '20040305 23:59:00'
我不赞同依靠不同的DATETIME类型而采用不同的出来方法,谁知道微软会不会在未来为时间部分添加精度更高的数据类型呢?像在SQLSERVER 2008所
做的一样呢?而到时不得不在WHERE子句中调整时间部分的表达式。下面是我推荐的做法:
SELECT c1, dt FROM #dts WHERE dt >= '20040305' AND dt < '20040306'
这会返回预料中的行数,也很好理解,sqlserver 也能利用dt列的索引。这个方法也能扩展为更大的范围,如返回2004年4月的数据:
 
SELECT c1, dt FROM #dts WHERE dt >= '20040301' AND dt < '20040401'

同样的道理,使用如下的脚本也能查找某个月的所有的行:

SELECT c1, dt FROM #dts WHERE DATEPART(year, dt) = 2004 AND DATENAME(month, dt) = 'March'

尽管如此,仍然有两个问题。第一是在dt列上使用函数而导致不能使用dt列上的索引,第二是DATENAME函数依赖SET LANGUAGE的设置

截去时间部分

截去时间部分,只保留日期部分是很常见的任务,但是当使用smalldatetime,datetime,datetime2,datetimeoffset类型是是不可能的,如果仅使用date

类型,这没任何问题,date就是专门为此设计的,当因为某种原因你要坚持使用smalldatetime,datetime,datetime2,datetimeoffset,就需要把时间

部分设置为00:00:00,我的建议就是忽略SET LANGUAGE 或者 DATEFORMAT的设置,把表达式转化为字符串,在显式的转化为datetime类型:

正如你所见,CONVERT函数使用第三个参数为112把DATETIEM类型转化为无分隔的字符串形式,这种显示形式被解析时是不依赖与LANGUAGE和

DATEFORMAT设置的,最后使用CAST把字符串转化为DATETIME类型:

客户端程序能接收这没有时间部分DATETIME类型,你也可以只返回无分隔符的字符串,也可以使用不同的转化代码(CONVERT函数中第三个参数),找到最适

合你的格式化形式。这里有另外一种方法也可以设置时间部分为00:00:00。想法是随便确定一个开始日期(只要sqlserver 支持),规定它的时间部分为00:00:00,

求出要转化的datetime类型与这个开始日期的相差天数,在把这天数加在这个开始日期上即可,T-SQL算法为:

SELECT DATEADD(day, DATEDIFF(day, '20040101', CURRENT_TIMESTAMP), '20040101')

一种争论是后一种方法比前一种字符串操作要快,根据我测试,只有几毫秒的差别,性能差别太小,甚至小到没法测量,但是有位SQL SERVER MVP 同仁

Dan Guzman发给我令人惊讶的信息,他在电子邮件中说到:

当我在一个长查询中使用时,我发现采用DATEADD方法的性能远远超过采用字符串的方法,使用DATEADD方法来利用日期分组,当包含时间

部分时,最近没有测试这个差别,但我猜想在某种情况下有几分钟的差别。Dan是我非常信任的人,我要重新研究一下DATEADD方法。Dan给我发来一份

脚本,测试显示,在SQLSERVER 2005,2008中对于10,000,000行,DATEADD方法花去6秒,字符串的方法花去13秒,Dan说在SQL SERVER 2000中

差别更惊人!

2009-04-03,我收到电子邮件中说道,通常使用下面的第一种方法而不是第二种:

SELECT DATEADD(DAY, 0, DATEDIFF(DAY, 0, CURRENT_TIMESTAMP)) 
SELECT DATEADD(DAY, 0, DATEDIFF(DAY, '', CURRENT_TIMESTAMP)) 

注意这与我们上面讨论的DATEDIFF有点不同,现在有四种不同的方法来完成。一种是原始的字符串操作,另外三个DATEADD替代方法,如下所示:

SELECT CAST(CONVERT(CHAR(8), CURRENT_TIMESTAMP, 112) AS datetime) --1
SELECT DATEADD(DAY, 0, DATEDIFF(DAY, '', CURRENT_TIMESTAMP)) --2
SELECT DATEADD(DAY, DATEDIFF(DAY, '20040101', CURRENT_TIMESTAMP), '20040101') --3
SELECT DATEADD(DAY, 0, DATEDIFF(DAY, 0, CURRENT_TIMESTAMP)) --4 

如果你认为性能要被重点关注,建议你亲自测试一下,以上四个版本中第一个最慢,剩下的三个非常接近,选择的时候还要考虑一下可读性。DATEADD

版本有巨大的灵活性。Hugo发给我的email中说道:

字符串版本的只能去除时间部分,而DATEADD/DATEDIFF版本非常容易获取月,季度,年的第一天即舍弃小时,分,秒,

SELECT DATEADD(month, DATEDIFF(month, '20000101', CURRENT_TIMESTAMP), '20000101')
或舍弃分钟和秒
SELECT DATEADD(hour, DATEDIFF(hour, '20000101', CURRENT_TIMESTAMP), '20000101')

或舍弃秒

SELECT DATEADD(minute, DATEDIFF(minute, '20000101', CURRENT_TIMESTAMP), '20000101')

更多的可能是:怎样获取前一个月的最后一天,或者当前月的最后一天呢?

SELECT DATEADD(month, DATEDIFF(month, '20000101', CURRENT_TIMESTAMP), '19991231') 
SELECT DATEADD(month, DATEDIFF(month, '20000101', CURRENT_TIMESTAMP), '20000131')

怎样获取明天的日期(不包含时间部分)

SELECT DATEADD(day, DATEDIFF(day, '20000101', CURRENT_TIMESTAMP), '20000102')

怎样把DATETIME向上舍入最接近的小时或者最接近的天数?

SELECT DATEADD(hour, DATEDIFF(hour, '20000101', DATEADD(minute, 30, CURRENT_TIMESTAMP)), '20000101')   
 --如果超过30分钟,向上舍入1个小时 
SELECT DATEADD(day, DATEDIFF(day, '20000101', DATEADD(hour, 12, CURRENT_TIMESTAMP)), '20000101')
 --如果超过12点,向上舍入1天
如何得到上一个星期五的日期,不使用日历表,忽视DATEFORMAT 设置
SELECT DATEADD(day, (DATEDIFF (day, '20000107', CURRENT_TIMESTAMP) / 7) * 7, '20000107')
--20000107是星期五

或者
SELECT DATEADD(day, (DATEDIFF (day, '20000108', CURRENT_TIMESTAMP) / 7) * 7, '20000107')
--20000108是星期六
一旦理解一般原理,在具体的环境当中,就没有什么搞不定的了。 
  

技巧:总是把时间设置为相同的值

如果对DATETIME的时间部分不敢兴趣,或者是没有DATE类型,可以把所有的行时间部分设置为相同的值如00:00:00.000。如在INSERT语句中

插入当前的日期,可以使用DEFAULT约束。

使用参数112,可以返回无分隔的字符串日期类型,但是,如果显式的设置时间部分,当然不是00:00:00.000,那我们就不能保证时间一致,这是可以添加

一个CHECK约束

ALTER TABLE #dts ADD CONSTRAINT dts_chk_d_date CHECK(CONVERT(char(12), d_date, 114) = '00:00:00:000')

这种技巧有什么优势?如果我们要查找一定时间范围内的行数,可在WHERE子句中:

WHERE dt = '20040305'

该方法易读,易于理解,非常直观通过日期连接两个表

原文地址:https://www.cnblogs.com/fly_zj/p/1772209.html