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

Forkerte resultater med Merge Join

Hvert produkt har fejl, og SQL Server er ingen undtagelse. At bruge produktfunktioner på en lidt usædvanlig måde (eller at kombinere relativt nye funktioner sammen) er en fantastisk måde at finde dem på. Fejl kan være interessante og endda lærerige, men måske går nogle af glæderne tabt, når opdagelsen resulterer i, at din personsøger går i gang kl. 04.00, måske efter en særlig social aften med venner...

Fejlen, der er emnet for dette indlæg, er nok rimelig sjælden i naturen, men det er ikke en klassisk kantsag. Jeg kender til mindst én konsulent, der er stødt på det i et produktionssystem. Med hensyn til et helt ubeslægtet emne, bør jeg benytte lejligheden til at sige "hej" til Grumpy Old DBA (blog).

Jeg vil starte med noget relevant baggrund om merge joins. Hvis du er sikker på, at du allerede ved alt, hvad der er at vide om merge join, eller bare ønsker at komme til benet, er du velkommen til at rulle ned til afsnittet med titlen "The Bug."

Flet tilmelding

Merge join er ikke en særlig kompliceret ting og kan være meget effektiv under de rigtige omstændigheder. Det kræver, at dets input er sorteret på join-tasterne og fungerer bedst i en-til-mange-tilstand (hvor i det mindste af dets input er unikke på join-tasterne). For moderat størrelse en-til-mange joinforbindelser er seriel merge join overhovedet ikke et dårligt valg, forudsat at inputsorteringskravene kan opfyldes uden at udføre en eksplicit sortering.

At undgå en sortering opnås oftest ved at udnytte rækkefølgen fra et indeks. Merge join kan også drage fordel af bevaret sorteringsrækkefølge fra en tidligere, uundgåelig sortering. En cool ting ved merge join er, at den kan stoppe med at behandle inputrækker, så snart begge input løber tør for rækker. En sidste ting:Merge join er ligeglad med, om sorteringsrækkefølgen for input er stigende eller faldende (selvom begge input skal være ens). Følgende eksempel bruger en standard Numbers-tabel til at illustrere de fleste af punkterne ovenfor:

CREATE TABLE #T1 (col1 integer CONSTRAINT PK1 PRIMARY KEY (col1 DESC));
CREATE TABLE #T2 (col1 integer CONSTRAINT PK2 PRIMARY KEY (col1 DESC));
 
INSERT #T1 SELECT n FROM dbo.Numbers WHERE n BETWEEN 10000 AND 19999;
INSERT #T2 SELECT n FROM dbo.Numbers WHERE n BETWEEN 18000 AND 21999;

Bemærk, at de indekser, der håndhæver de primære nøgler på disse to tabeller, er defineret som faldende. Forespørgselsplanen for INSERT har en række interessante funktioner:

Læses fra venstre mod højre (som kun er fornuftigt!) har Clustered Index Insert egenskabssættet "DML Request Sort". Dette betyder, at operatøren kræver rækker i Clustered Index nøglerækkefølge. Det klyngede indeks (der håndhæver den primære nøgle i dette tilfælde) er defineret som DESC , så rækker med højere værdier skal ankomme først. Det klyngede indeks på min Numbers-tabel er ASC , så forespørgselsoptimeringsværktøjet undgår en eksplicit sortering ved først at søge efter det højeste match i taltabellen (21.999) og derefter scanne mod det laveste match (18.000) i omvendt indeksrækkefølge. "Plan Tree"-visningen i SQL Sentry Plan Explorer viser den omvendte (bagud) scanning tydeligt:

Baglæns scanning vender den naturlige rækkefølge af indekset. En baglæns scanning af en ASC indeksnøgle returnerer rækker i faldende nøglerækkefølge; en baglæns scanning af en DESC indeksnøgle returnerer rækker i stigende nøglerækkefølge. "Scanningsretningen" angiver ikke returneret nøglerækkefølge i sig selv – du skal vide, om indekset er ASC eller DESC at træffe den beslutning.

Brug af disse testtabeller og data (T1 har 10.000 rækker nummereret fra 10.000 til 19.999 inklusive; T2 har 4.000 rækker nummereret fra 18.000 til 21.999) følgende forespørgsel forbinder de to tabeller og returnerer resultater i faldende rækkefølge af begge nøgler:

SELECT
    T1.col1,
    T2.col1
FROM #T1 AS T1 
JOIN #T2 AS T2 
    ON T2.col1 = T1.col1 
ORDER BY 
    T1.col1 DESC, 
    T2.col1 DESC;

Forespørgslen returnerer de korrekte matchende 2.000 rækker, som du ville forvente. Efterudførelsesplanen er som følger:

Merge Join kører ikke i mange-til-mange-tilstand (det øverste input er unikt på join-tasterne), og vurderingen af ​​2.000 rækkers kardinalitet er nøjagtigt korrekt. Clustered Index Scan af tabel T2 er bestilt (selvom vi skal vente et øjeblik for at finde ud af, om den rækkefølge er fremad eller bagud), og kardinalitetsestimatet på 4.000 rækker er også helt rigtigt. Clustered Index Scan af tabel T1 er også bestilt, men kun 2.001 rækker blev læst, hvorimod 10.000 blev anslået. Plantrævisningen viser, at begge Clustered Index Scans er bestilt frem:

Husk at læse en DESC indeks FORWARD vil producere rækker i omvendt nøglerækkefølge. Det er præcis, hvad der kræves af ORDER BY T1.col DESC, T2.col1 DESC klausul, så ingen eksplicit sortering er nødvendig. Pseudo-kode for én-til-mange Merge Join (gengivet fra Craig Freedmans Merge Join-blog) er:

Den faldende rækkefølge scanning af T1 returnerer rækker, der starter ved 19.999 og arbejder ned mod 10.000. Den faldende rækkefølge scanning af T2 returnerer rækker, der starter ved 21.999 og arbejder ned mod 18.000. Alle 4.000 rækker i T2 læses til sidst, men den iterative fletteproces stopper, når nøgleværdi 17.999 læses fra T1 , fordi T2 løber tør for rækker. Sammenfletningsbehandling afsluttes derfor uden at læse T1 fuldstændigt . Den læser rækker fra 19.999 ned til 17.999 inklusive; i alt 2.001 rækker som vist i udførelsesplanen ovenfor.

Kør gerne testen igen med ASC indekserer i stedet, og ændrer også ORDER BY klausul fra DESC til ASC . Den fremstillede udførelsesplan vil være meget ens, og der vil ikke være behov for sorteringer.

For at opsummere de punkter, der vil være vigtige om et øjeblik, kræver Merge Join join-nøglesorterede input, men det er lige meget, om tasterne er sorteret stigende eller faldende.

Bugen 

For at reproducere fejlen skal mindst én af vores tabeller partitioneres. For at holde resultaterne håndterbare vil dette eksempel kun bruge et lille antal rækker, så partitioneringsfunktionen har også brug for små grænser:

CREATE PARTITION FUNCTION PF (integer)
AS RANGE RIGHT
FOR VALUES (5, 10, 15);
 
CREATE PARTITION SCHEME PS
AS PARTITION PF
ALL TO ([PRIMARY]);


Den første tabel indeholder to kolonner og er opdelt på den PRIMÆRE NØGLE:

CREATE TABLE dbo.T1
(
    T1ID    integer IDENTITY (1,1) NOT NULL,
    SomeID  integer NOT NULL,
 
    CONSTRAINT [PK dbo.T1 T1ID]
        PRIMARY KEY CLUSTERED (T1ID)
        ON PS (T1ID)
);


Den anden tabel er ikke opdelt. Den indeholder en primær nøgle og en kolonne, der vil slutte sig til den første tabel:

CREATE TABLE dbo.T2
(
    T2ID    integer IDENTITY (1,1) NOT NULL,
    T1ID    integer NOT NULL,
 
    CONSTRAINT [PK dbo.T2 T2ID]
        PRIMARY KEY CLUSTERED (T2ID)
        ON [PRIMARY]
);

Eksempeldataene

Den første tabel har 14 rækker, alle med samme værdi i SomeID kolonne. SQL Server tildeler IDENTITY kolonneværdier, nummereret 1 til 14.

INSERT dbo.T1
    (SomeID)
VALUES
    (123), (123), (123),
    (123), (123), (123),
    (123), (123), (123),
    (123), (123), (123),
    (123), (123);


Den anden tabel er simpelthen udfyldt med IDENTITY værdier fra tabel et:

INSERT dbo.T2 (T1ID)
SELECT T1ID
FROM dbo.T1;

Dataene i de to tabeller ser således ud:

Testforespørgslen

Den første forespørgsel forbinder simpelthen begge tabeller ved at anvende et enkelt WHERE-sætningsprædikat (som tilfældigvis matcher alle rækker i dette meget forenklede eksempel):

SELECT
    T2.T2ID
FROM dbo.T1 AS T1
JOIN dbo.T2 AS T2
    ON T2.T1ID = T1.T1ID
WHERE
    T1.SomeID = 123;

Resultatet indeholder alle 14 rækker, som forventet:

På grund af det lille antal rækker vælger optimeringsværktøjet en indlejret sløjfer-sammenføjningsplan for denne forespørgsel:

Resultaterne er de samme (og stadig korrekte), hvis vi tvinger en hash- eller flette-join:

SELECT
    T2.T2ID
FROM dbo.T1 AS T1
JOIN dbo.T2 AS T2
    ON T2.T1ID = T1.T1ID
