揭密SQL Server DATETIME數據類型

 看完這篇文章的第一感覺是,雖然對於日期類型數據使用得很算順利,不過作者 提到的一些東西還真不知道。有時候在應用上,不覺得比老外差到那裏去。但是, 老外的一個優良習慣細扣概念並進行實證檢驗;而我們的習慣是概念是概念,應用 是應用。到最後會發現其實有些很基礎的東西,是不知其所以然的。 


--------------------------------------------------------------------------------

原文:Demystifying the SQL Server DATETIME Datatype
來源:SQL-Server-Performance.com
作者:Frank Kalis

When you follow online communities dedicated to SQL Server with open eyes, you certainly notice...... 
 



--------------------------------------------------------------------------------

你和發現網上很多SQL Server的問題是關於DATETIME數據類型的,這似乎說明熟練使用DATETIME並不容易。 

奇怪的是,我卻一直相信使用DATETIME是不難的事。DATETIME並非複雜的數據類型,也沒有深奧的日期算法。唯一需要 理解的是爲了安全的處理臨時數據,DATETIME數據類型的一些基本概念。本文的目的就是幫助讀者理解這些SQL Server有趣 的地方,以及弄清楚DATETIME數據類型的一些真相。 

本文我都會使用ISO日期格式 yyyymmdd。這是一種安全的日期格式,即無論你的電腦如何設置,該格式都可以運行正常,而且 它也不受SET DATEFORMAT或者SET LANGUAGE設置的影響。即使你不開發國際用戶的數據庫應用,也最好養成使用安全的 日期格式的習慣。SQL Server只有兩種日期類型格式編號是安全的,112和116。112是ISO格式,116是ISO8601格式。 在SQL Server聯機幫助的CAST和CONVERT主題中可以找到關於這兩種日期編號的介紹。你越早養成這些習慣,很多潛在的 問題就越少。 

你將注意到,我特意使用了隱示地將CHAR轉換成DATETIME。隱示轉換有時候不是一種良好的開發習慣,不過根據 SQL Server數據類型中,DATETIME轉換的優先級高,我認爲轉換是安全的。關於這點本文就不多闡述了。 

本文先來研究一下DATETIME數據類型的內部表現形式,然後將注意力轉移到DATETIME相關的查詢上,最後總結一些 注意事項,小技巧和常見問題的解決方法。 

所有的代碼示例都適用於SQL Server 2000,我相信對於以前的SQL Server版本也應該適用。對於2000以後的版本,我將 檢查本文並對不適用的地方作相應的改動。 

好了,讓我們開始吧! 

DATETIME類型的數據的可讀性

SELECT CAST(GETDATE() AS BINARY(8)) AS WhatIsReallyStored

WhatIsReallyStored
------------------
0x00009620016CE3A8

(1 row(s) affected)

 



我必須承認,我對16進制數並不那麼精通,所以我想我無法直接告訴你這串數字表示的是 2005-03-23 22:08:31.280. ;-) 

SQL Server聯機幫助(SQL Server Books Online :BOL)的解釋是 "DATETIME數據類型的值儲存在2個4byte長度的整數中。" 

說明就只有這些。 

注意,BOL並沒有說DATETIEM類型的值是保存在某種特定的日期或者時間格式中,而這些格式是依賴於 語言和計算機設置的! 而僅僅是說儲存在2個整型數值中。這並非100%的描述清楚了。其實是這樣的, 該值的確是儲存在2個4byte長度的整型中,但是被打包成了BINARY(8)。前4個字節保存和19000101這個日期的 差值,我們知道SQL Server的基準日期是19000101。 

後4個字節保存時間數據,該數據是以午夜開始的累積毫秒數。 

這些也能在BOL中找到相關的介紹。下面我們用一個簡單的腳本來檢驗。 

DECLARE @300 BINARY(8)
SET @300 = 0x00000000 + CAST(300 AS BINARY(4)) 
 



在討論運行結果前,我們先來看一下該腳本幹什麼用。首先定義了一個SQL Server DATETIME內部數據類型。 然後我們將前4個byte(即日期部分)賦值爲0x00000000。這意味着我們將它設置爲基準日期(=19000101)。 然後在日期部分後連接一個時間部分。時間部分賦值爲300,根據BOL,我們知道,這表示300毫秒,即 1/3秒。運行腳本 

