Risultati di selezione imprevedibili di SQL Server (errore dbms?)


37

Di seguito è riportato un semplice esempio, che restituisce risultati strani, imprevedibili e non possiamo spiegarlo nel nostro team. Stiamo facendo qualcosa di sbagliato o è un errore di SQL Server?

Dopo alcune indagini abbiamo ridotto l'area di ricerca alla clausola sindacale nella sottoquery , che seleziona un record dalla tabella "men"

Funziona come previsto in SQL Server 2000 (restituisce 12 righe), ma nel 2008 e 2012 restituisce solo una riga.

create table dual (dummy int)

insert into dual values (0)

create table men (
man_id int,
wife_id int )

-- there are 12 men, 6 married 
insert into men values (1, 1)
insert into men values (2, 2)
insert into men values (3, null)
insert into men values (4, null)
insert into men values (5, null)
insert into men values (6, 3)
insert into men values (7, 5)
insert into men values (8, 7)
insert into men values (9, null)
insert into men values (10, null)
insert into men values (11, null)
insert into men values (12, 9)

Ciò restituisce solo una riga: 1 1 2

select 
man_id,
wife_id,
(select count( * ) from 
    (select dummy from dual
     union select men.wife_id  ) family_members
) as family_size
from men
--where wife_id = 2 -- uncomment me and try again

Elimina il commento dall'ultima riga e restituisce: 2 2 2

Esistono molti comportamenti strani:

  • Dopo una serie di drop, crea, tronca e inserisce sulla tabella "men" a volte funziona (restituisce 12 righe)
  • Quando cambi "union select men.wife_id" in "union all select men.wife_id" o "union select isnull (men.wife_id, null)" (!!!) restituisce 12 righe (come previsto).
  • Lo strano comportamento sembra non essere correlato al tipo di dati della colonna "wife_id". Lo abbiamo osservato sul sistema di sviluppo con set di dati molto più grandi.
  • "where wife_id> 0" restituisce 6 righe
  • osserviamo anche comportamenti strani di opinioni con questo tipo di affermazioni. SELECT * restituisce un sottoinsieme di righe, SELECT TOP 1000 restituisce tutto

Risposte:


35

Stiamo facendo qualcosa di sbagliato o è un errore di SQL Server?

È un bug con risultati errati, che dovresti segnalare tramite il tuo solito canale di supporto. Se non si dispone di un accordo di supporto, può essere utile sapere che gli incidenti pagati vengono normalmente rimborsati se Microsoft conferma il comportamento come un bug.

Il bug richiede tre ingredienti:

  1. Cicli annidati con un riferimento esterno (un applicare)
  2. Una bobina di indice pigro sul lato interno che cerca il riferimento esterno
  3. Un operatore di concatenazione sul lato interno

Ad esempio, la query nella domanda produce un piano come il seguente:

Piano annotato

Esistono molti modi per rimuovere uno di questi elementi, quindi il bug non si riproduce più.

Ad esempio, si potrebbero creare indici o statistiche che potrebbero significare che l'ottimizzatore sceglie di non utilizzare una bobina di indice pigro. Oppure, si potrebbero usare i suggerimenti per forzare un hash o unire l'unione invece di usare la concatenazione. Si potrebbe anche riscrivere la query per esprimere la stessa semantica, ma ciò si traduce in una diversa forma del piano in cui mancano uno o più degli elementi richiesti.

Più dettagli

Una bobina di indice pigro memorizza pigramente le righe dei risultati del lato interno, in una tabella di lavoro indicizzata dai valori di riferimento esterno (parametro correlato). Se viene richiesto a un Lazy Index Spool un riferimento esterno che ha visto in precedenza, recupera la riga dei risultati memorizzata nella cache dalla sua tabella di lavoro (un "riavvolgimento"). Se viene chiesto alla bobina un valore di riferimento esterno che non ha mai visto prima, esegue la sua sottostruttura con il valore di riferimento esterno corrente e memorizza nella cache il risultato (un "rebind"). Il predicato di ricerca sullo spool Lazy Index indica le chiavi per la sua tabella di lavoro.

Il problema si verifica in questa specifica forma del piano quando il rocchetto verifica se un nuovo riferimento esterno è uguale a quello che aveva visto prima. The Nested Loops Join aggiorna correttamente i suoi riferimenti esterni e avvisa gli operatori sull'input interno tramite i loro PrepRecomputemetodi di interfaccia. All'inizio di questo controllo, gli operatori del lato interno leggono la CParamBounds:FNeedToReloadproprietà per vedere se il riferimento esterno è cambiato dall'ultima volta. Di seguito viene mostrato un esempio di stack stack:

CParamBounds: FNeedToReload

Quando esiste la sottostruttura mostrata sopra, in particolare quando viene utilizzata la concatenazione, qualcosa va storto (forse un problema ByVal / ByRef / Copy) con i binding in modo tale che CParamBounds:FNeedToReloadritorni sempre falso, indipendentemente dal fatto che il riferimento esterno sia effettivamente cambiato o meno.

