揭密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)
--------------------------------------------------------------------------------
原文: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)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.