![]() ![]() ![]() ![]() SQL Serverにおける日付/時刻計算のベストプラクティスこの記事のURLhttp://japan.internet.com/developer/20071113/26.html
著者:Alex Kozak
海外internet.com発の記事
はじめに 数か月前、経験豊かなデータベース管理者兼データベースプログラマが私に質問してきました。Microsoft SQL Serverの日付/時刻関数を使わずに、例えば 計算の仕組み SQL Serverの SELECT GETDATE(); SELECT DATEADD(dd, 1, GETDATE()); SELECT GETDATE() + 1; Results: 2007-03-15 16:21:41.630 2007-03-16 16:21:41.630 2007-03-16 16:21:41.630 ご覧の通り、 ご存知のように、datetimeデータ型は8バイトで、4バイトには1900年1月1日からの経過日数(または過去にさかのぼった日数)が格納され、もう4バイトには午前0時からのクロックティック数(1ティック=3.33ミリ秒)が格納されています。また、datetimeデータ型より精度は低くなりますが、4バイトのみを使用するsmalldatetimeデータ型もあります。smalldatetimeデータ型では、1900年1月1日から起算した日数と、午前0時から何分が経過したかが格納されます。数値はすべて整数として格納されます。従って、 次の例では、1をdatetime型に変換しています。この結果は、1900年1月1日から1日が経過した日付として扱われます。 SELECT CAST(1 as datetime); Results: 1900-01-02 00:00:00.000 SELECT GETDATE(); SELECT DATEADD(dd, 1, GETDATE()); SELECT GETDATE() + 1; SELECT GETDATE() + 'Jan 02, 1900'; Results: 2007-03-16 23:01:37.420 2007-03-17 23:01:37.420 2007-03-17 23:01:37.420 2007-03-17 23:01:37.420 日付/時刻の計算で加算演算子(+)を使用するには、datetime型の加数が少なくとも1つ必要で、その加数は優先度が最も高くないといけません。 次の例を試してみてください。 SELECT DATEADD(dd, 1, 'Mar 17, 2007'); SELECT 'Mar 17, 2007' + 1; SELECT 'Mar 17, 2007' + 'Jan 02, 1900'; SELECT GETDATE() + 1 + 'Jan 02, 1900'; お分かりのように、最初のSELECTではSQL Serverの 3番目のSELECTは動作しますが、特に意味をなしません。これは、2つのvarchar式を連結しているだけにすぎません。4番目のSELECTはとても興味深いものです。2番目と3番目の加数は、1番目の加数よりも優先度が低いため、暗黙的にdatetimeデータ型に変換する必要があります。それぞれは1日として解釈されるため、 最初の落とし穴この時点で、日付/時刻の計算に加減演算子を使ってもまったく問題ないと考え始めるかもしれません。ところが、そうでもないのです。例えば、2つの日付の間隔(日単位)を、次の2つの方法を使って計算してみましょう。 DECLARE @dt1 datetime, @dt2 datetime; SELECT @dt1 = 'Mar 17, 2007 09:09:00', @dt2 = 'Mar 17, 2007 22:09:00'; SELECT DATEDIFF(dd, @dt1, @dt2); SELECT CAST((@dt2 - @dt1) as int); Results: 0 1 SQL Serverの DECLARE @dt3 datetime, @dt4 datetime; SELECT @dt3 = 'Mar 17, 2007 11:59:59.994', @dt4 = 'Mar 17, 2007 11:59:59.997'; SELECT CAST(@dt3 AS int); SELECT CAST(@dt4 AS int) SELECT CAST(CAST(@dt3 AS int) AS datetime); SELECT CAST(CAST(@dt4 AS int) AS datetime); Results: 39156 39157 2007-03-17 00:00:00.000 2007-03-18 00:00:00.000 この例では、正午少し前のタイムスタンプを2つ使用しています。この2つのタイムスタンプの間隔は3ミリ秒です。しかし2つの日付の値を整数に変換し、その結果を日付の値に再変換すると、1日のずれが生じます。同様に、異なる日付(3月16日と3月17日)を表す2つのdatetime値が、次のように同じ日付に誤って変換されることもあります。 DECLARE @dt3 datetime, @dt4 datetime; SELECT @dt3 = 'Mar 16, 2007 12:00:01.000', @dt4 = 'Mar 17, 2007 11:59:59.994'; SELECT CAST(@dt3 AS int); SELECT CAST(@dt4 AS int) SELECT CAST(CAST(@dt3 AS int) AS datetime); SELECT CAST(CAST(@dt4 AS int) AS datetime); Results: 39156 39156 2007-03-17 00:00:00.000 2007-03-17 00:00:00.000 ここで、もう1つの例を見てみましょう。例えば、ここにID、時間、価格といった販売取引内容が格納されているテーブルがあります。そして、このテーブルから、1日あたりの取引合計金額と取引数を調べる必要があるとします(よくある処理です)。非常に大きなテーブルなので、日付ではなく期間ごとに取引をグループ化し、クエリのスピードを上げることにしました。ただし、SQL Serverの このシナリオではどうなるのかを見てみましょう。 SET NOCOUNT ON; IF OBJECT_ID('sales', 'U') IS NOT NULL DROP TABLE sales CREATE TABLE sales( transactionID int, transactionTime datetime, amount decimal(4,2)); INSERT INTO sales VALUES(1, 'Mar 17, 2007 08:00:23', 24.34); INSERT INTO sales VALUES(2, 'Mar 17, 2007 10:33:23', 88.54); INSERT INTO sales VALUES(3, 'Mar 17, 2007 12:00:44', 12.12); INSERT INTO sales VALUES(4, 'Mar 17, 2007 14:23:23', 43.25); INSERT INTO sales VALUES(5, 'Mar 17, 2007 16:45:22', 76.34); INSERT INTO sales VALUES(6, 'Mar 17, 2007 17:11:22', 51.11); INSERT INTO sales VALUES(7, 'Mar 17, 2007 19:45:23', 30.99); SELECT COUNT(*) AS #Trans, SUM(amount) AS totalAmount FROM sales GROUP BY DATEDIFF(dd, 0, transactionTime); SELECT COUNT(*) AS #Trans, SUM(amount) AS totalAmount FROM sales GROUP BY CAST(transactionTime AS int); SELECT COUNT(*) AS #Trans, SUM(amount) AS totalAmount FROM sales GROUP BY CAST((transactionTime - 0) AS int); Results: #Trans totalAmount ----------- --------------------------------------- 7 326.69 #Trans totalAmount ----------- --------------------------------------- 2 112.88 5 213.81 #Trans totalAmount ----------- --------------------------------------- 2 112.88 5 213.81 SQL Serverの 日付/時刻データの時間部分の算術演算 ここまでは、日付/時刻値の日付部分における算術演算について説明してきました。 そのやり方を確認してみましょう。1日は24時間で、分に換算すれば1,440分、秒に換算すれば86,400秒です。そこで、次のステートメントを実行します。
SELECT GETDATE() + 1/24
これで、現在のdatetimeに1時間をプラスした値が返されると思うでしょう。しかし実際は1/24だけでは使えません。除数と非除数はどちらも整数であるため、この除算の結果はゼロになるからです。正しい結果を得るには、両方の整数をdecimalデータ型またはfloatデータ型に変換する必要があります。 -- How to add one hour --------------------------------------- SELECT GETDATE() SELECT DATEADD(hh, 1, GETDATE()) SELECT GETDATE () + CAST(1 AS dec(9,4))/ CAST (24 AS dec(9,4)) SELECT GETDATE () + CAST(1 AS dec)/ CAST (24 AS dec) SELECT GETDATE () + CAST (1 AS float)/ CAST (24 AS float) Results: 2007-03-25 20:31:13.870 2007-03-25 21:31:13.870 2007-03-25 21:31:13.867 2007-03-25 21:31:13.870 2007-03-25 21:31:13.870 -- How to add one minute --------------------------------------- SELECT GETDATE() SELECT DATEADD(mi, 1, GETDATE()) SELECT GETDATE () + CAST(1 AS dec(9,4))/ CAST (1440 AS dec(9,4)) SELECT GETDATE () + CAST(1 AS dec(18.9))/ CAST (1440 AS dec(18.9)) SELECT GETDATE () + CAST (1 AS float)/ CAST (1440 AS float) Results: 2007-03-25 20:35:15.127 2007-03-25 20:36:15.127 2007-03-25 20:36:15.123 2007-03-25 20:36:15.127 2007-03-25 20:36:15.127 -- How to add one second --------------------------------------- SELECT GETDATE() SELECT DATEADD(ss, 1, GETDATE()) SELECT GETDATE () + CAST(1 AS dec(9,4))/ CAST (86400 AS dec(9,4)) SELECT GETDATE () + CAST(1 AS dec(18.9))/ CAST (86400 AS dec(18.9)) SELECT GETDATE () + CAST(1 AS dec(24.18))/ CAST (86400 AS dec(24.18)) SELECT GETDATE () + CAST (1 AS float)/ CAST (86400 AS float) Results: 2007-03-25 20:42:26.617 2007-03-25 20:42:27.617 2007-03-25 20:42:27.613 2007-03-25 20:42:27.613 2007-03-25 20:42:27.613 2007-03-25 20:42:27.617 -- How to add one second, using variables ------------------------------------------ DECLARE @dec1 dec(24,18), @dec2 dec(24,18), @dec3 dec(24,18), @dt datetime SELECT @dec1 = 1, @dec2 = 86400, @dt = GETDATE(); SELECT @dec3 = @dec1 / @dec2; SELECT @dt SELECT DATEADD(ss, 1, @dt) SELECT @dt + @dec3 SELECT @dt + CAST (1 AS float)/ CAST (86400 AS float) Results: 2007-03-25 20:49:16.817 2007-03-25 20:49:17.817 2007-03-25 20:49:17.813 2007-03-25 20:49:17.817 最後の例で分かるように、SQL Serverの これに対して、floatデータ型に変換すれば、加算演算子を使った方法でも安全かつ正確に時間を計算できるように見えます。しかし、実際に使用し始めると問題が発生する場合があります。値が重複したり欠落したりするのです。この問題を説明するために、補助テーブルを作成してデータを投入します。 SET NOCOUNT ON; DECLARE @max int, @cnt int; SELECT @cnt = 10000; IF EXISTS(SELECT * FROM sysobjects WHERE ID = (OBJECT_ID('sequence')) AND xtype = 'U') DROP TABLE sequence; CREATE TABLE sequence(num int NOT NULL); INSERT INTO sequence VALUES(1); SELECT @max = 1; WHILE(@max <= @cnt) BEGIN INSERT INTO sequence SELECT @max + num FROM sequence; SELECT @max = MAX(num) FROM sequence; END このスクリプトを実行すると、テーブルシーケンスに16384の連続する番号が順番に挿入されます(この16,384という数値に特別な意味はありません。説明のために適当に選んだ数値です)。 ここで、補助テーブルとSQLの日付/時刻関数を使用して、時間のシーケンスを生成します。 IF EXISTS(SELECT * FROM sysobjects WHERE id = OBJECT_ID('test')) DROP TABLE test; SELECT num, DATEADD(hh, num, 'Dec 31, 2006 23:00:00') dt INTO test FROM sequence; SELECT * FROM test; Results: num dt ----------- ----------------------- 1 2007-01-01 00:00:00.000 2 2007-01-01 01:00:00.000 3 2007-01-01 02:00:00.000 . . . . . . . . . . . . . . . . . . 3099 2007-05-10 02:00:00.000 3100 2007-05-10 03:00:00.000 . . . . . . . . . . . . . . . . . . 16381 2008-11-13 12:00:00.000 16382 2008-11-13 13:00:00.000 16383 2008-11-13 14:00:00.000 16384 2008-11-13 15:00:00.000 ここで、今生成したシーケンスを同じ SELECT DISTINCT DATEADD(hh, -num, dt) FROM test Results: 2006-12-31 23:00:00.000 この結果を見れば、SQL Serverの日付/時間関数によって日付/時刻の値が適切に生成されていることが分かります。次に、算術演算子(+)を使用したソリューションをテストするために、次の例を実行します。 DECLARE @time float SELECT @time = CAST(1 as float)/CAST(24 as float) IF EXISTS(SELECT * FROM sysobjects WHERE id = OBJECT_ID('test')) DROP TABLE test; SELECT num, (CAST('Dec 31, 2006 23:00:00' AS datetime) + @time * num) dt INTO test FROM sequence; SELECT * FROM test; num dt ----------- ----------------------- 1 2007-01-01 00:00:00.000 2 2007-01-01 01:00:00.000 3 2007-01-01 02:00:00.000 4 2007-01-01 03:00:00.000 5 2007-01-01 03:59:59.997 6 2007-01-01 05:00:00.000 7 2007-01-01 05:59:59.997 8 2007-01-01 07:00:00.000 9 2007-01-01 08:00:00.000 10 2007-01-01 08:59:59.997 . . . . . . . . . . . . . . . . . . 16380 2008-11-13 11:00:00.000 16381 2008-11-13 11:59:59.997 16382 2008-11-13 12:59:59.997 16383 2008-11-13 14:00:00.000 16384 2008-11-13 14:59:59.997 これを見ると、加算演算子によって得られる結果が正しくないことが分かります。場合によっては、期待する値との間に3ミリ秒ものずれが生じています。生成された日付/時刻値のシーケンスをロールアップすると、複数の「シード値」が導き出されます。これは正しい動作ではありません。 SELECT DISTINCT DATEADD(hh, -num, dt) FROM test Results: 2006-12-31 22:59:59.997 2006-12-31 23:00:00.000 この3ミリ秒のずれは、ほとんどのアプリケーションで許容されると考えるかもしれません。しかし、この一見すると取るに足りない問題が、大きな問題につながる可能性があるのです。次の例を見てください。 SELECT CONVERT(varchar(100), dt, 100) FROM test ORDER BY num Results: Jan 1 2007 12:00AM Jan 1 2007 1:00AM Jan 1 2007 2:00AM Jan 1 2007 3:00AM Jan 1 2007 3:59AM Jan 1 2007 5:00AM Jan 1 2007 5:59AM . . . . . . . . . . Nov 13 2008 5:59AM Nov 13 2008 6:59AM Nov 13 2008 8:00AM Nov 13 2008 8:59AM Nov 13 2008 9:59AM Nov 13 2008 11:00AM Nov 13 2008 11:59AM Nov 13 2008 12:59PM Nov 13 2008 2:00PM Nov 13 2008 2:59PM この例では、 DECLARE @time float SELECT @time = cast(1 as float)/cast(1440 as float) IF EXISTS(SELECT * FROM sysobjects WHERE id = OBJECT_ID('test')) DROP TABLE test; SELECT num, (CAST('Dec 31, 2006 23:59:00' AS datetime) + @time * num) dt INTO test FROM sequence; SELECT * FROM test; Results: num dt ----------- ----------------------- 1 2007-01-01 00:00:00.000 2 2007-01-01 00:01:00.000 . . . . . . . . . . . . . . . . . . 1579 2007-01-02 02:17:59.997 1580 2007-01-02 02:19:00.000 1581 2007-01-02 02:19:59.997 . . . . . . . . . . . . . . . . . . 16382 2007-01-12 09:01:00.000 16383 2007-01-12 09:01:59.997 16384 2007-01-12 09:03:00.000 このように、やはり不正確な値が生成されています。さらに、これらの値を別の書式に変換すると、日付が重複または欠落してしまいます。次の例を見てください。 SELECT CONVERT(varchar(100), dt, 100) FROM test ORDER BY num Results: Jan 1 2007 12:00AM Jan 1 2007 12:01AM . . . . . . . . . . Jan 2 2007 12:00AM Jan 2 2007 12:00AM Jan 2 2007 12:02AM Jan 2 2007 12:02AM Jan 2 2007 12:04AM Jan 2 2007 12:04AM . . . . . . . . . . Jan 12 2007 9:00AM Jan 12 2007 9:01AM Jan 12 2007 9:01AM Jan 12 2007 9:03AM 例えば、「Jan 02, 2007 12:02AM」という値は2つありますが、「Jan 02, 2007 12:03AM」はありません。 重複している値のリストを確認するには、次のクエリを実行します。 SELECT COUNT(*), CONVERT(varchar(100), dt, 100) FROM test GROUP BY CONVERT(varchar(100), dt, 100) HAVING COUNT(*) > 1 ORDER BY 2 最後に、同じ方法で秒のシーケンスを生成します。 DECLARE @time float SELECT @time = cast(1 as float)/cast(86400 as float) IF EXISTS(SELECT * FROM sysobjects WHERE id = OBJECT_ID('test')) DROP TABLE test; SELECT num, (CAST('Dec 31, 2006 23:59:59' AS datetime) + @time * num) dt INTO test FROM sequence; SELECT * FROM test; Results: num dt ----------- ----------------------- 1 2007-01-01 00:00:00.000 2 2007-01-01 00:00:01.000 3 2007-01-01 00:00:02.000 4 2007-01-01 00:00:03.000 5 2007-01-01 00:00:03.997 6 2007-01-01 00:00:05.000 7 2007-01-01 00:00:06.000 8 2007-01-01 00:00:07.000 9 2007-01-01 00:00:08.000 10 2007-01-01 00:00:08.997 11 2007-01-01 00:00:09.997 . . . . . . . . . . . . . . . . . . 16382 2007-01-01 04:33:00.997 16383 2007-01-01 04:33:02.000 16384 2007-01-01 04:33:03.000 SELECT CONVERT(varchar(100), dt, 120) FROM test ORDER BY num Results: 2007-01-01 00:00:00 2007-01-01 00:00:01 2007-01-01 00:00:02 2007-01-01 00:00:03 2007-01-01 00:00:03 2007-01-01 00:00:05 . . . . . . . . . . 2007-01-01 04:32:55 2007-01-01 04:32:56 2007-01-01 04:32:56 2007-01-01 04:32:58 2007-01-01 04:32:59 2007-01-01 04:33:00 2007-01-01 04:33:00 2007-01-01 04:33:02 2007-01-01 04:33:03 この最後の例でも、同じ問題が起きています。また、算術演算子を使って日付/時刻を操作することで、別の問題が発生する場合もあります。例えば、不思議な結果になったり、パフォーマンスが低下したり、ここで取り上げなかった問題が発生することもあります。これらの問題はすべて、SQL Serverの日付/時刻関数を使用すれば回避できます。 日付/時刻関数の副次的なメリット SQL Serverの日付/時刻関数のメリットはエラーを回避できるだけではありません。この関数を使用すると、プログラマが自分の作業をより簡単かつ快適に行えるようになります。例えば、2007年のそれぞれの月の5番目の日付を生成しなければならない場合、 SELECT DATEADD(mm, num - 1, 'Jan 05, 2007') AS [5th_Day] FROM sequence WHERE num <= 12 Results: 5th_Day ----------------------- 2007-01-05 00:00:00.000 2007-02-05 00:00:00.000 2007-03-05 00:00:00.000 2007-04-05 00:00:00.000 2007-05-05 00:00:00.000 2007-06-05 00:00:00.000 2007-07-05 00:00:00.000 2007-08-05 00:00:00.000 2007-09-05 00:00:00.000 2007-10-05 00:00:00.000 2007-11-05 00:00:00.000 2007-12-05 00:00:00.000 このクエリの優れた点は、その利便性です。それぞれの月または年の日数を把握している必要はありません。必要なロジックと計算は既にSQL Server関数に含まれており、プログラマがそれを意識する必要はありません。算術演算子(+)を使用して同じ作業を行うには、ロジックを自分自身で実装する必要があります。ロジックを自分自身で実装すると、開発やデバッグに時間がかかり、エラーが発生する可能性も高くなります。 SQL Serverの日付/時刻の計算で、算術演算子を使うべきでしょうか? いいえ。使うべきではないでしょう。非常にシンプルな計算、例えば、 著者紹介Alex Kozak(Alex Kozak)
SAP Canadaの上級DBA/アナリスト。データベースとプログラミングに15年以上従事。MSDNライブラリにも多数投稿。
|