1. 程式人生 > >SQL Server中的日期時間型別

SQL Server中的日期時間型別

一. 資料型別
SQL Server008在上一個版本的基礎上把日期時間類的資料型別增加到了六個。除了SQL Server2005的datetime和smalltime之外,加了date,datetime2,time和datetimeoffset。
datetime
資料庫內部用兩個4位元組的整數儲存datetime資料型別的值。第一個4位元組儲存基礎日期(即1900-1-1, base date)之前或之後的日期。日期範圍為1753-1-1至9999-12-31。當日期為1900-1-1時值為0;之前的日期是負數;之後日期是正數。另外一個4位元組儲存以午夜後三又三分之一(10/3)毫秒數所代表的每天的時間。精確度為百分之三秒(等於3.33毫秒或0.00333秒,至於為什麼選擇3.33毫秒的原因見下)。如下表所示,把值調整到.000、.003、或.007秒的增量。

由於datetime的單位是(10/3)毫秒,因此若要指定第二個4位元組的時間值,需要乘上單位(10/3),比如,指定第二個4位元組的值為10000的時間,我們需要如下的操作:
DECLARE @D1 DATETIME, @D2 DATETIME
SET @D1 = DATEADD(MS, 10000 * 10 / 3, 0)
SET @D2 = DATEADD(MS, 10000, 0)
SELECT @D1, @D2
–1900-01-01 00:00:33.333 1900-01-01 00:00:10.000

查詢中,我們常常需要搜尋指定日期範圍內的資料,比如返回1998-01-01當天內的資料,你可能會這樣寫:
Date >= ‘1998-01-01 00:00:00.000’and date <= ‘1998-01-01 23:59:59.999’

根據上面的調整規則,其實這句語句的實際搜尋範圍為:
Date >= ‘1998-01-01 00:00:00.000’ and date <= ‘1998-01-02 00:00:00.000’

你會看到這包括了1998-01-02的資料,所以最好的正確的搜尋語句為:
Date >= ‘1998-01-01 00:00:00.000’and date < ‘1998-01-02 00:00:00.000’

我們可以使用下面的程式碼來顯示datetime型別是如何儲存的(下面的smalldatetime方法同):
declare @dt datetime
set @dt = getdate()
select convert(int, substring(convert(varbinary(8), @dt), 1, 4)), datediff(day, 0, @dt)
select convert(int, substring(convert(varbinary(8), @dt), 5, 8)), datediff(ms, dateadd(day, datediff(day, 0, @dt), 0), @dt) * 3 / 10.0

smalldatetime
smalldatetime資料型別儲存日期和每天的時間,但精確度低於datetime。SQL Server將smalldatetime的值儲存為兩個2位元組的整數。第一個2位元組儲存1900-1-1後的天數。另外一個2位元組儲存午夜後的分鐘數。日期範圍從1900-1-1到2079-6-6,時間精確到分鐘。29.998秒或更低的smalldatetime值向下舍入為最接近的分鐘,29.999秒或更高的smalldatetime值向上舍入為最接近的分鐘。
–returns time as 12:35
SELECT CAST(‘2000-05-08 12:35:29.998’ AS smalldatetime)
GO
–returns time as 12:36
SELECT CAST(‘2000-05-08 12:35:29.999’ AS smalldatetime)
GO

注意:我以前的曾經了翻譯一篇文章,講述了一些在計算機中常用的表示時間的方法。這些時間表示方法都是以一個時間作比照點,記錄到指定時間的時間(比如秒、100毫秒等等)數。這和SQL SERVER中datetime/smalldatetime表示方法完全不同。
Datetime的精確度為什麼是3.33毫秒?
注意:本問題目前還沒有確切答案。
我看到這個問題的第一個答案就是:空間不夠唄,所以只能精確到3.33毫秒。
是這樣的嗎?假如精確到1毫秒,則一天的毫秒數為:24 * 60 * 60 * 1000 = 86400000,而四個位元組最大的數為2^32(42 9496 7296),這個數字遠遠大於8640 0000。所以肯定不是儲存空間不夠的原因。
但是有一點可以確定的:現在的SQL SERVER一定為了相容原來版本才會選擇3.33毫秒這個精確度的。因為我發現SYBASE中的DATETIME的儲存格式和SQL SERVER一模一樣,精確度也是3.33毫秒。眾所周知,他們兩個是有共同的祖先,是後來才分開的。
那麼當初SYBASE為什麼會選擇3.33毫秒的呢?這就要涉及到一些歷史因素了, 比如當初CPU頻率、當初的儲存空間少等情況了,只能猜測罷了[5]。

