Ottenere una scansione anche se mi aspetto una ricerca


9

Devo ottimizzare SELECTun'istruzione, ma SQL Server esegue sempre una scansione dell'indice anziché una ricerca. Questa è la query che, ovviamente, si trova in una procedura memorizzata:

CREATE PROCEDURE dbo.something
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL    
AS

    SELECT [IdNumber], [Code], [Status], [Sex], 
           [FirstName], [LastName], [Profession], 
           [BirthDate], [HireDate], [ActiveDirectoryUser]
    FROM Employee
    WHERE (@Status IS NULL OR [Status] = @Status)
    AND 
    (
      @IsUserGotAnActiveDirectoryUser IS NULL 
      OR 
      (
        @IsUserGotAnActiveDirectoryUser IS NOT NULL AND       
        (
          @IsUserGotAnActiveDirectoryUser = 1 AND ActiveDirectoryUser <> ''
        )
        OR
        (
          @IsUserGotAnActiveDirectoryUser = 0 AND ActiveDirectoryUser = ''
        )
      )
    )

E questo è l'indice:

CREATE INDEX not_relevent ON dbo.Employee
(
    [Status] DESC,
    [ActiveDirectoryUser] ASC
)
INCLUDE (...all the other columns in the table...); 

Il piano:

Foto di piano

Perché SQL Server ha scelto una scansione? Come posso ripararlo?

Definizioni delle colonne:

[Status] int NOT NULL
[ActiveDirectoryUser] VARCHAR(50) NOT NULL

I parametri di stato possono essere:

NULL: all status,
1: Status= 1 (Active employees)
2: Status = 2 (Inactive employees)

IsUserGotAnActiveDirectoryUser può essere:

NULL: All employees
0: ActiveDirectoryUser is empty for that employee
1: ActiveDirectoryUser  got a valid value (not null and not empty)

Puoi pubblicare il piano di esecuzione effettivo da qualche parte (non una sua foto, ma il file .sqlplan in formato XML)? Suppongo che tu abbia modificato la procedura ma non hai effettivamente ottenuto una nuova compilation a livello di istruzione. Puoi modificare del testo della query (come aggiungere il prefisso dello schema al nome della tabella ) e quindi passare un valore valido per @Status?
Aaron Bertrand

1
Anche la definizione dell'indice pone la domanda: perché la chiave è attiva Status DESC? Per quanti valori ci sono Status, quali sono (se il numero è piccolo) e ogni valore è rappresentato approssimativamente in modo uguale? Mostraci l'output diSELECT TOP (20) [Status], c = COUNT(*) FROM dbo.Employee GROUP BY [Status] ORDER BY c DESC;
Aaron Bertrand

Risposte:


11

Non credo che la scansione sia causata dalla ricerca di una stringa vuota (e mentre potresti aggiungere un indice filtrato per quel caso, aiuterà solo variazioni molto specifiche della query). È più probabile che tu sia vittima dello sniffing dei parametri e di un singolo piano non ottimizzato per tutte le varie combinazioni di parametri (e valori dei parametri) che fornirai a questa query.

Io chiamo questa procedura "lavello della cucina" , perché ti aspetti che una query fornisca tutte le cose, incluso il lavello della cucina.

Ho un video sulla mia soluzione qui , ma essenzialmente la migliore esperienza che ho per tali domande è:

  • Costruisci l'istruzione in modo dinamico : ciò ti consentirà di escludere le clausole che menzionano le colonne per le quali non sono stati forniti parametri e ti garantirà un piano ottimizzato con precisione per i parametri effettivi passati con i valori.
  • UsaOPTION (RECOMPILE) : questo impedisce a specifici valori di parametro di forzare un tipo di piano errato, particolarmente utile quando si hanno inclinazione dei dati, statistiche errate o quando la prima esecuzione di un'istruzione utilizza un valore atipico che porterà a un piano diverso rispetto a quello successivo e più frequente esecuzioni.
  • Utilizza l'opzione serveroptimize for ad hoc workloads : ciò impedisce alle variazioni di query utilizzate una sola volta di inquinare la cache del piano.

Abilita ottimizzazione per carichi di lavoro ad hoc:

EXEC sys.sp_configure 'show advanced options', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'optimize for ad hoc workloads', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'show advanced options', 0;
GO
RECONFIGURE WITH OVERRIDE;

Cambia la tua procedura:

ALTER PROCEDURE dbo.Whatever
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL
AS
BEGIN 
  SET NOCOUNT ON;
  DECLARE @sql NVARCHAR(MAX) = N'SELECT [IdNumber], [Code], [Status], 
     [Sex], [FirstName], [LastName], [Profession],
     [BirthDate], [HireDate], [ActiveDirectoryUser]
   FROM dbo.Employee -- please, ALWAYS schema prefix
   WHERE 1 = 1';

   IF @Status IS NOT NULL
     SET @sql += N' AND ([Status]=@Status)'

   IF @IsUserGotAnActiveDirectoryUser = 1
     SET @sql += N' AND ActiveDirectoryUser <> ''''';
   IF @IsUserGotAnActiveDirectoryUser = 0
     SET @sql += N' AND ActiveDirectoryUser = ''''';

   SET @sql += N' OPTION (RECOMPILE);';

   EXEC sys.sp_executesql @sql, N'@Status INT, @Status;
