web-dev-qa-db-ja.com

マスター/詳細(受信トレイのようなメール)SQLクエリのパフォーマンスチューニングを支援する

私は過去数日間、ビデオの検索、視聴に費やしてきましたが、私は自分の道をたどるだけの範囲に到達したと思います。以下の私の例から、私はより具体的な方向性を探しています。

使用しているテーブルが2つあります。 MessageThreads(400kレコード)&Messages(1Mレコード)。それらのスキーマを以下に示します。

messages tableenter image description here

MessageThreadsインデックス

https://Gist.github.com/timgabrhel/0a9ff88160ebc9e40559e1e10ecc7ee4

メッセージインデックス

https://Gist.github.com/timgabrhel/d649074cbe82016e8a90f918c58c4764

主な「受信トレイ」クエリのパフォーマンスを改善しようとしています。メールプロバイダーの受信トレイについて考えてみましょう。スレッドのリストが表示され、一部は新規、一部は既読、日付順にソートされています。また、自分宛であるかどうかに関係なく、最近送信されたメッセージのプレビューが表示されます。最後に、このクエリにはページングの要素があります。デフォルトでは、11個のアイテムが必要です。ページを表示する場合は10、次のページに他にあるかどうかを確認する場合は+1。

何人かの長いユーザーの場合、最大40Kのメッセージを受け取ることができます。

このクエリでは、過去数日間にさまざまな形式が見られましたが、ここで私がやってきました。 OUTER APPLY試してみましたが、実行時間と統計が悪化しています。

SET STATISTICS IO ON; /* And turn on the Actual Excecution Plan */

declare @UserId bigint
set @UserId = 9999

; WITH cte AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY SendDate DESC) AS RowNum, 
        MT.MessageThreadId, 
        MT.FromUserHasArchived, 
        MT.ToUserHasArchived, 
        MT.Created, 
        MT.ThreadStartedBy, 
        MT.ThreadSentTo, 
        MT.[Subject], 
        MT.CanReply, 
        MT.FromUserDeleted, 
        MT.ToUserDeleted,              
        LM.MessageId, 
        LM.Deleted, 
        LM.FromUserId, 
        LM.ToUserId, 
        LM.[Message], 
        LM.SendDate, 
        LM.ReadDate
    FROM MessageThreads MT 
    -- join the most recent non-deleted message where this user is the sender or receiver
    LEFT OUTER JOIN 
    (
        SELECT RANK() OVER (PARTITION BY MessageThreadId ORDER BY SendDate DESC) r, * 
        FROM [Messages] 
        WHERE (FromUserId=@UserId OR ToUserId=@UserId) 
        AND (Deleted=0)
    ) LM ON (LM.MessageThreadId = MT.MessageThreadId AND LM.r = 1) 
    --WHERE MT.ThreadSentTo=@UserId OR MT.ThreadStartedBy=@UserId   
)
SELECT
    cte.*,
    UserFrom.FirstName AS UserFromFirstName, 
    UserFrom.LastName AS UserFromLastName, 
    UserFrom.Email AS UserFromEmail,                  
    UserTo.FirstName AS UserToFirstName, 
    UserTo.LastName AS UserToLastName, 
    UserTo.Email AS UserToEmail  
FROM cte
LEFT OUTER JOIN Users AS UserFrom ON cte.FromUserId=UserFrom.UserId 
LEFT OUTER JOIN Users AS UserTo ON cte.ToUserId=UserTo.UserId 
WHERE RowNum >= 1 
AND RowNum <= 11   
ORDER BY RowNum ASC

上記のクエリの統計(SSMSの実行時間〜2秒)。この実行時間は許容範囲内ですが、統計は望ましいとは言えません。実際の実行計画を確認すると、なおさらです。 query stats

実行計画はここにリンクされています https://Gist.github.com/timgabrhel/f8d919d5728e965623fbd953f7a219ef