二. 日期時間的賦值
上面說了格式,當然我們是沒有辦法直接賦整數給日期時間變數的。給這些變數賦值通常是給它指定個一定格式的字串。SQL SERVER會自動將字串轉換成日期格式儲存的,注意:資料庫中是不會儲存資料格式的字串的。下面幾種日期格式的字串,SQL SERVER會非常輕易就“認出”的。
1) ISO 8061格式
ISO時間格式:yyyy-mm-dd hh:mm:ss[.mmm],必須指定每一個元素,只有毫秒是可選的,時間成分以24小時格式指定。
使用 ISO 8601 格式的優勢在於它是一個國際標準。另外,使用此格式指定的日期時間值很明確。同時,此格式不受 set dateformat設定或 set language設定的影響。

2) 字母日期格式
在 SQL Server 2008 中也可以以當前語言給出的月的全名(如 April)或月的縮寫(如 Apr)來指定日期資料;逗號是可選的,而且忽略大小寫。

這種日期格式只有在制定的語言中才能起作用。我們可以通過呼叫儲存過程sp_helplanguage來檢視SQL SERVER支援的所有語言以及這些語言的月份全名和簡稱。我們同時可以發現有三種語言:簡體中文,韓語(한국어)和日語(日本語)的月份名稱是數字而不是字母,因此在這三種語言中是不支援字母日期格式的。

以下是使用字母日期格式的一些原則:
a.把日期和時間資料括在單引號中 (‘)。
b.下面是 SQL Server 日期資料的有效字母格式(括號內的字元是可選字元):
Apr[il] [15][,] 1996
Apr[il] 15[,] [19]96
Apr[il] 1996 [15]

[15] Apr[il][,] 1996
15 Apr[il][,][19]96
15 [19]96 apr[il]
[15] 1996 apr[il]

1996 APR[IL] [15]
1996 [15] APR[IL]
注意 ,沒有這樣一種情況:省略日,縮寫年份。如 Apr[il] [15][,] [19]96 這是錯誤的格式。

c.如果只指定年份的最後兩位數字,則小於 [兩位數年份截止]配置選項值最後兩位數字的值與縮略形式的年份位於同一個世紀。大於或等於該選項值的值位於縮略形式年份的上一世紀。例如,如果[兩位數年份截止]為 2050(預設值),則 25 被解釋為 2025,50 被解釋為 1950。為避免模糊不清,請使用四位數的年份。

[兩位數年份截止]配置選項伺服器屬性中的高階配置下。該選項可以從1753 到 9999 之間指定一個整數來表示縮略形式的年份,以將兩位數的年份解釋為四位數的年份。
SQL Server 預設的時間範圍是 1950-2049,表示截止年份為 2049。這說明 SQL Server 將兩位數年份 49 解釋為 2049 年,將兩位數年份 50 解釋為 1950 年,而將兩位數年份 99 解釋為 1999 年。若要維護向後相容性,請將設定保持為預設值。

d.如果沒有指定日,則預設值為當月第一天。
e.當按字母形式指定月時,SET dateformat 會話設定不起作用。

3) ODBC日期時間格式
ODBC API 定義了轉義序列來表示日期和時間值,ODBC 稱之為時間戳資料。用於 SQL Server 的 Microsoft OLE DB 訪問介面所支援的 OLE DB 語言定義 (DBGUID-SQL) 也支援這種 ODBC 時間戳格式。使用 ADO、OLE DB 和基於 ODBC 的 API 的應用程式可以使用這種 ODBC 時間戳格式來表示日期和時間。
ODBC 時間戳的轉義序列格式為:{ literal_type ‘constant_value’ }
literal_type 指定轉義序列的型別。時間戳有三個 literal_type 說明符:
d = 僅日期
t = 僅時間
ts = 時間戳(時間 + 日期)
‘constant_value’
轉義序列的值。constant_value 必須遵循每個 literal_type 的格式。
literal_type constant_value 格式
d yyyy-mm-dd
t hh:mm:ss[.fff]
ts yyyy-mm-dd hh:mm:ss[.fff]
這些是 ODBC 時間和日期常量的例子:
{ ts ‘1998-05-02 01:23:56.123’ }
{ d ‘1990-10-02’ }
{ t ‘13:33:41’ }
不要混淆 ODBC 和 OLE DB 時間戳資料型別名稱與 Transact-SQL timestamp 資料型別名稱。ODBC 和 OLE DB 時間戳資料型別記錄日期和時間。Transact-SQL timestamp 資料型別是一個與時間值無關的二進位制資料型別。

4) 分隔字串格式
在 Microsoft SQL Server 2005 中,可以使用指定的數值月指定日期資料。例如,5/20/97 代表 1997 年 5 月 20 日。使用數值日期格式時,可在字串中使用斜槓 (/)、連字元 (-) 或句點 (.) 作為分隔符指定月、日和年。此字串必須使用以下格式:
number separator number separator number [time] [time]