SELECT
@300
, CAST(@300 AS DATETIME)

------------------ ------------------------------------------------------
0x000000000000012C     1900-01-01 00:00:01.000

(1 row(s) affected)
 



奇怪,我們的結果顯示是1秒,而不是300毫秒。反過來推理,賦值300表示1秒,也就是說它並不是 表示300毫秒,而是300/300秒,即1秒。 

實際上,SQL Server的確儲存從午夜開始的時鐘。每個時鐘單位是3.33毫秒。這也就是爲什麼DATETIME 數據類型最小精確度是1/300。在BOL關於也是如此介紹的。 

DATETIME數據類型的基本查詢

本節我們將利用SQL Server自帶的Northwind示例數據庫。 

DATETIME查詢和其它數值查詢方式是一樣的。也就是說,你可以使用數值類型可以使用的比較操作,如 =, <>, >= or <=。而且也可以使用邏輯操作,如 LIKE 或 BETWEEN。下面進行示例。 

Example 1: 檢索條件爲特定時間: 

SELECT
CustomerID
, OrderDate
FROM
Orders
WHERE
OrderDate = '19960704'

CustomerID OrderDate
---------- ------------------------------------------------------
VINET    1996-07-04 00:00:00.000

(1 row(s) affected) 
 



注意,這裏有一個使用DATETIME最重要的地方。既然DATETIME類型既包括日期部分,也包括時間部分, 查詢中就要考慮這兩個部分。幸運的是,Northwind Orders數據表只保存日期信息在OrderDate列中。爲了 說明問題,我們修改查詢如下: 

UPDATE
Orders
SET
OrderDate = '19960704 12:31:00'
WHERE
CustomerID='VINET'
AND
OrderDate = '19960704'
GO


CustomerID OrderDate
---------- ------------------------------------------------------

(0 row(s) affected) 
 



避免該情況唯一可做的就是在WHERE語法後同時指定時期和時間。然而在大多數情況下,我們並 不知道確切的時間或者只是對日期感興趣。 

Example 2: 檢索條件爲階段性時間

上面的例子中我們想把所有日期爲'19960704'的記錄查找出來,但是沒有成功。下面我們給出有效的辦法。 

SELECT
CustomerID
, OrderDate
FROM
Orders
WHERE
OrderDate >= '19960704'
AND
OrderDate < '19960705'

CustomerID OrderDate
---------- ------------------------------------------------------
VINET    1996-07-04 12:31:00.000

(1 row(s) affected) 
 



上面是滿足需求的最簡單辦法。基本思路就是你指定大於'19960704'(從凌晨00:00:00開始),小於 '19960705'(到午夜00:00:00結束)的時間段,搜索該時間段內的記錄。這樣處理,你就不必去做 分割時間格式等工作,上面的查詢還可以改寫爲: 

SELECT
CustomerID
, OrderDate
FROM
Orders
WHERE
OrderDate
BETWEEN
'19960704'
AND
'19960705'

CustomerID OrderDate
---------- ------------------------------------------------------
VINET   1996-07-04 12:31:00.000
TOMSP   1996-07-05 00:00:00.000

(2 row(s) affected) 
 



需要注意的是,上面的寫法也許會造成一點麻煩,因爲BETWEEN會搜索包括結束日期在內的記錄。 BETWEEN的確是這樣搜索的,比如我們改寫爲: 

SELECT
CustomerID
, OrderDate
FROM
Orders
WHERE
OrderDate
BETWEEN
'19960704'
AND
'19960704'

CustomerID OrderDate
---------- ------------------------------------------------------

(0 row(s) affected) 
 



這樣,查詢就無效了。如果不顯式設置,SQL Server缺省將時間設置爲午夜(00:00:00)。 因此,上面的查詢無法找出時間不爲午夜的任何記錄。前面三個例子中,第一個例子是安全的, 推薦使用。無論採取那種方式,首要的是要搞清楚我們想要的到底是什麼。 

順便提一下,上面查詢在執行計劃中,SQL Server會自動將BETWEEN解析爲 >= 和 <= 的方式。因此,無法查詢出記錄也就不奇怪了。在執行計劃中,解析的信息如下: 

...SEEK[Orders].[OrderDate] >= Convert([@1]) AND [Orders].[OrderDate] <= Convert([@2]))... 

