J'ai passé les deux derniers jours à chercher, regarder des vidéos, et je pense que je suis allé aussi loin que je peux me faufiler. Je cherche une direction plus spécifique étant donné mon exemple ci-dessous.
J'ai deux tables avec lesquelles je travaille. MessagesThreads (400 000 enregistrements) et Messages (1 million d'enregistrements). Leurs schémas sont présentés ci-dessous.
Index MessageThreads
https://Gist.github.com/timgabrhel/0a9ff88160ebc9e40559e1e10ecc7ee4
Index des messages
https://Gist.github.com/timgabrhel/d649074cbe82016e8a90f918c58c4764
J'essaie d'améliorer les performances de notre requête "boîte de réception" principale. Pensez à la boîte de réception de votre fournisseur de messagerie. Vous verrez une liste de fils de discussion, certains nouveaux, certains lus, triés par date, et vous donne également un aperçu du dernier message envoyé, que ce soit à vous ou de votre part. Enfin, il existe un élément de pagination sur cette requête. Par défaut, nous voulons 11 articles. 10 pour la page à afficher et +1 pour savoir s'il y a plus sur la page suivante.
Pour certains de nos utilisateurs de longue date, ils peuvent avoir jusqu'à 40 000 messages.
Cette requête a vu de nombreuses formes différentes au cours des derniers jours, mais c'est là que je suis arrivé. J'ai donné OUTER APPLY
un essai, mais je constate un temps d'exécution et des statistiques moins bons.
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
Statistiques pour la requête ci-dessus (temps d'exécution ~ 2 secondes dans SSMS). Ce temps d'exécution est acceptable, mais les statistiques semblent moins souhaitables, et encore plus lors de l'examen du plan d'exécution réel.
Le plan d'exécution est lié ici https://Gist.github.com/timgabrhel/f8d919d5728e965623fbd953f7a219ef
Un énorme hoquet que j'ai repéré est le balayage d'index de 400k lignes sur la table MessageThreads. Vraisemblablement parce que le primaire SELECT X FROM MessageThreads
la requête n'a pas de filtre. Lorsque j'y applique un prédicat (décommentez le WHERE de la requête), les statistiques s'améliorent considérablement (ci-dessous), mais le temps passe de ~ 2 secondes à ~ 18 secondes dans SSMS.
La zone à problème de la requête est le prédicat MessageThreads
https://Gist.github.com/timgabrhel/1383ff9362567fdf41ba011dead63ceb
Merci d'avance!
Quelques réflexions:
WHERE MT.ThreadSentTo=@UserId OR MT.ThreadStartedBy=@UserId
a vraiment besoin de deux index pour être efficace - un sur le champ ThreadSentTo et un sur le champ ThreadStartedBy. Sinon, le moteur SQL effectuera une analyse complète de la table pour récupérer les threads corrects.
À partir de SQL 2012, une nouvelle construction a été ajoutée à SQL Server pour gérer la pagination. Cela fonctionne comme ceci:
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
Dans ce cas, la requête sautera les 285 premières lignes ((20-1) * 15) et récupérera les 15 lignes suivantes. Il s'agit d'une méthode de pagination plus rapide que l'ancien filtre RowNumber () pour la pagination normale.
Recréation des tables
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);
Recréation du 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
Interrogation
Avec certaines pièces correspondant à votre requête d'origine:
L'utilisation de la méthode de décalage montre toujours le déversement sur la correspondance de hachage et d'autres problèmes
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.
Une note latérale, changer le LEFT OUTER JOIN
En un INNER JOIN
Réduit le temps processeur et le temps écoulé à
CPU time = 609 ms, elapsed time = 745 ms.
Mais ce n'est probablement pas possible mais nous donne un premier indice sur l'optimisation nécessaire.
À l'étape suivante, vous pourriez envisager de supprimer la RANK()
et d'utiliser MAX()
avec GROUP BY
Pour travailler avec moins de colonnes sur la partie problématique de votre requête.
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;
Cela supprime le déversement de correspondance de hachage de ma part, mais les délais sont toujours élevés
SQL Server Execution Times:
CPU time = 1950 ms, elapsed time = 1223 ms.
Nous pouvons ensuite supprimer l'une des recherches de clés en écrivant explicitement OR () en deux parties:
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;
Et en ajoutant ces deux index:
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;
Temps d'exécution:
SQL Server Execution Times:
CPU time = 1747 ms, elapsed time = 1050 ms.
Ce n'est toujours pas un résultat final idéal, c'est pourquoi dans la prochaine partie nous allons passer en revue le filtrage sur la table messagethread
, avec le filtre que vous avez spécifié dans la question.
La requête créée précédemment sera utilisée avec la clause where que vous avez spécifiée:
WHERE MT.ThreadSentTo=@UserId
OR MT.ThreadStartedBy=@UserId
Mises à jour pour un ensemble de données correspondant au vôtre:
UPDATE TOP (20000) MessageThreads
SET ThreadSentTo = 9999
FROM MessageThreads;
UPDATE TOP (20000) MessageThreads
SET ThreadStartedBy = 9999
FROM MessageThreads;
Requête complète avec le filtre WHERE
ajouté
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;
Le plan d'exécution semble alors beaucoup plus propre, même avec le LEFT OUTER JOIN
Temps d'exécution:
SQL Server Execution Times:
CPU time = 219 ms, elapsed time = 221 ms.
Nous avons toujours un prédicat résiduel qui peut être supprimé par ces deux index:
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);
Mais les performances passent de ~ 200 ms de temps écoulé à ~ 800 ms de temps écoulé lors de l'ajout des index de mon côté.
Plan d'exécution sans index ajoutés sur messagethread (~ 200 ms de temps écoulé)
Plan d'exécution avec ajout d'index sur messagethread (~ 800 ms de temps écoulé)
L'index existant de la table Message
n'est pas jusqu'à la marque.
Le principal sujet de préoccupation est 2 Window Function
Sur une grande table qui n'est pas requise.
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
Selon la requête actuelle
NONCLUSTERED INDEX [nci_wi_MessageThreads_4AE42CECCF44AA0519F913BAF59A3CFA] ON [dbo].[MessageThreads]
Ne nécessite pas
ALTER TABLE [dbo].[MessageThreads] ADD CONSTRAINT [PK_MessageThreads] PRIMARY KEY CLUSTERED
(
[MessageThreadId] DESC
)
GO
Il doit s'agir de DESC, car vous recherchez principalement des enregistrements récents
De même
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
Je ne pense pas qu'il y ait un avantage à inclure NVARCHAR(MAX)
comme Message
.
Ai-je raison ?
ALTER TABLE [dbo].[Users] ADD CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED
(
[UsersId] ASC
)
GO
Dans mon avis de script, SendDate
n'est pas utilisé dans le prédicat, donc pas d'index dessus. Jouer avec INT
et Index sur INT
est plus sûr.
C'est aussi une requête importante et où Deleted=0
Sera utilisé dans la plupart des requêtes. Il est donc préférable de Create Filtered Index
Dessus.
Si cela s'améliore par Leap and Bound, puis avec le dernier plan d'exécution, nous pouvons encore améliorer LEFT OUTER JOIN dbo.Users