web-dev-qa-db-ja.com

遅いWhileループ、クエリ改善支援

データウェアハウスの作成に取り組んでいます。時間ディメンション(Dim_Time)を5分間隔で作成しました。時間集計は[分] = NULLになります。この例の目的のために:

CREATE TABLE [dbo].[Dim_Time](
    [TimeID] [int] IDENTITY(1,1) NOT NULL,
    [StartDateTime] [datetime] NULL,
    [Hour] [int] NULL,
    [Minute] [int] NULL,
  CONSTRAINT [PK_Dim_Time] PRIMARY KEY CLUSTERED 
  ([TimeID] ASC)
  ) ON [PRIMARY]
GO

次に、OLTPデータベースから5分ごとに更新される受信テーブルがあります。

CREATE TABLE [dbo].[Stg_IncomingQueue](
    [IncomingID] [int] IDENTITY(1,1) NOT NULL,
    [CustomerID] [int] NOT NULL,
    [TimeID] [int] NULL,
    [InsertTime] [datetime] NULL,
 CONSTRAINT [PK_IncomingQueueMonitor] PRIMARY KEY CLUSTERED 
([IncomingID] ASC)
) ON [PRIMARY]
GO

次に、次のWhileループがあります。その目的は、特定の着信行に関連する正しい5分のタイムスロット(TimeID)を取得することです。

WHILE 0 < (SELECT COUNT(*) FROM [dba_local].[dbo].[Stg_IncomingQueue] WHERE TimeID IS NULL)
BEGIN

    SELECT TOP 1 @IncomingID = IncomingID, @RowInserTime = InsertTime 
    FROM [dba_local].[dbo].[Stg_IncomingQueue] WHERE TimeID IS NULL


    ;WITH DimTime
    AS (
        SELECT MAX(TimeID) AS MaxTimeID FROM [dba_local].[dbo].[Dim_Time]
        WHERE StartDateTime < @RowInserTime AND [Minute] IS NOT NULL
    )
    UPDATE [dba_local].[dbo].[Stg_IncomingQueue]
    SET TimeID = (SELECT MaxTimeID FROM DimTime)
    WHERE IncomingID = @IncomingID

END

これはとても簡単なプロセスですが、TimeIDを更新する簡単な方法はわかりません。ループのCTE SELECTに従って、StartDateTimeが行InsertTimeより小さいMAX(TimeID)を取得する必要があります。時間は唯一の関係なので、ループなしでこれを1つのクエリで実行するためのすべてのオプションに苦労していますが、それは可能だと感じています

より良いオプションを使用するか、これが最も簡単な方法であることを確認してください。

お時間とご協力ありがとうございました。ウェイド

4
WadeH