私が見つけた大きな問題の1つは、MessageThreadsテーブルの400k行のインデックススキャンです。おそらくこれは、プライマリSELECT X FROM MessageThreadsクエリにはフィルタがありません。述語を適用すると(クエリからのWHEREのコメントを外す)、統計は大幅に改善されますが(下)、SSMSの時間は〜2秒から〜18秒にジャンプします。

query stats 2

クエリの問題領域はMessageThreads述語です

Execution planhttps://Gist.github.com/timgabrhel/1383ff9362567fdf41ba011dead63ceb

前もって感謝します!

9
Tim Gabrhel

いくつかの考え:

  1. WHERE句にはサポートインデックスが必要です

WHERE MT.ThreadSentTo=@UserId OR MT.ThreadStartedBy=@UserIdを効率的にするためには、実際には2つのインデックスが必要です。1つはThreadSentToフィールド、もう1つはThreadStartedByフィールドです。それ以外の場合、SQLエンジンは全テーブルスキャンを実行して正しいスレッドを取得します。

  1. ROW_NUMBER()の代わりにOFFSET ... NEXT N ROWS ONLYを使用します

SQL 2012以降、ページングを処理するための新しい構成がSQL Serverに追加されました。これは次のように機能します。

DECLARE @PageNumber int = 20
DECLARE @RowsPerPage int = 15

SELECT *
FROM MyTable T
INNER JOIN MyDetailTable D
    ON T.MyTableID = D.MyTableID
OFFSET (@PageNumber - 1) * @RowsPerPage ROWS
FETCH NEXT @RowsPerPage ROWS ONLY

この場合、クエリは最初の285((20-1)* 15)行をスキップし、次の15行を取得します。これは、通常のページング用の古いRowNumber()フィルターよりも速いページング方法です。

4
Laughing Vergil

テーブルの再作成

_CREATE TABLE dbo.Messages(MessageID BIGINT NOT NULL PRIMARY KEY,
MessageThreadID bigint not null,
Deleted bit null,
FromUserID bigint null,
ToUserId bigint null,
Message nvarchar(max) not null,
SendDate Datetime not null,
ReadDate datetime null);



CREATE TABLE dbo.MessageThreads (
MessageThreadID bigint not null PRIMARY KEY,
FromUserHasArchived bit not null,
ToUserHasArchived bit not null,
Created datetime not null,
ThreadStartedBy bigint null,
ThreadSentTo bigint null,
Subject varchar(50) not null,
CanReply bit not null,
FromUserDeleted bit not null,
ToUserDeleted bit not null);
_

Data-ishの再作成

_DECLARE @message nvarchar(max)
SET @message = REPLICATE(CAST(N'B' as nvarchar(max)),200)

INSERT INTO Dbo.Messages WITH(TABLOCK)
(MessageID,MessageThreadID,Deleted,FromUserID,ToUserId,Message,SendDate,ReadDate)
SELECT TOP(1000000)
 ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
  ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
0,
 ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) % 10000,
(ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) + 1000) % 10000,
@message,
DATEADD(Second,- ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),getdate()),
DATEADD(Second,- ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),getdate())
FROM MASTER..spt_values spt1
CROSS APPLY MASTER..spt_values spt2;


INSERT INTO dbo.MessageThreads
SELECT TOP(400000)
 ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
0,
0,
DATEADD(Second,- ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),getdate()),
 ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
  ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
  'bla',
  0,
  0,
  0

FROM MASTER..spt_values spt1
CROSS APPLY MASTER..spt_values spt2;


UPDATE TOP(20000) Messages 
SET ToUserId= 9999

UPDATE TOP(20000) Messages 
SET FromUserID = 9999
_

クエリ

いくつかの部分が元のクエリと一致している場合:

enter image description here

オフセット方法を使用しても、ハッシュの一致などの問題が発生します

_SET STATISTICS IO ON; /* And turn on the Actual Excecution Plan */

