web-dev-qa-db-ja.com

単純なビューのクエリに非常に時間がかかる

Windows Server 2012、Microsoft SQL Server。

クエリする必要のあるビューを作成するストアドプロシージャ(下記参照)があります。ストアドプロシージャパーツは適切に機能し、終了まで5秒ほどかかり、ビューが作成されます。

ビューには約30〜35k行があります。

私の問題は、作成されたビューに対して単純なクエリを実行すると、約20分かかることです。次のような簡単なクエリ:

SELECT COUNT(*) FROM MY_VIEW

上記のクエリは、行数を返すまで約20分かかります。ビューに含まれる実際のテーブルに対して同じクエリを実行すると、即座に結果が返されます。

ビューが即座に作成され、クエリを実行することで問題が発生するため、ストアドプロシージャが関連しているかどうかはわかりませんが、念のため投稿します。

同じストアドプロシージャによって作成された、少量の行(数百)を含む他のビューがクエリにかなり高速に応答していることを述べておきます。

30k行のテーブルをクエリすると2秒で結果が返され、30k行のビューに対して同じクエリを実行すると20分かかるのはなぜですか。

ストアドプロシージャ

USE [QUARTERLY_SEC_REPORT]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER PROCEDURE [dbo].[DynamicView_QR_VisitsDistSummary] 
AS
BEGIN
DECLARE @CurrentView nvarchar(MAX) = null
DECLARE @SchemaName nvarchar(400)
DECLARE @TableName nvarchar(400)
DECLARE @DynSQL nvarchar(MAX)
DECLARE @DateModifier nvarchar(400)
DECLARE @DynDROP nvarchar(MAX) = 'DROP VIEW Unified_QR_VisitsDistSummary'
DECLARE @InclusionTable nvarchar(MAX) = '[dbo].[QUARTERLY_VIEW]'

Set @DynSQL = 'CREATE VIEW Unified_QR_VisitsDistSummary AS '

set @CurrentView = (select VIEW_DEFINITION from INFORMATION_SCHEMA.VIEWS WHERE TABLE_SCHEMA='dbo' and TABLE_NAME='Unified_QR_VisitsDistSummary')

DECLARE cursor1 CURSOR FOR 
    select TABLE_SCHEMA,TABLE_NAME
    from INFORMATION_SCHEMA.TABLES
    where 
    TABLE_SCHEMA='dbo' AND
    TABLE_NAME like 'visits_dist_summary_ACC_%'

OPEN cursor1

FETCH NEXT FROM cursor1 INTO @SchemaName, @TableName

WHILE @@FETCH_STATUS = 0
BEGIN
    -- Add the select code.
    Set @DateModifier = '( SELECT MAX([retrieved_at]) FROM '+ @SchemaName +'.' + @TableName + ')'
    Set @DynSQL = @DynSQL + 'Select * from ' + @SchemaName +'.' + @TableName +' INNER JOIN '+ @InclusionTable+ ' ON '+ @InclusionTable +'.AccountID  = ' + @SchemaName +'.' + @TableName+ '.Account_ID WHERE ' + @InclusionTable +'.Appear_In_View =''True'' AND (retrieved_at =' + @DateModifier +' OR retrieved_at = DATEADD (MINUTE, -1, '+@DateModifier+ ')'+' OR retrieved_at = DATEADD (MINUTE, -2, '+@DateModifier+ ')' +' OR retrieved_at = DATEADD (MINUTE, -3, '+@DateModifier+ '))'
    FETCH NEXT FROM cursor1
    INTO @SchemaName, @TableName

    -- If the loop continues, add the UNION ALL statement.
    If @@FETCH_STATUS = 0
    BEGIN
        Set @DynSQL = @DynSQL + ' UNION ALL '
    END

END
IF @CurrentView = @DynSQL 
    PRINT 'VIEW IS THE SAME, NEW VIEW WASN''T CREATED'
ELSE
    BEGIN
        if @CurrentView is not null
        BEGIN
        print @DynDROP
            exec sp_executesql @DynDROP
        END
        PRINT @DynSQL
        exec sp_executesql @DynSQL
    END
END

定義を表示

SELECT        *
FROM            dbo.visits_dist_summary_ACC_12345 INNER JOIN
                         [dbo].[QUARTERLY_VIEW] ON [dbo].[QUARTERLY_VIEW].AccountID = dbo.visits_dist_summary_ACC_12345.Account_ID
