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

Alcune QUALSIASI trasformazione aggregata sono interrotte

Il ANY aggregate non è qualcosa che possiamo scrivere direttamente in Transact SQL. È una funzionalità solo interna utilizzata da Query Optimizer e dal motore di esecuzione.

Personalmente sono abbastanza affezionato a ANY aggregato, quindi è stato un po' deludente apprendere che è rotto in un modo abbastanza fondamentale. Il particolare sapore di "rotto" a cui mi riferisco qui è la varietà con risultati sbagliati.

In questo post, darò un'occhiata a due luoghi particolari in cui ANY aggregato si presenta comunemente, mostra il problema dei risultati errati e suggerisce soluzioni alternative ove necessario.

Per informazioni su ANY aggregato, vedere il mio post precedente Piani di query non documentati:ANY Aggregate.

1. Una riga per query di gruppo

Questo deve essere uno dei requisiti di query più comuni di tutti i giorni, con una soluzione molto nota. Probabilmente scrivi questo tipo di query ogni giorno, seguendo automaticamente lo schema, senza pensarci davvero.

L'idea è di numerare il set di righe di input utilizzando il ROW_NUMBER funzione finestra, partizionata dalla colonna o dalle colonne di raggruppamento. Questo è racchiuso in un'Espressione di tabella comune o tabella derivata e filtrato fino alle righe in cui il numero di riga calcolato è uguale a uno. Dal ROW_NUMBER riparte da uno per ogni gruppo, questo ci dà la riga richiesta per gruppo.

Non ci sono problemi con questo schema generale. Il tipo di una riga per query di gruppo soggetta a ANY il problema aggregato è quello in cui non ci interessa quale riga particolare è selezionata da ogni gruppo.

In tal caso, non è chiaro quale colonna debba essere utilizzata nel ORDER BY obbligatorio clausola del ROW_NUMBER funzione finestra. Dopotutto, a noi esplicitamente non importa quale riga è selezionata. Un approccio comune consiste nel riutilizzare PARTITION BY colonna/e nel ORDER BY clausola. È qui che potrebbe verificarsi il problema.

Esempio

Diamo un'occhiata a un esempio utilizzando un set di dati giocattolo:

CREATE TABLE #Data
(
    c1 integer NULL,
    c2 integer NULL,
    c3 integer NULL
);
 
INSERT #Data
    (c1, c2, c3)
VALUES
    -- Group 1
    (1, NULL, 1),
    (1, 1, NULL),
    (1, 111, 111),
    -- Group 2
    (2, NULL, 2),
    (2, 2, NULL),
    (2, 222, 222);

Il requisito è restituire una riga completa di dati da ciascun gruppo, dove l'appartenenza al gruppo è definita dal valore nella colonna c1 .

Dopo il ROW_NUMBER pattern, potremmo scrivere una query come la seguente (notare il ORDER BY clausola del ROW_NUMBER la funzione della finestra corrisponde a PARTITION BY clausola):

WITH 
    Numbered AS 
    (
        SELECT 
            D.*, 
            rn = ROW_NUMBER() OVER (
                PARTITION BY D.c1
                ORDER BY D.c1) 
        FROM #Data AS D
    )
SELECT
    N.c1, 
    N.c2, 
    N.c3
FROM Numbered AS N
WHERE
    N.rn = 1;

Come presentato, questa query viene eseguita correttamente, con risultati corretti. I risultati sono tecnicamente non deterministici poiché SQL Server potrebbe restituire validamente una qualsiasi delle righe in ogni gruppo. Tuttavia, se esegui tu stesso questa query, è molto probabile che vedrai lo stesso risultato che vedo io:

Il piano di esecuzione dipende dalla versione di SQL Server utilizzata e non dal livello di compatibilità del database.

In SQL Server 2014 e versioni precedenti, il piano è:

Per SQL Server 2016 o versioni successive, vedrai:

Entrambi i piani sono sicuri, ma per ragioni diverse. Il ordinamento distinto il piano contiene un ANY aggregato, ma l'Ordinamento distinto l'implementazione dell'operatore non manifesta il bug.

Il piano SQL Server 2016+ più complesso non utilizza ANY aggregare affatto. Ordina mette le righe nell'ordine necessario per l'operazione di numerazione delle righe. Il segmento l'operatore imposta un flag all'inizio di ogni nuovo gruppo. Il progetto sequenza calcola il numero di riga. Infine, il Filtro l'operatore passa solo quelle righe che hanno un numero di riga calcolato pari a uno.

