web-dev-qa-db-ja.com

T-SQL夏時間参照テーブル-パフォーマンスの低いテーブル値関数

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の推奨に従います。

5
Mazhar

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);
3
Max Vernon

TzidDT_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 です。

enter image description here

予想どおり、クラスター化インデックスに対して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の場合。

4
Joe Obbish