Perché la mia query SELECT DISTINCT TOP N esegue la scansione dell'intera tabella?


28

Ho incontrato alcune SELECT DISTINCT TOP Nquery che sembrano essere scarsamente ottimizzate da Query Optimizer di SQL Server. Cominciamo prendendo in considerazione un esempio banale: un milione di righe con due valori alternati. Userò il GetNums funzione per generare i dati:

DROP TABLE IF EXISTS X_2_DISTINCT_VALUES;

CREATE TABLE X_2_DISTINCT_VALUES (PK INT IDENTITY (1, 1), VAL INT NOT NULL);

INSERT INTO X_2_DISTINCT_VALUES WITH (TABLOCK) (VAL)
SELECT N % 2
FROM dbo.GetNums(1000000);

UPDATE STATISTICS X_2_DISTINCT_VALUES WITH FULLSCAN;

Per la seguente query:

SELECT DISTINCT TOP 2 VAL
FROM X_2_DISTINCT_VALUES
OPTION (MAXDOP 1);

SQL Server può trovare due valori distinti semplicemente scansionando la prima pagina di dati della tabella ma scansiona invece tutti i dati . Perché SQL Server non esegue la scansione fino a quando non trova il numero richiesto di valori distinti?

Per questa domanda, utilizzare i seguenti dati di test che contengono 10 milioni di righe con 10 valori distinti generati in blocchi:

DROP TABLE IF EXISTS X_10_DISTINCT_HEAP;

CREATE TABLE X_10_DISTINCT_HEAP (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_10_DISTINCT_HEAP WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_HEAP WITH FULLSCAN;

Sono accettabili anche le risposte per una tabella con un indice cluster:

DROP TABLE IF EXISTS X_10_DISTINCT_CI;

CREATE TABLE X_10_DISTINCT_CI (PK INT IDENTITY (1, 1), VAL VARCHAR(10) NOT NULL, PRIMARY KEY (PK));

INSERT INTO X_10_DISTINCT_CI WITH (TABLOCK) (VAL)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_CI WITH FULLSCAN;

La query seguente analizza tutti i 10 milioni di righe dalla tabella . Come posso ottenere qualcosa che non scansiona l'intero tavolo? Sto usando SQL Server 2016 SP1.

SELECT DISTINCT TOP 10 VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);

Risposte:


30

Ci sono tre diverse regole di ottimizzazione che possono eseguire l' DISTINCToperazione nella query sopra. La query seguente genera un errore che suggerisce che l'elenco è completo:

SELECT DISTINCT TOP 10 ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, QUERYRULEOFF GbAggToSort, QUERYRULEOFF GbAggToHS, QUERYRULEOFF GbAggToStrm);

Messaggio 8622, livello 16, stato 1, riga 1

Il processore di query non è stato in grado di produrre un piano di query a causa dei suggerimenti definiti in questa query. Reinvia la query senza specificare alcun suggerimento e senza utilizzare SET FORCEPLAN.

GbAggToSortimplementa il raggruppamento per aggregato (distinto) come un ordinamento distinto. Questo è un operatore di blocco che leggerà tutti i dati dall'input prima di produrre qualsiasi riga. GbAggToStrmimplementa l'aggregazione gruppo per aggregato di flusso (che richiede anche un ordinamento di input in questa istanza). Questo è anche un operatore di blocco. GbAggToHSimplementa come una corrispondenza hash, che è ciò che abbiamo visto nel piano errato dalla domanda, ma può essere implementato come corrispondenza hash (aggregata) o corrispondenza hash (flusso distinto).

L' operatore di corrispondenza hash ( flusso distinto ) è un modo per risolvere questo problema perché non sta bloccando. SQL Server dovrebbe essere in grado di interrompere la scansione una volta rilevati abbastanza valori distinti.

