Ottimizza la selezione sulla sottoquery con COALESCE (...)


8

Ho una visione ampia che utilizzo all'interno di un'applicazione. Penso di aver ridotto il mio problema di prestazioni, ma non sono sicuro di come risolverlo. Una versione semplificata della vista è simile alla seguente:

SELECT ISNULL(SEId + '-' + PEId, '0-0') AS Id,
   *,
   DATEADD(minute, Duration, EventTime) AS EventEndTime
FROM (
    SELECT se.SEId, pe.PEId,
        COALESCE(pe.StaffName, se.StaffName) AS StaffName, -- << Problem!
        COALESCE(pe.EventTime, se.EventTime) AS EventTime,
        COALESCE(pe.EventType, se.EventType) AS EventType,
        COALESCE(pe.Duration, se.Duration) AS Duration,
        COALESCE(pe.Data, se.Data) AS Data,
        COALESCE(pe.Field, se.Field) AS Field,
        pe.ThisThing, se.OtherThing
    FROM PE pe FULL OUTER JOIN SE se 
      ON pe.StaffName = se.StaffName
     AND pe.Duration = se.Duration
     AND pe.EventTime = se.EventTime
    WHERE NOT(pe.ThisThing = 1 AND se.OtherThing = 0)
) Z

Ciò probabilmente non giustifica l'intero motivo della struttura della query, ma forse ti dà un'idea: questa vista unisce due tabelle progettate molto male su cui non ho il controllo e cerca di sintetizzare alcune informazioni da esso.

Quindi, poiché questa è una vista utilizzata dall'applicazione, mentre provo a ottimizzare la avvolgo in un'altra SELEZIONA, in questo modo:

SELECT * FROM (
    -- … above code …
) Q
WHERE StaffName = 'SMITH, JOHN Q'

perché l'applicazione sta cercando membri dello staff specifici nel risultato.

Il problema sembra essere la COALESCE(pe.StaffName, se.StaffName) AS StaffNamesezione e che sto selezionando dalla vista in poi StaffName. Se lo cambio su pe.StaffName AS StaffNameo se.StaffName AS StaffName, i problemi di prestazioni scompaiono (ma vedi l'aggiornamento 2 di seguito) . Ma ciò non funzionerà perché una parte o l'altra FULL OUTER JOINpotrebbero mancare, quindi l'uno o l'altro campo potrebbe essere NULL.

Posso riformattare questo sostituendo il COALESCE(…)con qualcos'altro, che verrà riscritto nella sottoquery?

Altre note:

  • Ho già aggiunto alcuni indici per risolvere i problemi di prestazioni con il resto della query, senza COALESCEche sia molto veloce.
  • Con mia grande sorpresa, guardare al piano di esecuzione non solleva alcun flag, anche quando la subquery e la WHEREdichiarazione di wrapping sono incluse. Il mio costo totale di subquery nell'analizzatore è 0.0065736. Mah. Sono necessari quattro secondi per l'esecuzione.
  • Cambiare l'applicazione in modo diverso per interrogare (es. Tornare pe.StaffName AS PEStaffName, se.StaffName AS SEStaffNamee fare WHERE PEStaffName = 'X' OR SEStaffName = 'X') potrebbe funzionare, ma come ultima risorsa - Spero davvero di poter ottimizzare la vista senza dover ricorrere a toccare l'applicazione.
  • Una procedura memorizzata avrebbe probabilmente più senso per questo, ma l'applicazione è costruita con Entity Framework e non sono riuscito a capire come farlo funzionare bene con un SP che restituisce un tipo di tabella (un altro argomento interamente).

indici

Gli indici che ho aggiunto finora assomigliano a questo:

CREATE NONCLUSTERED INDEX [IX_PE_EventTime]
ON [dbo].[PE] ([EventTime])
INCLUDE ([StaffName],[Duration],[EventType],[Data],[Field],[ThisThing])

CREATE NONCLUSTERED INDEX [IX_SE_EventTime]
ON [dbo].[SE] ([EventTime])
INCLUDE ([StaffName],[Duration],[EventType],[Data],[Field],[OtherThing])

Aggiornare

Hmm ... Ho provato a simulare il cambiamento colpito sopra, e non ha aiutato. Cioè, prima ) Zsopra, ho aggiunto AND (pe.StaffName = 'SMITH, JOHN Q' OR se.StaffName = 'SMITH, JOHN Q'), ma le prestazioni sono le stesse. Ora non so davvero da dove cominciare.