WHERE
    T1.SomeID = 123
OPTION (HASH JOIN);
 
SELECT
    T2.T2ID
FROM dbo.T1 AS T1
JOIN dbo.T2 AS T2
    ON T2.T1ID = T1.T1ID
WHERE
    T1.SomeID = 123
OPTION (MERGE JOIN);

Merge Join der er én-til-mange, med en eksplicit sortering på T1ID påkrævet til tabel T2 .

Det faldende indeksproblem

Alt er godt, indtil en dag (af gode grunde, som ikke behøver at bekymre os her) en anden administrator tilføjer et faldende indeks på SomeID kolonne i tabel 1:

CREATE NONCLUSTERED INDEX [dbo.T1 SomeID]
ON dbo.T1 (SomeID DESC);


Vores forespørgsel fortsætter med at give korrekte resultater, når optimeringsværktøjet vælger en Nested Loops eller Hash Join, men det er en anden historie, når en Merge Join bruges. Det følgende bruger stadig et forespørgselstip til at tvinge sammenfletningen, men dette er kun en konsekvens af det lave antal rækker i eksemplet. Optimizeren ville naturligvis vælge den samme Merge Join-plan med forskellige tabeldata.

SELECT
    T2.T2ID
FROM dbo.T1 AS T1
JOIN dbo.T2 AS T2
    ON T2.T1ID = T1.T1ID
WHERE
    T1.SomeID = 123
OPTION (MERGE JOIN);

Udførelsesplanen er:

Optimizeren har valgt at bruge det nye indeks, men forespørgslen producerer nu kun fem rækker med output:

Hvad skete der med de andre 9 rækker? For at være klar, er dette resultat forkert. Dataene er ikke ændret, så alle 14 rækker skal returneres (som de stadig er med en Nested Loops eller Hash Join-plan).

Årsag og forklaring

Det nye ikke-klyngede indeks på SomeID er ikke erklæret som unik, så den klyngede indeksnøgle føjes lydløst til alle ikke-klyngede indeksniveauer. SQL Server tilføjer T1ID kolonne (den klyngede nøgle) til det ikke-klyngede indeks, ligesom hvis vi havde oprettet indekset sådan her:

CREATE NONCLUSTERED INDEX [dbo.T1 SomeID]
ON dbo.T1 (SomeID DESC, T1ID);


Bemærk manglen på en DESC qualifier på det lydløst tilføjede T1ID nøgle. Indeksnøgler er ASC som standard. Dette er ikke et problem i sig selv (selvom det bidrager). Den anden ting, der automatisk sker med vores indeks, er, at det er opdelt på samme måde som basistabellen. Så den fulde indeksspecifikation, hvis vi skulle skrive den eksplicit, ville være:

CREATE NONCLUSTERED INDEX [dbo.T1 SomeID]
ON dbo.T1 (SomeID DESC, T1ID ASC)
ON PS (T1ID);


Dette er nu en ret kompleks struktur, med nøgler i alle mulige forskellige rækkefølger. Det er komplekst nok til, at forespørgselsoptimeringsværktøjet tager fejl, når det ræsonnerer om sorteringsrækkefølgen fra indekset. For at illustrere, overvej følgende enkle forespørgsel:

SELECT 
    T1ID,
    PartitionID = $PARTITION.PF(T1ID)
FROM dbo.T1
WHERE
    SomeID = 123
ORDER BY
    T1ID ASC;

Den ekstra kolonne vil blot vise os, hvilken partition den aktuelle række hører hjemme i. Ellers er det blot en simpel forespørgsel, der returnerer T1ID værdier i stigende rækkefølge, WHERE SomeID = 123 . Desværre er resultaterne ikke det, der er angivet af forespørgslen:

Forespørgslen kræver, at T1ID værdier skal returneres i stigende rækkefølge, men det er ikke det, vi får. Vi får værdier i stigende rækkefølge pr. partition , men selve partitionerne returneres i omvendt rækkefølge! Hvis partitionerne blev returneret i stigende rækkefølge (og T1ID værdier forblev sorteret inden for hver partition som vist), ville resultatet være korrekt.

Forespørgselsplanen viser, at optimeringsværktøjet blev forvirret af den førende DESC nøglen til indekset, og mente, at det var nødvendigt at læse partitionerne i omvendt rækkefølge for korrekte resultater:

Partitionssøgningen starter ved partitionen længst til højre (4) og fortsætter baglæns til partition 1. Du tror måske, at vi kunne løse problemet ved eksplicit at sortere efter partitionsnummer ASC i ORDER BY klausul:

SELECT 
    T1ID,
    PartitionID = $PARTITION.PF(T1ID)
FROM dbo.T1
WHERE
    SomeID = 123
ORDER BY
    PartitionID ASC, -- New!
    T1ID ASC;