declare @UserId bigint
set @UserId = 9999
DECLARE @PageNumber int = 1
DECLARE @RowsPerPage int = 11



; WITH cte AS (
    SELECT

        MT.MessageThreadId, 
        MT.FromUserHasArchived, 
        MT.ToUserHasArchived, 
        MT.Created, 
        MT.ThreadStartedBy, 
        MT.ThreadSentTo, 
        MT.[Subject], 
        MT.CanReply, 
        MT.FromUserDeleted, 
        MT.ToUserDeleted,              
        LM.MessageId, 
        LM.Deleted, 
        LM.FromUserId, 
        LM.ToUserId, 
        LM.[Message], 
        LM.SendDate, 
        LM.ReadDate
    FROM MessageThreads MT 
    -- join the most recent non-deleted message where this user is the sender or receiver
    LEFT OUTER JOIN 
    (
        SELECT RANK() OVER (PARTITION BY MessageThreadId ORDER BY SendDate DESC) r, * 
        FROM [Messages] 
        WHERE (FromUserId=@UserId OR ToUserId=@UserId) 
        AND (Deleted=0)
    ) LM ON (LM.MessageThreadId = MT.MessageThreadId AND LM.r = 1) 
    --WHERE MT.ThreadSentTo=@UserId OR MT.ThreadStartedBy=@UserId   
)
SELECT
    cte.*
FROM cte

ORDER BY SendDate DESC  
OFFSET (@PageNumber - 1) * @RowsPerPage ROWS
FETCH NEXT @RowsPerPage ROWS ONLY;


  SQL Server Execution Times:    CPU time = 2170 ms,  elapsed time =
2402 ms.
_

補足として、_LEFT OUTER JOIN_を_INNER JOIN_に変更すると、CPU時間と経過時間が

_   CPU time = 609 ms,  elapsed time = 745 ms.
_

enter image description here

しかし、それはおそらく不可能ですが、必要な最適化についての最初のヒントを与えてくれます。

次のステップとして、RANK()を削除し、MAX()を_GROUP BY_とともに使用して、クエリの問題のある部分の列を減らすことができます。

_SET STATISTICS IO,TIME ON; /* And turn on the Actual Excecution Plan */

declare @UserId bigint
set @UserId = 9999
DECLARE @PageNumber int = 1
DECLARE @RowsPerPage int = 11



; WITH cte AS (
    SELECT

        MT.MessageThreadId, 
        MT.FromUserHasArchived, 
        MT.ToUserHasArchived, 
        MT.Created, 
        MT.ThreadStartedBy, 
        MT.ThreadSentTo, 
        MT.[Subject], 
        MT.CanReply, 
        MT.FromUserDeleted, 
        MT.ToUserDeleted,              
        LM.SendDate

    FROM MessageThreads MT  WITH(INDEX([IX_MessageThreadId_SendDate]))
    -- join the most recent non-deleted message where this user is the sender or receiver
    LEFT OUTER JOIN 
    (
        SELECT MAX(SendDate) as SendDate,MessageThreadId
        FROM [Messages] 
        WHERE (FromUserId=@UserId OR ToUserId=@UserId) 
        AND (Deleted=0)
        GROUP BY MessageThreadId
    ) LM ON (LM.MessageThreadId = MT.MessageThreadId) 
    --WHERE MT.ThreadSentTo=@UserId OR MT.ThreadStartedBy=@UserId   
)
SELECT
    cte.*,        
        LM.MessageId, 
        LM.Deleted, 
        LM.FromUserId, 
        LM.ToUserId, 
        LM.[Message]

FROM cte
LEFT JOIN [Messages] LM
ON cte.MessageThreadID = LM.MessageThreadId
AND cte.SendDate = LM.SendDate
ORDER BY SendDate DESC  
OFFSET (@PageNumber - 1) * @RowsPerPage ROWS
FETCH NEXT @RowsPerPage ROWS ONLY;
_