Aggiornamento 2

Il commento di @ypercube sulla necessità del full join mi ha fatto capire che la mia query sintetizzata ha lasciato fuori un componente probabilmente importante. Mentre, sì, ho bisogno del join completo, il test che ho fatto sopra facendo cadere COALESCEe testando solo un lato del join per un valore non nullo renderebbe l'altro lato del join completo irrilevante e probabilmente l'ottimizzatore lo stava usando fatto per velocizzare la query. Inoltre, ho aggiornato l'esempio per mostrare che in StaffNamerealtà è una delle chiavi di join - che probabilmente ha un impatto significativo sulla domanda. Ora sto anche propendendo al suo suggerimento che suddividere questo in un'unione a tre invece che in unione completa possa essere la risposta e semplificherà l'abbondanza di cose COALESCEche sto facendo comunque. Provandolo ora.


Quali indici hai aggiunto? Includete StaffName nell'indice?
Mark Sinkinson,

@MarkSinkinson Ho un indice non cluster su ogni tabella KeyField, entrambi indicizza INCLUDEil StaffNamecampo e molti altri campi. Posso pubblicare le definizioni dell'indice nella domanda. Ci sto lavorando su un server di prova in modo da poter aggiungere tutti gli indici che ritieni possano essere utili da provare!
S'pht'Kr

1
Si ha la WHERE pe.ThisThing = 1 AND se.OtherThing = 0condizione che annulla il FULL OUTERjoin e rende la query equivalente a un join interno. Sei sicuro di aver bisogno di un abbonamento COMPLETO?
ypercubeᵀᴹ

@ypercube Mi dispiace, è stata una cattiva codifica aerea da parte mia, il punto è più che ho condizioni su entrambe le tabelle, ma sì, conto per i null su entrambi i lati nella query reale. Sto unendo le due tabelle e cerco corrispondenze, ma ho bisogno dei dati disponibili da entrambe le tabelle quando non c'è un record corrispondente a sinistra o a destra - quindi sì, ho bisogno del join completo.
S'pht'Kr

1
Un pensiero: è una cosa a lungo termine ma puoi provare a dividere la query interna in tre parti ( INNER JOIN, LEFT JOINcon WHERE IS NULLsegno di spunta, DESTRA UNISCITI con IS NULL) e poi UNION ALLle tre parti. In questo modo non sarà necessario utilizzarlo COALESCE()e potrebbe (potrebbe) aiutare l'ottimizzatore a capire la riscrittura.
ypercubeᵀᴹ

Risposte:


4

Questo è stato piuttosto lungo ma poiché l'OP dice che ha funzionato, lo sto aggiungendo come risposta (sentiti libero di correggerlo se trovi qualcosa di sbagliato).

Prova a suddividere la query interna in tre parti ( INNER JOIN, LEFT JOINcon WHERE IS NULLsegno di spunta, RIGHT JOINcon IS NULLsegno di spunta) e quindi UNION ALLle tre parti. Questo ha i seguenti vantaggi:

  • L'ottimizzatore ha meno opzioni di trasformazione disponibili per i FULLjoin rispetto a (i più comuni) INNERe i LEFTjoin.

  • La Ztabella derivata può essere rimossa (puoi farlo comunque) dalla definizione della vista.

  • La NOT(pe.ThisThing = 1 AND se.OtherThing = 0)saranno necessari solo sulla INNERparte unirsi.

  • Miglioramento minore, l'utilizzo COALESCE()sarà minimo se del caso (l'ho ipotizzato se.SEIde pe.PEIdnon è nulla. Se più colonne non sono nulla, sarai in grado di rimuovere più COALESCE()chiamate.)
    Più importante, l'ottimizzatore può ridurre qualsiasi condizione in le tue query che coinvolgono queste colonne (ora che COALESCE()non sta bloccando il push.)

  • Tutto quanto sopra fornirà all'ottimizzatore più opzioni per trasformare / riscrivere qualsiasi query che utilizza la vista in modo che possa trovare un piano di esecuzione che possa essere utilizzato indici nelle tabelle sottostanti.