L'operatore logico Flow Distinct esegue la scansione dell'input, rimuovendo i duplicati. Mentre l'operatore Distinct consuma tutti gli input prima di produrre qualsiasi output, l'operatore Flow Distinct restituisce ogni riga così come viene ottenuta dall'input (a meno che quella riga non sia un duplicato, nel qual caso viene scartata).

Perché la query nella domanda usa hash match (aggregato) anziché hash match (flusso distinto)? Poiché il numero di valori distinti cambia nella tabella, mi aspetto che il costo della query di corrispondenza hash (flusso distinto) diminuisca perché la stima del numero di righe che deve scansionare sulla tabella dovrebbe diminuire. Mi aspetto che il costo del piano di corrispondenza hash (aggregato) aumenti perché la tabella hash che deve costruire aumenterà. Un modo per indagare è creare una guida di piano . Se creo due copie dei dati ma applico una guida di piano a una di esse, dovrei essere in grado di confrontare la corrispondenza hash (aggregata) con la corrispondenza hash (distinta) fianco a fianco con gli stessi dati. Nota che non posso farlo disabilitando le regole di Query Optimizer perché la stessa regola si applica a entrambi i piani ( GbAggToHS).

Ecco un modo per ottenere la guida del piano che sto cercando:

DROP TABLE IF EXISTS X_PLAN_GUIDE_TARGET;

CREATE TABLE X_PLAN_GUIDE_TARGET (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT CAST(N % 10000 AS VARCHAR(10))
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_PLAN_GUIDE_TARGET WITH FULLSCAN;

-- run this query
SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

Ottieni l'handle del piano e utilizzalo per creare una guida di piano:

-- plan handle is 0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000
SELECT qs.plan_handle, st.text FROM 
sys.dm_exec_query_stats AS qs   
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st  
WHERE st.text LIKE '%X[_]PLAN[_]GUIDE[_]TARGET%'
ORDER BY last_execution_time DESC;

EXEC sp_create_plan_guide_from_handle 
'EVIL_PLAN_GUIDE', 
0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000;

Le guide di piano funzionano solo sull'esatto testo della query, quindi copiamolo dalla guida di piano:

SELECT query_text
FROM sys.plan_guides
WHERE name = 'EVIL_PLAN_GUIDE';

Ripristina i dati:

TRUNCATE TABLE X_PLAN_GUIDE_TARGET;

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

Ottieni un piano di query per la query con la guida di piano applicata:

SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

Questo ha l'operatore di corrispondenza hash (flusso distinto) che volevamo con i nostri dati di test. Si noti che SQL Server prevede di leggere tutte le righe dalla tabella e che il costo stimato è identico a quello del piano con la corrispondenza hash (aggregato). I test che ho fatto hanno suggerito che i costi per i due piani sono identici quando l'obiettivo di riga per il piano è maggiore o uguale al numero di valori distinti che SQL Server prevede dalla tabella, che in questo caso può essere semplicemente derivato dal statistiche. Sfortunatamente (per la nostra query) l'ottimizzatore sceglie la corrispondenza hash (aggregata) sulla corrispondenza hash (flusso distinto) quando i costi sono gli stessi. Quindi siamo 0,0000001 unità di ottimizzazione magica lontano dal piano che vogliamo.

Un modo per attaccare questo problema è diminuire l'obiettivo della fila. Se l'obiettivo di riga dal punto di vista dell'ottimizzatore è inferiore al conteggio distinto di righe probabilmente avremo una corrispondenza hash (flusso distinto). Questo può essere realizzato con il OPTIMIZE FORsuggerimento per la query:

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

Per questa query l'ottimizzatore crea un piano come se la query necessitasse solo della prima riga ma quando viene eseguita la query restituisce le prime 10 righe. Sulla mia macchina questa query analizza 892800 righe da X_10_DISTINCT_HEAPe si completa in 299 ms con 250 ms di tempo CPU e 2537 letture logiche.

Si noti che questa tecnica non funzionerà se le statistiche riportano solo un valore distinto, che potrebbe accadere per le statistiche campionate rispetto a dati distorti. Tuttavia, in quel caso è improbabile che i tuoi dati siano sufficientemente densi da giustificare l'utilizzo di tecniche come questa. Non si può perdere molto scansionando tutti i dati nella tabella, specialmente se ciò può essere fatto in parallelo.

Un altro modo per attaccare questo problema è gonfiare il numero di valori distinti stimati che SQL Server prevede di ottenere dalla tabella di base. Questo è stato più difficile del previsto. L'applicazione di una funzione deterministica non può eventualmente aumentare il conteggio distinto dei risultati. Se Query Optimizer è a conoscenza di quel fatto matematico (alcuni test suggeriscono che è almeno per i nostri scopi), l'applicazione di funzioni deterministiche (che include tutte le funzioni di stringa ) non aumenterà il numero stimato di righe distinte.

Molte delle funzioni non deterministiche non hanno funzionato neanche, comprese le ovvie scelte di NEWID()e RAND(). Tuttavia, LAG()fa il trucco per questa query. Query Optimizer prevede 10 milioni di valori distinti rispetto LAGall'espressione che incoraggerà un piano di corrispondenza hash (flusso distinto) :

SELECT DISTINCT TOP 10 LAG(VAL, 0) OVER (ORDER BY (SELECT NULL)) AS ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);