Quando esiste la stessa sottostruttura, ma viene utilizzata un'unione di unione o unione di hash, questa proprietà essenziale viene impostata correttamente su ogni iterazione e la bobina dell'indice pigro riavvolge o si riavvolge ogni volta, a seconda dei casi. A proposito, Distinct Sort e Stream Aggregate sono irreprensibili. Ho il sospetto che Merge e Hash Union facciano una copia del valore precedente, mentre Concatenation utilizza un riferimento. Purtroppo è quasi impossibile verificarlo senza l'accesso al codice sorgente di SQL Server.

Il risultato netto è che la Lazy Index Spool nella forma del piano problematico pensa sempre di aver già visto l'attuale riferimento esterno, si riavvolge cercando nella sua tabella di lavoro, generalmente non trova nulla, quindi non viene restituita alcuna riga per quel riferimento esterno. Passando attraverso l'esecuzione in un debugger, lo spool esegue sempre il suo RewindHelpermetodo e mai il suo ReloadHelpermetodo (ricarica = rebind in questo contesto). Ciò è evidente nel piano di esecuzione perché tutti gli operatori sotto lo spool hanno "Numero di esecuzioni = 1".

RewindHelper

L'eccezione, ovviamente, è per il primo riferimento esterno che viene dato il Lazy Index Spool. Questo esegue sempre la sottostruttura e memorizza nella cache una riga dei risultati nella tabella di lavoro. Tutte le successive iterazioni generano un riavvolgimento, che produrrà solo una riga (la singola riga memorizzata nella cache) quando l'attuale iterazione ha lo stesso valore per il riferimento esterno della prima volta.

Quindi, per ogni dato set di input sul lato esterno del Nested Loops Join, la query restituirà tutte le righe quanti sono i duplicati della prima riga elaborata (più una ovviamente per la prima riga stessa).

dimostrazione

Tabella e dati di esempio:

CREATE TABLE #T1 
(
    pk integer IDENTITY NOT NULL,
    c1 integer NOT NULL,

    CONSTRAINT PK_T1
    PRIMARY KEY CLUSTERED (pk)
);
GO
INSERT #T1 (c1)
VALUES
    (1), (2), (3), (4), (5), (6),
    (1), (2), (3), (4), (5), (6),
    (1), (2), (3), (4), (5), (6);

La seguente query (banale) produce un conteggio corretto di due per ogni riga (18 in totale) utilizzando un'unione unione:

SELECT T1.c1, C.c1
FROM #T1 AS T1
CROSS APPLY 
(
    SELECT COUNT_BIG(*) AS c1
    FROM
    (
        SELECT T1.c1
        UNION
        SELECT NULL
    ) AS U
) AS C;

Unisci piano dell'Unione

Se ora aggiungiamo un suggerimento per forzare una concatenazione:

SELECT T1.c1, C.c1
FROM #T1 AS T1
CROSS APPLY 
(
    SELECT COUNT_BIG(*) AS c1
    FROM
    (
        SELECT T1.c1
        UNION
        SELECT NULL
    ) AS U
) AS C
OPTION (CONCAT UNION);

Il piano di esecuzione ha la forma problematica:

Piano di concatenazione

E il risultato ora è errato, solo tre righe:

Risultato a tre righe

Sebbene questo comportamento non sia garantito, la prima riga della Scansione indice cluster ha un c1valore di 1. Vi sono altre due righe con questo valore, quindi vengono prodotte in totale tre righe.

Ora tronca la tabella dei dati e caricala con più duplicati della "prima" riga:

TRUNCATE TABLE #T1;

INSERT #T1 (c1)
VALUES
    (1), (2), (3), (4), (5), (6),
    (1), (2), (3), (4), (5), (6),
    (1), (1), (1), (1), (1), (1);

Ora il piano di concatenazione è:

Piano di concatenazione di 8 file

E, come indicato, vengono prodotte 8 righe, tutte c1 = 1ovviamente con:

Risultato di 8 righe

Ho notato che hai aperto un elemento Connect per questo bug, ma in realtà non è il posto giusto per segnalare problemi che hanno un impatto sulla produzione. In tal caso, è necessario contattare il supporto Microsoft.


Questo bug con risultati errati è stato corretto in qualche momento. Non si riproduce più per me su nessuna versione di SQL Server dal 2012 in poi. Riproduzione su SQL Server 2008 R2 SP3-GDR build 10.50.6560.0 (X64).


-3

Perché usi una sottoquery senza l'istruzione from? Penso che ciò possa causare la differenza nei server 2005 e 2008. Forse potresti andare con un join esplicito?

select 
m1.man_id,
m1.wife_id,
(select count( * ) from 
    (select dummy from dual
     union
     select m2.wife_id
     from men m2
     where m2.man_id = m1.man_id) family_members
) as family_size
from men m1

3
Sì, funziona, ma dovrebbe funzionare anche la mia versione. L'esempio sopra riportato è una versione molto semplificata della nostra query di produzione, il che ha molto più senso.
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.