Forcing Flow Distinct


19

Ho un tavolo come questo:

CREATE TABLE Updates
(
    UpdateId INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
    ObjectId INT NOT NULL
)

Sostanzialmente tenere traccia degli aggiornamenti degli oggetti con un ID crescente.

Il consumatore di questa tabella selezionerà un blocco di 100 ID oggetto distinti, ordinati per UpdateIde partendo da uno specifico UpdateId. In sostanza, tenere traccia di dove era stato interrotto e quindi eseguire una query per eventuali aggiornamenti.

Ho trovato che questo è un interessante problema di ottimizzazione perché sono stato in grado di generare un piano di query massimamente ottimale scrivendo query che capita di fare ciò che voglio a causa degli indici, ma non garantiscono ciò che voglio:

SELECT DISTINCT TOP 100 ObjectId
FROM Updates
WHERE UpdateId > @fromUpdateId

Dove si @fromUpdateIdtrova un parametro della procedura memorizzata.

Con un piano di:

SELECT <- TOP <- Hash match (flow distinct, 100 rows touched) <- Index seek

A causa della ricerca UpdateIdsull'indice in uso, i risultati sono già corretti e ordinati come ID di aggiornamento dal più basso al più alto come voglio. E questo genera un piano distinto di flusso , che è quello che voglio. Ma l'ordinamento ovviamente non è un comportamento garantito, quindi non voglio usarlo.

Questo trucco comporta anche lo stesso piano di query (sebbene con un TOP ridondante):

WITH ids AS
(
    SELECT ObjectId
    FROM Updates
    WHERE UpdateId > @fromUpdateId
    ORDER BY UpdateId OFFSET 0 ROWS
)
SELECT DISTINCT TOP 100 ObjectId FROM ids

Tuttavia, non sono sicuro (e sospetto di no) se questo garantisce veramente l'ordinazione.

Una query che speravo che SQL Server fosse abbastanza intelligente da semplificare era questa, ma finisce per generare un piano di query molto scadente:

SELECT TOP 100 ObjectId
FROM Updates
WHERE UpdateId > @fromUpdateId
GROUP BY ObjectId
ORDER BY MIN(UpdateId)

Con un piano di:

SELECT <- Top N Sort <- Hash Match aggregate (50,000+ rows touched) <- Index Seek

Sto cercando di trovare un modo per generare un piano ottimale con una ricerca dell'indice attivata UpdateIde un flusso distinto per rimuovere i duplicati ObjectId. Qualche idea?

Dati di esempio se lo si desidera. Gli oggetti raramente avranno più di un aggiornamento e non dovrebbero quasi mai averne più di uno in un set di 100 righe, motivo per cui sto cercando un flusso distinto , a meno che non ci sia qualcosa di meglio che non conosco? Tuttavia, non esiste alcuna garanzia che un singolo ObjectIdnon abbia più di 100 righe nella tabella. La tabella ha oltre 1.000.000 di righe e dovrebbe crescere rapidamente.

Supponiamo che l'utente abbia un altro modo per trovare il successivo appropriato @fromUpdateId. Non è necessario restituirlo in questa query.

Risposte:


15

L'ottimizzatore di SQL Server non è in grado di produrre il piano di esecuzione che stai cercando con la garanzia di cui hai bisogno, poiché l' operatore Hash Match Flow Distinct non mantiene gli ordini.

Tuttavia, non sono sicuro (e sospetto di no) se questo garantisce veramente l'ordinazione.

In molti casi è possibile osservare la conservazione dell'ordine, ma si tratta di un dettaglio di implementazione; non esiste alcuna garanzia, quindi non puoi fare affidamento su di esso. Come sempre, l'ordine di presentazione può essere garantito solo da una ORDER BYclausola di livello superiore .

Esempio

Lo script seguente mostra che Hash Match Flow Distinct non mantiene l'ordine. Imposta la tabella in questione con i numeri corrispondenti 1-50.000 in entrambe le colonne:

IF OBJECT_ID(N'dbo.Updates', N'U') IS NOT NULL
    DROP TABLE dbo.Updates;
GO
CREATE TABLE Updates
(
    UpdateId INT NOT NULL IDENTITY(1,1),
    ObjectId INT NOT NULL,

    CONSTRAINT PK_Updates_UpdateId PRIMARY KEY (UpdateId)
);
GO
INSERT dbo.Updates (ObjectId)
SELECT TOP (50000)
    ObjectId =
        ROW_NUMBER() OVER (
            ORDER BY C1.[object_id]) 
FROM sys.columns AS C1
CROSS JOIN sys.columns AS C2
ORDER BY
    ObjectId;

La query di prova è:

DECLARE @Rows bigint = 50000;

-- Optimized for 1 row, but will be 50,000 when executed
SELECT DISTINCT TOP (@Rows)
    U.ObjectId 
FROM dbo.Updates AS U
WHERE 
    U.UpdateId > 0
OPTION (OPTIMIZE FOR (@Rows = 1));

Il piano stimato mostra un indice di ricerca e flusso distinti:

Piano stimato

L'output sembra sicuramente iniziare con:

Inizio dei risultati

... ma più in basso i valori iniziano a diventare "mancanti":

Ripartizione del motivo

...ed eventualmente:

Scoppia il caos