END
GO

Una volta che hai un carico di lavoro basato su quel set di query che puoi monitorare, puoi analizzare le esecuzioni e vedere quali trarrebbero maggiori benefici da indici aggiuntivi o diversi - puoi farlo da una varietà di angolazioni, da semplici "quale combinazione di i parametri sono forniti più spesso? " a "quali singole query hanno i tempi di esecuzione più lunghi?" Non possiamo rispondere a queste domande solo in base al tuo codice, possiamo solo suggerire che qualsiasi indice sarà utile solo per un sottoinsieme di tutte le possibili combinazioni di parametri che stai tentando di supportare. Ad esempio, se@Statusè NULL, quindi non è possibile cercare tale indice non cluster. Quindi per quei casi in cui agli utenti non interessa lo stato, otterrai una scansione, a meno che tu non abbia un indice che si rivolga alle altre clausole (ma tale indice non sarà utile, data la tua logica di query corrente - la stringa vuota o la stringa vuota non è esattamente selettiva).

In questo caso, a seconda della serie di Statusvalori possibili e della loro distribuzione, OPTION (RECOMPILE)potrebbe non essere necessario. Ma se hai alcuni valori che genereranno 100 righe e alcuni valori che produrranno centinaia di migliaia, potresti volerlo lì (anche al costo della CPU, che dovrebbe essere marginale data la complessità di questa query), in modo da poter cerca nel maggior numero di casi possibile. Se l'intervallo di valori è abbastanza limitato, potresti persino fare qualcosa di complicato con l'SQL dinamico, in cui dici "Ho questo valore molto selettivo per @Status, quindi quando viene passato quel valore specifico, apporta questa leggera modifica al testo della query in modo che questa è considerata una query diversa e ottimizzata per quel valore param. "


3
Ho usato questo approccio molte volte ed è un modo fantastico per ottenere l'ottimizzatore per fare le cose nel modo in cui pensi che dovrebbe farlo comunque. Kim Tripp parla di una soluzione simile qui: sqlskills.com/blogs/kimberly/high-performance-procedures E ha un video di una sessione che ha fatto a PASS un paio di anni fa, che entra davvero nei dettagli folli sul perché funziona. Detto questo, in realtà non aggiunge molto a ciò che il signor Bertrand ha detto qui. Questo è uno di quegli strumenti che tutti dovrebbero tenere nella propria cintura. Può davvero salvare alcuni dolori enormi per quelle domande generali.
mskinner,

3

Disclaimer : alcune delle cose in questa risposta possono far sussultare il DBA. Mi sto avvicinando da un punto di vista puramente prestazionale: come ottenere Index Seeks quando ottieni sempre Scansioni Index.

Con quello fuori mano, ecco qui.

La tua query è nota come "query del lavello della cucina", una singola query pensata per soddisfare una serie di possibili condizioni di ricerca. Se l'utente imposta @statusun valore, si desidera filtrare su quello stato. Se @statusè NULL, restituire tutti gli stati, e così via.

Ciò introduce problemi con l'indicizzazione, ma non sono correlati alla sargability, poiché tutte le condizioni di ricerca sono criteri "uguali a".

Questo è sargable:

WHERE [status]=@status

Non è possibile eseguire il sarging in quanto SQL Server deve valutare ISNULL([status], 0)per ogni riga anziché cercare un singolo valore nell'indice:

WHERE ISNULL([status], 0)=@status

Ho ricreato il problema del lavello della cucina in una forma più semplice:

CREATE TABLE #work (
    A    int NOT NULL,
    B    int NOT NULL
);

CREATE UNIQUE INDEX #work_ix1 ON #work (A, B);

INSERT INTO #work (A, B)
VALUES (1,  1), (2,  1),
       (3,  1), (4,  1),
       (5,  2), (6,  2),
       (7,  2), (8,  3),
       (9,  3), (10, 3);

Se provi quanto segue, otterrai una scansione dell'indice, anche se A è la prima colonna dell'indice:

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE (@a IS NULL OR @a=A) AND
      (@b IS NULL OR @b=B);

Questo, tuttavia, produce una ricerca indice:

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL;

Finché stai usando una quantità gestibile di parametri (due nel tuo caso), potresti probabilmente solo UNIONun mucchio di query di ricerca - praticamente tutte le permutazioni dei criteri di ricerca. Se hai tre criteri, questo sembrerà disordinato, con quattro sarà completamente ingestibile. Sei stato avvisato.

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL
UNION ALL
SELECT *
FROM #work
WHERE @a=A AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b IS NULL;

