Perché SQL Server utilizza un piano di esecuzione migliore quando inserisco la variabile?


32

Ho una query SQL che sto cercando di ottimizzare:

DECLARE @Id UNIQUEIDENTIFIER = 'cec094e5-b312-4b13-997a-c91a8c662962'

SELECT 
  Id,
  MIN(SomeTimestamp),
  MAX(SomeInt)
FROM dbo.MyTable
WHERE Id = @Id
  AND SomeBit = 1
GROUP BY Id

MyTable ha due indici:

CREATE NONCLUSTERED INDEX IX_MyTable_SomeTimestamp_Includes
ON dbo.MyTable (SomeTimestamp ASC)
INCLUDE(Id, SomeInt)

CREATE NONCLUSTERED INDEX IX_MyTable_Id_SomeBit_Includes
ON dbo.MyTable (Id, SomeBit)
INCLUDE (TotallyUnrelatedTimestamp)

Quando eseguo la query esattamente come scritto sopra, SQL Server esegue la scansione del primo indice, ottenendo 189.703 letture logiche e una durata di 2-3 secondi.

Quando inserisco la @Idvariabile ed eseguo nuovamente la query, SQL Server cerca il secondo indice, ottenendo solo 104 letture logiche e una durata di 0,001 secondi (sostanzialmente istantanea).

Ho bisogno della variabile, ma voglio che SQL usi il buon piano. Come soluzione temporanea ho inserito un suggerimento indice sulla query e la query è sostanzialmente istantanea. Tuttavia, provo a stare lontano dai suggerimenti sull'indice, quando possibile. Di solito presumo che se Query Optimizer non è in grado di fare il suo lavoro, allora c'è qualcosa che posso fare (o smettere di fare) per aiutarlo senza dirgli esplicitamente cosa fare.

Quindi, perché SQL Server ha un piano migliore quando inserisco la variabile?

Risposte:


44

In SQL Server esistono tre forme comuni di predicato non join:

Con un valore letterale :

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = 1;

Con un parametro :

CREATE PROCEDURE dbo.SomeProc(@Reputation INT)
AS
BEGIN
    SELECT COUNT(*) AS records
    FROM   dbo.Users AS u
    WHERE  u.Reputation = @Reputation;
END;

Con una variabile locale :

DECLARE @Reputation INT = 1

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = @Reputation;

risultati

Quando usi un valore letterale e il tuo piano non è a) Trivial eb) Parametrizzazione semplice oc) non hai attivato la parametrizzazione forzata , l'ottimizzatore crea un piano molto speciale solo per quel valore.

Quando si utilizza un parametro , l'ottimizzatore creerà un piano per quel parametro (questo si chiama sniffing dei parametri ), quindi riutilizzerà quel piano, assenti suggerimenti di ricompilazione, sfratto della cache del piano, ecc.

Quando usi una variabile locale , l'ottimizzatore fa un piano per ... Qualcosa .

Se dovessi eseguire questa query:

DECLARE @Reputation INT = 1

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = @Reputation;

Il piano sarebbe simile al seguente:

NOCCIOLINE

E il numero stimato di righe per quella variabile locale sarebbe simile al seguente:

NOCCIOLINE

Anche se la query restituisce un conteggio di 4.744.427.

Le variabili locali, essendo sconosciute, non usano la parte "buona" dell'istogramma per la stima della cardinalità. Usano un'ipotesi basata sul vettore di densità.

NOCCIOLINE

SELECT 5.280389E-05 * 7250739 AS [poo]

Questo ti darà 382.86722457471, che è la supposizione che l'ottimizzatore fa.

Queste ipotesi sconosciute sono generalmente ipotesi molto sbagliate e spesso possono portare a piani sbagliati e scelte di indice errate.

Risolvendolo?

Le opzioni in genere sono:

  • Suggerimenti sull'indice fragile
  • Suggerimenti per la ricompilazione potenzialmente costosi
  • SQL dinamico con parametri
  • Una procedura memorizzata
  • Migliora l'indice corrente

Le tue opzioni sono in particolare:

Migliorare l'indice corrente significa estenderlo per coprire tutte le colonne necessarie alla query:

CREATE NONCLUSTERED INDEX IX_MyTable_Id_SomeBit_Includes
ON dbo.MyTable (Id, SomeBit)
INCLUDE (TotallyUnrelatedTimestamp, SomeTimestamp, SomeInt)
WITH (DROP_EXISTING = ON);

Supponendo che i Idvalori siano ragionevolmente selettivi, questo ti darà un buon piano e aiuterà l'ottimizzatore dandogli un metodo di accesso ai dati "ovvio".

Più lettura

