web-dev-qa-db-ja.com

1100万行のテーブルで集計クエリを高速化する

高速化したいクエリがあります。

SELECT 
  sum(case when FlagDTD = 1 then Success else 0 end)   as SuccessDTD
, sum(case when FlagDTD = 1 then [Error] else 0 end)   as ErrorDTD
, round(sum(case when FlagDTD = 1 then Success else 0 end) * 100.0 / sum(FlagDTD),2) 
    as RateDTD
, sum(case when FlagYTD = 1 then Success else 0 end)   as SuccessYTD
, sum(case when FlagYTD = 1 then [Error] else 0 end)   as ErrorYTD
, round(sum(case when FlagYTD = 1 then Success else 0 end) * 100.0 / sum(FlagYTD),2)  
    as RateYTD
FROM
(
    SELECT 
      CASE WHEN Message = 'OK'  then 1 else 0 end as Success
    , CASE WHEN Message <> 'OK'  then 1 else 0 end as [Error]    
    , CASE WHEN DateCreated > 
      dateadd(HOUR, datediff(hh,GetUTCDate(), GetDate())*-1,  DATEADD(yy,
        DATEDIFF(yy,0,getdate()), 0)) then 1 else 0 end as FlagYTD
    , CASE WHEN DateCreated > 
      dateadd(HOUR, datediff(hh,GetUTCDate(), GetDate())*-1 , 
        convert(varchar(10), getdate(), 101)) then 1 else 0 end as FlagDTD
    FROM
      [Channels4].[dbo].[NotificationResult]
) Cnts

サブクエリに基づいてビューまたはインデックス付きビューを作成できると思いました。ただし、テストでは「ビューは文字列からdatetimeまたはsmalldatetimeへの暗黙的な変換を使用する」ため、インデックス付きビューを作成できません。

従来のビューを使用してみましたが、パフォーマンスはまったく向上しませんでした。私の次の考えは、おそらくクエリ全体を書き直すことでしょう。みんなの考えは?

予定:

https://www.brentozar.com/pastetheplan/?id=rJqGY7iZW

テーブル構造:

CREATE TABLE [dbo].[NotificationResult]
(
    [IdNotificationResult] [bigint] IDENTITY(1,1) NOT NULL,
    [ApplicationGuid] [nvarchar](48) NOT NULL,
    [MessageGuid] [nvarchar](48) NOT NULL,
    [IdNotificationResultTypeStatus] [int] NOT NULL,
    [MessageStatusCode] [int] NULL,
    [Message] [varchar](max) NULL,
    [ExceptionStatusCode] [int] NULL,
    [ExceptionMessage] [varchar](max) NULL,
    [Subject] [varchar](max) NULL,
    [From] [varchar](max) NULL,
    [Timestamp] [datetime] NOT NULL,
    [IdCreatedBy] [bigint] NOT NULL,
    [IdLastUpdatedBy] [bigint] NOT NULL,
    [DateCreated] [datetime] NOT NULL,
    [DateLastUpdated] [datetime] NOT NULL,
  CONSTRAINT [PK_NotificationResult] PRIMARY KEY CLUSTERED 
  (
    [IdNotificationResult] 
  )
);

CREATE NONCLUSTERED INDEX [IX_NotificationResult_DateMessage] 
  ON [dbo].[NotificationResult] ( [DateCreated] ASC ) INCLUDE ( [Message]);

YTDとDTDのクイックカウントを行うと、次の両方の数値が表示されます:11739267。「OK」である行の数:11782564。

5
homerj742

書かれているように、あなたは技術的にここで質問をしていません。クエリのパフォーマンスを向上させたいと思いますが、許容できる応答時間を定義することが、パフォーマンスチューニングの重要な部分になる場合があることを覚えておいてください。クエリが1日に1回実行され、終了するまでに1分かかる場合、1秒で実行するのに8時間の時間を費やす価値がありますか?

パフォーマンスよりも重要なのは正確さです。クエリが間違った結果を返す場合、クエリにかかる時間はそれほど重要ではありませんが、間違った結果を返すのに長い時間がかかることは、間違った結果を返すのに短い時間をかけることよりも悪いことです。タイムゾーンによっては、UTC変換が期待どおりに機能しない場合があります。夏時間の影響を受けるデータがある場合、現地時間とUTC時間の現在の時差を使用して古いデータを変換することはできません。

