私は過去数日間、ビデオの検索、視聴に費やしてきましたが、私は自分の道をたどるだけの範囲に到達したと思います。以下の私の例から、私はより具体的な方向性を探しています。
使用しているテーブルが2つあります。 MessageThreads(400kレコード)&Messages(1Mレコード)。それらのスキーマを以下に示します。
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秒)。この実行時間は許容範囲内ですが、統計は望ましいとは言えません。実際の実行計画を確認すると、なおさらです。
実行計画はここにリンクされています https://Gist.github.com/timgabrhel/f8d919d5728e965623fbd953f7a219ef
私が見つけた大きな問題の1つは、MessageThreadsテーブルの400k行のインデックススキャンです。おそらくこれは、プライマリSELECT X FROM MessageThreads
クエリにはフィルタがありません。述語を適用すると(クエリからのWHEREのコメントを外す)、統計は大幅に改善されますが(下)、SSMSの時間は〜2秒から〜18秒にジャンプします。
クエリの問題領域はMessageThreads述語です
https://Gist.github.com/timgabrhel/1383ff9362567fdf41ba011dead63ceb
前もって感謝します!
いくつかの考え:
WHERE MT.ThreadSentTo=@UserId OR MT.ThreadStartedBy=@UserId
を効率的にするためには、実際には2つのインデックスが必要です。1つはThreadSentToフィールド、もう1つはThreadStartedByフィールドです。それ以外の場合、SQLエンジンは全テーブルスキャンを実行して正しいスレッドを取得します。
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()フィルターよりも速いページング方法です。
テーブルの再作成
_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
_
クエリ
いくつかの部分が元のクエリと一致している場合:
オフセット方法を使用しても、ハッシュの一致などの問題が発生します
_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.
_
しかし、それはおそらく不可能ですが、必要な最適化についての最初のヒントを与えてくれます。
次のステップとして、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;
_
これは私の端にあるハッシュ一致の流出を削除しますが、タイミングはまだ高いです
_ 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
_を使用しても、かなりきれいに見えます
実行時間:
_ 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経過時間)
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
は述語で使用されていないため、インデックスはありません。 INT
とINT
のインデックスで遊んだ方が安全です。
また、これは重要なクエリの1つであり、ほとんどのクエリで_Deleted=0
_が使用されるため、_Create Filtered Index
_を使用することをお勧めします。
これがLeap and Boundによって改善され、最新の実行プランでさらに改善できる場合は、_LEFT OUTER JOIN dbo.Users
_をさらに改善できます。