La query non risponde quando si aggiungono due colonne


9

Quando aggiungo due colonne alla mia selezione, la query non risponde. Il tipo di colonna è nvarchar(2000). È un po 'insolito.

  • La versione di SQL Server è 2014.
  • C'è solo un indice primario.
  • L'intero record è di soli 1000 righe.

Ecco il piano di esecuzione prima ( showplan XML ):

inserisci qui la descrizione dell'immagine

Piano di esecuzione dopo ( XML showplan ):

inserisci qui la descrizione dell'immagine

Ecco la query:

select top(100)
  Batch_Tasks_Queue.id,
  btq.id,
  Batch_Tasks_Queue.[Parameters], -- this field
  btq.[Parameters]  -- and this field
from
        Batch_Tasks_Queue with(nolock)
    inner join  Batch_Tasks_Queue btq with(nolock)  on  Batch_Tasks_Queue.Start_Time < btq.Start_Time
                            and btq.Start_Time < Batch_Tasks_Queue.Finish_Time
                            and Batch_Tasks_Queue.id <> btq.id                            
                            and btq.Start_Time is not null
                            and btq.State in (3, 4)                          
where
    Batch_Tasks_Queue.Start_Time is not null      
    and Batch_Tasks_Queue.State in (3, 4)
    and Batch_Tasks_Queue.Operation_Type = btq.Operation_Type
    and Batch_Tasks_Queue.Operation_Type not in (23, 24, 25, 26, 27, 28, 30)

order by
    Batch_Tasks_Queue.Start_Time desc

L'intero conteggio dei risultati è di 17 righe. I dati sporchi (suggerimento nolock) non sono importanti.

Ecco la struttura della tabella:

CREATE TABLE [dbo].[Batch_Tasks_Queue](
    [Id] [int] NOT NULL,
    [OBJ_VERSION] [numeric](8, 0) NOT NULL,
    [Operation_Type] [numeric](2, 0) NULL,
    [Request_Time] [datetime] NOT NULL,
    [Description] [varchar](1000) NULL,
    [State] [numeric](1, 0) NOT NULL,
    [Start_Time] [datetime] NULL,
    [Finish_Time] [datetime] NULL,
    [Parameters] [nvarchar](2000) NULL,
    [Response] [nvarchar](max) NULL,
    [Billing_UserId] [int] NOT NULL,
    [Planned_Start_Time] [datetime] NULL,
    [Input_FileId] [uniqueidentifier] NULL,
    [Output_FileId] [uniqueidentifier] NULL,
    [PRIORITY] [numeric](2, 0) NULL,
    [EXECUTE_SEQ] [numeric](2, 0) NULL,
    [View_Access] [numeric](1, 0) NULL,
    [Seeing] [numeric](1, 0) NULL,
 CONSTRAINT [PKBachTskQ] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [Batch_Tasks_QueueData]
) ON [Batch_Tasks_QueueData] TEXTIMAGE_ON [Batch_Tasks_QueueData]
GO    
SET ANSI_PADDING OFF
GO
ALTER TABLE [dbo].[Batch_Tasks_Queue]  WITH NOCHECK ADD  CONSTRAINT [FK0_BtchTskQ_BlngUsr] FOREIGN KEY([Billing_UserId])
REFERENCES [dbo].[BILLING_USER] ([ID])
GO
ALTER TABLE [dbo].[Batch_Tasks_Queue] CHECK CONSTRAINT [FK0_BtchTskQ_BlngUsr]
GO

La discussione su questa domanda è stata spostata in questa chat room .
Paul White 9

Risposte:


15

Sommario

I problemi principali sono:

  • La selezione del piano dell'ottimizzatore presuppone una distribuzione uniforme dei valori.
  • Una mancanza di indici adeguati significa:
    • La scansione della tabella è l'unica opzione.
    • Il join è un join loop nidificato ingenuo , anziché un join loop nidificato indice . In un join ingenuo, i predicati del join vengono valutati al join anziché essere spinti verso il basso sul lato interno del join.