Il bug

Per ottenere risultati errati con questo set di dati, è necessario utilizzare SQL Server 2014 o versioni precedenti e ANY gli aggregati devono essere implementati in un aggregato di flusso o Desideroso Hash Aggregate operatore (Aggregato di corrispondenza hash distinti di flusso non produce il bug).

Un modo per incoraggiare l'ottimizzatore a scegliere un aggregato di flusso invece di Ordinamento distinto consiste nell'aggiungere un indice cluster per fornire l'ordinamento per colonna c1 :

CREATE CLUSTERED INDEX c ON #Data (c1);

Dopo tale modifica, il piano di esecuzione diventa:

Il ANY gli aggregati sono visibili nelle Proprietà finestra quando l'aggregazione flusso è selezionato l'operatore:

Il risultato della query è:

Questo è sbagliato . SQL Server ha restituito righe che non esistono nei dati di origine. Non ci sono righe di origine in cui c2 = 1 e c3 = 1 Per esempio. Ricordiamo che i dati di origine sono:

Il piano di esecuzione calcola erroneamente separato ANY aggregati per il c2 e c3 colonne, ignorando i valori null. Ciascuno aggregato indipendentemente restituisce il primo non null valore che incontra, dando un risultato dove i valori per c2 e c3 provengono da diverse righe di origine . Questo non è ciò che richiedeva la specifica della query SQL originale.

Lo stesso risultato errato può essere prodotto con o senza l'indice cluster aggiungendo un OPTION (HASH GROUP) suggerimento per produrre un piano con un Eager Hash Aggregate invece di un aggregato di flusso .

Condizioni

Questo problema può verificarsi solo quando più ANY sono presenti aggregati e i dati aggregati contengono valori null. Come notato, il problema riguarda solo Stream Aggregate e Deager Hash Aggregate operatori; Ordinamento distinto e Flusso distinto non sono interessati.

SQL Server 2016 in poi fa uno sforzo per evitare di introdurre più ANY aggregati per il modello di query di numerazione delle righe qualsiasi riga per gruppo quando le colonne di origine non supportano valori nulla. Quando ciò accade, il piano di esecuzione conterrà Segmento , Progetto sequenza e Filtro operatori anziché un aggregato. Questa forma del piano è sempre sicura, poiché nessun ANY vengono utilizzati aggregati.

Riproduzione del bug in SQL Server 2016+

L'ottimizzatore di SQL Server non è perfetto per rilevare quando una colonna originariamente vincolata a essere NOT NULL potrebbe ancora produrre un valore intermedio nullo attraverso manipolazioni di dati.

Per riprodurre questo, inizieremo con una tabella in cui tutte le colonne sono dichiarate come NOT NULL :

IF OBJECT_ID(N'tempdb..#Data', N'U') IS NOT NULL
BEGIN
    DROP TABLE #Data;
END;
 
CREATE TABLE #Data
(
    c1 integer NOT NULL,
    c2 integer NOT NULL,
    c3 integer NOT NULL
);
 
CREATE CLUSTERED INDEX c ON #Data (c1);
 
INSERT #Data
    (c1, c2, c3)
VALUES
    -- Group 1
    (1, 1, 1),
    (1, 2, 2),
    (1, 3, 3),
    -- Group 2
    (2, 1, 1),
    (2, 2, 2),
    (2, 3, 3);

Possiamo produrre valori nulli da questo set di dati in molti modi, la maggior parte dei quali l'ottimizzatore può rilevare con successo, evitando così di introdurre ANY aggregati durante l'ottimizzazione.

Di seguito è mostrato un modo per aggiungere valori nulli che sfuggono al radar:

SELECT
    D.c1,
    OA1.c2,
    OA2.c3
FROM #Data AS D
OUTER APPLY (SELECT D.c2 WHERE D.c2 <> 1) AS OA1
OUTER APPLY (SELECT D.c3 WHERE D.c3 <> 2) AS OA2;

Tale query produce il seguente output:

Il passaggio successivo consiste nell'utilizzare la specifica della query come dati di origine per la query standard "qualsiasi riga per gruppo":

WITH
    SneakyNulls AS 
    (
        -- Introduce nulls the optimizer can't see
        SELECT
            D.c1,
            OA1.c2,
            OA2.c3
        FROM #Data AS D
        OUTER APPLY (SELECT D.c2 WHERE D.c2 <> 1) AS OA1
        OUTER APPLY (SELECT D.c3 WHERE D.c3 <> 2) AS OA2
    ),
    Numbered AS 
    (
        SELECT
            D.c1,
            D.c2,
            D.c3,
            rn = ROW_NUMBER() OVER (
                PARTITION BY D.c1
                ORDER BY D.c1) 
        FROM SneakyNulls AS D
    )