これは私の端にあるハッシュ一致の流出を削除しますが、タイミングはまだ高いです enter image description here

_ SQL Server Execution Times:
   CPU time = 1950 ms,  elapsed time = 1223 ms.
_

次に、OR()を2つの部分に明示的に書き込むことにより、キー検索の1つを削除できます。

_SET STATISTICS IO,TIME ON; /* And turn on the Actual Excecution Plan */

declare @UserId bigint
set @UserId = 9999
DECLARE @PageNumber int = 1
DECLARE @RowsPerPage int = 11



; WITH cte AS (
    SELECT

        MT.MessageThreadId, 
        MT.FromUserHasArchived, 
        MT.ToUserHasArchived, 
        MT.Created, 
        MT.ThreadStartedBy, 
        MT.ThreadSentTo, 
        MT.[Subject], 
        MT.CanReply, 
        MT.FromUserDeleted, 
        MT.ToUserDeleted,              
        LM.SendDate

    FROM MessageThreads MT  WITH(INDEX([IX_MessageThreadId_SendDate]))
    -- join the most recent non-deleted message where this user is the sender or receiver
    LEFT OUTER JOIN 
    (
        SELECT MAX(SendDate) as SendDate,MessageThreadId
        FROM  
        (SELECT SendDate,MessageThreadId
         FROM [Messages]     
         WHERE (FromUserId=@UserId ) 
         AND (Deleted=0) 
        UNION
        SELECT SendDate,MessageThreadId
        FROM [Messages]  
        WHERE  ToUserId=@UserId
        AND (Deleted=0)) AS A2
        GROUP BY MessageThreadId
    ) LM ON (LM.MessageThreadId = MT.MessageThreadId) 
    --WHERE MT.ThreadSentTo=@UserId OR MT.ThreadStartedBy=@UserId   
)
SELECT
    cte.*,        
        LM.MessageId, 
        LM.Deleted, 
        LM.FromUserId, 
        LM.ToUserId, 
        LM.[Message]

FROM cte
LEFT JOIN [Messages] LM
ON cte.MessageThreadID = LM.MessageThreadId
AND cte.SendDate = LM.SendDate
ORDER BY SendDate DESC  
OFFSET (@PageNumber - 1) * @RowsPerPage ROWS
FETCH NEXT @RowsPerPage ROWS ONLY;
_

次の2つのインデックスを追加します。

_CREATE INDEX IX_Messages_FromUserId_MessageThreadId_SendDate
ON Dbo.Messages(FromUserId,MessageThreadId,SendDate)
INCLUDE(Deleted)
WHERE Deleted = 0;

CREATE INDEX IX_Messages_ToUserID_MessageThreadId_SendDate
ON Dbo.Messages(ToUserID,MessageThreadId,SendDate)
INCLUDE(Deleted)
WHERE Deleted = 0;
_

実行時間:

_ SQL Server Execution Times:
   CPU time = 1747 ms,  elapsed time = 1050 ms.
_

これはまだ理想的な最終結果ではありません。そのため、次のパートでは、質問で指定したフィルターを使用して、messagethreadテーブルのフィルター処理を行います。


メッセージスレッドテーブルでのフィルタリング

以前に作成したクエリは、指定したwhere句と一緒に使用されます。

_ WHERE MT.ThreadSentTo=@UserId 
    OR MT.ThreadStartedBy=@UserId
_

あなたに一致するデータセットの更新:

_UPDATE  TOP (20000) MessageThreads
SET ThreadSentTo = 9999
FROM MessageThreads;
UPDATE  TOP (20000) MessageThreads
SET ThreadStartedBy = 9999
FROM MessageThreads;
_

WHEREフィルターを追加した完全なクエリ

_SET STATISTICS IO,TIME ON; /* And turn on the Actual Excecution Plan */