這種格式常常會因為不同國家可能解釋不同,比如像“01/02/03”這個日期格式:

像這種情況,我們若要直接賦值給日期變數,我們就可以使用SET DATEFORMAT來設定格式:引數包括 mdy、dmy、ymd、ydm、myd 和 dym。每種語言都有自己的預設DATEFORMAT,可以通過sp_helplanguage來檢視每種語言的日期格式。

下列是分隔字串日期格式有效例子:
[0]4/15/[19]96 – (mdy)
[0]4-15-[19]96 – (mdy)
[0]4.15.[19]96 – (mdy)
[0]4/[19]96/15 – (myd)

15/[0]4/[19]96 – (dmy)
15/[19]96/[0]4 – (dym)
[19]96/15/[0]4 – (ydm)
[19]96/[0]4/15 – (ymd)

5) 未分隔字串格式
Microsoft SQL Server 2005 允許您將日期資料指定為未分隔字串。日期資料能夠用 4、6 或 8 位數字、空字串或不帶日期值的時間值來指定。
SET DATEFORMAT會話設定並不適用於全數值日期項,例如不帶分隔符的數值項。6 位或 8 位數字的字串始終被解釋為 ymd。月和日必須始終是 2 位數字。
下面是有效的未分隔字串格式:[19]960415
只有 4 位數字的字串被解釋為年。月和日期被設定為 1 月 1 日。當只指定 4 位數字時,必須包括世紀。

6) 時間部分格式
上面說的日期格式重點談了日期部分的格式,若日期格式的時間部分沒有定義,那麼SQL SERVER就將子夜(midnight)時間作為預設的時間值。
現在來看看時間部分的格式。SQL Server 2005 能夠識別下列格式的時間資料。用單引號 (‘) 把每一種格式括起來。
14:30
14:30[:20:999]
14:30[:20.9]
4am
4 PM
[0]4[:30:20:500]AM

如果日期部分沒有進行定義,那麼SQL SERVER就將1900年1月1日作為預設的日期值。

可以用一個 AM 或 PM 字尾來表明時間值是在中午 12 點之前還是之後。AM 或 PM 的大小寫可忽略。
小時可以用 12 小時或 24 小時的時鐘來指定。小時值解釋如下:
a. 小時值 0 表示午夜 (AM) 後的小時,不論是否指定了 AM。當小時值等於 0 時,不能指定 PM。
b. 如果未指定 AM 或 PM,小時值 1 到 11 表示中午以前的小時。當指定 AM 時,也表示中午以前的小時。當指定 PM 時,則表示中午以後的小時。
c. 如果未指定 AM 或 PM,小時值 12 表示始於中午的小時。如果指定 AM,則表示始於午夜的小時。如果指定 PM,則表示始於中午的小時。例如:12:01 是指中午過後 1 分鐘,即 12:01 PM,而 12:01 AM 是指午夜過後 1 分鐘。指定 12:01 AM 與指定 00:01 或 00:01 AM 相同。
d. 如果未指定 AM 或 PM,小時值 13 到 23 表示中午以後的小時。當指定 PM 時,也表示中午以後的小時。當小時值從 13 到 23 時,不能指定 AM。
e. 小時值 24 無效,用 12:00 AM 或 00:00 表示午夜。
可以在毫秒之前加上冒號 (:) 或者句號 (.)。如果前面加冒號,這個數字表示千分之一秒。如果前面加句號,單個數字表示十分之一秒,兩個數字表示百分之一秒,三個數字表示千分之一秒。例 如,12:30:20:1 表示 12:30 過了 20 又千分之一秒;12:30:20.1 表示 12:30 過了 20 又十分之一秒。

上面說了SQL SERVER可以“自動識別”的所有日期時間型別。識別時可能要考慮當前的語言(試用set lanuage設定)和日期格式(試用set dateformat設定)的影響。你若厭倦於此,可以使用最後的“殺手鐗”——CONVERT函式來顯式轉換,CONVERT的第三個引數對於日期格式和字串格式定義如下:

三. 日期的輸出
和日期的賦值不同,SQL SERVER不會“自動”識別你需要哪種輸出格式。我們只有試用CONVERT函式來實現日期輸出格式的控制。
當使用CONVERT函式處理日期時間的輸出格式時,我們可以使用與處理輸入資料時完全相同的風格設定,唯一的區別是:處理輸出資料格式時,CONVERT函式將一個日期時間例項轉換為一個字串,而處理輸入資料格式時,則是從一個字串生成一個日期時間例項。

