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

Skal jeg bruge NOT IN, OUTER APPLY, LEFT OUTER JOIN, UNDTAGET eller IKKE FINDER?

Lad os sige, at du vil finde alle de patienter, der aldrig har fået en influenzasprøjte. Eller i AdventureWorks2012 , kan et lignende spørgsmål være, "vis mig alle de kunder, der aldrig har afgivet en ordre." Udtrykt ved hjælp af NOT IN , et mønster, jeg ser alt for ofte, der ville se sådan ud (jeg bruger de forstørrede overskrifts- og detaljetabeller fra dette script af Jonathan Kehayias (@SQLPoolBoy)):

SELECT CustomerID 
FROM Sales.Customer 
WHERE CustomerID NOT IN 
(
  SELECT CustomerID 
  FROM Sales.SalesOrderHeaderEnlarged
);

Når jeg ser dette mønster, kryber jeg sammen. Men ikke af præstationsmæssige årsager – det skaber trods alt en anstændig nok plan i dette tilfælde:

Hovedproblemet er, at resultaterne kan være overraskende, hvis målkolonnen er NULLable (SQL Server behandler dette som en venstre anti-semi join, men kan ikke pålideligt fortælle dig, om en NULL på højre side er lig med – eller ikke lig med – referencen i venstre side). Desuden kan optimering opføre sig anderledes, hvis kolonnen er NULLbar, selvom den faktisk ikke indeholder nogen NULL-værdier (Gail Shaw talte om dette tilbage i 2010).

I dette tilfælde er målkolonnen ikke nullbar, men jeg ville gerne nævne de potentielle problemer med NOT IN – Jeg vil muligvis undersøge disse spørgsmål mere grundigt i et fremtidigt indlæg.

TL;DR-version

I stedet for NOT IN , brug en korreleret NOT EXISTS for dette forespørgselsmønster. Altid. Andre metoder kan konkurrere med det med hensyn til ydeevne, når alle andre variabler er de samme, men alle de andre metoder introducerer enten ydeevneproblemer eller andre udfordringer.

Alternativer