Affinché la terza di quelle quattro utilizzi una ricerca indice, avrai bisogno di un secondo indice (B, A). Ecco come potrebbe apparire la tua query con queste modifiche (incluso il mio refactoring della query per renderla più leggibile).

DECLARE @Status int = NULL,
        @IsUserGotAnActiveDirectoryUser bit = NULL;

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='')

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='');

... inoltre avrai bisogno di un indice aggiuntivo attivo Employeecon le due colonne dell'indice invertite.

Per completezza, dovrei dire che x=@ximplicitamente significa che xnon può essere NULLperché NULLnon è mai uguale a NULL. Ciò semplifica un po 'la query.

E, sì, la risposta dinamica SQL di Aaron Bertrand è una scelta migliore nella maggior parte dei casi (ovvero ogni volta che puoi convivere con i ricompilamenti).


3

La tua domanda di base sembra essere "Perché" e penso che potresti trovare la risposta al minuto 55 circa di questa grande presentazione di Adam Machanic a TechEd qualche anno fa.

Cito i 5 minuti al minuto 55 ma l'intera presentazione vale la pena. Se guardi il piano di query per la tua query, sono sicuro che troverai dei Predicati residui per la ricerca. Fondamentalmente SQL non può "vedere" tutte le parti dell'indice perché alcune sono nascoste dalle disuguaglianze e da altre condizioni. Il risultato è una scansione dell'indice per un super set basato sul Predicato. Tale risultato viene inviato in spool e quindi scansionato nuovamente utilizzando il predicato residuo.

Controllare le proprietà dell'operatore di scansione (F4) e verificare se nell'elenco "Proprietà" sono presenti sia "Cerca predicato" che "Predicato".

Come altri hanno indicato, la query è difficile da indicizzare così com'è. Di recente ho lavorato su molti simili e ognuno ha richiesto una soluzione diversa. :(


0

Prima di chiederci se la ricerca dell'indice sia preferita rispetto alla scansione dell'indice, una regola empirica è controllare quante righe vengono restituite rispetto alle righe totali della tabella sottostante. Ad esempio, se ti aspetti che la tua query restituisca 10 righe su 1 milione di righe, probabilmente la ricerca dell'indice è altamente preferita rispetto alla scansione dell'indice. Tuttavia, se alcune query (o più) devono essere restituite dalla query, la ricerca dell'indice NON può necessariamente essere preferita.

La tua query non è complessa, quindi se puoi pubblicare un piano di esecuzione, potremmo avere idee migliori per aiutarti.


Filtrando alcune migliaia di righe da una tabella di 1 milione, mi piacerebbe comunque una ricerca: è ancora un enorme miglioramento delle prestazioni rispetto alla scansione dell'intera tabella.
Daniel Hutmacher,

-6

questo è solo l'originale formattato

DECLARE @Status INT = NULL,
        @IsUserGotAnActiveDirectoryUser BIT = NULL    

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName], [Profession],
       [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE (@Status IS NULL OR [Status]=@Status)  
AND (            @IsUserGotAnActiveDirectoryUser IS NULL 
      OR (       @IsUserGotAnActiveDirectoryUser IS NOT NULL 
           AND (     @IsUserGotAnActiveDirectoryUser = 1 
                 AND ActiveDirectoryUser <> '') 
           OR  (     @IsUserGotAnActiveDirectoryUser = 0 
                 AND ActiveDirectoryUser =  '')
         )
    )

questa è la revisione - non sicuro al 100% a riguardo ma (forse) provalo
anche uno o probabilmente sarà un problema che
si spezzerebbe su ActiveDirectoryUser null

  WHERE isnull(@Status, [Status]) = [Status]
    AND (      (     isnull(@IsUserGotAnActiveDirectoryUser, 1) = 1 
                 AND ActiveDirectoryUser <> '' ) 
           OR  (     isnull(@IsUserGotAnActiveDirectoryUser, 0) = 0 
                 AND ActiveDirectoryUser =  '' )
        )

3
Non mi è chiaro come questa risposta risolva la domanda del PO.
Erik,

@Erik Potremmo forse lasciare che l'OP ci provasse? Due OR sono andati via. Sai per certo che questo non può aiutare le prestazioni delle query?
paparazzo,

@ ypercubeᵀᴹ IsUserGotAnActiveDirectoryUser IS NOT NULL è stato rimosso. Quei due inutili rimuovono un OR e rimuovono IsUserGotAnActiveDirectoryUser IS NULL. Sei sicuro che questa query non verrà eseguita rapidamente dopo l'OP?
paparazzo,

@ ypercubeᵀᴹ Avrebbe potuto fare molte cose. Non sto cercando più semplice. Due o se ne sono andati. O è in genere negativo per i piani di query. Ci arrivo qui è una specie di club e non faccio parte del club. Ma lo faccio per vivere e post ciò che so ha funzionato. Le mie risposte non sono influenzate dai voti negativi.
paparazzo,
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.