Sulla mia macchina questa query analizza 892800 righe da X_10_DISTINCT_HEAPe si completa in 1165 ms con 1109 ms di tempo CPU e 2537 letture logiche, quindi LAG()aggiunge un po 'di sovraccarico relativo. @Paul White ha suggerito di provare l'elaborazione in modalità batch per questa query. Su SQL Server 2016 possiamo ottenere l'elaborazione in modalità batch anche con MAXDOP 1. Un modo per ottenere l'elaborazione in modalità batch per una tabella di archivio righe è quello di unirsi a un CCI vuoto come segue:

CREATE TABLE #X_DUMMY_CCI (ID INT NOT NULL);

CREATE CLUSTERED COLUMNSTORE INDEX X_DUMMY_CCI ON #X_DUMMY_CCI;

SELECT DISTINCT TOP 10 VAL
FROM
(
    SELECT LAG(VAL, 1) OVER (ORDER BY (SELECT NULL)) AS VAL
    FROM X_10_DISTINCT_HEAP
    LEFT OUTER JOIN #X_DUMMY_CCI ON 1 = 0
) t
WHERE t.VAL IS NOT NULL
OPTION (MAXDOP 1);

Tale codice risulta in questo piano di query .

Paul ha sottolineato che ho dovuto modificare la query da utilizzare LAG(..., 1)perché LAG(..., 0)non sembra essere idoneo per l'ottimizzazione di Window Aggregate. Questa modifica ha ridotto il tempo trascorso a 520 ms e il tempo della CPU a 454 ms.

Si noti che l' LAG()approccio non è il più stabile. Se Microsoft modifica l'assunto di unicità rispetto alla funzione, potrebbe non funzionare più. Ha una stima diversa con l'eredità CE. Anche questo tipo di ottimizzazione rispetto a un heap non è necessaria una buona idea. Se la tabella viene ricostruita, è possibile finire nel peggiore dei casi in cui quasi tutte le righe devono essere lette dalla tabella.

Su una tabella con una colonna univoca (come l'esempio dell'indice cluster nella domanda) abbiamo opzioni migliori. Ad esempio, possiamo ingannare l'ottimizzatore usando SUBSTRINGun'espressione che restituisce sempre una stringa vuota. SQL Server non ritiene che SUBSTRINGcambierà il numero di valori distinti, quindi se lo applichiamo a una colonna univoca, come PK, il numero stimato di righe distinte è di 10 milioni. Questa query seguente ottiene l'operatore di corrispondenza hash (flusso distinto):

