it-swarm.com.de

Hilfe zur Leistungsoptimierung Master / Detail (E-Mail wie Posteingang) SQL-Abfrage

Ich habe die letzten Tage damit verbracht, zu suchen, Videos anzuschauen, und ich glaube, ich bin so weit gekommen, dass ich mich nur noch durcharbeiten kann. Ich suche nach einer genaueren Richtung anhand meines Beispiels unten.

Ich habe zwei Tische, mit denen ich arbeite. MessageThreads (400.000 Datensätze) und Nachrichten (1 Million Datensätze). Ihre Schemata sind unten gezeigt.

(messages tableenter image description here

MessageThreads-Indizes

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

Nachrichtenindizes

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

Ich versuche, die Leistung unserer primären "Posteingang" -Abfrage zu verbessern. Denken Sie an den Posteingang Ihres E-Mail-Anbieters. Sie sehen eine Liste von Threads, einige neu, einige gelesen, sortiert nach Datum, und Sie erhalten eine Vorschau der zuletzt gesendeten Nachricht, unabhängig davon, ob sie an Sie oder von Ihnen gesendet wurde. Schließlich gibt es ein Element des Paging in dieser Abfrage. Standardmäßig möchten wir 11 Artikel. 10, damit die Seite angezeigt wird, und +1, um festzustellen, ob auf der nächsten Seite mehr angezeigt wird.

Für einige unserer langjährigen Benutzer können sie bis zu 40.000 Nachrichten haben.

Diese Abfrage hat in den letzten Tagen viele verschiedene Formen gesehen, aber hier bin ich angekommen. Ich habe OUTER APPLY Ausprobiert, sehe aber schlechtere Ausführungszeiten und Statistiken.

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 ([email protected] OR [email protected]) 
        AND (Deleted=0)
    ) LM ON (LM.MessageThreadId = MT.MessageThreadId AND LM.r = 1) 
    --WHERE [email protected] OR [email protected]   
)
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

Statistiken für die obige Abfrage (Ausführungszeit ~ 2 Sekunden in SSMS). Diese Ausführungszeit ist akzeptabel, aber die Statistiken fühlen sich weniger als wünschenswert an, und dies umso mehr, wenn der tatsächliche Ausführungsplan überprüft wird. (query stats

Der Ausführungsplan ist hier verlinkt https://Gist.github.com/timgabrhel/f8d919d5728e965623fbd953f7a219ef

Ein großes Problem, das ich entdeckt habe, ist der Index-Scan mit 400.000 Zeilen in der MessageThreads-Tabelle. Vermutlich liegt dies daran, dass die primäre SELECT X FROM MessageThreads - Abfrage keinen Filter enthält. Wenn ich ein Prädikat darauf anwende (das WHERE aus der Abfrage auskommentieren), verbessert sich die Statistik erheblich (siehe unten), aber die Zeit springt in SSMS von ~ 2 Sekunden auf ~ 18 Sekunden.

(query stats 2

Der Problembereich in der Abfrage ist das MessageThreads-Prädikat

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

Vielen Dank im Voraus!

9
Tim Gabrhel

Ein paar Gedanken:

  1. Ihre WHERE-Klausel benötigt einen unterstützenden Index

WHERE [email protected] OR [email protected] benötigt wirklich zwei Indizes, um effizient zu sein - einen im ThreadSentTo-Feld und einen im ThreadStartedBy-Feld. Andernfalls führt die SQL-Engine einen vollständigen Tabellenscan durch, um die richtigen Threads abzurufen.

  1. Verwenden Sie OFFSET ... NUR NÄCHSTE N REIHEN anstelle von ROW_NUMBER ()

Ab SQL 2012 wurde SQL Server ein neues Konstrukt für die Verarbeitung von Paging hinzugefügt. Das funktioniert so:

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

In diesem Fall überspringt die Abfrage die ersten 285 ((20-1) * 15) Zeilen und ruft die nächsten 15 Zeilen ab. Dies ist eine schnellere Paging-Methode als der ältere RowNumber () -Filter für normales Paging.

4
Laughing Vergil

Neuerstellung der Tabellen

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

Neuerstellen des Daten-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

Abfragen

Mit einigen Teilen, die Ihrer ursprünglichen Abfrage entsprechen:

(enter image description here

Bei Verwendung der Offset-Methode werden weiterhin die Auswirkungen auf die Hash-Übereinstimmung und andere Probleme angezeigt

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 ([email protected] OR [email protected]) 
        AND (Deleted=0)
    ) LM ON (LM.MessageThreadId = MT.MessageThreadId AND LM.r = 1) 
    --WHERE [email protected] OR [email protected]   
)
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.

Eine Randnotiz, bei der LEFT OUTER JOIN In INNER JOIN Geändert wird, reduziert die CPU-Zeit und die verstrichene Zeit auf

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

(enter image description here

Das ist aber wahrscheinlich nicht möglich, gibt uns aber einen ersten Hinweis auf die notwendige Optimierung.

Als nächsten Schritt könnten Sie versuchen, RANK() zu entfernen und MAX() mit GROUP BY Zu verwenden, um mit weniger Spalten im Problemteil Ihrer Abfrage zu arbeiten.

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 ([email protected] OR [email protected]) 
        AND (Deleted=0)
        GROUP BY MessageThreadId
    ) LM ON (LM.MessageThreadId = MT.MessageThreadId) 
    --WHERE [email protected] OR [email protected]   
)
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;

