Query molto simili, prestazioni notevolmente diverse


9

Ho due domande molto simili

Prima query:

SELECT count(*)
FROM Audits a
    JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
    and a.TargetTypeId IN 
    (1,2,3,4,5,6,7,8,9,
    11,12,13,14,15,16,17,18,19,
    21,22,23,24,25,26,27,28,29,30,
    31,32,33,34,35,36,37,38,39,
    41,42,43,44,45,46,47,48,49,
    51,52,53,54,55,56,57,58,59,
    61,62,63,64,65,66,67,68,69,
    71,72,73,74,75,76,77,78,79)

Risultato: 267479

Piano: https://www.brentozar.com/pastetheplan/?id=BJWTtILyS


Seconda query:

SELECT count(*)
FROM Audits a
    JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
    and a.TargetTypeId IN 
    (1,2,3,4,5,6,7,8,9,
    11,12,13,14,15,16,17,18,19,
    21,22,23,24,25,26,27,28,29,
    31,32,33,34,35,36,37,38,39,
    41,42,43,44,45,46,47,48,49,
    51,52,53,54,55,56,57,58,59,
    61,62,63,64,65,66,67,68,69,
    71,72,73,74,75,76,77,78,79)

Risultato: 25650

Piano: https://www.brentozar.com/pastetheplan/?id=S1v79U8kS


Il completamento della prima query richiede circa un secondo, mentre la seconda query richiede circa 20 secondi. Questo è del tutto controintuitivo perché la prima query ha un conteggio molto più elevato della seconda. Questo è su SQL Server 2012

Perché c'è così tanta differenza? Come posso velocizzare la seconda query per essere veloce come la prima?


Ecco lo script Crea tabella per entrambe le tabelle:

CREATE TABLE [dbo].[AuditRelatedIds](
    [AuditId] [bigint] NOT NULL,
    [RelatedId] [uniqueidentifier] NOT NULL,
    [AuditTargetTypeId] [smallint] NOT NULL,
 CONSTRAINT [PK_AuditRelatedIds] PRIMARY KEY CLUSTERED 
(
    [AuditId] ASC,
    [RelatedId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

CREATE NONCLUSTERED INDEX [IX_AuditRelatedIdsRelatedId_INCLUDES] ON [dbo].[AuditRelatedIds]
(
    [RelatedId] ASC
)
INCLUDE (   [AuditId]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

ALTER TABLE [dbo].[AuditRelatedIds]  WITH CHECK ADD  CONSTRAINT [FK_AuditRelatedIds_AuditId_Audits_Id] FOREIGN KEY([AuditId])
REFERENCES [dbo].[Audits] ([Id])

ALTER TABLE [dbo].[AuditRelatedIds] CHECK CONSTRAINT [FK_AuditRelatedIds_AuditId_Audits_Id]

ALTER TABLE [dbo].[AuditRelatedIds]  WITH CHECK ADD  CONSTRAINT [FK_AuditRelatedIds_AuditTargetTypeId_AuditTargetTypes_Id] FOREIGN KEY([AuditTargetTypeId])
REFERENCES [dbo].[AuditTargetTypes] ([Id])

ALTER TABLE [dbo].[AuditRelatedIds] CHECK CONSTRAINT [FK_AuditRelatedIds_AuditTargetTypeId_AuditTargetTypes_Id]

CREATE TABLE [dbo].[Audits](
    [Id] [bigint] IDENTITY(1,1) NOT NULL,
    [TargetTypeId] [smallint] NOT NULL,
    [TargetId] [nvarchar](40) NOT NULL,
    [TargetName] [nvarchar](max) NOT NULL,
    [Action] [tinyint] NOT NULL,
    [ActionOverride] [tinyint] NULL,
    [Date] [datetime] NOT NULL,
    [UserDisplayName] [nvarchar](max) NOT NULL,
    [DescriptionData] [nvarchar](max) NULL,
    [IsNotification] [bit] NOT NULL,
 CONSTRAINT [PK_Audits] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

SET ANSI_PADDING ON

CREATE NONCLUSTERED INDEX [IX_AuditsTargetId] ON [dbo].[Audits]
(
    [TargetId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

SET ANSI_PADDING ON

CREATE NONCLUSTERED INDEX [IX_AuditsTargetTypeIdAction_INCLUDES] ON [dbo].[Audits]
(
    [TargetTypeId] ASC,
    [Action] ASC
)
INCLUDE (   [TargetId],
    [UserDisplayName]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 100) ON [PRIMARY]

ALTER TABLE [dbo].[Audits]  WITH CHECK ADD  CONSTRAINT [FK_Audits_TargetTypeId_AuditTargetTypes_Id] FOREIGN KEY([TargetTypeId])
REFERENCES [dbo].[AuditTargetTypes] ([Id])

ALTER TABLE [dbo].[Audits] CHECK CONSTRAINT [FK_Audits_TargetTypeId_AuditTargetTypes_Id]

3
Saremo in grado di ottenere alcuni schemi di tabella e dettagli sull'indice. Come sono certo, hai notato che i piani sono leggermente diversi, ma evidentemente sta facendo una grande differenza. Se riusciamo a ottenere quei dettagli di quanto forse possiamo vedere quali opzioni abbiamo.
Kirk Saunders,

2
Come suggerimento molto veloce, invece di utilizzare IN, creare una TempTable con una singola colonna TINYINT / INT (raggruppata) con i numeri desiderati, quindi INNER JOIN. A parte questo probabilmente avremo bisogno delle informazioni DDL come @KirkSaunders menzionato sopra
George.Palacios,

2
C'è qualcosa di speciale TargetTypeId = 30? Sembra che i piani siano diversi perché questo unico valore distorce davvero la quantità di dati (che si prevede saranno restituiti).
Aaron Bertrand

Mi rendo conto che è terribilmente pedante ma l'affermazione "la prima query restituisce molte più righe della seconda". non è corretto. Entrambi restituiscono 1 riga;)
ypercubeᵀᴹ

1
Ho aggiornato la domanda con le istruzioni create table per entrambe le tabelle
Chocoman,

Risposte:


8

Tl; dr in fondo

Perché è stato scelto il cattivo piano

Il motivo principale per la scelta di un piano rispetto all'altro è il Estimated total subtreecosto.

Questo costo era inferiore per il piano negativo rispetto al piano con prestazioni migliori.

Il costo totale stimato della sottostruttura per il piano errato:

inserisci qui la descrizione dell'immagine

Il costo totale stimato della sottostruttura per il piano con prestazioni migliori

inserisci qui la descrizione dell'immagine


L'operatore ha stimato i costi

Alcuni operatori possono sostenere la maggior parte di questi costi e potrebbero essere un motivo per cui l'ottimizzatore sceglie un percorso / piano diverso.

Nel nostro piano con le migliori prestazioni, la maggior parte del Subtreecostviene calcolata sul join index seek& in nested loops operatoresecuzione:

inserisci qui la descrizione dell'immagine

Mentre per il nostro piano di query errato, il Clustered index seekcosto dell'operatore è inferiore

inserisci qui la descrizione dell'immagine

Il che dovrebbe spiegare perché l'altro piano avrebbe potuto essere scelto.

(E aggiungendo il parametro 30aumentando il costo del piano negativo dove è aumentato al di sopra del 871.510000costo stimato). Stima stimata ™

Il piano con le migliori prestazioni

inserisci qui la descrizione dell'immagine

Il cattivo piano

inserisci qui la descrizione dell'immagine


Dove ci porta questo?

Queste informazioni ci consentono di forzare il piano di query errato nel nostro esempio (vedere DML per quasi replicare il problema di OP per i dati utilizzati per replicare il problema)

Aggiungendo un INNER LOOP JOINsuggerimento di join

SELECT count(*)
FROM Audits a
   INNER LOOP JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
    and a.TargetTypeId IN 
    (1,2,3,4,5,6,7,8,9,
    11,12,13,14,15,16,17,18,19,
    21,22,23,24,25,26,27,28,29,
    31,32,33,34,35,36,37,38,39,
    41,42,43,44,45,46,47,48,49,
    51,52,53,54,55,56,57,58,59,
    61,62,63,64,65,66,67,68,69,
    71,72,73,74,75,76,77,78,79)

È più vicino, ma presenta alcune differenze nell'ordine di join:

inserisci qui la descrizione dell'immagine


riscrittura

Il mio primo tentativo di riscrittura potrebbe invece archiviare tutti questi numeri in una tabella temporanea:

CREATE TABLE #Numbers(Numbering INT)
INSERT INTO #Numbers(Numbering)
VALUES
(1),(2),(3),(4),(5),(6),(7),(8),(9),(11),(12),(13),(14),(15),(16),(17),(18),(19),
(21),(22),(23),(24),(25),(26),(27),(28),(29),(30),(31),(32),(33),(34),(35),
(36),(37),(38),(39),(41),(42),(43),(44),(45),(46),(47),(48),(49),(51),(52),
(53),(54),(55),(56),(57),(58),(59),(61),(62),(63),(64),(65),(66),(67),(68),
(69),(71),(72),(73),(74),(75),(76),(77),(78),(79);

E poi aggiungendo un JOINinvece del grandeIN()

SELECT count(*)
FROM Audits a
   INNER LOOP JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
   INNER JOIN #Numbers
   ON Numbering = a.TargetTypeId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1';

Il nostro piano di query è diverso ma non ancora risolto:

inserisci qui la descrizione dell'immagine

con un enorme costo stimato per l'operatore sul AuditRelatedIdstavolo

inserisci qui la descrizione dell'immagine


Qui è dove l'ho notato

Il motivo per cui non riesco a ricreare direttamente il tuo piano è il filtro bitmap ottimizzato.

Posso ricreare il tuo piano disabilitando i filtri bitmap ottimizzati utilizzando traceflags 7497&7498

SELECT count(*)
FROM Audits a 
   INNER JOIN AuditRelatedIds  ari ON a.Id = ari.AuditId 
   INNER JOIN #Numbers
   ON Numbering = a.TargetTypeId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
OPTION (QUERYTRACEON 7497, QUERYTRACEON 7498);

Maggiori informazioni sui filtri bitmap ottimizzati qui .

inserisci qui la descrizione dell'immagine

Ciò significa che senza i filtri bitmap, l'ottimizzatore ritiene migliore unirsi prima alla #numbertabella e quindi alla AuditRelatedIdstabella.

Quando forziamo l'ordine OPTION (QUERYTRACEON 7497, QUERYTRACEON 7498, FORCE ORDER);possiamo vedere perché:

inserisci qui la descrizione dell'immagine

& inserisci qui la descrizione dell'immagine

Non bene


Rimuovere la possibilità di andare in parallelo con maxdop 1

Quando si aggiunge MAXDOP 1la query si esegue più velocemente, a thread singolo.

E aggiungendo questo indice

CREATE NONCLUSTERED INDEX [IX_AuditRelatedIdsRelatedId_AuditId] ON [dbo].[AuditRelatedIds]
(
    [RelatedId] ASC,
    [AuditId] ASC
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY];

inserisci qui la descrizione dell'immagine

Durante l'utilizzo di unisci unione. inserisci qui la descrizione dell'immagine

Lo stesso vale quando rimuoviamo il suggerimento della query dell'ordine di forzatura o non usiamo la tabella #Numbers e utilizziamo IN()invece.

Il mio consiglio sarebbe quello di esaminare l'aggiunta MAXDOP(1)e vedere se questo aiuta la tua query, con una riscrittura se necessario.

Ovviamente dovresti anche tenere a mente che dalla mia parte funziona ancora meglio grazie al filtro bitmap ottimizzato e usando effettivamente più thread con buoni risultati:

inserisci qui la descrizione dell'immagine

inserisci qui la descrizione dell'immagine


TL; DR

I costi stimati definiranno il piano scelto, sono stato in grado di replicare il comportamento e ho visto che optimized bitmap filters+ parallellismoperatori sono stati aggiunti dalla mia parte per eseguire la query in modo performante e veloce.

Potresti cercare di aggiungere MAXDOP(1)alla tua query come un modo per ottenere ogni volta lo stesso risultato controllato, con un merge joine nessun 'cattivo' parallellism.

L'aggiornamento a una versione più recente e l'utilizzo di una versione di stima della cardinalità più elevata di quanto CardinalityEstimationModelVersion="70"potrebbe anche aiutare.

Una tabella temporanea di numeri per eseguire il filtro multi valore può anche aiutare.


DML per quasi replicare il problema di OP

Ho trascorso più tempo su questo di quanto vorrei ammettere

set NOCOUNT ON;
DECLARE @I INT = 0
WHILE @I < 56
BEGIN
INSERT INTO  [dbo].[Audits] WITH(TABLOCK) 
([TargetTypeId],
    [TargetId],
    [TargetName],
    [Action],
    [ActionOverride] ,
    [Date] ,
    [UserDisplayName],
    [DescriptionData],
    [IsNotification]) 
SELECT top(500000) CASE WHEN ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 10000 = 30 then 29 ELSE ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 10000 END as rownum2 -- TILL 50 and no 30
,'bla','bla2',1,1,getdate(),'bla3','Bla4',1
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2;
SET @I +=1;
END

-- 'Bad Query matches'
INSERT INTO  [dbo].[AuditRelatedIds] WITH(TABLOCK)
    ([AuditId] ,
    [RelatedId]  ,
    [AuditTargetTypeId])
SELECT
TOP(25650)
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) as rownum1, 
('1DD87CF1-286B-409A-8C60-3FFEC394FDB1') , 
CASE WHEN ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 510 = 30 then 29 ELSE ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 510 END as rownum2 -- TILL 50 and no 30
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2

-- Extra matches with 30
SELECT MAX([Id]) FROM [dbo].[Audits];
--28000001 Upper value

INSERT INTO  [dbo].[Audits] WITH(TABLOCK) 
([TargetTypeId],
    [TargetId],
    [TargetName],
    [Action],
    [ActionOverride] ,
    [Date] ,
    [UserDisplayName],
    [DescriptionData],
    [IsNotification]) 
SELECT top(241829) 30 as rownum2 -- TILL 50 and no 30
,'bla','bla2',1,1,getdate(),'bla3','Bla4',1
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2;



;WITH CTE AS
(SELECT
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) as rownum1, 
('1DD87CF1-286B-409A-8C60-3FFEC394FDB1') as gu , 
30 as rownum2 -- TILL 50 and no 30
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2
CROSS APPLY master.dbo.spt_values spt3
)
--267479 - 25650 = 241829
INSERT INTO  [dbo].[AuditRelatedIds] WITH(TABLOCK)
    ([AuditId] ,
    [RelatedId]  ,
    [AuditTargetTypeId])

SELECT TOP(241829) rownum1,gu,rownum2 FROM CTE
WHERE rownum1 > 28000001
ORDER BY rownum1 ASC;

Spiegazione molto bella! L'aggiunta MAXDOP 0sembra averlo corretto. Grazie mille!
Chocoman,

1
MAXDOP 1 ** (errore di battitura)
Chocoman

@Chocoman Great! Happy to help :)
Randi Vertongen,

1

Da quello che posso dire la differenza principale tra i due piani è la differenza in quello che è il "Filtro primario".

Con la prima versione derivava il filtro principale che Audit.IDè correlato ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'quindi filtrare quell'elenco fino a quelli che Audit.TargetTypeIDerano nell'elenco.

Con la seconda versione derivava il filtro principale che Audit.IDè correlato all'elenco di Audit.TargetTypeID.

Dal momento che l'aggiunta di Audit.TargetTypeID = 30sembrava aumentare notevolmente il conteggio dei record (rispettivamente 267.479 e 25.650 secondo la domanda originale). Questo è probabilmente il motivo per cui i piani di esecuzione sono diversi. (A quanto ho capito) SQL proverà prima a svolgere la funzione più selettiva e successivamente applicherà il resto delle regole. Con la prima versione, AuditRelatedID.RelatedIDeseguire una query per poi trovare Audit.IDera probabilmente più selettivo rispetto al tentativo di utilizzare Audit.TargetTypeIDper trovare Audit.ID.

A credito di ypercube. Puoi sicuramente aggiornare [AuditRelatedIds].[IX_AuditRelatedIdsRelatedId_INCLUDES]per avere entrambi RelatedIDe AuditIDcome parte di in index invece di avere AuditIDcome parte di un INCLUDE. Non dovrebbe occupare spazio aggiuntivo sull'indice e consentirebbe di utilizzare entrambe le colonne nelle JOINclausole. Ciò può aiutare lo Strumento per ottimizzare le query a creare lo stesso piano di esecuzione per entrambe le query.

Operando con una logica simile, potrebbero esserci dei vantaggi per un indice su Auditcui sono contenuti TargetTypeID ASC, ID ASCi nodi di ordinamento / filtro effettivi (non come parte di INCLUDE). Ciò dovrebbe consentire all'ottimizzatore di query di filtrare e Audit.TargetTypeIDquindi unirsi rapidamente a AuditReferenceIds.AuditID. Ora questo potrebbe finire con entrambe le domande che scelgono il piano meno efficiente, quindi vorrei provarlo solo dopo aver provato la raccomandazione di ypercube.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.