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

Nogle ENHVER Aggregerede Transformationer er ødelagte

ANY aggregat er ikke noget vi kan skrive direkte i Transact SQL. Det er kun en intern funktion, der bruges af forespørgselsoptimerings- og udførelsesmotoren.

Jeg er personligt ret glad for ANY samlet, så det var lidt skuffende at erfare, at det er brudt på en ganske grundlæggende måde. Den særlige smag af 'brudt', jeg henviser til her, er sorten med forkerte resultater.

I dette indlæg tager jeg et kig på to bestemte steder, hvor ANY aggregering dukker ofte op, viser det forkerte resultatproblem og foreslår løsninger, hvor det er nødvendigt.

Til baggrund om ANY samlet, se venligst mit tidligere indlæg Udokumenterede forespørgselsplaner:ANY-aggregatet.

1. En række pr. gruppeforespørgsler

Dette må være et af de mest almindelige daglige forespørgselskrav med en meget velkendt løsning. Du skriver sandsynligvis denne slags forespørgsler hver dag, automatisk efter mønsteret, uden egentlig at tænke over det.

Ideen er at nummerere inputsættet af rækker ved hjælp af ROW_NUMBER vinduesfunktion, opdelt efter grupperingskolonnen eller -kolonnerne. Det er pakket ind i et fælles tabeludtryk eller afledt tabel , og filtreret ned til rækker, hvor det beregnede rækkenummer er lig med én. Siden ROW_NUMBER genstarter ved én for hver gruppe, hvilket giver os den nødvendige række pr. gruppe.

Der er ikke noget problem med det generelle mønster. Typen af ​​en række pr. gruppeforespørgsel, der er underlagt ANY det samlede problem er det, hvor vi er ligeglade med, hvilken bestemt række der er valgt fra hver gruppe.

I så fald er det ikke klart, hvilken kolonne der skal bruges i den obligatoriske ORDER BY klausul i ROW_NUMBER vinduesfunktion. Når alt kommer til alt, er vi udtrykkeligt ligeglade hvilken række der er valgt. En almindelig fremgangsmåde er at genbruge PARTITION BY kolonne(r) i ORDER BY klausul. Det er her, problemet kan opstå.

Eksempel

Lad os se på et eksempel ved hjælp af et legetøjsdatasæt:

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);

Kravet er at returnere en komplet række af data fra hver gruppe, hvor gruppemedlemskab er defineret af værdien i kolonne c1 .

Følger ROW_NUMBER mønster, kan vi skrive en forespørgsel som følgende (læg mærke til ORDER BY klausul i ROW_NUMBER vinduesfunktionen matcher PARTITION BY klausul):

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;

Som præsenteret udføres denne forespørgsel med korrekte resultater. Resultaterne er teknisk ikke-deterministiske da SQL Server gyldigt kunne returnere en af ​​rækkerne i hver gruppe. Ikke desto mindre, hvis du selv kører denne forespørgsel, vil du sandsynligvis se det samme resultat, som jeg gør:

Udførelsesplanen afhænger af den anvendte version af SQL Server og afhænger ikke af databasekompatibilitetsniveauet.

På SQL Server 2014 og tidligere er planen:

For SQL Server 2016 eller nyere vil du se:

Begge planer er sikre, men af ​​forskellige årsager. Distinct Sort planen indeholder en ANY samlet, men Distinct Sort operatørimplementering viser ikke fejlen.

Den mere komplekse SQL Server 2016+ plan bruger ikke ANY samlet overhovedet. Sorter placerer rækkerne i den rækkefølge, der er nødvendige for rækkenummereringsoperationen. Segmentet operatør sætter et flag i starten af ​​hver ny gruppe. Sekvensprojektet beregner rækkenummeret. Til sidst, Filtret operator videregiver kun de rækker, der har et beregnet rækkenummer på én.

Fejlen

For at få forkerte resultater med dette datasæt, skal vi bruge SQL Server 2014 eller tidligere, og ANY aggregater skal implementeres i et Stream Aggregate eller Eager Hash Aggregate operatør (Flow Distinct Hash Match Aggregate producerer ikke fejlen).

En måde at tilskynde optimeringsværktøjet til at vælge et Stream Aggregate i stedet for Distinct Sort er at tilføje et klynget indeks for at give rækkefølge efter kolonne c1 :

CREATE CLUSTERED INDEX c ON #Data (c1);

Efter den ændring bliver udførelsesplanen:

ANY aggregater er synlige i Egenskaber vinduet, når Stream Aggregate operator er valgt:

Resultatet af forespørgslen er:

Dette er forkert . SQL Server har returneret rækker, der ikke eksisterer i kildedataene. Der er ingen kilderækker, hvor c2 = 1 og c3 = 1 for eksempel. Som en påmindelse er kildedataene:

Udførelsesplanen beregner fejlagtigt separat ANY aggregater for c2 og c3 kolonner, ignorerer null. Hver aggregater uafhængigt returnerer den første ikke-null værdi den støder på, hvilket giver et resultat hvor værdierne for c2 og c3 kommer fra forskellige kilderækker . Dette er ikke, hvad den oprindelige SQL-forespørgselsspecifikation anmodede om.

Det samme forkerte resultat kan produceres med eller uden det klyngede indeks ved at tilføje en OPTION (HASH GROUP) tip til at lave en plan med et Eager Hash Aggregate i stedet for et Stream Aggregate .