上面例子中,我們實際用到了 >= 和 <= ,所以也不用專門給出這兩個操作的例子了。 我們來看一下LIKE操作符的用法。BOL中指出LIKE可以用於搜索日期和時間部分,基本用法如下: 

SELECT
CustomerID
, OrderDate
FROM
Orders
WHERE
OrderDate
LIKE
'%1996%'

CustomerID OrderDate
---------- ------------------------------------------------------
VINET   1996-07-04 12:31:00.000
TOMSP   1996-07-05 00:00:00.000
HANAR   1996-07-08 00:00:00.000
...

 



在這個例子中,我們要查找1996年的記錄。現在我們擴展一下需求,查找1996年7月份的所有記錄。也許有人會用 LIKE '%1996-07%'的WHERE條件。然而運行這種查詢後,檢索不到任何記錄。爲了敘述簡單,我只是建議大家不要 侷限於DATETIME的LIKE操作用法。當處理日期或者時間的一部分時,SQL Server提供了一個更方便的內置函數,上面 的例子可改寫爲: 

SELECT
CustomerID
, OrderDate
FROM
Orders
WHERE
DATEPART(yyyy,OrderDate)=1996

CustomerID OrderDate
---------- ------------------------------------------------------
VINET   1996-07-04 12:31:00.000
TOMSP   1996-07-05 00:00:00.000
HANAR   1996-07-08 00:00:00.000
...

 



利用DATEPART函數,可以很容易對月份進行查詢。 

SELECT
CustomerID
, OrderDate
FROM
Orders
WHERE
DATEPART(yyyy,OrderDate)=1996
AND
DATEPART(mm,OrderDate)=7

CustomerID OrderDate
---------- ------------------------------------------------------
VINET   1996-07-04 12:31:00.000
TOMSP   1996-07-05 00:00:00.000
HANAR   1996-07-08 00:00:00.000
...

 



我還是建議使用Example 2的方式,如: 

SELECT
CustomerID
, OrderDate
FROM
Orders
WHERE
OrderDate >= '19960701'
AND
OrderDate < '19960801' 
 



DATETIME高級查詢

本節中要討論的是,當對DATETIME查詢時,我們是否應該多選擇DATEADD()或者DATEDIFF()函數。 實際上,DATEADD(),DATEDIFF()和>=,<=並沒有多大的區別。假設OrderDate字段有聚集索引,那麼 這兩種方式得到的性能結果基本是一樣的。不過,我還是建議多使用DATEADD函數。原因下面闡述。 

假設你想要知道從某天開始起,經過7天后的那個日期的訂單情況,查詢大致如下: 

DECLARE @dt DATETIME
SET @dt = '19960701'
SELECT
OrderID
, CustomerID
FROM
Orders
WHERE
OrderDate = DATEADD(DAY,7,@dt)

OrderID CustomerID
----------- ----------
10250   HANAR
10251   VICTE

(2 row(s) affected) 
 



既然OrderDate字段上有索引,那麼執行計劃顯示爲:

...|--Index Seek(OBJECT[Northwind].[dbo].[Orders].[OrderDate])...

SQL Server當然會用到該字段的索引,然後再看看I/O情況

Table 'Orders'. Scan count 1, logical reads 6, physical reads 0, read-ahead reads 0.

然後我們看看DATEDIFF函數的效果: 

SELECT
OrderID
, CustomerID
FROM
Orders
WHERE
DATEDIFF(d,@dt,OrderDate)=7

OrderID CustomerID
----------- ----------
10250   HANAR
10251   VICTE

(2 row(s) affected) 
 



當然查詢結果是一樣的,那麼執行計劃如何呢?

...|--Clustered Index Scan(OBJECT[Northwind].[dbo].[Orders].[PK_Orders])...

顯然,SQL Server無法使用OrderDate字段的索引,因此只有掃描整個表才能給出查詢結果。從I/O性能 上我們能得出相同的推斷:

Table 'Orders'. Scan count 1, logical reads 21, physical reads 0, read-ahead reads 0.

使用DATEDIFF的邏輯讀取次數是使用DATEADD的3倍多! 

當然,Orders表只有830條記錄,因此無論那種方式運行都很快。但是如果記錄很多時,情況就不一樣了, 至少我肯定願意使用DATEADD函數。 

如何在當前日期加上(減去)N天?

正確的回答是使用DATEADD函數,例如: 