In tutto, la vista può essere scritta come:

SELECT 
    se.SEId + '-' + pe.PEId AS Id,
    se.SEId, pe.PEId,
    pe.StaffName, 
    pe.EventTime,
    COALESCE(pe.EventType, se.EventType) AS EventType,
    pe.Duration,
    COALESCE(pe.Data, se.Data) AS Data,
    COALESCE(pe.Field, se.Field) AS Field,
    pe.ThisThing, se.OtherThing,
    DATEADD(minute, pe.Duration, pe.EventTime) AS EventEndTime
FROM PE pe INNER JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (pe.ThisThing = 1 AND se.OtherThing = 0) 

UNION ALL

SELECT 
    '0-0',
    NULL, pe.PEId,
    pe.StaffName, 
    pe.EventTime,
    pe.EventType,
    pe.Duration,
    pe.Data,
    pe.Field,
    pe.ThisThing, NULL,
    DATEADD(minute, pe.Duration, pe.EventTime) AS EventEndTime
FROM PE pe LEFT JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (pe.ThisThing = 1)
  AND se.StaffName IS NULL

UNION ALL

SELECT 
    '0-0',
    se.SEId, NULL,
    se.StaffName, 
    se.EventTime,
    se.EventType,
    se.Duration,
    se.Data,
    se.Field,
    NULL, se.OtherThing, 
    DATEADD(minute, se.Duration, se.EventTime) AS EventEndTime
FROM PE pe RIGHT JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (se.OtherThing = 0)
  AND pe.StaffName IS NULL ;

0

La mia intuizione sarebbe che questo non dovrebbe essere un problema in quanto, al momento COALESCE(pe.StaffName, se.StaffName) AS StaffName, qualsiasi cosa tutte le righe delle due fonti dovrebbero essere già state inserite e abbinate, quindi la chiamata di funzione è un semplice confronto in memoria con null-e- pick. Ovviamente questo non è il caso, quindi forse qualcosa in una delle fonti (se sono viste o tabelle derivate incorporate) o le tabelle di base (cioè la mancanza di indici) sta facendo pensare al pianificatore di query di dover scansionare queste colonne separatamente.

Senza ulteriori dettagli sulla query esatta in esecuzione, sulle strutture di supporto e sui piani di query prodotti, tutto ciò che suggeriamo è congettura.

Per provare a forzare il confronto da eseguire dopo tutto il resto, potresti provare a selezionare entrambi i valori nella tabella derivata ( pe.StaffName AS pe.StaffName, se.StaffName AS seStaffName), quindi fare la scelta nella query esterna ( COALESCE(peStaffName, seStaffName) AS StaffName), oppure puoi persino inserire i dati dalla query interna in una tabella temporanea esegue quindi la query esterna selezionando da quella (ma ciò richiederebbe una procedura memorizzata e, a seconda del numero di righe, questo dump-to-tempdb potrebbe essere costoso e quindi problematico a sé stante).


Grazie David, sto sbagliando dalla parte della paranoia su quanto dovrei rivelare su questo anche per quanto riguarda la struttura (pe => PatientEvent, quindi ...) ma so che lo rende più difficile. Penso che stia effettivamente facendo il join in base agli indici e quindi facendo un "semplice confronto in memoria" per filtrare ... ma la tabella derivata non filtrata al Zmomento torna con ~ 1,5m righe. Quello che voglio che sia fare è riscrivere quel predicato nella query per Zcosì userà gli indici ... ma ora sono anche confuso perché quando inserisco manualmente il predicato, non usa ancora un indice ... così ora Non ne sono sicuro.
S'pht'Kr
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.