Inclusione di ORDER BY su query che non restituisce righe che influisce drasticamente sulle prestazioni


15

Dato un semplice join a tre tabelle, le prestazioni della query cambiano drasticamente quando ORDER BY è incluso anche senza restituire righe. Lo scenario del problema effettivo impiega 30 secondi per restituire zero righe ma è istantaneo quando ORDER BY non è incluso. Perché?

SELECT * 
FROM tinytable t                          /* one narrow row */
JOIN smalltable s on t.id=s.tinyId        /* one narrow row */
JOIN bigtable b on b.smallGuidId=s.GuidId /* a million narrow rows */
WHERE t.foreignId=3                       /* doesn't match */
ORDER BY b.CreatedUtc          /* try with and without this ORDER BY */

Capisco che avrei potuto avere un indice su bigtable.smallGuidId, ma credo che in questo caso peggiorerebbe la situazione.

Ecco lo script per creare / popolare le tabelle per il test. Curiosamente, sembra importare che il smalltable abbia un campo nvarchar (max). Sembra anche che mi unisca al bigtable con un guid (che immagino gli faccia venir voglia di usare l'hash matching).

CREATE TABLE tinytable
  (
     id        INT PRIMARY KEY IDENTITY(1, 1),
     foreignId INT NOT NULL
  )

CREATE TABLE smalltable
  (
     id     INT PRIMARY KEY IDENTITY(1, 1),
     GuidId UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
     tinyId INT NOT NULL,
     Magic  NVARCHAR(max) NOT NULL DEFAULT ''
  )

CREATE TABLE bigtable
  (
     id          INT PRIMARY KEY IDENTITY(1, 1),
     CreatedUtc  DATETIME NOT NULL DEFAULT GETUTCDATE(),
     smallGuidId UNIQUEIDENTIFIER NOT NULL
  )

INSERT tinytable
       (foreignId)
VALUES(7)

INSERT smalltable
       (tinyId)
VALUES(1)

-- make a million rows 
DECLARE @i INT;

SET @i=20;

INSERT bigtable
       (smallGuidId)
SELECT GuidId
FROM   smalltable;

WHILE @i > 0
  BEGIN
      INSERT bigtable
             (smallGuidId)
      SELECT smallGuidId
      FROM   bigtable;

      SET @i=@i - 1;
  END 

Ho provato su SQL 2005, 2008 e 2008R2 con gli stessi risultati.

Risposte:


32

Concordo con la risposta di Martin Smith, ma il problema non è semplicemente quello delle statistiche, esattamente. Le statistiche per la colonna foreignId (presupponendo che le statistiche automatiche siano abilitate) mostrano accuratamente che non esistono righe per un valore di 3 (ce n'è solo una, con un valore di 7):

DBCC SHOW_STATISTICS (tinytable, foreignId) WITH HISTOGRAM

uscita statistica

SQL Server sa che le cose potrebbero essere cambiate da quando sono state acquisite le statistiche, quindi potrebbe esserci una riga per il valore 3 quando viene eseguito il piano . Inoltre, potrebbe trascorrere un periodo di tempo tra la compilazione del piano e l'esecuzione (dopo tutto i piani vengono memorizzati nella cache per il riutilizzo). Come afferma Martin, SQL Server contiene una logica per rilevare quando sono state apportate modifiche sufficienti per giustificare la ricompilazione di qualsiasi piano memorizzato nella cache per motivi di ottimalità.

Nulla di tutto ciò alla fine conta, comunque. Con un'eccezione edge case, l'ottimizzatore non stimerà mai il numero di righe prodotte da un'operazione di tabella su zero. Se può determinare staticamente che l'output deve essere sempre zero righe, l'operazione è ridondante e verrà rimossa completamente.

Il modello dell'ottimizzatore stima invece un minimo di una riga. L'utilizzo di questa euristica tende a produrre piani migliori in media rispetto a quanto accadrebbe se fosse possibile una stima più bassa. Un piano che produce una stima a zero righe a un certo punto sarebbe inutile da quel momento in poi nel flusso di elaborazione, poiché non vi sarebbe alcuna base per prendere decisioni basate sui costi (zero righe sono zero righe, non importa quale). Se la stima risulta errata, la forma del piano sopra la stima della riga zero non ha quasi alcuna possibilità di essere ragionevole.

Il secondo fattore è un altro presupposto modellistico chiamato Assunzione di contenimento. Questo in sostanza dice che se una query unisce un intervallo di valori con un altro intervallo di valori, è perché gli intervalli si sovrappongono. Un altro modo per dirlo è dire che il join viene specificato perché si prevede che le righe vengano restituite. Senza questo ragionamento, i costi sarebbero generalmente sottostimati, con conseguenti piani inadeguati per una vasta gamma di query comuni.

In sostanza, quello che hai qui è una query che non si adatta al modello dell'ottimizzatore. Non c'è niente che possiamo fare per "migliorare" le stime con indici multi-colonna o filtrati; non c'è modo di ottenere una stima inferiore a 1 riga qui. Un vero database potrebbe avere chiavi esterne per garantire che questa situazione non possa insorgere, ma supponendo che non sia applicabile qui, ci resta che utilizzare i suggerimenti per correggere la condizione fuori modello. Qualsiasi numero di approcci di suggerimento diversi funzionerà con questa query. OPTION (FORCE ORDER)è uno che funziona bene con la query come scritto.


21

Il problema di base qui è una delle statistiche.

Per entrambe le query, il conteggio delle righe stimato mostra che ritiene che il finale SELECTrestituirà 1.048.580 righe (lo stesso numero di righe in cui si stima che esista bigtable) anziché lo 0 che ne consegue effettivamente.

Entrambe le tue JOINcondizioni corrispondono e preserverebbero tutte le righe. Finiscono per essere eliminati perché la riga singola in tinytablenon corrisponde al t.foreignId=3predicato.

Se corri

SELECT * 
FROM tinytable t  
WHERE t.foreignId=3  AND id=1 

e guarda il numero stimato di righe che è 1piuttosto che 0e questo errore si propaga in tutto il piano. tinytableattualmente contiene 1 riga. Le statistiche non verrebbero ricompilate per questa tabella fino a quando non si fossero verificate modifiche di 500 righe in modo da poter aggiungere una riga corrispondente e non attivare una ricompilazione.

Il motivo per cui l'Ordine di join cambia quando si aggiunge la ORDER BYclausola e è presente una varchar(max)colonna smalltableè perché stima che le varchar(max)colonne aumenteranno la dimensione delle righe di 4.000 byte in media. Moltiplicalo per 1048580 righe e ciò significa che l'operazione di ordinamento avrebbe bisogno di una stima di 4 GB, quindi decide sensatamente di eseguire l' SORToperazione prima di JOIN.

È possibile forzare la ORDER BYquery ad adottare la ORDER BYstrategia di non join con l'uso di suggerimenti come di seguito.

SELECT *
FROM   tinytable t /* one narrow row */
       INNER MERGE JOIN smalltable s /* one narrow row */
                        INNER LOOP JOIN bigtable b
                          ON b.smallGuidId = s.GuidId /* a million narrow rows */
         ON t.id = s.tinyId
WHERE  t.foreignId = 3 /* doesn't match */
ORDER  BY b.CreatedUtc
OPTION (MAXDOP 1) 

Il piano mostra un operatore di ordinamento con un costo sotto l'albero 12,000stimato di conteggi delle righe quasi errati e dimensioni dei dati stimate.

Piano

A proposito non ho trovato la sostituzione delle UNIQUEIDENTIFIERcolonne con quelle intere cose alterate nel mio test.


2

Attiva il pulsante Mostra piano di esecuzione e puoi vedere cosa sta succedendo. Ecco il piano per la query "lenta": inserisci qui la descrizione dell'immagine

Ed ecco la query "veloce": inserisci qui la descrizione dell'immagine

Guarda che, eseguiti insieme, la prima query è ~ 33 volte più "costosa" (rapporto 97: 3). SQL sta ottimizzando la prima query per ordinare la BigTable in base al datetime, quindi eseguendo un piccolo ciclo "cerca" su SmallTable e TinyTable, eseguendoli 1 milione di volte ciascuno (è possibile passare il mouse sopra l'icona "Ricerca indice cluster" per ottenere più statistiche). Quindi, l'ordinamento (27%) e 2 x 1 milione di "ricerche" su piccoli tavoli (23% e 46%) sono la maggior parte della query costosa. In confronto, la non ORDER BYquery esegue un totale complessivo di 3 scansioni.

Fondamentalmente, hai trovato un buco nella logica dell'ottimizzatore SQL per il tuo particolare scenario. Ma come affermato da TysHTTP, se aggiungi un indice (che rallenta l'inserimento / aggiorna un po '), la tua scansione diventa pazza velocemente.


2

Quello che sta succedendo è che SQL sta decidendo di eseguire l'ordine prima della restrizione.

Prova questo:

SELECT *
(
SELECT * 
FROM tinytable t
    INNER JOIN smalltable s on t.id=s.tinyId
    INNER JOIN bigtable b on b.smallGuidId=s.GuidId
WHERE t.foreignId=3
) X
ORDER BY b.CreatedUtc

Questo ti dà le prestazioni migliorate (in questo caso in cui il conteggio dei risultati restituiti è molto piccolo), senza avere il successo delle prestazioni aggiungendo un altro indice. Sebbene sia strano quando l'ottimizzatore SQL decide di eseguire l'ordine prima del join, è probabile che se in realtà avessi i dati di ritorno, ordinarli dopo i join richiederebbe più tempo dell'ordinamento senza.

Infine, prova a eseguire il seguente script e vedi se le statistiche e gli indici aggiornati risolvono il problema che stai riscontrando:

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "

EXEC [sp_MSforeachtable] @command1="RAISERROR('DBCC DBREINDEX(''?'') ...',10,1) WITH NOWAIT DBCC DBREINDEX('?')"

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "

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.