DECLARE @dt DATETIME
SET @dt = '20050325'
SELECT
DATEADD(d,1,@dt)

------------------------------------------------------
2005-03-26 00:00:00.000

(1 row(s) affected) 
 



其實,既然SQL Server's的基礎日期單位是天,因此也可以: 

SELECT
@dt+1

------------------------------------------------------
2005-03-26 00:00:00.000

(1 row(s) affected) 
 



兩種方式是等價的,都對某日期加上N天。如果是減去N天,把N改成-N天就行了。 

這種處理同樣適用於天數的分割。假設你要對上面的日期加上2個小時,可以如下使用: 

DECLARE @dt DATETIME
SET @dt = '20050325'
SELECT
DATEADD(hh,2,@dt)

------------------------------------------------------
2005-03-25 02:00:00.000

(1 row(s) affected) 
 



或者寫爲: 

SELECT
@dt+0.08333333333333333

------------------------------------------------------
2005-03-25 02:00:00.000

(1 row(s) affect 
 



從上面的分析中,我們可以知道爲什麼選擇用DATEADD函數。因爲它比較容易被理解,而 也不必將2個小時具體換算成天爲單位,例如 2小時=2/24天=0.08333333333333333天。 

爲什麼可以用 DATEADD(d, DATEDIFF(d, 0...))的方法來去掉時間部分對結果的影響?

準確的說,SQL Server 2000以及以前的版本,DATETIME數據類型總是包含兩部分:日期和時間。你不能 把這兩部分分割開。所以用“去掉”這個詞可能會造成誤解。其實我們是把時間部分設置爲午夜時間,從而 來避免錯誤的發生。下面是其中的一種用法: 

SELECT
DATEADD(d,DATEDIFF(d,0,GETDATE()),0)

------------------------------------------------------
2005-03-23 00:00:00.000

(1 row(s) affected) 
 



我們來解剖該語句,看看它爲什麼能去掉時間影響。首先 

SELECT
DATEDIFF(d,0,GETDATE())

-----------
38432

(1 row(s) affected) 
 



這句查詢是關鍵! 

DATEDIFF函數返回兩個日期間的天數差,如: 

SELECT
DATEDIFF(d,'20050228 23:59:59.997', '20050301 00:00:00.000')

-----------
1

(1 row(s) affected) 
 



當然沒有人會認爲上面那兩個時間差是1天。但是,既然DATEPART參數是d,DATEDIFF函數就只會考慮 日期部分,下面的語句和上面的查詢實際上效果是一樣的。 

SELECT
DATEDIFF(d,'20050228', '20050301')

-----------
1

(1 row(s) affected) 
 



因此無論實際上兩個時間有多接近,你最後得到的結果總是1天。回到上面的例子,DATEDIFF返回基準日期 和當前日期之間的天數。太棒了,這樣我們就可以得到想要的結果,而不必去處理時間部分。我們只需要 將DATEDIFF得到的結果放入DATEADD函數中去計算即可。 

SELECT
DATEADD(d,DATEDIFF(d,0,GETDATE()),0)
, DATEADD(d,38432,0)

------------------------------------------------------ ------------------------------------------------------
2005-03-23 00:00:00.000   2005-03-23 00:00:00.000

(1 row(s) affected) 
 



當寫本文時,我覺得這真是簡單。 

SELECT
CAST(DATEDIFF(d,0,GETDATE()) AS DATETIME)

------------------------------------------------------
2005-03-23 00:00:00.000

(1 row(s) affected) 
 



上面的例子也能很好的達到目的。 

正如你所看到的,這不涉及任何複雜的日期或者數值算法。坦率地說,這其實很簡單。而且,對於DATEDIFF 和DATEADD函數的任何參數來說,道理是一樣的。 

最後提一下,我們還可以利用DATETIME的內部存儲格式來設置一個DATETIME類型的時間部分爲午夜時間,如: 

SELECT
CAST(SUBSTRING(CAST(GETDATE() AS BINARY(8)),1,4) + 0x00000000 AS DATETIME)
, CAST(CAST(SUBSTRING(CAST(GETDATE() AS BINARY(8)),1,4) AS INT) AS DATETIME)

------------------------------------------------------ ------------------------------------------------------
2005-03-23 00:00:00.000   2005-03-23 00:00:00.000

(1 row(s) affected) 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章