Dies entfernt zwar die Hash-Match-Verschüttung an meinem Ende, aber die Timings sind immer noch hoch enter image description here

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

Wir können dann eine der Schlüsselsuchen entfernen, indem wir das OR () explizit in zwei Teile schreiben:

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 ([email protected] ) 
         AND (Deleted=0) 
        UNION
        SELECT SendDate,MessageThreadId
        FROM [Messages]  
        WHERE  [email protected]
        AND (Deleted=0)) AS A2
        GROUP BY MessageThreadId
    ) LM ON (LM.MessageThreadId = MT.MessageThreadId) 
    --WHERE [email protected] OR [email protected]   
)
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;

Und diese beiden Indizes hinzufügen:

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;

Ausführungszeit:

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

Dies ist immer noch kein ideales Endergebnis. Deshalb werden wir im nächsten Teil die Filterung in der Tabelle messagethread mit dem in der Frage angegebenen Filter durchgehen.


Filtern in der Messagethread-Tabelle

Die zuvor erstellte Abfrage wird zusammen mit der von Ihnen angegebenen where-Klausel verwendet:

 WHERE [email protected] 
    OR [email protected]

Aktualisierungen für einen Datensatz, der Ihrem entspricht:

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

Vollständige Abfrage mit dem Filter WHERE hinzugefügt

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 [email protected] OR [email protected] 



; 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 ([email protected] ) 
         AND (Deleted=0) 
        UNION
        SELECT SendDate,MessageThreadId
        FROM [Messages]  
        WHERE  [email protected]
        AND (Deleted=0)) AS A2
        GROUP BY MessageThreadId
    ) LM ON (LM.MessageThreadId = MT.MessageThreadId) 
WHERE [email protected] 
OR [email protected] 
)
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;

Der Ausführungsplan sieht dann auch mit dem LEFT OUTER JOIN Viel sauberer aus.

(enter image description here

Ausführungszeit:

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

Wir haben immer noch ein Restprädikat, das durch diese beiden Indizes entfernt werden kann:

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

Aber die Leistung sinkt von ~ 200 ms verstrichener Zeit auf ~ 800 ms verstrichene Zeit, wenn ich die Indizes an meinem Ende hinzufüge.

Ausführungsplan ohne hinzugefügte Indizes für den Messagethread (~ 200 ms verstrichene Zeit)

Ausführungsplan mit hinzugefügten Indizes für den Messagethread (~ 800 ms verstrichene Zeit)

4
Randi Vertongen

Der vorhandene Index der Tabelle Message entspricht nicht den Anforderungen.

Hauptanliegen ist 2 Window Function Auf einem großen Tisch, der nicht benötigt wird.

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 [email protected]  
        AND Deleted=0
        group by MessageThreadId
        union all

        SELECT MessageThreadId,max(MessageId)MessageId
        FROM [Messages] 
        WHERE [email protected]
        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

Laut aktueller Abfrage

NONCLUSTERED INDEX [nci_wi_MessageThreads_4AE42CECCF44AA0519F913BAF59A3CFA] ON [dbo].[MessageThreads] Nicht erforderlich

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

Es sollte DESC sein, da Sie hauptsächlich nach aktuellen Datensätzen suchen

Ähnlich

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

Ich glaube nicht, dass es von Vorteil ist, NVARCHAR(MAX) wie Message einzuschließen.

Habe ich recht ?

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

In meinem Skript-Hinweis wird SendDate nicht als Prädikat verwendet. Also kein Index dafür. Das Spielen mit INT und Index auf INT ist sicherer.

Dies ist auch eine wichtige Abfrage, bei der Deleted=0 In den meisten Abfragen verwendet wird. Daher ist es besser, Create Filtered Index Darauf zu setzen.

Wenn sich dies durch Leap and Bound und dann mit dem neuesten Ausführungsplan verbessert, können wir LEFT OUTER JOIN dbo.Users Weiter verbessern.

1
KumarHarsh