GMT地域の「夏時間」ルックアップカレンダーテーブルを作成しました。テーブルをクエリしてUTC日時からローカルの日時を返すために使用している関数のパフォーマンスが低下しています。
TVFのコーディング方法の変更を含め、これを改善するための助けがあれば、喜んでいただけます。
この関数は、1m以上の行を頻繁に返すクエリで使用されます。この関数は、旅行データを含む倉庫のテーブルをクエリするときに使用されます。
旅行の開始日時と終了日時はUTCに保存され、上記の関数を使用して現地時間に変換されます。開発者は、会社を去ってからずっと、UTC時間を現地時間に変換するスカラー関数を作成しました。 TVFはスカラー関数よりもパフォーマンスが高いはずなので、カレンダーテーブルとTVFを使用してその関数を書き換える必要がありました。
機能なし:
SQL Server Execution Times: CPU time = 4633 ms, elapsed time = 4909 ms.
関数を使用して:
SQL Server Execution Times: CPU time = 20795 ms, elapsed time = 21176 ms.
これはテーブルからのサンプル出力です
CREATE TABLE dbo.DSTLookup
(
[Id] int,
[Tzid] int,
[DT_WhenSwitch] datetime,
[DSTOffSetSeconds] int,
[GMTOffSetSeconds] int
)
INSERT INTO dbo.DSTLookup
VALUES (29, 2, N'2014-03-30T01:00:00', 3600, 0),
(30, 2, N'2014-10-26T02:00:00', 0, 0),
(31, 2, N'2015-03-29T01:00:00', 3600, 0),
(32, 2, N'2015-10-25T02:00:00', 0, 0),
(33, 2, N'2016-03-27T01:00:00', 3600, 0),
(34, 2, N'2016-10-30T02:00:00', 0, 0),
(35, 2, N'2017-03-26T01:00:00', 3600, 0),
(36, 2, N'2017-10-29T02:00:00', 0, 0),
(37, 2, N'2018-03-25T01:00:00', 3600, 0),
(38, 2, N'2018-10-28T02:00:00', 0, 0)
これはTVFです。
CREATE FUNCTION dbo.FN_GetLocalTime_FromUTC_BasedOnTZId
(@StartDateTime DATETIME, @EndDateTime DATETIME, @Tzid INT)
/*=========================================================================
* 2017-03-27
* Returns local time from UTC time based on timeZoneId
*
==========================================================================*/
RETURNS TABLE
AS
RETURN
(
WITH cteStartDate AS
(
SELECT
RN = ROW_NUMBER() OVER (ORDER BY D.Id DESC),
D.DSTOffSetSeconds 's_DST_OffSet',
D.GMTOffSetSeconds 's_GMT_OffSet'
FROM
dbo.DSTLookup D
WHERE
D.DT_WhenSwitch <= @StartDateTime
AND D.Tzid = @Tzid
),
cteEndDate AS
(
SELECT
RN = ROW_NUMBER() OVER (ORDER BY D.Id DESC),
D.DSTOffSetSeconds 'e_DST_OffSet',
D.GMTOffSetSeconds 'e_GMT_OffSet'
FROM
dbo.DSTLookup D
WHERE
D.DT_WhenSwitch <= @EndDateTime
AND D.Tzid = @Tzid
),
cteConvertStartDate AS
(
SELECT
DATEADD(SECOND, (COALESCE(S.s_DST_OffSet, 0) + COALESCE(S.s_GMT_OffSet, 0)), @StartDateTime) 'LocalStartDateTime'
FROM
cteStartDate S
WHERE
S.RN = 1
),
cteConvertEndDate AS
(
SELECT
DATEADD(SECOND, (COALESCE(E.e_DST_OffSet, 0) + COALESCE(E.e_GMT_OffSet, 0)), @EndDateTime) 'LocalEndDateTime'
FROM
cteEndDate E
WHERE
E.RN = 1
)
SELECT
S.LocalStartDateTime, E.LocalEndDateTime
FROM
cteConvertStartDate S, cteConvertEndDate E
);
GO
TVFを照会するには:
SELECT *
FROM dbo.FN_GetLocalTime_FromUTC_BasedOnTzId
('2017-03-27 10:00:30', '2017-03-27 10:15:54', 2);
実行計画 主キーを含めるためのMaxの推奨に従います。
WITH SCHEMABINDING
句にRETURNS TABLE
を追加して、関数をスキーマバインドテーブル値関数にします。
そう:
CREATE FUNCTION dbo.FN_GetLocalTime_FromUTC_BasedOnTZId
(@StartDateTime DATETIME, @EndDateTime DATETIME, @Tzid INT)
/*=========================================================================
* 2017-03-27
* Returns local time from UTC time based on timeZoneId
*
==========================================================================*/
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
WITH cteStartDate AS
(
SELECT
RN = ROW_NUMBER() OVER (ORDER BY D.Id DESC),
D.DSTOffSetSeconds 's_DST_OffSet',
D.GMTOffSetSeconds 's_GMT_OffSet'
FROM
dbo.DSTLookup D
WHERE
D.DT_WhenSwitch <= @StartDateTime
AND D.Tzid = @Tzid
),
cteEndDate AS
(
SELECT
RN = ROW_NUMBER() OVER (ORDER BY D.Id DESC),
D.DSTOffSetSeconds 'e_DST_OffSet',
D.GMTOffSetSeconds 'e_GMT_OffSet'
FROM
dbo.DSTLookup D
WHERE
D.DT_WhenSwitch <= @EndDateTime
AND D.Tzid = @Tzid
),
cteConvertStartDate AS
(
SELECT
DATEADD(SECOND, (COALESCE(S.s_DST_OffSet, 0) + COALESCE(S.s_GMT_OffSet, 0)), @StartDateTime) 'LocalStartDateTime'
, S.RN
FROM
cteStartDate S
WHERE
S.RN = 1
),
cteConvertEndDate AS
(
SELECT
DATEADD(SECOND, (COALESCE(E.e_DST_OffSet, 0) + COALESCE(E.e_GMT_OffSet, 0)), @EndDateTime) 'LocalEndDateTime'
, E.RN
FROM
cteEndDate E
WHERE
E.RN = 1
)
SELECT
S.LocalStartDateTime, E.LocalEndDateTime
FROM
cteConvertStartDate S
INNER JOIN cteConvertEndDate E ON S.RN = E.RN
);
これにより、クエリプロセッサは関数を「インライン」で実行できます。これにより、いくつかの最適化が可能になります。特に、関数で参照されるオブジェクトの統計を適切に理解する機能があります。
クラスター化インデックスをdbo.DSTLookup
テーブルに追加します。これにより、クエリはスキャンの代わりにルックアップを実行できます。サンプルデータの行数の場合、これは大きな違いにはなりませんが、実際のテーブルでは、非常に大きな違いが生じる可能性があります。
単調に増加する整数のように見えるId
列があるので、クラスター化された主キーとして使用するのに適した候補キーである可能性があります。
CREATE TABLE dbo.DSTLookup
(
[Id] int
CONSTRAINT PK_DSTLookup
PRIMARY KEY CLUSTERED,
[Tzid] int,
[DT_WhenSwitch] datetime,
[DSTOffSetSeconds] int,
[GMTOffSetSeconds] int
);
TVFに基づいて次のインデックスを追加することを検討します。
CREATE INDEX IX_DSTLookup_001
ON dbo.DSTLookup (DT_WhenSwitch, Tzid)
INCLUDE (DSTOffSetSeconds, GMTOffSetSeconds);
Tzid
とDT_WhenSwitch
が一意の行を定義する場合、dbo.DSTLookup
テーブルをこれらの2つの列でクラスター化することをお勧めします。必要に応じて、これらの列を主キーにすることも、それらをクラスター化インデックスにすることもできます。
CREATE TABLE dbo.DSTLookup
(
[Id] int,
[Tzid] int,
[DT_WhenSwitch] datetime,
[DSTOffSetSeconds] int,
[GMTOffSetSeconds] int
);
CREATE CLUSTERED INDEX CI_DSTLookup ON dbo.DSTLookup ([Tzid], [DT_WhenSwitch]); -- new
INSERT INTO dbo.DSTLookup
VALUES (29, 2, N'2014-03-30T01:00:00', 3600, 0),
(30, 2, N'2014-10-26T02:00:00', 0, 0),
(31, 2, N'2015-03-29T01:00:00', 3600, 0),
(32, 2, N'2015-10-25T02:00:00', 0, 0),
(33, 2, N'2016-03-27T01:00:00', 3600, 0),
(34, 2, N'2016-10-30T02:00:00', 0, 0),
(35, 2, N'2017-03-26T01:00:00', 3600, 0),
(36, 2, N'2017-10-29T02:00:00', 0, 0),
(37, 2, N'2018-03-25T01:00:00', 3600, 0),
(38, 2, N'2018-10-28T02:00:00', 0, 0);
これを行う理由は、非常に高速な個々の行の検索を可能にするためです。テーブルに対する両方のクエリに対して、[Tzid]
でフィルタリングし、降順で最初の[DT_WhenSwitch]
値を検索します。適切なクラスター化インデックスを使用してその行を取得すると、単一のクラスター化インデックスシークが可能になります。
その計画を実現するために、APPLY
およびTOP
演算子を使用してTVFを少し簡略化します。また、毎回1行しか返さないことをオプティマイザに明らかにしたいと思います。ここに1つの実装があります:
CREATE FUNCTION dbo.FN_GetLocalTime_FromUTC_BasedOnTZId
(@StartDateTime DATETIME, @EndDateTime DATETIME, @Tzid INT)
/*=========================================================================
* 2017-03-27
* Returns local time from UTC time based on timeZoneId
*
==========================================================================*/
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
SELECT
DATEADD(SECOND, (COALESCE(S.s_DST_OffSet, 0) + COALESCE(S.s_GMT_OffSet, 0)), @StartDateTime) 'LocalStartDateTime'
, DATEADD(SECOND, (COALESCE(E.e_DST_OffSet, 0) + COALESCE(E.e_GMT_OffSet, 0)), @EndDateTime) 'LocalEndDateTime'
FROM (SELECT 1 t) t
OUTER APPLY (
SELECT TOP 1
D.DSTOffSetSeconds 's_DST_OffSet',
D.GMTOffSetSeconds 's_GMT_OffSet'
FROM dbo.DSTLookup D
WHERE D.DT_WhenSwitch <= @StartDateTime AND D.Tzid = @Tzid
ORDER BY D.DT_WhenSwitch DESC
) s
OUTER APPLY (
SELECT TOP 1
D.DSTOffSetSeconds 'e_DST_OffSet',
D.GMTOffSetSeconds 'e_GMT_OffSet'
FROM dbo.DSTLookup D
WHERE D.DT_WhenSwitch <= @EndDateTime AND D.Tzid = @Tzid
ORDER BY D.DT_WhenSwitch DESC
) e
);
これが、質問の例のクエリの query plan です。
予想どおり、クラスター化インデックスに対して2つのシークのみを実行します。
(1行が影響を受けました)
テーブル「DSTLookup」。スキャンカウント2、論理読み取り4、物理読み取り0、先読み読み取り0、lob論理読み取り0、lob物理読み取り0、lob先読み読み取り0。
SQL Server実行時間:
CPU時間= 0 ms、経過時間= 1 ms。
SQL Server 2008に対してテストすることはできませんでしたが、構文はそのプラットフォームで機能すると思います。 db fiddle SQL Server 2014の場合。