元の質問の2つの表に基づいて、次の 最低限完全で検証可能な例 を作成しました。 [〜#〜] lead [〜#〜] T-SQLステートメントを使用して、dbo.Dim_Timeテーブルから時間範囲を取得します。これは、入力行と非常に簡単に比較できます。

_IF OBJECT_ID(N'dbo.Stg_IncomingQueue', N'U') IS NOT NULL
DROP TABLE dbo.Stg_IncomingQueue;

IF OBJECT_ID(N'dbo.Dim_Time', N'U') IS NOT NULL
DROP TABLE dbo.Dim_time;

CREATE TABLE dbo.Dim_Time(
    TimeID int IDENTITY(1,1) NOT NULL,
    StartDateTime time(0) NULL,
  CONSTRAINT PK_Dim_Time PRIMARY KEY CLUSTERED 
  (TimeID ASC)
  ) ON [PRIMARY]
GO

;WITH src AS
(
    SELECT TOP (10) sv.number
    FROM master.dbo.spt_values sv
    WHERE sv.type = N'P'
    ORDER BY sv.number
)
INSERT INTO dbo.Dim_Time (StartDateTime)
SELECT TOP(289) CONVERT(time(0), DATEADD(minute, (s3.number * 100 + s2.number * 10 + s1.number) * 5, CONVERT(time(0), '00:00:00')))
FROM src s1
    CROSS JOIN src s2
    CROSS JOIN src s3
ORDER BY s3.number * 100 + s2.number * 10 + s1.number

CREATE TABLE dbo.Stg_IncomingQueue(
    IncomingID int IDENTITY(1,1) NOT NULL,
    CustomerID int NOT NULL,
    TimeID int NULL,
    InsertTime datetime NULL,
 CONSTRAINT PK_IncomingQueueMonitor PRIMARY KEY CLUSTERED 
(IncomingID ASC)
) ON [PRIMARY]
GO

INSERT INTO dbo.Stg_IncomingQueue (CustomerID, InsertTime)
VALUES (1, DATEADD(SECOND, CONVERT(int, CRYPT_GEN_RANDOM(4), 0), '1901-01-01 00:00:00'))
    , (2, DATEADD(SECOND, CONVERT(int, CRYPT_GEN_RANDOM(4), 0), '1901-01-01 00:00:00'))
    , (3, DATEADD(SECOND, CONVERT(int, CRYPT_GEN_RANDOM(4), 0), '1901-01-01 00:00:00'))
    , (4, DATEADD(SECOND, CONVERT(int, CRYPT_GEN_RANDOM(4), 0), '1901-01-01 00:00:00'));
_

この部分は、WHILEループ全体を単一のUPDATEステートメントに置き換えます。これは、より効率的で理解しやすいものです。

_UPDATE dbo.Stg_IncomingQueue
SET TimeID = t.TimeID
FROM dbo.Stg_IncomingQueue iq
    INNER JOIN (
        SELECT dt.TimeID
            , dt.StartDateTime
            , EndDateTime = LEAD(dt.StartDateTime, 1) OVER (ORDER BY dt.StartDateTime)
        FROM dbo.Dim_Time dt 
        ) t ON CONVERT(time(0), iq.InsertTime) >= t.StartDateTime AND CONVERT(time(0), iq.InsertTime) < t.EndDateTime;
_

結果を、Dim_Timeテーブルと並べて比較します。

_SELECT *
FROM dbo.Stg_IncomingQueue iq
    INNER JOIN dbo.Dim_Time dt ON iq.TimeID = dt.TimeID;
_

出力は次のようになります。

╔════════════╦════════════╦════════╦══════════════ ═══════════╦════════╦═══════════════╗
║IncomingID║CustomerID║TimeID║InsertTime ║TimeID║StartDateTime║
╠════════════╬════════════╬════════╬════ ═════════════════════╬════════╬═══════════════╣
║1║1║271║1875-06-30 22:31:49.000║271║22:30:00║
║2║2║116║1857-07-01 09:38:59.000 ║116║09:35:00║
║3║3║218║1854-09-18 18:08:39.000║218║18:05:00║
║4║4║ 221║1860-05-31 18:22:25.000║221║18:20:00║
╚════════════╩══════════ ══╩════════╩═════════════════════════╩════════╩═══ ════════════╝

大量の入力行がない場合、これはかなりうまくいくかもしれません。注意してください、私はCONVERT()を使用して、着信datetime列をtime(0)値に変換しています。これは、クエリオプティマイザーが使用できないという犠牲を伴います優れた計画の作成に役立つ利用可能な統計。 insertステートメントの「実際の」クエリプランは次の警告を表示します。

式での型変換(CONVERT(time(0)、[iq]。[InsertTime]、0)> = [dt]。[StartDateTime])は、クエリプランの選択での "SeekPlan"に影響する可能性があります。式での型変換(CONVERT(time (0)、[iq]。[InsertTime]、0)<[Expr1002])は、クエリプランの選択で「SeekPlan」に影響を与える可能性があります。

更新中に型変換を回避する必要がある場合は、次のように、永続化された計算列を含めるように_dbo.Stg_IncomingQueue_の定義を更新することで、ワークロードを挿入操作に移動できます。

_CREATE TABLE dbo.Stg_IncomingQueue(
    IncomingID int IDENTITY(1,1) NOT NULL,
    CustomerID int NOT NULL,
    TimeID int NULL,
    InsertTime datetime NULL,
    InsertTime0 AS CONVERT(TIME(0), InsertTime) PERSISTED
 CONSTRAINT PK_IncomingQueueMonitor PRIMARY KEY CLUSTERED 
(IncomingID ASC)
) ON [PRIMARY]
GO
_

次に、更新ステートメントは次のようになります。

_UPDATE dbo.Stg_IncomingQueue
SET TimeID = t.TimeID
FROM dbo.Stg_IncomingQueue iq
    INNER JOIN (
        SELECT dt.TimeID
            , dt.StartDateTime
            , EndDateTime = LEAD(dt.StartDateTime, 1) OVER (ORDER BY dt.StartDateTime)
        FROM dbo.Dim_Time dt 
        ) t ON iq.InsertTime0 >= t.StartDateTime AND iq.InsertTime0 < t.EndDateTime;
_
7
Max Vernon