これらすべてを脇に置いて、質問のクエリを高速化するいくつかの方法を紹介します。特に無関係なblobデータの読み取りを回避するため、適切な開始であるカバリングインデックスがあります。ただし、クエリを高速化する方法はまだあります。他の人を助け、将来的にデータが変更された場合に役立つ可能性のあるより一般的な答えにしたいので、データの配布についてあなたが与えた手がかりを意図的に無視しています。

1000万行をモックアップしました。そのうちの半分はメッセージに「OK」があり、残りの半分は長い文字列になっています。日付は数年に渡って広がっています。 警告:このコードは約60 GBのスペースを占有し、私のマシンでは約10分で実行されました。

_CREATE TABLE [dbo].[NotificationResult]
(
    [IdNotificationResult] [bigint] IDENTITY(1,1) NOT NULL,
    [Message] [varchar](max) NULL,
    [DateCreated] [datetime] NOT NULL,
    [DateCreatedUTC] [date] NOT NULL,
    [Filler] VARCHAR(1000) NOT NULL,
CONSTRAINT [PK_NotificationResult] PRIMARY KEY CLUSTERED 
  (
    [IdNotificationResult] 
  )
);

INSERT INTO [dbo].[NotificationResult] WITH (TABLOCK) ([Message], [DateCreated], [DateCreatedUTC], [Filler])
SELECT CASE WHEN RN % 2 = 1 THEN 'OK' ELSE REPLICATE('Z', 3000) END
, DATEADD(SECOND, 11 * RN, '20140101')
, CAST(DATEADD(SECOND, 11 * RN, '20140101') AS DATE)
, REPLICATE('FILLER', 166)
FROM
(
    SELECT TOP (10000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
    CROSS JOIN master..spt_values t3
) t;

CREATE NONCLUSTERED INDEX [IX_NotificationResult_DateMessage] 
  ON [dbo].[NotificationResult] ( [DateCreated] ASC ) INCLUDE ( [Message]);
_

質問のクエリを実行すると、同じクエリプランが表示されます。 38秒かかりました。実行のパフォーマンス統計は次のとおりです。

テーブル 'NotificationResult'。スキャンカウント5、論理読み取り2509562、物理読み取り0、先読み読み取り2499799

SQL Server実行時間:CPU時間= 20626ミリ秒、経過時間= 37663ミリ秒。

パフォーマンスを向上させる最初の機会は、WHEREステートメントに暗黙のCASE句述語があることです。クエリオプティマイザーは、前年度の行が合計に寄与しないことを認識するほどスマートではありません。 24時間は常に現地時間とUTC時間の差よりも長いため、次のようなフィルターを追加しても結果は変わりません。

_WHERE DateCreated > DATEADD(DAY, -1, dateadd(YEAR, datediff(YEAR, 0, getdate()), 0))
_

SQL Serverは、インデックスから1,000万行を読み取って集計する代わりに、140万行を処理するだけで済みます。この最適化によって得られる節約は、データが計画でどのように分散されるかによって異なります。すべてのデータが現在の年にある場合、パフォーマンスはまだ改善されません。私のデータの場合、クエリは5秒で終了し、パフォーマンスが大幅に向上します。

テーブル 'NotificationResult'。スキャンカウント5、論理読み取り352033、物理読み取り1、先読み読み取り350073

SQL Server実行時間:CPU時間= 3062ミリ秒、経過時間= 5354ミリ秒。

first plan

私たちはそれよりもうまくやることができます。列の値が "OK"と一致するかどうかだけを知る必要がある場合、VARCHAR(MAX)列をインデックスに格納します。テーブルの定義を変更せずに、3つのフィルター処理されたインデックスを作成することで、シークまたはスキャンする小さなインデックスを作成できます。

_CREATE NONCLUSTERED INDEX [IX_NotificationResult_Date_OK] 
  ON [dbo].[NotificationResult] ( [DateCreated] ASC )
  WHERE [Message] = 'OK';

CREATE NONCLUSTERED INDEX [IX_NotificationResult_Date_NOT_OK] 
  ON [dbo].[NotificationResult] ( [DateCreated] ASC )
  WHERE [Message] <> 'OK';

CREATE NONCLUSTERED INDEX [IX_NotificationResult_Date_NULL] 
  ON [dbo].[NotificationResult] ( [DateCreated] ASC )
  WHERE [Message] IS NULL;
_

ここでの考え方は、これらのインデックスには必要なデータが含まれているが、ディスク上では既存の_IX_NotificationResult_DateMessage_インデックスよりもはるかに小さいということです。クエリオプティマイザーでフィルター処理されたインデックスを使用するには、クエリの書き換えとインデックスヒントが必要です(理由は不明)。クエリを書き換える1つの方法を次に示します。

_SELECT 
  sum(case when FlagDTD = 1 then Success else 0 end)   as SuccessDTD
, sum(case when FlagDTD = 1 then [Error] else 0 end)   as ErrorDTD
, round(sum(case when FlagDTD = 1 then Success else 0 end) * 100.0 / sum(FlagDTD),2) 
    as RateDTD
, sum(case when FlagYTD = 1 then Success else 0 end)   as SuccessYTD
, sum(case when FlagYTD = 1 then [Error] else 0 end)   as ErrorYTD
, round(sum(case when FlagYTD = 1 then Success else 0 end) * 100.0 / sum(FlagYTD),2)  
    as RateYTD
FROM
(
    SELECT 
      Success
    , [Error]    
    , CASE WHEN DateCreated > 
      dateadd(HOUR, datediff(hh,GetUTCDate(), GetDate())*-1,  DATEADD(yy,
        DATEDIFF(yy,0,getdate()), 0)) then 1 else 0 end as FlagYTD
    , CASE WHEN DateCreated > 
      dateadd(HOUR, datediff(hh,GetUTCDate(), GetDate())*-1 , 
        convert(varchar(10), getdate(), 101)) then 1 else 0 end as FlagDTD
FROM
    (
    SELECT 1 Success, 0 Error, DateCreated 
    FROM
    [dbo].[NotificationResult] WITH (INDEX (IX_NotificationResult_Date_OK))
    WHERE DateCreated > DATEADD(DAY, -1, dateadd(YEAR, datediff(YEAR, 0, getdate()), 0))
    AND [Message] = 'OK'

    UNION ALL

    SELECT 0 Success, 1 Error, DateCreated 
    FROM
    [dbo].[NotificationResult] WITH (INDEX (IX_NotificationResult_Date_NOT_OK))
    WHERE DateCreated > DATEADD(DAY, -1, dateadd(YEAR, datediff(YEAR, 0, getdate()), 0))
    AND [Message] <> 'OK'

    UNION ALL

    SELECT 0 Success, 1 Error, DateCreated 
    FROM
    [dbo].[NotificationResult] WITH (INDEX (IX_NotificationResult_Date_NULL))
    WHERE DateCreated > DATEADD(DAY, -1, dateadd(YEAR, datediff(YEAR, 0, getdate()), 0))
    AND [Message] IS NULL
    ) t
) Cnts;
_

これで、クエリは1秒未満で終了します。

テーブル 'NotificationResult'。スキャンカウント10、論理読み取り3874、物理読み取り0、先読み読み取り0

SQL Server実行時間:CPU時間= 2499ミリ秒、経過時間= 890ミリ秒。

(以下のプランにはインデックスの1つがありません)

second plan

以前よりも多くの行をインデックスから読み取ることは事実ですが、インデックスは合計で元のインデックスよりも約100倍小さくなっています。

それでもクエリが十分高速でない場合は、インデックス付きビューを検討できます。テーブルにUTC日付列がある場合、インデックスを作成するのに適したビューを作成するのは簡単です。

_CREATE VIEW [NotificationResult_indexed]
WITH SCHEMABINDING
AS
SELECT
 [DateCreatedUTC]
, COUNT_BIG(*) AS CNT_BIG
, SUM(CASE WHEN Message = 'OK'  then 1 else 0 end) as Success
, SUM(CASE WHEN Message IS NULL OR Message <> 'OK'  then 1 else 0 end) as [Error]   
FROM dbo.[NotificationResult]
GROUP BY [DateCreatedUTC];

CREATE UNIQUE CLUSTERED INDEX CLU_NotificationResult_indexed   
    ON [NotificationResult_indexed] ([DateCreatedUTC]);  
GO  
_

私はおそらくいくつかの詳細を間違っていましたが、このクエリは大まかにあなたの意図を捉えていると思います:

_SELECT 
  sum(case when FlagDTD = 1 then Success else 0 end)   as SuccessDTD
, sum(case when FlagDTD = 1 then [Error] else 0 end)   as ErrorDTD
, round(sum(case when FlagDTD = 1 then Success else 0 end) * 100.0 / sum(FlagDTD),2) 
    as RateDTD
, sum(case when FlagYTD = 1 then Success else 0 end)   as SuccessYTD
, sum(case when FlagYTD = 1 then [Error] else 0 end)   as ErrorYTD
, round(sum(case when FlagYTD = 1 then Success else 0 end) * 100.0 / sum(FlagYTD),2)  
    as RateYTD
FROM
(
    SELECT 
      Success
    , [Error]    
    , CASE WHEN [DateCreatedUTC] > dateadd(YEAR, datediff(YEAR, 0, getdate()), 0)
       then 1 else 0 end as FlagYTD
    , CASE WHEN [DateCreatedUTC] > CAST(GETDATE() AS DATE)
       then 1 else 0 end as FlagDTD
    FROM
      [dbo].[NotificationResult_indexed]
      WHERE [DateCreatedUTC] > dateadd(YEAR, datediff(YEAR, 0, getdate()), 0)
) Cnts;
_

66ミリ秒で終了します。

テーブル 'NotificationResult_indexed'。スキャンカウント1、論理読み取り7、物理読み取り0、先読み読み取り0

SQL Server実行時間:CPU時間= 0ミリ秒、経過時間= 66ミリ秒。

third plan

7
Joe Obbish

Joe Obbishを読むのはいつも喜びです。

メッセージ列については、1 =「OK」、0 =できれば格納できます。

それ以外の場合は、Joeが提案するように、フィルターされたインデックスを作成する必要があります。

日付ロジックを変数に格納できます。これにより、スクリプトが短く、読みやすくなり、パフォーマンスが向上する場合があります。

「昨日」以降のすべてのレコードを選択できます。これには、昨日と今日のすべてのレコードが含まれます。欲望の出力には、これだけのレコードが必要だと思います。

私はちょうどあなたのクエリを書き直してみて、アプローチを理解し、マイナーな間違いを修正してみました。

    DECLARE @TodayDate DATETIME = dateadd(HOUR, datediff(hh, GetUTCDate(), GetDate()) * - 1
                                , DATEADD(yy, DATEDIFF(yy, 0, getdate()), 0))

DECLARE @YesterdayDate DATETIME = dateadd(HOUR, datediff(hh, GetUTCDate(), GetDate()) * - 1
                                , convert(VARCHAR(10), getdate(), 101))

SELECT sum(CASE 
            WHEN DateCreated > @YesterdayDate
                AND Message = 'OK'
                THEN 1
            ELSE 0
            END) AS SuccessDTD
    ,sum(CASE 
            WHEN DateCreated > @YesterdayDate
                AND Message <> 'OK'
                THEN 1
            ELSE 0
            END) AS ErrorDTD
    ,round(sum(CASE 
                WHEN DateCreated > @YesterdayDate
                    AND Message = 'OK'
                    THEN 1
                ELSE 0
                END) * 100.0 / sum((
                CASE 
                    WHEN DateCreated > @YesterdayDate
                        THEN 1
                    ELSE 0
                    END
                )), 2) AS RateDTD
    ,sum(CASE 
            WHEN DateCreated > @TodayDate
                AND Message = 'OK'
                THEN 1
            ELSE 0
            END) AS SuccessYTD
    ,sum(CASE 
            WHEN DateCreated > @TodayDate
                AND Message <> 'OK'
                THEN 1
            ELSE 0
            END) AS ErrorYTD
    ,round(sum(CASE 
                WHEN DateCreated > @TodayDate
                    AND Message = 'OK'
                    THEN 1
                ELSE 0
                END) * 100.0 / sum((
                CASE 
                    WHEN DateCreated > @TodayDate
                        THEN 1
                    ELSE 0
                    END
                )), 2) AS RateYTD
FROM [Channels4].[dbo].[NotificationResult] WITH (NOLOCK)
where DateCreated>=@YesterdayDate
1
KumarHarsh