Så hvilke andre måder kan vi skrive denne forespørgsel på?

    YDRE ANVENDELSE

    En måde, vi kan udtrykke dette resultat på, er at bruge en korreleret OUTER APPLY .

    SELECT c.CustomerID 
    FROM Sales.Customer AS c
    OUTER APPLY 
    (
     SELECT CustomerID 
       FROM Sales.SalesOrderHeaderEnlarged
       WHERE CustomerID = c.CustomerID
    ) AS h
    WHERE h.CustomerID IS NULL;

    Logisk set er dette også en venstre anti semi join, men den resulterende plan mangler den venstre anti semi join operator og ser ud til at være en del dyrere end NOT IN tilsvarende. Dette er fordi det ikke længere er en venstre anti semi join; det behandles faktisk på en anden måde:en ydre join bringer alle matchende og ikke-matchende rækker ind, og *derefter* anvendes et filter for at eliminere matchene:

    VENSTRE YDRE JOIN

    Et mere typisk alternativ er LEFT OUTER JOIN hvor højre side er NULL . I dette tilfælde ville forespørgslen være:

    SELECT c.CustomerID 
    FROM Sales.Customer AS c
    LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS h
    ON c.CustomerID = h.CustomerID
    WHERE h.CustomerID IS NULL;

    Dette giver de samme resultater; dog, ligesom OUTER APPLY, bruger den den samme teknik til at samle alle rækkerne, og først derefter eliminere kampene:

    Du skal dog være forsigtig med, hvilken kolonne du tjekker for NULL . I dette tilfælde CustomerID er det logiske valg, fordi det er sammenføjningskolonnen; det er tilfældigvis også indekseret. Jeg kunne have valgt SalesOrderID , som er clustering-nøglen, så den er også i indekset på CustomerID . Men jeg kunne have valgt en anden kolonne, der ikke er i (eller som senere bliver fjernet fra) det indeks, der blev brugt til joinforbindelsen, hvilket fører til en anden plan. Eller endda en NULLbar kolonne, hvilket fører til forkerte (eller i det mindste uventede) resultater, da der ikke er nogen måde at skelne mellem en række, der ikke eksisterer, og en række, der eksisterer, men hvor den kolonne er NULL . Og det er måske ikke indlysende for læseren/udvikleren/fejlfinderen, at dette er tilfældet. Så jeg vil også teste disse tre WHERE klausuler:

    WHERE h.SalesOrderID IS NULL; -- clustered, so part of index
     
    WHERE h.SubTotal IS NULL; -- not nullable, not part of the index
     
    WHERE h.Comment IS NULL; -- nullable, not part of the index

    Den første variation producerer den samme plan som ovenfor. De to andre vælger en hash join i stedet for en merge join og et smallere indeks i Customer tabel, selvom forespørgslen i sidste ende ender med at læse nøjagtigt det samme antal sider og mængden af ​​data. Men mens h.SubTotal variation giver de korrekte resultater:

    h.Comment variation gør det ikke, da den inkluderer alle rækker, hvor h.Comment IS NULL , samt alle de rækker, der ikke eksisterede for nogen kunde. Jeg har fremhævet den subtile forskel i antallet af rækker i outputtet, efter at filteret er blevet anvendt:

    Ud over at jeg skal være forsigtig med kolonnevalg i filteret, har jeg et andet problem med LEFT OUTER JOIN form er, at den ikke er selvdokumenterende, på samme måde som en indre joinforbindelse i den "gammeldags" form af FROM dbo.table_a, dbo.table_b WHERE ... er ikke selvdokumenterende. Med det mener jeg, at det er nemt at glemme joinkriterierne, når det skubbes til WHERE klausul, eller for at den blandes med andre filterkriterier. Jeg ved godt, at dette er ret subjektivt, men der er det.

    UNDTAGET

    Hvis alt, vi er interesseret i, er sammenføjningskolonnen (som per definition er i begge tabeller), kan vi bruge EXCEPT – et alternativ, der tilsyneladende ikke kommer meget op i disse samtaler (sandsynligvis fordi – normalt – du skal udvide forespørgslen for at inkludere kolonner, du ikke sammenligner):

    SELECT CustomerID 
    FROM Sales.Customer AS c 
    EXCEPT
    SELECT CustomerID
    FROM Sales.SalesOrderHeaderEnlarged;

    Dette kommer med nøjagtig samme plan som NOT IN variation ovenfor:

    En ting at huske på er, at EXCEPT inkluderer en implicit DISTINCT – så hvis du har tilfælde, hvor du vil have flere rækker med samme værdi i tabellen "venstre", vil denne formular eliminere disse dubletter. Ikke et problem i dette specifikke tilfælde, bare noget at huske på – ligesom UNION versus UNION ALL .

    FINDER IKKE

    Min præference for dette mønster er absolut NOT EXISTS :

    SELECT CustomerID 
    FROM Sales.Customer AS c 
    WHERE NOT EXISTS 
    (
      SELECT 1 
        FROM Sales.SalesOrderHeaderEnlarged 
        WHERE CustomerID = c.CustomerID
    );

    (Og ja, jeg bruger SELECT 1 i stedet for SELECT * … ikke af ydeevnemæssige årsager, da SQL Server er ligeglad med hvilke(n) kolonne(r) du bruger inde i EXISTS og optimerer dem væk, men blot for at tydeliggøre hensigten:dette minder mig om, at denne "underforespørgsel" faktisk ikke returnerer nogen data.)

    Dens ydeevne ligner NOT IN og EXCEPT , og det producerer en identisk plan, men er ikke tilbøjelig til de potentielle problemer forårsaget af NULL'er eller dubletter:

    Performancetests

    Jeg kørte et væld af tests, med både en kold og varm cache, for at bekræfte, at min mangeårige opfattelse af NOT EXISTS at være det rigtige valg forblev sandt. Det typiske output så således ud:

    Jeg tager det forkerte resultat ud af blandingen, når jeg viser den gennemsnitlige ydeevne af 20 kørsler på en graf (jeg inkluderede det kun for at vise, hvor forkerte resultaterne er), og jeg udførte forespørgslerne i forskellig rækkefølge på tværs af tests for at sikre at én forespørgsel ikke konsekvent dragede fordel af arbejdet i en tidligere forespørgsel. Med fokus på varighed, her er resultaterne:

    Hvis vi ser på varighed og ignorerer læsninger, er NOT EXISTS din vinder, men ikke meget. EXCEPT og NOT IN er ikke langt bagefter, men igen skal du se på mere end ydeevne for at afgøre, om disse muligheder er gyldige, og teste i dit scenario.

    Hvad hvis der ikke er noget understøttende indeks?

    Ovenstående forespørgsler drager naturligvis fordel af indekset på Sales.SalesOrderHeaderEnlarged.CustomerID . Hvordan ændres disse resultater, hvis vi dropper dette indeks? Jeg kørte det samme sæt test igen, efter at have droppet indekset:

    DROP INDEX [IX_SalesOrderHeaderEnlarged_CustomerID] 
    ON [Sales].[SalesOrderHeaderEnlarged];

    Denne gang var der meget mindre afvigelse med hensyn til ydeevne mellem de forskellige metoder. Først vil jeg vise planerne for hver metode (hvoraf de fleste, ikke overraskende, indikerer anvendeligheden af ​​det manglende indeks, vi lige har droppet). Så viser jeg en ny graf, der viser præstationsprofilen både med en kold cache og en varm cache.

    IKKE I, UNDTAGET, FINDER IKKE (alle tre var identiske)

    YDRE ANVENDELSE

    VENSTRE YDRE JOIN (alle tre var identiske bortset fra antallet af rækker)

    Ydeevneresultater

    Vi kan med det samme se, hvor nyttigt indekset er, når vi ser på disse nye resultater. I alle tilfælde undtagen ét (den venstre ydre joinforbindelse, der alligevel går uden for indekset), er resultaterne klart dårligere, når vi har droppet indekset:

    Så vi kan se, at selvom der er mindre mærkbar effekt, NOT EXISTS er stadig din marginale vinder i forhold til varighed. Og i situationer, hvor de andre tilgange er modtagelige for skemavolatilitet, er det også dit sikreste valg.

    Konklusion

    Dette var bare en meget langhåret måde at fortælle dig, at for mønsteret med at finde alle rækker i tabel A, hvor en betingelse ikke eksisterer i tabel B, NOT EXISTS vil typisk være dit bedste valg. Men som altid skal du teste disse mønstre i dit eget miljø ved at bruge dit skema, data og hardware og blandet med dine egne arbejdsbelastninger.


  1. Hvad er logisk ELLER-operatør i SQL Server - SQL Server / TSQL Tutorial Del 119

  2. Hvordan kommer du til grænser på 8060 bytes pr. række og 8000 pr. (varchar, nvarchar) værdi?

  3. PDOException SQLSTATE[HY000] [2002] Ingen sådan fil eller mappe

  4. Django + Postgres + Large Time Series