SELECT
    N.c1, 
    N.c2, 
    N.c3
FROM Numbered AS N
WHERE
    N.rn = 1;

Su qualsiasi versione di SQL Server, che produce il seguente piano:

L'aggregato di flusso contiene più ANY aggregati e il risultato è errato . Nessuna delle righe restituite viene visualizzata nel set di dati di origine:

db<>dimostrazione online di violino

Soluzione alternativa

L'unica soluzione completamente affidabile fino a quando questo bug non viene risolto è evitare lo schema in cui il ROW_NUMBER ha la stessa colonna in ORDER BY clausola come è nel PARTITION BY clausola.

Quando non ci interessa quale viene selezionata una riga da ogni gruppo, è un peccato che un ORDER BY la clausola è assolutamente necessaria. Un modo per aggirare il problema è utilizzare una costante di runtime come ORDER BY @@SPID nella funzione finestra.

2. Aggiornamento non deterministico

Il problema con più ANY aggregati su input nullable non è limitato al modello di query di una riga per gruppo. Query Optimizer può introdurre un ANY interno aggregare in una serie di circostanze. Uno di questi casi è un aggiornamento non deterministico.

Un non deterministico update è dove l'istruzione non garantisce che ogni riga di destinazione verrà aggiornata al massimo una volta. In altre parole, esistono più righe di origine per almeno una riga di destinazione. La documentazione avverte esplicitamente di questo:

Fai attenzione quando specifichi la clausola FROM per fornire i criteri per l'operazione di aggiornamento.
I risultati di un'istruzione UPDATE non sono definiti se l'istruzione include una clausola FROM che non è specificata in modo tale che sia disponibile un solo valore per ogni occorrenza di colonna che viene aggiornata, che è se l'istruzione UPDATE non è deterministica.

Per gestire un aggiornamento non deterministico, l'ottimizzatore raggruppa le righe in base a una chiave (indice o RID) e applica ANY aggregati alle colonne rimanenti. L'idea di base è scegliere una riga tra più candidati e utilizzare i valori di quella riga per eseguire l'aggiornamento. Ci sono evidenti parallelismi con il precedente ROW_NUMBER problema, quindi non sorprende che sia abbastanza facile dimostrare un aggiornamento errato.

A differenza del problema precedente, attualmente SQL Server non esegue nessuna procedura speciale per evitare più ANY aggregati su colonne nullable durante l'esecuzione di un aggiornamento non deterministico. Di conseguenza, quanto segue si riferisce a tutte le versioni di SQL Server , incluso SQL Server 2019 CTP 3.0.

Esempio

DECLARE @Target table
(
    c1 integer PRIMARY KEY, 
    c2 integer NOT NULL, 
    c3 integer NOT NULL
);
 
DECLARE @Source table 
(
    c1 integer NULL, 
    c2 integer NULL, 
    c3 integer NULL, 
 
    INDEX c CLUSTERED (c1)
);
 
INSERT @Target 
    (c1, c2, c3) 
VALUES 
    (1, 0, 0);
 
INSERT @Source 
    (c1, c2, c3) 
VALUES 
    (1, 2, NULL),
    (1, NULL, 3);
 
UPDATE T
SET T.c2 = S.c2,
    T.c3 = S.c3
FROM @Target AS T
JOIN @Source AS S
    ON S.c1 = T.c1;
 
SELECT * FROM @Target AS T;

db<>dimostrazione online di violino

Logicamente, questo aggiornamento dovrebbe sempre produrre un errore:la tabella di destinazione non consente valori null in nessuna colonna. Qualunque riga corrispondente sia stata scelta dalla tabella di origine, un tentativo di aggiornare la colonna c2 o c3 a null deve verificarsi.

Sfortunatamente, l'aggiornamento ha esito positivo e lo stato finale della tabella di destinazione non è coerente con i dati forniti:

Ho segnalato questo come un bug. La soluzione è evitare di scrivere UPDATE non deterministico dichiarazioni, quindi ANY gli aggregati non sono necessari per risolvere l'ambiguità.

Come accennato, SQL Server può introdurre ANY aggrega in più circostanze rispetto ai due esempi qui riportati. Se ciò si verifica quando la colonna aggregata contiene valori null, è possibile che vengano generati risultati errati.