Database
 sql >> Database >  >> RDS >> Database

Prestare attenzione alle stime

La scorsa settimana ho pubblicato un post chiamato #BackToBasics :DATEFROMPARTS() , dove ho mostrato come utilizzare questa funzione 2012+ per query di intervalli di date più pulite e modificabili. L'ho usato per dimostrare che se si utilizza un predicato di data aperto e si dispone di un indice nella colonna data/ora pertinente, è possibile ottenere un utilizzo dell'indice molto migliore e un I/O inferiore (o, nel peggiore dei casi , lo stesso, se una ricerca non può essere utilizzata per qualche motivo, o se non esiste un indice adatto):

Ma questa è solo una parte della storia (e per essere chiari, DATEFROMPARTS() non è tecnicamente richiesto per ottenere una ricerca, è solo più pulito in quel caso). Se rimpiccioliamo un po', notiamo che le nostre stime sono tutt'altro che accurate, una complessità che non volevo introdurre nel post precedente:

Questo non è raro sia per i predicati di disuguaglianza che con le scansioni forzate. E, naturalmente, il metodo che ho suggerito non produrrebbe le statistiche più imprecise? Ecco l'approccio di base (puoi ottenere lo schema della tabella, gli indici e i dati di esempio dal mio post precedente):

CREATE PROCEDURE dbo.MonthlyReport_Original
  @Year  int,
  @Month int
AS
BEGIN
  SET NOCOUNT ON;
  DECLARE @Start date = DATEFROMPARTS(@Year, @Month, 1);
  DECLARE @End   date = DATEADD(MONTH, 1, @Start);
 
  SELECT DateColumn 
    FROM dbo.DateEntries
    WHERE DateColumn >= @Start
      AND DateColumn <  @End;
END
GO

Ora, stime imprecise non saranno sempre un problema, ma possono causare problemi con scelte di piani inefficienti ai due estremi. Un singolo piano potrebbe non essere ottimale quando l'intervallo scelto produrrà una percentuale molto piccola o molto grande della tabella o dell'indice e questo può essere molto difficile da prevedere per SQL Server quando la distribuzione dei dati non è uniforme. Joseph Sack ha delineato le cose più tipiche che le stime sbagliate possono influenzare nel suo post "Dieci minacce comuni alla qualità del piano di esecuzione:"

"[…] stime di riga errate possono influire su una varietà di decisioni, tra cui la selezione dell'indice, le operazioni di ricerca rispetto a scansione, l'esecuzione parallela rispetto a quella seriale, la selezione dell'algoritmo di join, la selezione di join fisico interno rispetto a quello esterno (ad es. build vs. probe), la generazione di spool, ricerche di segnalibri rispetto all'accesso completo a cluster o tabella heap, selezione di flussi o aggregati hash e se una modifica dei dati utilizza o meno un piano ampio o ristretto".

Ce ne sono anche altri, come le concessioni di memoria che sono troppo grandi o troppo piccole. Continua descrivendo alcune delle cause più comuni di stime errate, ma la causa principale in questo caso manca dalla sua lista:le stime. Perché stiamo usando una variabile locale per cambiare il int in entrata parametri a una singola date locale variabile, SQL Server non sa quale sarà il valore, quindi effettua ipotesi standardizzate di cardinalità in base all'intera tabella.

Abbiamo visto sopra che la stima per il mio approccio suggerito era di 5.170 righe. Ora sappiamo che con un predicato di disuguaglianza e con SQL Server che non conosce i valori dei parametri, indovinerà il 30% della tabella. 31,645 * 0.3 non è 5.170. Né 31,465 * 0.3 * 0.3 , quando ricordiamo che in realtà esistono due predicati che lavorano sulla stessa colonna. Quindi da dove viene questo valore di 5.170?

