Perché l'aggiunta di una TOP 1 peggiora notevolmente le prestazioni?


39

Ho una domanda abbastanza semplice

SELECT TOP 1 dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER

Questo mi sta dando prestazioni orribili (come non si è mai preso la briga di aspettare che finisca). Il piano di query è simile al seguente:

inserisci qui la descrizione dell'immagine

Tuttavia se rimuovo TOP 1ottengo un piano simile a questo e viene eseguito in 1-2 secondi:

inserisci qui la descrizione dell'immagine

PK e indicizzazione corretti di seguito.

Il fatto che il TOP 1piano di query sia stato modificato non mi sorprende, sono solo un po 'sorpreso che lo peggiori molto.

Nota: ho letto i risultati di questo post e ho compreso il concetto di un Row Goalecc. Ciò di cui sono curioso è come posso fare per modificare la query in modo che utilizzi il piano migliore. Attualmente sto scaricando i dati in una tabella temporanea, quindi estraendo la prima riga da esso. Mi chiedo se esiste un metodo migliore.

Modifica Per le persone che leggono questo dopo il fatto, ecco alcune informazioni in più.

  • Document_Queue - PK / CI è D_ID e ha ~ 5k righe.
  • Correspondence_Journal - PK / CI è FILE_NUMBER, CORRESPONDENCE_ID e ha ~ 1,4 milioni di righe.

Quando ho iniziato non c'erano altri indici. Ho finito con uno su Correspondence_Journal (Document_Id, File_Number)


1
Hai un vincolo di chiave esterna che impone la DOCUMENT_IDrelazione tra le due tabelle (o ogni record in CORRESPONDENCE_JOURNALha un record corrispondente in DOCUMENT_QUEUE)?
Daniel Hutmacher,

Risposte:


28

Prova a forzare un hash join *

SELECT TOP 1 
       dc.DOCUMENT_ID,
       dc.COPIES,
       dc.REQUESTOR,
       dc.D_ID,
       cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
INNER HASH JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
       AND dc.QUEUE_DATE <= GETDATE()
       AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER

L'ottimizzatore probabilmente pensava che un loop sarebbe stato migliore con la top 1 e quel tipo di senso ha senso, ma in realtà non ha funzionato qui. Solo un'ipotesi qui, ma forse il costo stimato di quel rocchetto era spento - usa TEMPDB - potresti avere un TEMPDB scarsamente performante.


* Prestare attenzione con i suggerimenti di join , poiché impongono all'ordine di accesso alle tabelle del piano di corrispondere all'ordine scritto delle tabelle nella query (proprio come se OPTION (FORCE ORDER)fosse stato specificato). Dal collegamento alla documentazione:

Estratto di BOL

Questo potrebbe non produrre alcun effetto indesiderato nell'esempio, ma in generale potrebbe benissimo. FORCE ORDER(implicito o esplicito) è un suggerimento molto potente che va oltre l'ordine di esecuzione; impedisce l'applicazione di una vasta gamma di tecniche di ottimizzazione, tra cui aggregazioni parziali e riordino.

Un suggerimento per la OPTION (HASH JOIN) query può essere meno invadente in casi appropriati, poiché ciò non implica FORCE ORDER. Si applica, tuttavia, a tutti i join nella query. Sono disponibili altre soluzioni.


1
Sembra la risposta corretta e l'unica differenza tra esso e il piano più semplice era un ordinamento aggiuntivo nella parte anteriore.
Kenneth Fisher,

3
Non sono sicuro che questa risposta mi piaccia. I suggerimenti sui join sono molto invasivi. È necessario provare prima alcune semplici modifiche all'indicizzazione, ad esempio l'indice nella colonna della data.
usr

@usr È un semplice join PK che viene eseguito in meno di un secondo. Scommessa abbastanza sicura qui.
paparazzo,

4
Nel forzare un hash join, stai forzando una scansione della tabella di grandi dimensioni. Ci sono opzioni migliori.
Rob Farley,

30

Dal momento che ottieni il piano corretto con il ORDER BY, forse potresti semplicemente rotolare il tuo TOPoperatore?