四. 時間函式
常用的時間函式如下:
DATEADD:可以對時間型別的指定部分進行加減計算。我們常常根據一個時間來構造出另外一個時間,比如下個月的今天,本月底等等,我們應該也儘量使用DATEADD函式來構造,它可以避免一些閏月、年底、月底之類的錯誤。對一個日期時間變數直接加減一個整數和使用DATEADD(DAY, n,@D)的功能是一樣的。

DATEDIFF:該函式對兩個時間變數對指定部分進行比較計算。此函式不考慮比指定日期部分更高的粒度級別,它只考慮更低級別的部分。對時間的比較應儘量使用本函式。使用DATEDIFF和DATEADD可以對日期時間變數進行“截尾”的操作(舉例見下面的常用查詢)。

DATEPART:返回日期時間變數的指定部分的值。
DATENAME:返回日期時間變數的指定部分的值,和DATAPART不同的是本函式返回的是個字串型別。
GETDATE()返回本機器的當前時間。CURRENT_TIMESTAMP變數與本函式功能相同。
GETUTCDATE()返回本機器的當前UTC(格林尼治標準)時間。
DATEADD、DATEDIFF、DATEPART和DATENAME函式使用到一些共同的引數與縮寫如下:

利用上面的函式,總結一些常用的查詢(可以看看是如何進行“截尾”操作的):
–本月開始,相當於將本月的日期“截尾”
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0)
–本年開始,相當於將本年的月份“截尾”
SELECT DATEADD(YEAR, DATEDIFF(YEAR, 0, GETDATE()), 0)
–本週一,相當於將本週的日期“截尾”
SELECT DATEADD(WEEK, DATEDIFF(WEEK, 0, GETDATE()), 0)
–本季度開始,相當於將本季的日期“截尾”
SELECT DATEADD(QQ, DATEDIFF(QQ, 0, GETDATE()), 0)
–本月結束
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()) + 1, 0) -1
–本年結束
SELECT DATEADD(YEAR, DATEDIFF(YEAR, 0, GETDATE()) + 1, 0) - 1
–本週結束
SELECT DATEADD(WEEK, DATEDIFF(WEEK, 0, GETDATE()) + 1, 0) - 1
–本季度結束
SELECT DATEADD(QQ, DATEDIFF(QQ, 0, GETDATE()) + 1, 0) - 1
–足年計演算法一.
使用PUBS資料庫中的EMPLOYEE表。(下同)
SELECT *, DATEDIFF(YEAR, HIRE_DATE, GETDATE()) - CASE WHEN DATEADD(YEAR, DATEDIFF(YEAR, HIRE_DATE, GETDATE()), HIRE_DATE) > GETDATE() THEN 1
ELSE 0
END
FROM EMPLOYEE
–此法對於閏年的2月29日和平年的2月28日是相等的。

–足年計演算法二.
SELECT , DATEDIFF(YEAR, HIRE_DATE, GETDATE()) - CASE WHEN 100 MONTH(HIRE_DATE) + DAY(HIRE_DATE) > 100 * MONTH(GETDATE()) + DAY(GETDATE()) THEN 1
ELSE 0
END
FROM EMPLOYEE

SELECT *, DATEDIFF(YEAR, HIRE_DATE, GETDATE()) - CASE WHEN SUBSTRING(CONVERT(VARCHAR(8), HIRE_DATE, 112), 5, 4) > SUBSTRING(CONVERT(VARCHAR(8), GETDATE(), 112), 5, 4) THEN 1
ELSE 0
END, SUBSTRING(CONVERT(VARCHAR(8), HIRE_DATE, 112), 5, 4), SUBSTRING(CONVERT(VARCHAR(8), GETDATE(), 112), 5, 4)
FROM EMPLOYEE
此法對於閏年的2月29日和平年的3月1日是相等的。

足年計演算法三.
SELECT *, DATEDIFF(YEAR, HIRE_DATE, GETDATE()) - CASE WHEN DATEPART(DAYOFYEAR, HIRE_DATE) > DATEPART(DAYOFYEAR, GETDATE()) THEN 1
ELSE 0
END
FROM EMPLOYEE
–此法比較簡單,但是在閏年和平年3月份以後的日期時有1日的差別。

–足月計演算法一.
SELECT *, DATEDIFF(MONTH, HIRE_DATE, GETDATE()) - CASE WHEN DATEADD(MONTH, DATEDIFF(MONTH, HIRE_DATE, GETDATE()), GETDATE()) > GETDATE() THEN 1
ELSE 0
END
FROM EMPLOYEE

–足月計演算法二.
SELECT *, DATEDIFF(MONTH, HIRE_DATE, GETDATE()) - CASE WHEN DAY(HIRE_DATE) > DAY(GETDATE()) THEN 1
ELSE 0
END
FROM EMPLOYEE