TSQL で 2 つの日時が同じ暦日にあるかどうかを確認する良い方法は何ですか?
-
09-06-2019 - |
質問
私が抱えている問題は次のとおりです。where 句の日時を比較して 2 つの日付が同じ日かどうかを確認する必要がある大規模なクエリがあります。私の現在の解決策は、最悪ですが、日時を UDF に送信して同日の午前 0 時に変換し、それらの日付が等しいかどうかをチェックすることです。クエリ プランに関しては、結合や where 句のほとんどすべての UDF と同様に、これは大惨事です。これは、アプリケーション内で関数を根絶できず、最適なインデックスを見つけるために実際に使用できるものをクエリ オプティマイザーに提供できなかった唯一の場所の 1 つです。
この場合、関数コードをクエリにマージし直すのは現実的ではないと思われます。
ここには単純な何かが欠けていると思います。
参考までに関数を載せておきます。
if not exists (select * from dbo.sysobjects
where id = object_id(N'dbo.f_MakeDate') and
type in (N'FN', N'IF', N'TF', N'FS', N'FT'))
exec('create function dbo.f_MakeDate() returns int as
begin declare @retval int return @retval end')
go
alter function dbo.f_MakeDate
(
@Day datetime,
@Hour int,
@Minute int
)
returns datetime
as
/*
Creates a datetime using the year-month-day portion of @Day, and the
@Hour and @Minute provided
*/
begin
declare @retval datetime
set @retval = cast(
cast(datepart(m, @Day) as varchar(2)) +
'/' +
cast(datepart(d, @Day) as varchar(2)) +
'/' +
cast(datepart(yyyy, @Day) as varchar(4)) +
' ' +
cast(@Hour as varchar(2)) +
':' +
cast(@Minute as varchar(2)) as datetime)
return @retval
end
go
問題を複雑にするために、タイム ゾーン テーブルに結合して日付を現地時間と照合しています。現地時間は行ごとに異なる可能性があります。
where
dbo.f_MakeDate(dateadd(hh, tz.Offset +
case when ds.LocalTimeZone is not null
then 1 else 0 end, t.TheDateINeedToCheck), 0, 0) = @activityDateMidnight
[編集]
@Todd の提案を取り入れています。
where datediff(day, dateadd(hh, tz.Offset +
case when ds.LocalTimeZone is not null
then 1 else 0 end, t.TheDateINeedToCheck), @ActivityDate) = 0
datediff の仕組みに関する私の誤解 (連続する年の同じ日の結果は、期待した 0 ではなく 366 になります) により、多大な労力を無駄にすることになりました。
しかし、クエリ計画は変更されませんでした。すべてを振り出しに戻す必要があると思います。
解決
これはもっと簡潔です:
where
datediff(day, date1, date2) = 0
他のヒント
where 句の左側をきれいな状態に保つ必要があります。したがって、通常は次のようにします。
WHERE MyDateTime >= @activityDateMidnight
AND MyDateTime < (@activityDateMidnight + 1)
(代わりに DATEADD(d, 1, @activityDateMidnight) を好む人もいますが、それは同じことです)。
ただし、TimeZone テーブルは問題を少し複雑にします。スニペットからは少し不明確ですが、t.TheDateInTable はタイムゾーン識別子を持つ GMT であり、現地時間の @activityDateMidnight と比較するためにオフセットを追加しているようです。ただし、ds.LocalTimeZone が何なのかはわかりません。
その場合は、代わりに @activityDateMidnight を GMT に取得する必要があります。
where
year(date1) = year(date2)
and month(date1) = month(date2)
and day(date1) = day(date2)
必ずお読みください 数行のコードを変更するだけで 1000% 以上の改善が得られるのはデータベースだけです これにより、オプティマイザーが日付を操作するときに確実にインデックスを効果的に利用できるようになります。
エリック・Z・ビアード:
私はすべての日付を GMT で保存します。使用例は次のとおりです。1日の東部時間午後11時、つまりグリニッジ標準時第2時に何かが起こりました。1 日のアクティビティを見たいのですが、EST にいるので午後 11 時のアクティビティを見たいと思っています。生の GMT 日時を比較するだけだと、何かを見落としてしまうでしょう。レポートの各行は、異なるタイムゾーンのアクティビティを表すことができます。
そうですが、2008 年 1 月 1 日 (東部標準時) のアクティビティに興味があると言った場合:
SELECT @activityDateMidnight = '1/1/2008', @activityDateTZ = 'EST'
変換する必要があるだけです それ GMT へ (EST から EDT へ、またはその逆の前日のクエリの複雑さは無視しています):
Table: TimeZone
Fields: TimeZone, Offset
Values: EST, -4
--Multiply by -1, since we're converting EST to GMT.
--Offsets are to go from GMT to EST.
SELECT @activityGmtBegin = DATEADD(hh, Offset * -1, @activityDateMidnight)
FROM TimeZone
WHERE TimeZone = @activityDateTZ
これにより、「1/1/2008 4:00 AM」が表示されるはずです。次に、GMT で検索するだけです。
SELECT * FROM EventTable
WHERE
EventTime >= @activityGmtBegin --1/1/2008 4:00 AM
AND EventTime < (@activityGmtBegin + 1) --1/2/2008 4:00 AM
問題のイベントは、GMT EventTime 2008 年 1 月 2 日 3:00 AM で保存されています。EventTable に TimeZone さえ必要ありません (少なくともこの目的では)。
EventTime は関数内にないため、これは単純なインデックス スキャンであり、非常に効率的です。EventTime をクラスター化インデックスにすると、動作します。;)
個人的には、クエリを実行する前に、アプリで検索時刻を GMT に変換させると思います。
これにより、日付から時間コンポーネントが削除されます。
select dateadd(d, datediff(d, 0, current_timestamp), 0)
ここではオプションの選択肢が豊富です。Sybase または SQL Server 2008 を使用している場合は、日付型の変数を作成し、それらに日時値を割り当てることができます。データベース エンジンは時間を無駄にしてくれません。以下に簡単で汚いテストを示します (コードは Sybase 方言です)。
declare @date1 date
declare @date2 date
set @date1='2008-1-1 10:00'
set @date2='2008-1-1 22:00'
if @date1=@date2
print 'Equal'
else
print 'Not equal'
SQL 2005 以前の場合、日付を時刻コンポーネントを含まない形式の varchar に変換することができます。たとえば、次の場合は 2008.08.22 が返されます。
select convert(varchar,'2008-08-22 18:11:14.133',102)
102 の部分は書式を指定します (Books online では、使用可能なすべての書式をリストできます)。
したがって、できることは、日時を取得して日付要素を抽出し、時刻を破棄する関数を作成することです。そのようです:
create function MakeDate (@InputDate datetime) returns datetime as
begin
return cast(convert(varchar,@InputDate,102) as datetime);
end
その後、コンパニオン向けの機能を使用できるようになります
Select * from Orders where dbo.MakeDate(OrderDate) = dbo.MakeDate(DeliveryDate)
エリック・Z・ビアード:
アクティビティの日付は現地のタイムゾーンを示すことを目的としていますが、特定のタイムゾーンを示すものではありません
さて、振り出しに戻ります。これを試して:
where t.TheDateINeedToCheck BETWEEN (
dateadd(hh, (tz.Offset + ISNULL(ds.LocalTimeZone, 0)) * -1, @ActivityDate)
AND
dateadd(hh, (tz.Offset + ISNULL(ds.LocalTimeZone, 0)) * -1, (@ActivityDate + 1))
)
これにより、@ActivityDate が現地時間に変換され、それと比較されます。これがインデックスを使用する最善の方法ですが、うまくいくかどうかはわかりません。試してクエリ プランを確認してください。
次のオプションは、インデックス付きで計算された TimeINeedToCheck を含むインデックス付きビューです。 現地時間で. 。その後、次の場所に戻るだけです。
where v.TheLocalDateINeedToCheck BETWEEN @ActivityDate AND (@ActivityDate + 1)
これは間違いなくインデックスを使用しますが、INSERT と UPDATE にわずかなオーバーヘッドが発生します。
datepart の dayofyear 関数を使用します。
Select *
from mytable
where datepart(dy,date1) = datepart(dy,date2)
and
year(date1) = year(date2) --assuming you want the same year too
datepart リファレンスを参照してください。 ここ.
タイムゾーンに関しては、すべての日付を単一のタイムゾーン (できれば UTC) に保存するもう 1 つの理由があります。とにかく、datediff、datepart、およびさまざまな組み込みの日付関数を使用した答えが最善の策だと思います。