SELECT DISTINCT TOP 10 VAL + SUBSTRING(CAST(PK AS VARCHAR(10)), 11, 1)
FROM X_10_DISTINCT_CI
OPTION (MAXDOP 1);

Sulla mia macchina questa query analizza 900000 righe X_10_DISTINCT_CIe si completa in 333 ms con 297 ms di tempo CPU e 3011 letture logiche.

In breve, Query Optimizer sembra supporre che tutte le righe verranno lette dalla tabella per le SELECT DISTINCT TOP Nquery quando N> = il numero di righe distinte stimate dalla tabella. L'operatore di corrispondenza hash (aggregato) può avere lo stesso costo dell'operatore di corrispondenza hash (flusso distinto) ma l'ottimizzatore seleziona sempre l'operatore aggregato. Ciò può portare a letture logiche non necessarie quando si trovano abbastanza valori distinti vicino all'inizio della scansione della tabella. Due modi per indurre l'ottimizzatore a utilizzare l'operatore di corrispondenza hash (flusso distinto) sono abbassare l'obiettivo di riga usando il OPTIMIZE FORsuggerimento o aumentare il numero stimato di righe distinte utilizzando LAG()o SUBSTRINGsu una colonna univoca.


12

Hai già risposto correttamente alle tue domande.

Voglio solo aggiungere un'osservazione che il modo più efficace è in realtà scansionare l'intera tabella, se può essere organizzata come un 'heap' di archivio colonne :

CREATE CLUSTERED COLUMNSTORE INDEX CCSI 
ON dbo.X_10_DISTINCT_HEAP;

La semplice query:

SELECT DISTINCT TOP (10)
    XDH.VAL 
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (MAXDOP 1);

quindi dà:

Progetto esecutivo

Tabella 'X_10_DISTINCT_HEAP'. Conteggio scansioni 1,
 letture logiche 0, letture fisiche 0, read-ahead letture 0, 
 lob logico legge 66 , lob fisico legge 0, lob read-ahead legge 0.
Tabella 'X_10_DISTINCT_HEAP'. Il segmento legge 13, il segmento ha saltato 0.

 Tempi di esecuzione di SQL Server:
   Tempo CPU = 0 ms, tempo trascorso = 11 ms.

Hash Match (Flow Distinct) non può attualmente essere eseguito in modalità batch. I metodi che lo utilizzano sono molto più lenti a causa della transizione (invisibile) costosa dall'elaborazione batch a quella riga. Per esempio:

SET ROWCOUNT 10;

SELECT DISTINCT 
    XDH.VAL
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (FAST 1);

SET ROWCOUNT 0;

dà:

Piano di esecuzione distinto del flusso

Tabella 'X_10_DISTINCT_HEAP'. Conteggio scansioni 1,
 letture logiche 0, letture fisiche 0, read-ahead letture 0, 
 lob logico legge 20 , lob fisico legge 0, lob read-ahead legge 0.
Tabella 'X_10_DISTINCT_HEAP'. Il segmento legge 4 , il segmento ha saltato 0.

 Tempi di esecuzione di SQL Server:
   Tempo CPU = 640 ms, tempo trascorso = 680 ms.

Questo è più lento rispetto a quando la tabella è organizzata come un heap del rowstore.


5

Ecco un tentativo di emulare una scansione parziale ripetuta (simile ma non uguale a una scansione saltata) usando un CTE ricorsivo. L'obiettivo - poiché non abbiamo alcun indice attivo (id)- è quello di evitare ordinamenti e scansioni multiple sul tavolo.

Fa alcuni trucchi per aggirare alcune restrizioni CTE ricorsive:

  • Non TOPconsentito nella parte ricorsiva. Usiamo una subquery e ROW_NUMBER()invece.
  • Non possiamo avere più riferimenti alla parte costante o utilizzare LEFT JOINo utilizzare NOT IN (SELECT id FROM cte)dalla parte ricorsiva. Per bypassare, costruiamo una VARCHARstringa che accumula tutti i idvalori, simili a STRING_AGGo con hierarchyID e poi confrontiamo con LIKE.

