本文的目是解释在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上,有一些更好,更适用一些,通过阅读本章,我希望你能理解“更好,更适用”。
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设置
--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')
可惜,没有返回任何行,怎么呐?在WHERE子句后面有两种数据类型,一种为DATETIME,另一种为String,SqlServer根据数据类型的优先级把
一种数据类型转化为另一种类型。DATETIME比字符串的优先级高,因此字符串被转换为DATETIME类型,当我们没有指明时间部分时,该值被转换为
2004-03-5 00:00:00,因此没有返回任何的数据行。好,我们换种方式。使用CONVERT函数把等号左边的列转换成字符串,然后与等号右边的字符串
做相等比较。
这会返回预想中的行,当仍然有问题。当我们使用CONVERT函数来计算某列的值时,SQLSERVER不会使用索引,这对性能造成损害。让我们试试BETWEEN
因为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 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(char(8), CURRENT_TIMESTAMP, 112)
正如你所见,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约束
这种技巧有什么优势?如果我们要查找一定时间范围内的行数,可在WHERE子句中:
WHERE dt = '20040305'
该方法易读,易于理解,非常直观通过日期连接两个表