Come descrive Paul White nel suo post "Stima della cardinalità per predicati multipli", il nuovo stimatore di cardinalità in SQL Server 2014 utilizza il backoff esponenziale, quindi moltiplica il conteggio delle righe della tabella (31.465) per la selettività del primo predicato (0,3) , quindi lo moltiplica per la radice quadrata della selettività del secondo predicato (~0,547723).

31.645 * (0,3) * SQRT(0,3) ~=5.170,227

Quindi, ora possiamo vedere dove è arrivato SQL Server con la sua stima; quali sono alcuni dei metodi che possiamo usare per fare qualcosa al riguardo?

  • Passa i parametri della data. Quando possibile, puoi modificare l'applicazione in modo che trasmetta parametri di data appropriati anziché parametri interi separati.
  • Utilizzare una procedura wrapper. Una variazione del metodo n. 1, ad esempio se non è possibile modificare l'applicazione, consiste nel creare una seconda stored procedure che accetti i parametri della data costruita dalla prima.
  • Usa OPTION (RECOMPILE) . Con un leggero costo di compilazione ogni volta che viene eseguita la query, ciò costringe SQL Server a eseguire l'ottimizzazione in base ai valori presentati ogni volta, invece di ottimizzare un singolo piano per valori di parametro sconosciuti, primi o medi. (Per una trattazione approfondita di questo argomento, vedere "Parameter Sniffing, Embedding, and the RECOMPILE Options" di Paul White.
  • Utilizza SQL dinamico. Avere SQL dinamico accetta la date costruita variabile forza la parametrizzazione corretta (proprio come se avessi chiamato una procedura memorizzata con un date parametro), ma è un po' brutto e più difficile da mantenere.
  • Disordine con suggerimenti e contrassegni di traccia. Paul White ne parla nel post di cui sopra.
  • Non ho intenzione di suggerire che questo sia un elenco esaustivo, e non ho intenzione di ribadire il consiglio di Paul su suggerimenti o flag di traccia, quindi mi concentrerò solo sul mostrare come i primi quattro approcci possono mitigare il problema con stime errate .

      1. Parametri della data

      CREATE PROCEDURE dbo.MonthlyReport_TwoDates
        @Start date,
        @End   date
      AS
      BEGIN
        SET NOCOUNT ON;
       
        SELECT /* Two Dates */ DateColumn
          FROM dbo.DateEntries
          WHERE DateColumn >= @Start
            AND DateColumn <  @End;
      END
      GO

      2. Procedura di confezionamento

      CREATE PROCEDURE dbo.MonthlyReport_WrapperTarget
        @Start date,
        @End   date
      AS
      BEGIN
        SET NOCOUNT ON;
       
        SELECT /* Wrapper */ DateColumn
          FROM dbo.DateEntries
          WHERE DateColumn >= @Start
            AND DateColumn <  @End;
      END
      GO
       
      CREATE PROCEDURE dbo.MonthlyReport_WrapperSource
        @Year  int,
        @Month int
      AS
      BEGIN
        SET NOCOUNT ON;
        DECLARE @Start date = DATEFROMPARTS(@Year, @Month, 1);
        DECLARE @End   date = DATEADD(MONTH, 1, @Start);
       
        EXEC dbo.MonthlyReport_WrapperTarget @Start = @Start, @End = @End;
      END
      GO

      3. OPZIONE (RICIMPILA)

      CREATE PROCEDURE dbo.MonthlyReport_Recompile
        @Year  int,
        @Month int
      AS
      BEGIN
        SET NOCOUNT ON;
        DECLARE @Start date = DATEFROMPARTS(@Year, @Month, 1);
        DECLARE @End   date = DATEADD(MONTH, 1, @Start);
       
        SELECT /* Recompile */ DateColumn
          FROM dbo.DateEntries
            WHERE DateColumn >= @Start
            AND DateColumn < @End OPTION (RECOMPILE);
      END
      GO

      4. SQL dinamico

      CREATE PROCEDURE dbo.MonthlyReport_DynamicSQL
        @Year  int,
        @Month int
      AS
      BEGIN
        SET NOCOUNT ON;
        DECLARE @Start date = DATEFROMPARTS(@Year, @Month, 1);
        DECLARE @End   date = DATEADD(MONTH, 1, @Start);
       
        DECLARE @sql nvarchar(max) = N'SELECT /* Dynamic SQL */ DateColumn
          FROM dbo.DateEntries
          WHERE DateColumn >= @Start
          AND DateColumn < @End;';
       
        EXEC sys.sp_executesql @sql, N'@Start date, @End date', @Start, @End;
      END
      GO

    Le prove

    Con i quattro insiemi di procedure in atto, è stato facile costruire test che mi avrebbero mostrato i piani e le stime derivate da SQL Server. Poiché alcuni mesi sono più impegnativi di altri, ho scelto tre mesi diversi e li ho eseguiti tutti più volte.

    DECLARE @Year  int = 2012, @Month int = 7; -- 385 rows
    DECLARE @Start date = DATEFROMPARTS(@Year, @Month, 1);
    DECLARE @End   date = DATEADD(MONTH, 1, @Start);
     
    EXEC dbo.MonthlyReport_Original      @Year  = @Year, @Month = @Month;
    EXEC dbo.MonthlyReport_TwoDates      @Start = @Start,  @End = @End;
    EXEC dbo.MonthlyReport_WrapperSource @Year  = @Year, @Month = @Month;
    EXEC dbo.MonthlyReport_Recompile     @Year  = @Year, @Month = @Month;
    EXEC dbo.MonthlyReport_DynamicSQL    @Year  = @Year, @Month = @Month;
     
    /* repeat for @Year = 2011, @Month = 9  --    157 rows */
     
    /* repeat for @Year = 2014, @Month = 4  --  2,115 rows */

    Il risultato? Ogni singolo piano produce la stessa ricerca dell'indice, ma le stime sono corrette solo in tutti e tre gli intervalli di date nel OPTION (RECOMPILE) versione. Gli altri continuano a utilizzare le stime derivate dal primo set di parametri (luglio 2012), e così, mentre ottengono stime migliori per il primo esecuzione, tale stima non sarà necessariamente migliore per il successivo esecuzioni che utilizzano parametri diversi (un classico caso di sniffing dei parametri da manuale):

    Si noti che quanto sopra non è un output *esatto* da SQL Sentry Plan Explorer:ad esempio, ho rimosso le righe dell'albero delle istruzioni che mostravano le chiamate della stored procedure esterna e le dichiarazioni dei parametri.

    Starà a te determinare se la tattica di compilare ogni volta è la migliore per te o se devi "aggiustare" qualcosa in primo luogo. Qui, abbiamo finito con gli stessi piani e nessuna differenza evidente nelle metriche delle prestazioni di runtime. Ma su tabelle più grandi, con una distribuzione dei dati più distorta e varianze maggiori nei valori dei predicati (ad esempio, si consideri un rapporto che può coprire una settimana, un anno e qualsiasi altra via di mezzo), potrebbe valere la pena indagare. E nota che puoi combinare i metodi qui, ad esempio, puoi passare ai parametri della data corretti *e* aggiungere OPTION (RECOMPILE) , se vuoi.

    Conclusione

    In questo caso specifico, che è una semplificazione intenzionale, lo sforzo per ottenere le stime corrette non è stato davvero ripagato:non abbiamo ottenuto un piano diverso e le prestazioni di runtime erano equivalenti. Ci sono certamente altri casi, tuttavia, in cui questo farà la differenza, ed è importante riconoscere la disparità di stima e determinare se potrebbe diventare un problema man mano che i tuoi dati crescono e/o la tua distribuzione si inclina. Sfortunatamente, non esiste una risposta in bianco o nero, poiché molte variabili influenzeranno la giustificazione del sovraccarico di compilazione, come in molti scenari, IT DEPENDS™