web-dev-qa-db-fra.com

Aide à l'optimisation des performances maître / détail (e-mail comme boîte de réception) requête SQL

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.

messages tableenter image description here

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. query stats

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.

query stats 2

La zone à problème de la requête est le prédicat MessageThreads

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

Merci d'avance!

9
Tim Gabrhel

Quelques réflexions:

  1. Votre clause WHERE a besoin d'un index de prise en charge

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.

  1. Utilisez OFFSET ... NEXT N ROWS ONLY au lieu de ROW_NUMBER ()

À 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.

4
Laughing Vergil

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:

enter image description here

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.

enter image description here

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 enter image description here

 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.


Filtrage sur la table des messages

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

enter image description here

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é)

4
Randi Vertongen

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

1
KumarHarsh