zoukankan      html  css  js  c++  java
  • 时间类型的终极指南(翻译)

    本文的目是解释在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'

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

  • 相关阅读:
    [php代码]从svn获取指定版本,并同步到ftp上。
    java程序用pid重启
    Gearman安装,测试笔记
    ant编译android项目
    jquery代码收藏
    [读书笔记]读《code complete》有感
    无法解析的外部符号_main,该符号在函数_tmainCRTStartup中被引用
    4路电梯调度——pair program总结
    阅读作业2
    必应缤纷桌面的使用测试
  • 原文地址:https://www.cnblogs.com/fly_zj/p/1772209.html
Copyright © 2011-2022 走看看