declare @UserId bigint
set @UserId = 9999
DECLARE @PageNumber int = 1
DECLARE @RowsPerPage int = 11
--WHERE MT.ThreadSentTo=@UserId OR MT.ThreadStartedBy=@UserId 



; WITH cte AS (
    SELECT

        MT.MessageThreadId, 
        MT.FromUserHasArchived, 
        MT.ToUserHasArchived, 
        MT.Created, 
        MT.ThreadStartedBy, 
        MT.ThreadSentTo, 
        MT.[Subject], 
        MT.CanReply, 
        MT.FromUserDeleted, 
        MT.ToUserDeleted,              
        LM.SendDate

    FROM MessageThreads MT  
    -- join the most recent non-deleted message where this user is the sender or receiver
    LEFT OUTER JOIN 
    (
        SELECT MAX(SendDate) as SendDate,MessageThreadId
        FROM  
        (SELECT SendDate,MessageThreadId
         FROM [Messages]     
         WHERE (FromUserId=@UserId ) 
         AND (Deleted=0) 
        UNION
        SELECT SendDate,MessageThreadId
        FROM [Messages]  
        WHERE  ToUserId=@UserId
        AND (Deleted=0)) AS A2
        GROUP BY MessageThreadId
    ) LM ON (LM.MessageThreadId = MT.MessageThreadId) 
WHERE MT.ThreadSentTo=@UserId 
OR MT.ThreadStartedBy=@UserId 
)
SELECT
    cte.*,        
        LM.MessageId, 
        LM.Deleted, 
        LM.FromUserId, 
        LM.ToUserId, 
        LM.[Message]

FROM cte
LEFT JOIN [Messages] LM
ON cte.MessageThreadID = LM.MessageThreadId
AND cte.SendDate = LM.SendDate
ORDER BY SendDate DESC  
OFFSET (@PageNumber - 1) * @RowsPerPage ROWS
FETCH NEXT @RowsPerPage ROWS ONLY;
_

その場合、実行計画は、_LEFT OUTER JOIN_を使用しても、かなりきれいに見えます

enter image description here

実行時間:

_ SQL Server Execution Times:
   CPU time = 219 ms,  elapsed time = 221 ms.
_

これらの2つのインデックスによって削除できる残余述語はまだあります。

_CREATE INDEX IX_ThreadSentTo_MessageThreadId
ON MessageThreads(ThreadSentTo,MessageThreadId)
INCLUDE
(
 FromUserHasArchived, 
 ToUserHasArchived, 
 Created, 
 ThreadStartedBy, 
 [Subject], 
 CanReply, 
 FromUserDeleted, 
 ToUserDeleted);
CREATE INDEX IX_ThreadStartedBy_MessageThreadId
ON MessageThreads(ThreadStartedBy,MessageThreadId)
INCLUDE
(

        FromUserHasArchived, 
        ToUserHasArchived, 
        Created, 
        ThreadSentTo, 
        [Subject], 
        CanReply, 
        FromUserDeleted, 
        ToUserDeleted);
_

しかし、私のエンドにインデックスを追加すると、パフォーマンスが経過時間の200ミリ秒から800ミリ秒に低下します。

メッセージスレッドにインデックスを追加しない実行プラン(〜200ms経過時間)

messagethreadにインデックスが追加された実行プラン(〜800ms経過時間)

4
Randi Vertongen

Messageテーブルの既存のインデックスがマークアップされていません。

重要な主な領域は、必要のない大きなテーブルの_2 Window Function_です。

_declare @UserId bigint
set @UserId = 9999

DECLARE @PageNumber int = 20
DECLARE @RowsPerPage int = 15

-- In #Temp table define all require column with same data type.
--
Create #Temp Table (MessageId,MessageThreadId,FromUserId,ToUserId
,Deleted,Message,SendDate,ReadDate)