Per un Heap (supponendo che la colonna sia denominata id) test-1 su rextester.com .

Questo - come hanno dimostrato i test - non evita più scansioni ma funziona OK quando nelle prime pagine vengono trovati valori diversi. Se tuttavia i valori non vengono distribuiti uniformemente, è possibile che vengano eseguite più scansioni su grandi parti della tabella, il che ovviamente comporta prestazioni scadenti.

WITH ct (id, found, list) AS
  ( SELECT TOP (1) id, 1, CAST('/' + id + '/' AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.ID, ct.found + 1, CAST(ct.list + y.id + '/' AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 3         -- the TOP (n) parameter here
      AND y.rn = 1
  )
SELECT id FROM ct ;

e quando la tabella è raggruppata (CI attivounique_key ), test-2 su rextester.com .

Questo utilizza l'indice cluster ( WHERE x.unique_key > ct.unique_key) per evitare più scansioni:

WITH ct (unique_key, id, found, list) AS
  ( SELECT TOP (1) unique_key, id, 1, CAST(CONCAT('/',id, '/') AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.unique_key, y.ID, ct.found + 1, 
        CAST(CONCAT(ct.list, y.id, '/') AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.unique_key, x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE x.unique_key > ct.unique_key
          AND ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 5       -- the TOP (n) parameter here
      AND y.rn = 1
  )
-- SELECT * FROM ct ;        -- for debugging
SELECT id FROM ct ;

C'è un problema di prestazioni abbastanza sottile con questa soluzione. Finisce per fare una ricerca extra sul tavolo dopo aver trovato l'ennesimo valore. Quindi se ci sono 10 valori distinti per i primi 10 cercherà un 11 ° valore che non c'è. Si finisce con un'ulteriore scansione completa e i 10 milioni di calcoli ROW_NUMBER () si sommano davvero. Ho una soluzione alternativa qui che accelera la query 20X sulla mia macchina. Cosa pensi? brentozar.com/pastetheplan/?id=SkDhAmFKe
Joe Obbish

2

Per completezza, un altro modo per affrontare questo problema è utilizzare OUTER APPLY . Possiamo aggiungere un OUTER APPLYoperatore per ogni valore distinto che dobbiamo trovare. Questo concetto è simile all'approccio ricorsivo di ypercube, ma ha effettivamente la ricorsione scritta a mano. Un vantaggio è che siamo in grado di utilizzare TOPnelle tabelle derivate invece della ROW_NUMBER()soluzione alternativa. Un grande svantaggio è che il testo della query si allunga Nall'aumentare.

Ecco un'implementazione per la query sull'heap:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t2 WHERE t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t3 WHERE t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t4 WHERE t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t5 WHERE t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t6 WHERE t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t7 WHERE t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t8 WHERE t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t9 WHERE t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t10 WHERE t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

Ecco il piano di query effettivo per la query sopra. Sulla mia macchina questa query si completa in 713 ms con 625 ms di tempo CPU e 12605 letture logiche. Otteniamo un nuovo valore distinto ogni 100k righe, quindi mi aspetto che questa query esegua la scansione di circa 900000 * 10 * 0,5 = 4500000 righe. In teoria questa query dovrebbe fare cinque volte le letture logiche di questa query dall'altra risposta:

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

Quella query ha fatto 2537 letture logiche. 2537 * 5 = 12685 che è abbastanza vicino a 12605.

Per la tabella con l'indice cluster possiamo fare di meglio. Questo perché possiamo passare l'ultimo valore della chiave cluster nella tabella derivata per evitare di scansionare le stesse righe due volte. Un'implementazione:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t2 WHERE PK > t1.PK AND t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t3 WHERE PK > t2.PK AND t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t4 WHERE PK > t3.PK AND t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t5 WHERE PK > t4.PK AND t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t6 WHERE PK > t5.PK AND t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t7 WHERE PK > t6.PK AND t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t8 WHERE PK > t7.PK AND t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t9 WHERE PK > t8.PK AND t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t10 WHERE PK > t9.PK AND t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

Ecco il piano di query effettivo per la query sopra. Sulla mia macchina questa query si completa in 154 ms con 140 ms di tempo CPU e 3203 letture logiche. Questo sembrava funzionare un po 'più veloce della OPTIMIZE FORquery sulla tabella dell'indice cluster. Non me lo aspettavo, quindi ho cercato di misurare le prestazioni con più attenzione. La mia metodologia consisteva nell'eseguire ogni query dieci volte senza set di risultati e guardare i numeri aggregati da sys.dm_exec_sessionse sys.dm_exec_session_wait_stats. La sessione 56 era la APPLYquery e la sessione 63 era la OPTIMIZE FORquery.

Uscita di sys.dm_exec_sessions:

╔════════════╦══════════╦════════════════════╦═══════════════╗
 session_id  cpu_time  total_elapsed_time  logical_reads 
╠════════════╬══════════╬════════════════════╬═══════════════╣
         56      1360                1373          32030 
         63      2094                2091          30400 
╚════════════╩══════════╩════════════════════╩═══════════════╝

Sembra esserci un chiaro vantaggio in cpu_time e elapsed_time per la APPLYquery.

Uscita di sys.dm_exec_session_wait_stats:

╔════════════╦════════════════════════════════╦═════════════════════╦══════════════╦══════════════════╦═════════════════════╗
 session_id            wait_type             waiting_tasks_count  wait_time_ms  max_wait_time_ms  signal_wait_time_ms 
╠════════════╬════════════════════════════════╬═════════════════════╬══════════════╬══════════════════╬═════════════════════╣
         56  SOS_SCHEDULER_YIELD                             340             0                 0                    0 
         56  MEMORY_ALLOCATION_EXT                            38             0                 0                    0 
         63  SOS_SCHEDULER_YIELD                             518             0                 0                    0 
         63  MEMORY_ALLOCATION_EXT                            98             0                 0                    0 
         63  RESERVED_MEMORY_ALLOCATION_EXT                  400             0                 0                    0 
╚════════════╩════════════════════════════════╩═════════════════════╩══════════════╩══════════════════╩═════════════════════╝

La OPTIMIZE FORquery ha un tipo di attesa aggiuntivo, RESERVED_MEMORY_ALLOCATION_EXT . Non so esattamente cosa significhi. Potrebbe essere solo una misurazione del sovraccarico nell'operatore di hash match (flusso distinto). In ogni caso, forse non vale la pena preoccuparsi di una differenza di 70 ms nel tempo della CPU.


1

Penso che tu abbia una risposta sul perché
questo potrebbe essere un modo per affrontarlo,
so che sembra disordinato, ma il piano di esecuzione ha detto che la top 2 distinta era dell'84% del costo

SELECT distinct top (2)  [enumID]
FROM [ENRONbbb].[dbo].[docSVenum1]

declare @table table (enumID tinyint);
declare @enumID tinyint;
set @enumID = (select top (1) [enumID] from [docSVenum1]);
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
select enumID from @table;

Questo codice ha impiegato 5 secondi sulla mia macchina. Sembra che i join alla variabile della tabella aggiungano un po 'di sovraccarico. Nella query finale la variabile della tabella è stata scansionata 892800 volte. Quella query ha richiesto 1359 ms di tempo CPU e 1374 ms di tempo trascorso. Decisamente più di quanto mi aspettassi. L'aggiunta di una chiave primaria alla variabile della tabella sembra aiutare, anche se non sono sicuro del perché. Potrebbero esserci altre possibili ottimizzazioni.
Joe Obbish,
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.