Denne forespørgsel giver de samme resultater (dette er ikke en trykfejl eller en copy/paste-fejl):

Partitions-id'et er stadig faldende rækkefølge (ikke stigende, som specificeret) og T1ID er kun sorteret stigende inden for hver partition. Sådan er optimizerens forvirring, den tror virkelig (tag en dyb indånding nu), at scanning af det partitionerede indeks for ledende-faldende nøgle i en fremadgående retning, men med omvendte partitioner, vil resultere i den rækkefølge, der er angivet af forespørgslen.

Jeg bebrejder det ikke for at være ærlig, de forskellige sorteringsrækkefølge-overvejelser gør også, at jeg får ondt i hovedet.

Som et sidste eksempel kan du overveje:

SELECT 
    T1ID
FROM dbo.T1
WHERE
    SomeID = 123
ORDER BY
    T1ID DESC;

Resultaterne er:

Igen, T1ID sorteringsrækkefølge inden for hver partition er korrekt faldende, men selve partitionerne er listet baglæns (de går fra 1 til 3 ned ad rækkerne). Hvis partitionerne blev returneret i omvendt rækkefølge, ville resultaterne korrekt være 14, 13, 12, 11, 10, 9, … 5, 4, 3, 2, 1 .

Tilbage til Merge Join

Årsagen til de forkerte resultater med Merge Join-forespørgslen er nu tydelig:

SELECT
    T2.T2ID
FROM dbo.T1 AS T1
JOIN dbo.T2 AS T2
    ON T2.T1ID = T1.T1ID
WHERE
    T1.SomeID = 123
OPTION (MERGE JOIN);

Merge Join kræver sorterede input. Inputtet fra T2 er eksplicit sorteret efter T1TD så det er ok. Optimizeren begrunder forkert, at indekset på T1 kan levere rækker i T1ID bestille. Som vi har set, er dette ikke tilfældet. Indekssøgningen producerer det samme output som en forespørgsel, vi allerede har set:

SELECT 
    T1ID
FROM dbo.T1
WHERE
    SomeID = 123
ORDER BY
    T1ID ASC;

Kun de første 5 rækker er i T1ID bestille. Den næste værdi (5) er bestemt ikke i stigende rækkefølge, og Merge Join fortolker dette som end-of-stream snarere end at producere en fejl (personligt forventede jeg en detailpåstand her). Under alle omstændigheder er effekten, at Merge Join fejlagtigt afslutter behandlingen tidligt. Som en påmindelse er de (ufuldstændige) resultater:

Konklusion

Dette er en meget alvorlig fejl efter min mening. En simpel indekssøgning kan returnere resultater, der ikke respekterer ORDER BY klausul. Mere til sagen er optimizerens interne ræsonnement fuldstændig brudt for partitionerede ikke-unikke ikke-klyngede indekser med en faldende indledende nøgle.

Ja, dette er en lidt usædvanlige arrangement. Men som vi har set, kan korrekte resultater pludselig erstattes af forkerte resultater, bare fordi nogen tilføjede et faldende indeks. Husk det tilføjede indeks så uskyldigt nok ud:ingen eksplicit ASC/DESC nøglemismatch og ingen eksplicit partitionering.

Fejlen er ikke begrænset til Merge Joins. Potentielt kan enhver forespørgsel, der involverer en opdelt tabel, og som er afhængig af indekssorteringsrækkefølge (eksplicit eller implicit), blive ofre. Denne fejl findes i alle versioner af SQL Server fra 2008 til 2014 CTP 1 inklusive. Windows SQL Azure Database understøtter ikke partitionering, så problemet opstår ikke. SQL Server 2005 brugte en anden implementeringsmodel til partitionering (baseret på APPLY ) og lider heller ikke af dette problem.

Hvis du har et øjeblik, kan du overveje at stemme på mit Connect-emne for denne fejl.

Opløsning

Løsningen til dette problem er nu tilgængelig og dokumenteret i en Knowledge Base-artikel. Bemærk venligst, at rettelsen kræver en kodeopdatering og sporingsflag 4199 , som muliggør en række andre ændringer i forespørgselsprocessoren. Det er usædvanligt, at en fejl med ukorrekte resultater bliver rettet under 4199. Jeg bad om en forklaring på det, og svaret var:

Selvom dette problem involverer forkerte resultater som andre hotfixes, der involverer forespørgselsprocessoren, har vi kun aktiveret denne rettelse under sporingsflag 4199 for SQL Server 2008, 2008 R2 og 2012. Denne rettelse er dog "til" af standard uden sporingsflaget i SQL Server 2014 RTM.


  1. Indstilling af application_name på Postgres/SQLAlchemy

  2. Sikkerhedskopier en database i SQL Server 2017

  3. Udskalering af Moodle-databasen

  4. Sådan konverteres datetime til UTC i MySQL