;With CTE as
(
SELECT MessageThreadId,max(MessageId)MessageId
        FROM [Messages] 
        WHERE FromUserId=@UserId  
        AND Deleted=0
        group by MessageThreadId
        union all

        SELECT MessageThreadId,max(MessageId)MessageId
        FROM [Messages] 
        WHERE ToUserId=@UserId
        AND Deleted=0
        group by MessageThreadId

)
    insert into #Temp(mention require column)
    select M.* --- do not use *,mention require column
    From dbo.Message M
    where exists(select 1 from CTE C 
where c.MessageId=M.MessageId 
and c.MessageThreadId=M.MessageThreadId)

    -- In #Temp only MessageThreadId with LM.r = 1 logic
    --if #Temp contains more than 100  record then create CI index MessageThreadId

    SELECT
        --ROW_NUMBER() OVER (ORDER BY SendDate DESC) AS RowNum, 
        MT.MessageThreadId, 
        MT.FromUserHasArchived, 
        MT.ToUserHasArchived, 
        MT.Created, 
        MT.ThreadStartedBy, 
        MT.ThreadSentTo, 
        MT.[Subject], 
        MT.CanReply, 
        MT.FromUserDeleted, 
        MT.ToUserDeleted,              
        LM.MessageId, 
        LM.Deleted,
        LM.FromUserId, 
        LM.ToUserId, 
        LM.[Message], 
        LM.SendDate, 
        LM.ReadDate,
        UserFrom.FirstName AS UserFromFirstName, 
    UserFrom.LastName AS UserFromLastName, 
    UserFrom.Email AS UserFromEmail,                  
    UserTo.FirstName AS UserToFirstName, 
    UserTo.LastName AS UserToLastName, 
    UserTo.Email AS UserToEmail 
    FROM MessageThreads MT 
    left join #Temp LM ON (LM.MessageThreadId = MT.MessageThreadId ) 
    LEFT OUTER JOIN dbo.Users AS UserFrom ON LM.FromUserId=UserFrom.UserId 
    LEFT OUTER JOIN dbo.Users AS UserTo ON LM.ToUserId=UserTo.UserId 
        OFFSET (@PageNumber - 1) * @RowsPerPage ROWS
        FETCH NEXT @RowsPerPage ROWS ONLY
_

現在のクエリによると

_NONCLUSTERED INDEX [nci_wi_MessageThreads_4AE42CECCF44AA0519F913BAF59A3CFA] ON [dbo].[MessageThreads]_不要

_ALTER TABLE [dbo].[MessageThreads] ADD  CONSTRAINT [PK_MessageThreads] PRIMARY KEY CLUSTERED 
(
    [MessageThreadId] DESC
)
GO
_

ほとんどが最近のレコードを探しているため、DESCである必要があります

同様に

_ALTER TABLE [dbo].[Messages] ADD  CONSTRAINT [PK_Messages] PRIMARY KEY CLUSTERED 
(
    [MessageId] DESC
)
GO


CREATE NONCLUSTERED INDEX [ix_Messages_MessageThreadId] ON [dbo].[Messages]
(
    [MessageThreadId] ASC,
    [ToUserId],
    FromUserId,
    Deleted
)
include(SendDate,ReadDate)
where Deleted=0
GO
_

MessageのようにNVARCHAR(MAX)を含めることには何の利点もないと思います。

私は正しいですか?

_ALTER TABLE [dbo].[Users] ADD  CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED 
(
    [UsersId] ASC
)
GO
_

私のスクリプトの通知では、SendDateは述語で使用されていないため、インデックスはありません。 INTINTのインデックスで遊んだ方が安全です。

また、これは重要なクエリの1つであり、ほとんどのクエリで_Deleted=0_が使用されるため、_Create Filtered Index_を使用することをお勧めします。

これがLeap and Boundによって改善され、最新の実行プランでさらに改善できる場合は、_LEFT OUTER JOIN dbo.Users_をさらに改善できます。

1
KumarHarsh