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

Indici filtrati e parametrizzazione forzata (redux)

Dopo aver bloggato su come gli indici filtrati potrebbero essere più potenti e, più recentemente, su come possono essere resi inutili dalla parametrizzazione forzata, sto rivisitando l'argomento indici filtrati/parametrizzazione. Di recente è emersa al lavoro una soluzione apparentemente troppo semplice e ho dovuto condividerla.

Prendi l'esempio seguente, in cui abbiamo un database di vendita contenente una tabella degli ordini. A volte vogliamo solo un elenco (o un conteggio) dei soli ordini ancora da spedire, che, nel tempo, (si spera!) rappresentano una percentuale sempre più piccola della tabella complessiva:

CREATE DATABASE Sales;
GO
USE Sales;
GO
 
-- simplified, obviously:
CREATE TABLE dbo.Orders
(
    OrderID   int IDENTITY(1,1) PRIMARY KEY,
    OrderDate datetime  NOT NULL,
    filler    char(500) NOT NULL DEFAULT '',
    IsShipped bit       NOT NULL DEFAULT 0
);
GO
 
-- let's put some data in there; 7,000 shipped orders, and 50 unshipped:
 
INSERT dbo.Orders(OrderDate, IsShipped)
  -- random dates over two years
  SELECT TOP (7000) DATEADD(DAY, ABS(object_id % 730), '20171101'), 1 
  FROM sys.all_columns
UNION ALL 
  -- random dates from this month
  SELECT TOP (50)   DATEADD(DAY, ABS(object_id % 30),  '20191201'), 0 
  FROM sys.all_columns;

Potrebbe avere senso in questo scenario creare un indice filtrato come questo (che rende veloce il lavoro di tutte le query che stanno cercando di ottenere quegli ordini non spediti):

CREATE INDEX ix_OrdersNotShipped 
  ON dbo.Orders(IsShipped, OrderDate) 
  WHERE IsShipped = 0;

Possiamo eseguire una query rapida come questa per vedere come utilizza l'indice filtrato:

SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;

Il piano di esecuzione è abbastanza semplice, ma c'è un avviso su UnmatchedIndexes:

Il nome dell'avviso è leggermente fuorviante:l'ottimizzatore alla fine è stato in grado di utilizzare l'indice, ma suggerisce che sarebbe "meglio" senza parametri (che non abbiamo utilizzato esplicitamente), anche se l'istruzione sembra parametrizzata:

Se lo desideri davvero, puoi eliminare l'avviso, senza alcuna differenza nelle prestazioni effettive (sarebbe solo cosmetico). Un modo è aggiungere un predicato a impatto zero, come AND (1 > 0) :

SELECT wadd = OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 AND (1 > 0);

Un altro (probabilmente più comune) è aggiungere OPTION (RECOMPILE) :

SELECT wrecomp = OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 OPTION (RECOMPILE);

Entrambe queste opzioni producono lo stesso piano (una ricerca senza avvisi):

Fin qui tutto bene; il nostro indice filtrato viene utilizzato (come previsto). Questi non sono gli unici trucchi, ovviamente; guarda i commenti qui sotto per altri che i lettori hanno già inviato.

Poi, la complicazione

Poiché il database è soggetto a un numero elevato di query ad hoc, qualcuno attiva la parametrizzazione forzata, tentando di ridurre la compilazione ed eliminare i piani a basso utilizzo e monouso dall'inquinamento della cache dei piani:

ALTER DATABASE Sales SET PARAMETERIZATION FORCED;

Ora la nostra query originale non può utilizzare l'indice filtrato; è costretto a scansionare l'indice cluster:

SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;

Ritorna l'avviso sugli indici non corrispondenti e riceviamo nuovi avvisi sull'I/O residuo. Nota che l'istruzione è parametrizzata, ma ha un aspetto leggermente diverso:

Questo è in base alla progettazione, poiché l'intero scopo della parametrizzazione forzata è parametrizzare query come questa. Ma vanifica lo scopo del nostro indice filtrato, poiché ha lo scopo di supportare un singolo valore nel predicato, non un parametro che può cambiare.

Scimmia

Anche la nostra query "trucco" che utilizza il predicato aggiuntivo non è in grado di utilizzare l'indice filtrato e finisce con un piano leggermente più complicato per l'avvio:

SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 AND (1 > 0);

OPZIONE (RICIMPILA)

La reazione tipica in questo caso, proprio come con la rimozione dell'avviso in precedenza, è aggiungere OPTION (RECOMPILE) alla dichiarazione. Funziona e consente di scegliere l'indice filtrato per una ricerca efficiente...

SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 OPTION (RECOMPILE);

…ma aggiungendo OPTION (RECOMPILE) e prendere questo colpo di compilazione aggiuntivo contro ogni esecuzione della query non sarà sempre accettabile in ambienti ad alto volume (soprattutto se sono già vincolati alla CPU).

Suggerimenti

Qualcuno ha suggerito di suggerire esplicitamente l'indice filtrato per evitare i costi di ricompilazione. In generale, questo è piuttosto fragile, perché si basa sull'indice che sopravvive al codice; Tendo a usarlo come ultima risorsa. In questo caso non è comunque valido. Quando le regole di parametrizzazione impediscono all'ottimizzatore di selezionare automaticamente l'indice filtrato, impediscono anche all'utente di selezionarlo manualmente. Stesso problema con un FORCESEEK generico suggerimento:

SELECT OrderID, OrderDate FROM dbo.Orders WITH (INDEX (ix_OrdersNotShipped)) WHERE IsShipped = 0;
 
SELECT OrderID, OrderDate FROM dbo.Orders WITH (FORCESEEK) WHERE IsShipped = 0;

Entrambi producono questo errore:

Msg 8622, livello 16, stato 1
Il processore di query non ha potuto produrre un piano di query a causa dei suggerimenti definiti in questa query. Invia nuovamente la query senza specificare alcun suggerimento e senza utilizzare SET FORCEPLAN.

E questo ha senso, perché non c'è modo di sapere che il valore sconosciuto per IsShipped il parametro corrisponderà all'indice filtrato (o supporterà un'operazione di ricerca su qualsiasi indice).

SQL dinamico?

Ti ho suggerito di utilizzare l'SQL dinamico, almeno per pagare quel colpo di ricompilazione solo quando sai che vuoi colpire l'indice più piccolo:

DECLARE @IsShipped bit = 0;
 
DECLARE @sql nvarchar(max) = N'SELECT dynsql = OrderID, OrderDate FROM dbo.Orders'
  + CASE WHEN @IsShipped IS NOT NULL THEN N' WHERE IsShipped = @IsShipped'
    ELSE N'' END
  + CASE WHEN @IsShipped = 0 THEN N' OPTION (RECOMPILE)' ELSE N'' END;
 
EXEC sys.sp_executesql @sql, N'@IsShipped bit', @IsShipped;

Questo porta allo stesso piano efficiente di cui sopra. Se hai modificato la variabile in @IsShipped = 1 , quindi ottieni la scansione dell'indice cluster più costosa che dovresti aspettarti:

Ma a nessuno piace usare l'SQL dinamico in un caso limite come questo:rende il codice più difficile da leggere e mantenere, e anche se questo codice fosse fuori nell'applicazione, è comunque una logica aggiuntiva che dovrebbe essere aggiunta lì, rendendolo meno che desiderabile .

Qualcosa di più semplice

Abbiamo parlato brevemente dell'implementazione di una guida al piano, che non è certamente più semplice, ma poi un collega ha suggerito che potresti ingannare l'ottimizzatore "nascondendo" l'istruzione parametrizzata all'interno di una procedura memorizzata, di una vista o di una funzione con valori di tabella inline. Era così semplice che non credevo avrebbe funzionato.

Ma poi l'ho provato:

CREATE PROCEDURE dbo.GetUnshippedOrders
AS
BEGIN
  SET NOCOUNT ON;
  SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;
END
GO
 
CREATE VIEW dbo.vUnshippedOrders
AS
  SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;
GO
 
CREATE FUNCTION dbo.fnUnshippedOrders()
RETURNS TABLE
AS
  RETURN (SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0);
GO

Tutte e tre queste query eseguono la ricerca efficiente rispetto all'indice filtrato:

EXEC dbo.GetUnshippedOrders;
GO
SELECT OrderID, OrderDate FROM dbo.vUnshippedOrders;
GO
SELECT OrderID, OrderDate FROM dbo.fnUnshippedOrders();

Conclusione

Sono rimasto sorpreso che fosse così efficace. Naturalmente, ciò richiede di modificare l'applicazione; se non è possibile modificare il codice dell'app per chiamare una stored procedure o fare riferimento alla vista o alla funzione (o anche aggiungere OPTION (RECOMPILE) ), dovrai continuare a cercare altre opzioni. Ma se puoi modificare il codice dell'applicazione, inserire il predicato in un altro modulo potrebbe essere la strada da percorrere.