La spiegazione in questo caso particolare è che l'operatore hash versa:

Piano post-esecuzione

Una volta che una partizione si riversa, si riversano anche tutte le righe che hanno l'hash nella stessa partizione. Le partizioni rovesciate vengono elaborate in un secondo momento, interrompendo l'aspettativa che valori distinti rilevati vengano emessi immediatamente nella sequenza in cui vengono ricevuti.


Esistono molti modi per scrivere una query efficiente per produrre il risultato ordinato desiderato, come la ricorsione o l'utilizzo di un cursore. Tuttavia, non può essere fatto utilizzando Hash Match Flow Distinct .


11

Non sono soddisfatto di questa risposta perché non sono riuscito a ottenere un operatore distinto di flusso con risultati garantiti come corretti. Tuttavia, ho un'alternativa che dovrebbe ottenere buone prestazioni insieme a risultati corretti. Sfortunatamente richiede che un indice non cluster venga creato sulla tabella.

Ho affrontato questo problema cercando di pensare a una combinazione di colonne che potevo ORDER BYe ottenere i risultati corretti dopo DISTINCTaverli applicati . Il valore minimo di UpdateIdper ObjectIdinsieme a ObjectIdè una tale combinazione. Tuttavia, la richiesta diretta del minimo UpdateIdsembra comportare la lettura di tutte le righe dalla tabella. Invece, possiamo indirettamente chiedere il valore minimo di UpdateIdcon un altro join alla tabella. L'idea è di scansionare la Updatestabella in ordine, eliminare tutte le righe per le quali UpdateIdnon è il valore minimo per quella riga ObjectIde mantenere le prime 100 righe. In base alla tua descrizione della distribuzione dei dati non dovremmo aver bisogno di eliminare molte righe.

Per la preparazione dei dati, ho inserito 1 milione di righe in una tabella con 2 righe per ciascun ObjectId distinto:

INSERT INTO Updates WITH (TABLOCK)
SELECT t.RN / 2
FROM 
(
    SELECT TOP 1000000 -1 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) t;

CREATE INDEX IX On Updates (Objectid, UpdateId);

L'indice non cluster su Objectided UpdateIdè importante. Ci consente di eliminare in modo efficiente righe che non hanno il minimo UpdateIdper Objectid. Esistono molti modi per scrivere una query che corrisponde alla descrizione sopra. Ecco uno di questi modi usando NOT EXISTS:

DECLARE @fromUpdateId INT = 9999;
SELECT ObjectId
FROM (
    SELECT DISTINCT TOP 100 u1.UpdateId, u1.ObjectId
    FROM Updates u1
    WHERE UpdateId > @fromUpdateId
    AND NOT EXISTS (
        SELECT 1
        FROM Updates u2
        WHERE u2.UpdateId > @fromUpdateId
        AND u1.ObjectId = u2.ObjectId
        AND u2.UpdateId < u1.UpdateId
    )
    ORDER BY u1.UpdateId, u1.ObjectId
) t;

Ecco un'immagine del piano di query :

piano di query

Nel migliore dei casi, SQL Server eseguirà solo 100 ricerche di indice rispetto all'indice non cluster. Per simulare di essere molto sfortunato ho modificato la query per restituire le prime 5000 righe al client. Ciò ha comportato la ricerca di 9999 indici, quindi è come ottenere una media di 100 righe per distinto ObjectId. Ecco l'output di SET STATISTICS IO, TIME ON:

Tabella "Aggiornamenti". Conteggio scansioni 10000, letture logiche 31900, letture fisiche 0

Tempi di esecuzione di SQL Server: tempo CPU = 31 ms, tempo trascorso = 42 ms.


9

Adoro la domanda: Flow Distinct è uno dei miei operatori preferiti.

Ora, la garanzia è il problema. Quando pensi all'operatore FD che tira le righe dall'operatore Seek in modo ordinato, producendo ogni riga in quanto determina che è unica, questo ti darà le righe nell'ordine giusto. Ma è difficile sapere se potrebbero esserci alcuni scenari in cui l'FD non gestisce una singola riga alla volta.

Teoricamente, l'FD potrebbe richiedere 100 righe al Seek e produrle nell'ordine in cui ne ha bisogno.

I suggerimenti per le query OPTION (FAST 1, MAXDOP 1)potrebbero essere d'aiuto, poiché eviterà di ottenere più righe di quelle necessarie dall'operatore Seek. È una garanzia però? Non proprio. Poteva ancora decidere di tirare una pagina di righe alla volta o qualcosa del genere.

Penso che la OPTION (FAST 1, MAXDOP 1)tua OFFSETversione ti darebbe molta fiducia nell'ordine, ma non è una garanzia.


Come ho capito, il problema è che l'operatore Flow Distinct utilizza una tabella hash che può essere versata su disco. In caso di fuoriuscita, le righe che possono essere elaborate utilizzando la parte ancora nella RAM vengono elaborate immediatamente, ma le altre righe non vengono elaborate fino a quando i dati versati non vengono riletti dal disco. Da quello che posso dire, qualsiasi operatore che utilizza una tabella hash (come un join hash) non è garantito per preservare l'ordine a causa del suo comportamento di fuoriuscita.
sam.bishop

Corretta. Vedi la risposta di Paul White.
Rob Farley,
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.