Dettagli

I due piani sono fondamentalmente abbastanza simili, sebbene le prestazioni possano essere molto diverse:

Pianifica con le colonne extra

Prendendo quello con le colonne extra che non si completano prima in un tempo ragionevole:

Piano lento

Le caratteristiche interessanti sono:

  1. La parte superiore del nodo 0 limita le righe restituite a 100. Imposta inoltre un obiettivo di riga per l'ottimizzatore, quindi tutto ciò che si trova sotto di esso nel piano viene scelto per restituire rapidamente le prime 100 righe.
  2. La scansione al nodo 4 trova le righe dalla tabella in cui Start_Timenon è null,State è 3 o 4 ed Operation_Typeè uno dei valori elencati. La tabella viene completamente analizzata una volta, con ogni riga testata rispetto ai predicati menzionati. Solo le righe che superano tutti i test passano all'ordinamento. L'ottimizzatore stima che si qualificheranno 38.283 righe.
  3. L'ordinamento al nodo 3 consuma tutte le righe dalla scansione al nodo 4 e le ordina in ordine di Start_Time DESC . Questo è l'ordine di presentazione finale richiesto dalla query.
  4. L'ottimizzatore stima che 93 righe (in realtà 93.2791) dovranno essere lette dall'ordinamento affinché l'intero piano restituisca 100 righe (tenendo conto dell'effetto atteso del join).
  5. Si prevede che il join Nested Loops sul nodo 2 eseguirà il suo input interno (il ramo inferiore) 94 volte (in realtà 94.2791). La riga aggiuntiva è richiesta dallo scambio di parallelismo di arresto nel nodo 1 per motivi tecnici.
  6. Scansione sul nodo 5 esegue la scansione completa della tabella su ogni iterazione. Trova righe in cui Start_Timenon è null ed Stateè 3 o 4. Si stima che produca 400.875 righe per ogni iterazione. Oltre 94.2791 iterazioni, il numero totale di righe è quasi 38 milioni.
  7. Il join Nested Loops nel nodo 2 applica anche i predicati del join. Verifica che Operation_Typecorrisponda, che il Start_Timenodo from 4 sia inferiore al Start_Timenodo from 5, che il Start_Timenodo from 5 sia inferiore al Finish_Timenodo from 4 e che i due Idvalori non corrispondano.
  8. The Gather Streams (stop scambio di parallelismo) al nodo 1 unisce i flussi ordinati da ciascun thread fino a quando non sono state prodotte 100 righe. La natura che preserva l'ordine dell'unione tra più flussi è ciò che richiede la riga aggiuntiva menzionata nel passaggio 5.

La grande inefficienza è ovviamente ai passaggi 6 e 7 sopra. La scansione completa della tabella nel nodo 5 per ogni iterazione è anche solo leggermente ragionevole se si verifica solo 94 volte come prevede l'ottimizzatore. Anche il set di confronti di circa 38 milioni per riga nel nodo 2 rappresenta un costo elevato.

Fondamentalmente, è molto probabile che anche la stima dell'obiettivo della riga 93/94 sia sbagliata, poiché dipende dalla distribuzione dei valori. L'ottimizzatore presuppone una distribuzione uniforme in assenza di informazioni più dettagliate. In termini semplici, ciò significa che se si prevede che l'1% delle righe della tabella si qualificherà, l'ottimizzatore ritiene che per trovare 1 riga corrispondente, sia necessario leggere 100 righe.

Se eseguissi questa query fino al completamento (che potrebbe richiedere molto tempo), molto probabilmente scoprirai che molte più di 93/94 righe devono essere lette dall'ordinamento per produrre finalmente 100 righe. Nel peggiore dei casi, la 100a riga verrà trovata utilizzando l'ultima riga dell'ordinamento. Supponendo che la stima dell'ottimizzatore sul nodo 4 sia corretta, ciò significa eseguire la scansione sul nodo 5 38.284 volte, per un totale di circa 15 miliardi di righe. Potrebbe essere maggiore se anche le stime di scansione sono disattivate.