Betingelser

Dette problem kan kun opstå, når der er flere ANY aggregater er til stede, og de aggregerede data indeholder nuller. Som nævnt påvirker problemet kun Stream Aggregate og Eager Hash Aggregate operatører; Særlig sortering og Flow Distinct er ikke berørt.

SQL Server 2016 og fremefter gør en indsats for at undgå at introducere flere ANY aggregeres for forespørgselsmønsteret for en række pr. gruppe rækkenummerering, når kildekolonnerne er nullbare. Når dette sker, vil eksekveringsplanen indeholde Segment , Sekvensprojekt og Filter operatører i stedet for et aggregat. Denne planform er altid sikker, da ingen ANY aggregater anvendes.

Gengivelse af fejlen i SQL Server 2016+

SQL Server optimizer er ikke perfekt til at registrere, hvornår en kolonne oprindeligt var begrænset til at være NOT NULL kan stadig producere en nul mellemværdi gennem datamanipulationer.

For at reproducere dette starter vi med en tabel, hvor alle kolonner er erklæret som 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);

Vi kan producere nuller fra dette datasæt på mange måder, hvoraf de fleste optimeringsværktøjer med succes kan detektere, og så undgå at introducere ANY aggregater under optimering.

En måde at tilføje nuller, der tilfældigvis glider under radaren, er vist nedenfor:

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;

Den forespørgsel producerer følgende output:

Det næste trin er at bruge denne forespørgselsspecifikation som kildedata for standardforespørgslen "enhver række pr. gruppe":

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;

en hvilken som helst version af SQL Server, der producerer følgende plan:

Strømaggregatet indeholder flere ANY aggregater, og resultatet er forkert . Ingen af ​​de returnerede rækker vises i kildedatasættet:

db<>fiddle online demo

Løsning

Den eneste fuldt pålidelige løsning, indtil denne fejl er rettet, er at undgå det mønster, hvor ROW_NUMBER har den samme kolonne i ORDER BY klausul som er i PARTITION BY klausul.

Når vi er ligeglade med hvilken en række er valgt fra hver gruppe, er det uheldigt at en ORDER BY klausul er overhovedet nødvendig. En måde at omgå problemet på er at bruge en køretidskonstant som ORDER BY @@SPID i vinduesfunktionen.

2. Ikke-deterministisk opdatering

Problemet med flere ANY aggregater på nul-input er ikke begrænset til forespørgselsmønsteret med én række pr. gruppe. Forespørgselsoptimeringsværktøjet kan introducere en intern ANY samlet under en række omstændigheder. Et af disse tilfælde er en ikke-deterministisk opdatering.

En ikke-deterministisk opdatering er, hvor erklæringen ikke garanterer, at hver målrække højst bliver opdateret én gang. Med andre ord er der flere kilderækker for mindst én målrække. Dokumentationen advarer eksplicit om dette:

Vær forsigtig, når du angiver FROM-sætningen for at angive kriterierne for opdateringshandlingen.
Resultaterne af en UPDATE-sætning er udefinerede, hvis sætningen indeholder en FROM-sætning, der ikke er specificeret på en sådan måde, at der kun er én værdi tilgængelig for hver kolonneforekomst, der opdateres, dvs. er, hvis UPDATE-sætningen ikke er deterministisk.

For at håndtere en ikke-deterministisk opdatering grupperer optimeringsværktøjet rækkerne efter en nøgle (indeks eller RID) og anvender ANY aggregeres til de resterende kolonner. Den grundlæggende idé er at vælge en række blandt flere kandidater og bruge værdier fra den række til at udføre opdateringen. Der er åbenlyse paralleller til den tidligere ROW_NUMBER problem, så det er ingen overraskelse, at det er ret nemt at påvise en forkert opdatering.

I modsætning til det forrige nummer tager SQL Server i øjeblikket ingen særlige trin for at undgå flere ANY aggregeres på nullbare kolonner, når der udføres en ikke-deterministisk opdatering. Det følgende vedrører derfor alle SQL Server-versioner , inklusive SQL Server 2019 CTP 3.0.

Eksempel

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<>fiddle online demo

Logisk set bør denne opdatering altid give en fejl:Måltabellen tillader ikke nulværdier i nogen kolonne. Uanset hvilken matchende række der vælges fra kildetabellen, et forsøg på at opdatere kolonne c2 eller c3 til null skal forekomme.

Desværre lykkes opdateringen, og den endelige tilstand af måltabellen er inkonsistent med de leverede data:

Jeg har rapporteret dette som en fejl. Arbejdet omkring er at undgå at skrive ikke-deterministisk UPDATE udsagn, så ANY aggregater er ikke nødvendige for at løse tvetydigheden.

Som nævnt kan SQL Server introducere ANY aggregater under flere omstændigheder end de to eksempler, der er givet her. Hvis dette sker, når den aggregerede kolonne indeholder nuller, er der risiko for forkerte resultater.


  1. PostgreSQL 9.2 JDBC driver bruger klient tidszone?

  2. Auto Generer Database Diagram MySQL

  3. Hvordan får man en alder fra et D.O.B-felt i MySQL?

  4. Hvad er SQL-operatører, og hvordan fungerer de?