WHERE        [dbo].[QUARTERLY_VIEW].Appear_In_View = 'True' AND (retrieved_at =
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_12345) OR
                         retrieved_at = DATEADD(MINUTE, - 1,
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_12345)) OR
                         retrieved_at = DATEADD(MINUTE, - 2,
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_12345)) OR
                         retrieved_at = DATEADD(MINUTE, - 3,
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_12345)))
UNION ALL
SELECT        *
FROM            dbo.visits_dist_summary_ACC_22222 INNER JOIN
                         [dbo].[QUARTERLY_VIEW] ON [dbo].[QUARTERLY_VIEW].AccountID = dbo.visits_dist_summary_ACC_22222.Account_ID
WHERE        [dbo].[QUARTERLY_VIEW].Appear_In_View = 'True' AND (retrieved_at =
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_22222) OR
                         retrieved_at = DATEADD(MINUTE, - 1,
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_22222)) OR
                         retrieved_at = DATEADD(MINUTE, - 2,
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_22222)) OR
                         retrieved_at = DATEADD(MINUTE, - 3,
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_22222)))
UNION ALL
SELECT        *
FROM            dbo.visits_dist_summary_ACC_77777 INNER JOIN
                         [dbo].[QUARTERLY_VIEW] ON [dbo].[QUARTERLY_VIEW].AccountID = dbo.visits_dist_summary_ACC_77777.Account_ID
WHERE        [dbo].[QUARTERLY_VIEW].Appear_In_View = 'True' AND (retrieved_at =
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_77777) OR
                         retrieved_at = DATEADD(MINUTE, - 1,
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_77777)) OR
                         retrieved_at = DATEADD(MINUTE, - 2,
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_77777)) OR
                         retrieved_at = DATEADD(MINUTE, - 3,
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_77777)))

テーブル構造(sp_helpの出力)

(ビューは、複数のアカウントに対して同じテーブルを「集約」します)

Name
visits_dist_summary_ACC_12345

account_id,varchar,no,12,     ,     ,yes,no,yes,SQL_Latin1_General_CP1_CI_AS
siteid,varchar,no,12,     ,     ,yes,no,yes,SQL_Latin1_General_CP1_CI_AS
countryCode,varchar,no,50,     ,     ,yes,no,yes,SQL_Latin1_General_CP1_CI_AS
countryCount,float,no,8,53   ,NULL,yes,(n/a),(n/a),NULL
agentCode,varchar,no,100,     ,     ,yes,no,yes,SQL_Latin1_General_CP1_CI_AS
agentCount,float,no,8,53   ,NULL,yes,(n/a),(n/a),NULL
retrieved_at,varchar,no,100,     ,     ,yes,no,yes,SQL_Latin1_General_CP1_CI_AS
relevant_month,varchar,no,100,     ,     ,yes,no,yes,SQL_Latin1_General_CP1_CI_AS
domain_name,varchar,no,100,     ,     ,yes,no,yes,SQL_Latin1_General_CP1_CI_AS

Identity    Seed    Increment   Not For Replication
No identity column defined. NULL    NULL    NULL

RowGuidCol
No rowguidcol column defined.

Data_located_on_filegroup
PRIMARY

これが 実行計画 です。

1
workstufz

ビューには約30〜35k行があります。

ビュー(クラスター化インデックスなし)は、単に格納されたクエリ定義です。行は直接含まれていません。

私の問題は、作成されたビューに対して単純なクエリを実行すると約20分かかることです

これには、保存されたクエリ定義を実行する必要があります。ベーステーブル(およびビュークエリ)には、いくつかのデータ型の問題と有用なインデックスの不足があり、ビューにアクセスするたびに異常な量の作業が実行されます(以下で説明します)。

データ型と正確性の問題

列_retrieved_at_は現在varchar(100)として型指定されています。代わりに適切な日付/時刻型を使用する必要があります。パフォーマンスの考慮事項は別として、現在、ほぼ確実に正しくない結果が得られています。

MAX(retrieved_at)は、datetime条件の最新の値ではなく、最高にソートされるstringを検索します。 DATEADDを含む比較は、最終的にdatetimeに変換されますが、MAXが(文字列として)見つかった後でのみです。

理想的には、正しいデータ型が使用されるようにベーステーブルを変換します。

_ALTER TABLE dbo.visits_dist_summary_ACC_12345
ALTER COLUMN retrieved_at datetime NOT NULL;
_

全体的な戦略

質問からは明らかではありませんが、時々データのスナップショットを保存しようとしている可能性があります。その場合は、ビューよりも動的クエリの結果を永続テーブルに書き込む方がはるかに効率的です。

現在の実行計画