SELECT DOCUMENT_ID, COPIES, REQUESTOR, D_ID, FILE_NUMBER
FROM (
    SELECT dc.DOCUMENT_ID,
           dc.COPIES,
           dc.REQUESTOR,
           dc.D_ID,
           cj.FILE_NUMBER,
           ROW_NUMBER() OVER (ORDER BY cj.FILE_NUMBER) AS _rownum
    FROM DOCUMENT_QUEUE dc
    INNER JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
    WHERE dc.QUEUE_DATE <= GETDATE()
      AND dc.PRINT_LOCATION = 2
) AS sub
WHERE _rownum=1;

Nella mia mente, il piano di query per quanto ROW_NUMBER()sopra dovrebbe essere lo stesso di se avessi un ORDER BY. Il piano di query dovrebbe ora avere un segmento, un progetto di sequenza e infine un operatore di filtro, il resto dovrebbe apparire proprio come il tuo buon piano.


3
In realtà, mentre forniva all'operatore principale (e un sacco di altre cose (un progetto di sequenza, un segmento e un ordinamento)) continuava a funzionare per un secondo. Darò la risposta corretta a @frisbee anche se dal suo primo ed è più semplice. Ottima risposta però.
Kenneth Fisher,

10
@KennethFisher, la risposta del frisbee è più semplice, ma nel modo in cui una mazza guida un chiodo di finitura più semplicemente di un martello standard. Inoltre comporta molti rischi, soprattutto se lasciati in posizione per il lungo raggio. Non userei suggerimenti del genere se non nei test o forse, FORSE un'eccezione marginale.
Steve Mangiameli,

@SteveMangiameli In questo caso particolare c'è solo un join, quindi un certo numero di preoccupazioni scompare. Sono consapevole dei rischi derivanti dall'utilizzo di un suggerimento join (o suggerimento query). Penso solo che sia giustificato in questo caso.
Kenneth Fisher,

5
@KennethFisher Imo, il rischio principale di suggerimenti per le query è che quando i tuoi dati crescono o cambiano, il piano di query che imponi può peggiorare rispetto a quello che il sistema avrebbe trovato da solo. Hai già visto come un piccolo errore nel piano può influire seriamente sulle prestazioni. L'uso di un suggerimento in produzione sta dichiarando: "So che questo piano sarà sempre, sempre il migliore, perché comprendo così a fondo il pianificatore e come i miei dati si comporteranno per tutta la durata di questa query in produzione". Non sono mai stato così sicuro di una domanda.
jpmc26,

29

Modifica: +1 funziona in questa situazione perché si scopre che FILE_NUMBERè una versione di stringa con riempimento zero di un numero intero. Una soluzione migliore qui per le stringhe è quella di aggiungere ''(la stringa vuota), poiché l'aggiunta di un valore può influire sull'ordine o per i numeri di aggiungere qualcosa che è una costante ma contiene una funzione non deterministica, comesign(rand()+1) . L'idea di "rompere l'ordinamento" è ancora valida qui, è solo che il mio metodo non era l'ideale.

+1

No, non intendo che sono d'accordo con nulla, intendo questo come soluzione. Se si modifica la query inORDER BY cj.FILE_NUMBER + 1 allora TOP 1si comporterà diversamente.

Vedete, con l'obiettivo di una piccola riga in atto per una query ordinata, il sistema proverà a consumare i dati in ordine, per evitare di avere un operatore di ordinamento. Eviterà anche di costruire una tabella hash, immaginando che probabilmente non deve fare troppo lavoro per trovare quella prima riga. Nel tuo caso, questo è sbagliato - dallo spessore di quelle frecce, sembra che debba consumare molti dati per trovare una singola corrispondenza.

Lo spessore di quelle frecce suggerisce che la DOCUMENT_QUEUEtabella (DQ) è molto più piccola della CORRESPONDENCE_JOURNALtabella (CJ). E che il piano migliore sarebbe effettivamente quello di controllare le righe DQ fino a quando non viene trovata una riga CJ. In effetti, questo è ciò che farebbe Query Optimizer (QO) se non avesse questo fastidioso problema ORDER BY, che è ben supportato da un indice di copertura su CJ.