Questo piano di esecuzione include anche un avviso indice mancante:

/*
The Query Processor estimates that implementing the following index
could improve the query cost by 72.7096%.

WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/

CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([Operation_Type],[State],[Start_Time])
INCLUDE ([Id],[Parameters])

L'ottimizzatore ti avvisa del fatto che l'aggiunta di un indice alla tabella migliorerebbe le prestazioni.

Pianifica senza le colonne extra

Piano meno lento

Questo è essenzialmente lo stesso piano del precedente, con l'aggiunta della bobina di indice al nodo 6 e il filtro al nodo 5. Le differenze importanti sono:

  1. Lo spool di indice nel nodo 6 è uno spool desideroso. Consuma avidamente il risultato della scansione sotto di esso e crea un indice temporaneo con chiave Operation_Typee Start_Time, Idcome colonna non chiave.
  2. Il join dei loop nidificati nel nodo 2 è ora un join di indice. N aderire predicati vengono valutati qui, invece i valori per iterazione corrente Operation_Type, Start_Time, Finish_Time, e Iddalla scansione nel nodo 4 vengono passati al ramo lato interno come riferimenti esterni.
  3. La scansione sul nodo 7 viene eseguita una sola volta.
  4. L'indice spool sul nodo 6 cerca le righe dall'indice temporaneo dove Operation_Typecorrisponde al valore di riferimento esterno corrente e Start_Timeè compreso nell'intervallo definito dai riferimenti esterno Start_Timee Finish_Time.
  5. Il filtro sul nodo 5 verifica i Idvalori dallo spool di indice per la disuguaglianza rispetto al valore di riferimento esterno corrente di Id.

I miglioramenti chiave sono:

  • La scansione del lato interno viene eseguita una sola volta
  • Un indice temporaneo su ( Operation_Type, Start_Time) con Idcome colonna inclusa consente un indice di cicli annidati. L'indice viene utilizzato per cercare le righe corrispondenti su ogni iterazione anziché eseguire la scansione dell'intera tabella ogni volta.

Come in precedenza, l'ottimizzatore include un avviso relativo a un indice mancante:

/*
The Query Processor estimates that implementing the following index
could improve the query cost by 24.1475%.

WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/

CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([State],[Start_Time])
INCLUDE ([Id],[Operation_Type])
GO

Conclusione

Il piano senza le colonne extra è più veloce perché l'ottimizzatore ha scelto di creare un indice temporaneo per te.

Il piano con le colonne aggiuntive renderebbe l'indice temporaneo più costoso da costruire. La [Parameterscolonna] è nvarchar(2000), che aggiungerebbe fino a 4000 byte per ogni riga dell'indice. Il costo aggiuntivo è sufficiente per convincere l'ottimizzatore che la costruzione dell'indice temporaneo su ogni esecuzione non si ripagherà da sola.

L'ottimizzatore avverte in entrambi i casi che un indice permanente sarebbe una soluzione migliore. La composizione ideale dell'indice dipende dal carico di lavoro più ampio. Per questa particolare query, gli indici suggeriti sono un punto di partenza ragionevole, ma è necessario comprendere i vantaggi e i costi.

Raccomandazione

Una vasta gamma di possibili indici sarebbe utile per questa query. L'importante da asporto è che è necessaria una sorta di indice non cluster. Dalle informazioni fornite, un indice ragionevole secondo me sarebbe:

CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time);

Sarei anche tentato di organizzare un po 'meglio la query, e ritardare a cercare le ampie [Parameters]colonne dell'indice cluster fino a quando non sono state trovate le prime 100 righe (usando Idcome chiave):

SELECT TOP (100)
    BTQ1.id,
    BTQ2.id,
    BTQ3.[Parameters],
    BTQ4.[Parameters]
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
    ON BTQ2.Operation_Type = BTQ1.Operation_Type
    AND BTQ2.Start_Time > BTQ1.Start_Time
    AND BTQ2.Start_Time < BTQ1.Finish_Time
    AND BTQ2.id != BTQ1.id
    -- Look up the [Parameters] values
JOIN dbo.Batch_Tasks_Queue AS BTQ3
    ON BTQ3.Id = BTQ1.Id
JOIN dbo.Batch_Tasks_Queue AS BTQ4
    ON BTQ4.Id = BTQ2.Id
WHERE
    BTQ1.[State] IN (3, 4)
    AND BTQ2.[State] IN (3, 4)
    AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
    AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
    -- These predicates are not strictly needed
    AND BTQ1.Start_Time IS NOT NULL
    AND BTQ2.Start_Time IS NOT NULL
ORDER BY
    BTQ1.Start_Time DESC;

Laddove le [Parameters]colonne non sono necessarie, la query può essere semplificata per:

SELECT TOP (100)
    BTQ1.id,
    BTQ2.id
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
    ON BTQ2.Operation_Type = BTQ1.Operation_Type
    AND BTQ2.Start_Time > BTQ1.Start_Time
    AND BTQ2.Start_Time < BTQ1.Finish_Time
    AND BTQ2.id != BTQ1.id
WHERE
    BTQ1.[State] IN (3, 4)
    AND BTQ2.[State] IN (3, 4)
    AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
    AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
    AND BTQ1.Start_Time IS NOT NULL
    AND BTQ2.Start_Time IS NOT NULL
ORDER BY
    BTQ1.Start_Time DESC;

Il FORCESEEKsuggerimento è lì per aiutare a garantire che l'ottimizzatore scelga un piano di cicli nidificati indicizzati (esiste una tentazione basata sul costo per l'ottimizzatore di selezionare un hash o (molti-molti) unire altrimenti, che tende a non funzionare bene con questo tipo di query in pratica. Entrambi finiscono con grandi residui; molti elementi per bucket nel caso dell'hash e molti riavvolgimenti per l'unione).

Alternativa

Se la query (inclusi i suoi valori specifici) fosse particolarmente critica per le prestazioni di lettura, prenderei invece in considerazione due indici filtrati:

CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time)
WHERE 
    Start_Time IS NOT NULL
    AND [State] IN (3, 4)
    AND Operation_Type <> 23
    AND Operation_Type <> 24
    AND Operation_Type <> 25
    AND Operation_Type <> 26
    AND Operation_Type <> 27
    AND Operation_Type <> 28
    AND Operation_Type <> 30;

CREATE NONCLUSTERED INDEX i2
ON dbo.Batch_Tasks_Queue (Operation_Type, [State], Start_Time)
WHERE 
    Start_Time IS NOT NULL
    AND [State] IN (3, 4)
    AND Operation_Type <> 23
    AND Operation_Type <> 24
    AND Operation_Type <> 25
    AND Operation_Type <> 26
    AND Operation_Type <> 27
    AND Operation_Type <> 28
    AND Operation_Type <> 30;

Per la query che non necessita della [Parameters]colonna, il piano stimato che utilizza gli indici filtrati è:

Piano di indice filtrato semplice

La scansione dell'indice restituisce automaticamente tutte le righe idonee senza valutare ulteriori predicati. Per ogni iterazione del join dei cicli nidificati dell'indice, la ricerca dell'indice esegue due operazioni di ricerca:

  1. Un prefisso seek corrisponde a Operation_Typee State= 3, quindi cerca l'intervallo di Start_Timevalori, predicato residuo sulla Iddisuguaglianza.
  2. Un prefisso seek corrisponde a Operation_Typee State= 4, quindi cerca l'intervallo di Start_Timevalori, predicato residuo sulla Iddisuguaglianza.

Dove [Parameters]è necessaria la colonna, il piano di query aggiunge semplicemente un massimo di 100 ricerche singleton per ogni tabella:

Piano di indice filtrato con colonne aggiuntive

Come nota finale, dovresti prendere in considerazione l'uso dei tipi interi standard incorporati anziché numericladdove applicabile.


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.