あなたが提供した計画はいくつかの問題を強調しています。すでに言及されている点を考慮すると、これの一部は無関係である可能性があるため、これは関心のために提供されています。

Plan

アカウントIDのハッシュ結合は36,222行を生成しますが、1行しか期待されていませんでした。これは、その結合に関係するテーブルの一方または両方の統計が古くなっていることを示しています。

統計を更新すると、その見積もりが改善される可能性がありますが、さらに進める必要がある場合もあります。 _[QUARTERLY_VIEW]_述語を使用して、_Appear_In_View = 'true'_テーブルにフィルター選択されたインデックス(または統計)を作成します。補足として、その列がtrue/falseの場合、varcharよりも優れたデータ型の選択はbitになります。

残りの計画は、ネストされたループの左準結合によって駆動されます。ハッシュ結合からの36,222行のそれぞれについて、SQL Serverは次のことを行います。

  • ベーステーブルからすべての行を読み取ります(テーブルスキャン)
  • MAX() retrieved_at`(Stream Aggregate)を検索します
  • 結果が外側の_retrieved_at_値と一致するかどうかをテストして確認します。

このプロセス(フルスキャン、集計、フィルター)は、最初のハッシュ結合によって生成された36,222行のすべての行に対して発生することに注意してください。

さらに悪いことに、最初のscan-aggregate-filterブランチで一致が見つからない場合(準結合を満たす)、SQL Serverは、-1、-2、および-3分の場合に、同じプロセスを完全に再度実行し続けます。

上記の実行計画(SQL Sentry Plan Explorerを使用)に示されている数値は、ネストされたループ結合のすべての反復で各演算子によって生成された行の総数を示しています。 SSMSでは、各演算子の実際の行数プロパティを確認する必要があります。

一番上のscan-aggregate-filterブランチの場合、この合計は1,312,033,284行です。 2番目のブランチは、追加の436,185,324行を提供します。一致する行を見つけるために-2分と-3分のケースが必要になった場合は、さらに悪くなります。うまくいけば、「単純なクエリ」が20分間実行される理由を確認できます。

行動

  • データ型を修正する
  • 統計を更新する
  • 必要に応じて、テーブルを使用して静的スナップショットを保存します
  • _retrieved_at_にインデックスを作成します。

    _CREATE NONCLUSTERED INDEX i2 
    ON dbo.visits_dist_summary_ACC_12345 (retrieved_at);
    _
  • _account_id_でクラスター化インデックスを評価します。

    _CREATE CLUSTERED INDEX i1 
    ON dbo.visits_dist_summary_ACC_12345 (account_id);
    _

上記の手順により、特に_retrieved_at_(日時として入力)のインデックスが大幅に改善されます。

オプティマイザの制限/優先度のため、MAXの計算を4回回避するためにクエリの書き換えが必要になる場合がありますが、インデックスはその操作を簡単にする必要があります(インデックスの最後から1行を読み取る)。実際に必要です。


便利な場合のクエリ書き換えアプローチの1つは次のとおりです。

_SELECT
    QV.AccountID,
    QV.Appear_In_View,
    QV.Report_Name,
    VDSA.account_id,
    VDSA.siteid,
    VDSA.countryCode,
    VDSA.countryCount,
    VDSA.agentCode,
    VDSA.agentCount,
    VDSA.retrieved_at,
    VDSA.relevant_month,
    VDSA.domain_name
FROM dbo.QUARTERLY_VIEW AS QV
JOIN  dbo.visits_dist_summary_ACC_12345 AS VDSA
    ON VDSA.account_id = QV.AccountID
WHERE
    QV.Appear_In_View = 'True' 
    AND VDSA.retrieved_at IN
    (
        SELECT 
            V.max_date_candidates
        FROM 
        (
            -- Compute maximum date once
            SELECT TOP (1)
                VDSA2.retrieved_at
            FROM dbo.visits_dist_summary_ACC_12345 AS VDSA2
            ORDER BY
                VDSA2.retrieved_at DESC
        ) AS Q (max_retrieved_at)
        CROSS APPLY
        (
            -- Generate four rows based on the maximum date
            VALUES 
                (DATEADD(MINUTE, -0, Q.max_retrieved_at)),
                (DATEADD(MINUTE, -1, Q.max_retrieved_at)),
                (DATEADD(MINUTE, -2, Q.max_retrieved_at)),
                (DATEADD(MINUTE, -3, Q.max_retrieved_at))
        ) AS V (max_date_candidates)
    );
_

上記のデータ型とインデックスの変更を伴うこのクエリの推定プラン:

Improved plan

3
Paul White 9