Puoi leggere ulteriori informazioni sull'incorporamento dei parametri qui:


12

Presumo che tu abbia dati distorti, che non desideri utilizzare i suggerimenti per le query per forzare l'ottimizzatore su cosa fare e che devi ottenere buone prestazioni per tutti i possibili valori di input di @Id. È possibile ottenere un piano di query che richieda solo alcune manciate di letture logiche per ogni possibile valore di input se si è disposti a creare la seguente coppia di indici (o il loro equivalente):

CREATE INDEX GetMinSomeTimestamp ON dbo.MyTable (Id, SomeTimestamp) WHERE SomeBit = 1;
CREATE INDEX GetMaxSomeInt ON dbo.MyTable (Id, SomeInt) WHERE SomeBit = 1;

Di seguito sono riportati i miei dati di test. Ho inserito 13 M righe nella tabella e ho fatto in modo che metà di esse avesse un valore '3A35EA17-CE7E-4637-8319-4C517B6E48CA'per la Idcolonna.

DROP TABLE IF EXISTS dbo.MyTable;

CREATE TABLE dbo.MyTable (
    Id uniqueidentifier,
    SomeTimestamp DATETIME2,
    SomeInt INT,
    SomeBit BIT,
    FILLER VARCHAR(100)
);

INSERT INTO dbo.MyTable WITH (TABLOCK)
SELECT NEWID(), CURRENT_TIMESTAMP, 0, 1, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

INSERT INTO dbo.MyTable WITH (TABLOCK)
SELECT '3A35EA17-CE7E-4637-8319-4C517B6E48CA', CURRENT_TIMESTAMP, 0, 1, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

Questa query potrebbe sembrare inizialmente un po 'strana:

DECLARE @Id UNIQUEIDENTIFIER = '3A35EA17-CE7E-4637-8319-4C517B6E48CA'

SELECT
  @Id,
  st.SomeTimestamp,
  si.SomeInt
FROM (
    SELECT TOP (1) SomeInt, Id
    FROM dbo.MyTable
    WHERE Id = @Id
    AND SomeBit = 1
    ORDER BY SomeInt DESC
) si
CROSS JOIN (
    SELECT TOP (1) SomeTimestamp, Id
    FROM dbo.MyTable
    WHERE Id = @Id
    AND SomeBit = 1
    ORDER BY SomeTimestamp ASC
) st;

È progettato per sfruttare l'ordinamento degli indici per trovare il valore minimo o massimo con alcune letture logiche. L' CROSS JOINè lì per ottenere risultati corretti quando non sono presenti righe corrispondenti per il @Idvalore. Anche se filtro sul valore più popolare nella tabella (corrispondente a 6,5 ​​milioni di righe) ottengo solo 8 letture logiche:

Tabella "MyTable". Conteggio scansioni 2, letture logiche 8

Ecco il piano di query:

inserisci qui la descrizione dell'immagine

Entrambe le ricerche dell'indice trovano 0 o 1 righe. È estremamente efficiente, ma la creazione di due indici potrebbe essere eccessiva per il tuo scenario. È possibile invece considerare il seguente indice:

CREATE INDEX CoveringIndex ON dbo.MyTable (Id) INCLUDE (SomeTimestamp, SomeInt) WHERE SomeBit = 1;

Ora il piano di query per la query originale (con un MAXDOP 1suggerimento opzionale ) ha un aspetto leggermente diverso:

inserisci qui la descrizione dell'immagine

Le ricerche chiave non sono più necessarie. Con un percorso di accesso migliore che dovrebbe funzionare bene per tutti gli input, non dovresti preoccuparti che l'ottimizzatore scelga il piano di query errato a causa del vettore di densità. Tuttavia, questa query e questo indice non saranno efficienti quanto l'altro se si cerca un @Idvalore popolare .

Tabella "MyTable". Conteggio scansione 1, lettura logica 33757


2

Non posso rispondere al perché qui, ma il modo più rapido per garantire che la query venga eseguita nel modo desiderato è:

DECLARE @Id UNIQUEIDENTIFIER = 'cec094e5-b312-4b13-997a-c91a8c662962'
SELECT 
  Id,
  MIN(SomeTimestamp),
  MAX(SomeInt)
FROM dbo.MyTable WITH (INDEX(IX_MyTable_Id_SomeBit_Includes))
WHERE Id = @Id
  AND SomeBit = 1
GROUP BY Id

Ciò comporta il rischio che la tabella o gli indici possano cambiare in futuro in modo tale che questa ottimizzazione diventi disfunzionale, ma è disponibile se necessario. Spero che qualcuno possa offrirti una risposta alla causa principale, come hai richiesto, piuttosto che questa soluzione alternativa.

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.