Quindi, se abbandoni ORDER BYcompletamente, mi aspetto che otterrai un piano che includa un ciclo annidato, ripetendo le righe in DQ, cercando in CJ per assicurarsi che la riga esista. E con TOP 1questo, questo si fermerebbe dopo che una singola fila era stata tirata.

Ma se in realtà hai bisogno della prima riga in FILE_NUMBERordine, allora potresti indurre il sistema a ignorare quell'indice che (in modo errato) sembra essere così utile, facendo ORDER BY CJ.FILE_NUMBER+1- che sappiamo manterrà lo stesso ordine di prima, ma soprattutto il QO non lo fa. Il QO si concentrerà sulla realizzazione dell'intero set, in modo tale che un operatore Top N Sort possa essere soddisfatto. Questo metodo dovrebbe produrre un piano che contenga un operatore di calcolo scalare per calcolare il valore per l'ordinamento e un operatore di ordinamento Top N per ottenere la prima riga. Ma a destra di questi, dovresti vedere un bel Nested Loop, facendo un sacco di ricerche su CJ. E prestazioni migliori rispetto a una grande tabella di righe che non corrisponde a nulla in DQ.

L'Hash Match non è necessariamente terribile, ma se l'insieme di righe che stai tornando da DQ è molto più piccolo di CJ (come mi aspetterei che fosse), allora Hash Match scansionerà molto più CJ del necessario.

Nota: ho usato +1 anziché +0 perché è probabile che Query Optimizer riconosca che +0 non cambi nulla. Certo, la stessa cosa potrebbe valere per il +1, se non ora, poi ad un certo punto in futuro.


7

Ho letto i risultati di questo post e ho compreso il concetto di obiettivo di riga, ecc. Ciò di cui sono curioso è come posso fare per modificare la query in modo che utilizzi il piano migliore

L'aggiunta OPTION (QUERYTRACEON 4138)disattiva l'effetto degli obiettivi di riga solo per quella query, senza essere eccessivamente prescrittivi sul piano finale e probabilmente sarà il modo più semplice / diretto.

Se l'aggiunta di questo suggerimento genera un errore di autorizzazione (obbligatorio per DBCC TRACEON), è possibile applicarlo utilizzando una guida di piano:

Utilizzo QUERYTRACEONnelle guide di piano di spaghettidba

... o semplicemente usa una procedura memorizzata:

Quali autorizzazioni sono QUERYTRACEONnecessarie? di Kendra Little


3

Le versioni più recenti di SQL Server offrono opzioni diverse (e probabilmente migliori) per gestire le query che ottengono prestazioni non ottimali quando l'ottimizzatore è in grado di applicare le ottimizzazioni degli obiettivi di riga. In SQL Server 2016 SP1 è stato introdotto il risultato DISABLE_OPTIMIZER_ROWGOAL USE HINTche ha lo stesso effetto del flag di traccia 4138. Se non si utilizza quella versione, è possibile prendere in considerazione l'utilizzo del OPTIMIZE FORsuggerimento per ottenere un piano di query progettato per restituire tutte le righe anziché solo 1. La query seguente restituirà gli stessi risultati di quello nella domanda ma non verrà creato con l'obiettivo di ottenere solo 1 riga.

DECLARE @top INT = 1;

SELECT TOP (@top) dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER
OPTION (OPTIMIZE FOR (@top = 987654321));

2

Dato che stai facendo un TOP(1), ti consiglio di fare il ORDER BYdeterministico per iniziare. Per lo meno, ciò garantirà risultati funzionalmente prevedibili (sempre utili per i test di regressione). Sembra che tu debba aggiungere DC.D_IDe CJ.CORRESPONDENCE_IDper quello.

Quando osservo i piani di query, a volte trovo istruttivo semplificare la query: eventualmente selezionare tutte le righe CC rilevanti in una tabella temporanea in anticipo, per eliminare i problemi con la stima della cardinalità su QUEUE_DATEe PRINT_LOCATION. Questo dovrebbe essere veloce dato il conteggio delle righe basso. È quindi possibile aggiungere indici a questa tabella temporanea, se necessario, senza modificare la tabella permanente.

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.