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

IGNORE_DUP_KEY langsommere på klyngede indekser

IGNORE_DUP_KEY mulighed for unikke indekser angiver, hvordan SQL Server reagerer på et forsøg på at INSERT duplikerede værdier:Det gælder kun for tabeller (ikke visninger) og kun for inserts. En hvilken som helst indsæt del af en MERGE sætning ignorerer enhver IGNORE_DUP_KEY indeksindstilling.

Når IGNORE_DUP_KEY er OFF , resulterer den første dublet, der stødes på, i en fejl , og ingen af ​​de nye rækker er indsat.

Når IGNORE_DUP_KEY er ON , indsatte rækker, der ville krænke unikhed, kasseres. De resterende rækker er indsat. En advarsel meddelelsen udsendes i stedet for en fejl:

Dubletnøgle blev ignoreret.

Artikeloversigt

IGNORE_DUP_KEY indeksindstilling kan angives for både klyngede og ikke-klyngede unikke indekser. Brug af det på et klynget indeks kan resultere i meget dårligere ydeevne end for et ikke-klynget unikt indeks.

Størrelsen af ​​præstationsforskellen afhænger af, hvor mange unikke krænkelser, der stødes på under INSERT operation. Jo flere overtrædelser, jo dårligere klarer det klyngede unikke indeks i sammenligning. Hvis der overhovedet ikke er nogen overtrædelser, kan den klyngede indeksindsættelse endda fungere bedre.

Klyngede unikke indeksindsættelser

For et klynget unikt indeks med IGNORE_DUP_KEY indstillet, håndteres dubletter af lagermotoren .

Meget af det arbejde, der er involveret i at indsætte hver række, udføres, før duplikatet detekteres. For eksempel en Clustered Index Insert Operatøren navigerer ned i det klyngede indeks b-træ til det punkt, hvor den nye række vil gå, og tager sidelåse og det sædvanlige hierarki af låse, før den opdager den dubletnøgle.

Når duplikatnøgletilstanden detekteres, vises en fejl er hævet. I stedet for at annullere eksekvering og returnere fejlen til klienten, håndteres fejlen internt. Den problematiske række er ikke indsat, og udførelsen fortsætter og leder efter den næste række, der skal indsættes. Hvis den række støder på en dubletnøgle, bliver en anden fejl rejst og håndteret, og så videre.

Undtagelser er meget dyre at kaste og fange. Et betydeligt antal dubletter vil bremse eksekveringen meget mærkbart.

Ikke-klyngede unikke indeksindsættelser

For et ikke-klynget unikt indeks med IGNORE_DUP_KEY indstillet, håndteres dubletter af forespørgselsprocessoren . Dubletter detekteres, og der udsendes en advarsel, før hver indsættelse forsøges.

Forespørgselsprocessoren fjerner dubletter fra indsættelsesstrømmen og sikrer, at lagermotoren ikke kan se dubletter. Som et resultat heraf bliver ingen unikke nøgleovertrædelsesfejl rejst eller håndteret internt.

Afvejningen

Der er en afvejning mellem omkostningerne ved at opdage og fjerne dubletnøgler i udførelsesplanen, versus omkostningerne ved at udføre betydeligt indsatsrelateret arbejde, og smide og fange fejl, når en dublet er fundet.

Hvis dubletter forventes at være meget sjældne , kan storage engine-løsningen (clustered index) meget vel være mere effektiv. Når dubletter er mindre sjældne, vil forespørgselsbehandlertilgangen sandsynligvis betale sig. Det nøjagtige overgangspunkt vil afhænge af faktorer såsom køretidseffektiviteten af ​​de udførelsesplankomponenter, der bruges til at opdage og fjerne dubletter.

Resten af ​​denne artikel giver en demo og ser mere detaljeret på, hvorfor storage engine-tilgangen kan præstere så dårligt.

Demo

Følgende script opretter en midlertidig tabel med en million rækker. Den har 1.000 unikke værdier og 1.000 rækker for hver unik værdi. Dette datasæt vil blive brugt som datakilde for indsættelser i tabeller med forskellige indekskonfigurationer.

DROP TABLE IF EXISTS #Data;
GO
CREATE TABLE #Data (c1 integer NOT NULL);
GO
SET NOCOUNT ON;
SET STATISTICS XML OFF;
 
DECLARE
    @Loop integer = 1,
    @N integer = 1;
 
WHILE @N <= 1000
BEGIN
    SET @Loop = 1;
 
    BEGIN TRANSACTION;
 
        -- Add 1,000 copies of the current loop value
        WHILE @Loop <= 50
        BEGIN
            INSERT #Data 
                (c1) 
            VALUES 
                (@N), (@N), (@N), (@N), (@N),
                (@N), (@N), (@N), (@N), (@N),
                (@N), (@N), (@N), (@N), (@N),
                (@N), (@N), (@N), (@N), (@N);
 
            SET @Loop += 1;
        END;
 
    COMMIT TRANSACTION;
 
    SET @N += 1;
END;
 
CREATE CLUSTERED INDEX cx 
ON #Data (c1) 
WITH (MAXDOP = 1);

Basislinje

Følgende indsættelse i en tabelvariabel med et ikke-unikt klynget indeks tager omkring 900 ms :

DECLARE @T table 
(
    c1 integer NOT NULL
        INDEX cuq CLUSTERED (c1)
);
 
INSERT @T 
    (c1) 
SELECT 
    D.c1 
FROM #Data AS D;

Bemærk manglen på IGNORE_DUP_KEY på måltabelvariablen.

Klyngede unikke indeks

Indsættelse af de samme data til en unik klynge indeks med IGNORE_DUP_KEY sæt ON tager omkring 15.900 ms — næsten 18 gange værre:

DECLARE @T table 
(
    c1 integer NOT NULL
        UNIQUE CLUSTERED 
        WITH (IGNORE_DUP_KEY = ON)
);
 
INSERT @T 
    (c1) 
SELECT 
    D.c1 
FROM #Data AS D;

Ikke-klyngede unikke indeks

Indsættelse af data til en unik ikke-klyngede indeks med IGNORE_DUP_KEY sæt ON tager omkring 700 ms :

DECLARE @T table 
(
    c1 integer NOT NULL
        UNIQUE NONCLUSTERED
        WITH (IGNORE_DUP_KEY = ON)
);
 
INSERT @T 
    (c1) 
SELECT 
    D.c1 
FROM #Data AS D;

Ydeevneoversigt

Baseline-testen tager 900 ms for at indsætte alle en million rækker. Den ikke-klyngede indekstest tager 700 ms for kun at indsætte de 1.000 forskellige nøgler. Den klyngede indekstest tager 15.900 ms for at indsætte de samme 1.000 unikke rækker.

Denne test er bevidst sat op for at fremhæve dårlig ydeevne af lagermotorimplementeringen ved at generere 999 enheder spildt arbejde (låse, låse, fejlhåndtering) for hver succesfuld række.

Den tilsigtede besked er ikke den IGNORE_DUP_KEY vil altid præstere dårligt på klyngede indekser, bare hvad det kan, og der kan være stor forskel mellem klyngede og ikke-klyngede indekser.

Clustered Index Execution Plan

Der er ikke en enorm mængde at se i den klyngede indeksindsættelsesplan:

Der er 1.000.000 rækker, der sendes til Clustered Index Insert operator, som vises som 'returnerende' 1.000 rækker. Når vi graver i plandetaljerne, kan vi se:

  • 1.244.008 logiske læsninger hos indsættelsesoperatøren.
  • Størstedelen af ​​udførelsestiden bruges på Indsæt operatør.
  • 11 ms af SOS_SCHEDULER_YIELD venter (dvs. ingen andre venter).

Intet, der virkelig forklarer 15.900 ms af forløbet tid.

Hvorfor ydeevnen er så dårlig

Det er tydeligt, at denne plan skal udføre en masse arbejde for hver række:

  • Naviger de klyngede indeks b-træ-niveauer, lås og lås, mens det går, for at finde indsættelsespunktet for den nye post.
  • Hvis nogen af ​​de nødvendige indekssider ikke er i hukommelsen, skal de hentes fra disken.
  • Konstruer en ny b-træ række i hukommelsen.
  • Forbered logposter.
  • Hvis der findes en nøgleduplikat (det er ikke en spøgelsesrecord), skal du rejse en fejl, håndtere den fejl internt, frigive den aktuelle række og fortsætte på et passende sted i koden for at behandle den næste kandidatrække.

Det er alt sammen en rimelig mængde arbejde, og husk, at det hele sker for hver række .

Den del, jeg vil koncentrere mig om, er fejlsøgningen og håndteringen, fordi det er ekstremt dyrt. De resterende aspekter nævnt ovenfor var allerede gjort så billige som muligt ved at bruge en tabelvariabel og midlertidig tabel i demoen.

Undtagelser

Den første ting, jeg vil gøre, er at vise, at Clustered Index Insert operatøren rejser virkelig en undtagelse, når den støder på en dubletnøgle.

En måde at vise dette direkte på er ved at vedhæfte en debugger og fange et stakspor på det punkt, hvor undtagelsen er kastet:

Det vigtige her er, at det er meget dyrt at kaste og fange undtagelser.

Overvågning af SQL Server ved hjælp af Windows Performance Recorder, mens testen kørte, og analyse af resultaterne i Windows Performance Analyzer viser:

Næsten al forespørgselsudførelsestid bruges i sqlmin!IndexDataSetSession::InsertRowInternal som man kunne forvente for en forespørgsel, der ikke gør andet end at indsætte rækker.

Overraskelsen er, at 45 % af den tid bruges på at rejse undtagelser via sqlmin!RaiseDuplicateKeyException og yderligere 47 % bruges i den tilknyttede undtagelses catch-blok (ntdll!RcConsolidateFrames hierarki).

For at opsummere:At hæve og fange undtagelser udgør 92 % af eksekveringstiden af vores testklyngede indeksindsættelsesforespørgsel.

Problemer med dataindsamling

Skarpøjede læsere bemærker muligvis en betydelig mængde – omkring 12 % – af undtagelsestiden, hvilket øger tiden brugt i sqlmin!DumpKey i Windows Performance Analyzer-grafikken. Dette er værd at undersøge hurtigt sammen med et par relaterede emner.

Som en del af at rejse en undtagelse skal SQL Server indsamle nogle data, der kun er tilgængelige på det tidspunkt, hvor fejlen opstod. Fejlnummeret forbundet med en dubletnøgleundtagelse er 2627. Meddelelsesteksten i sys.messages for det fejlnummer er:

Overtrædelse af %ls-begrænsningen '%.*ls'. Kan ikke indsætte dubletnøgle i objektet '%.*ls'. Dubletnøgleværdien er %ls.

Oplysninger til at udfylde disse stedsmarkører skal indsamles på det tidspunkt, hvor fejlen opstår - den vil ikke være tilgængelig senere! Det betyder at slå op og formatere typen af ​​begrænsning, dens navn, det fulde navn på målobjektet og den specifikke nøgleværdi. Alt det tager tid.

Følgende staksporing viser serveren, der formaterer dubletnøgleværdien som en Unicode-streng under DumpKey ring:

Undtagelseshåndtering involverer også indfangning af et stakspor:

SQL Server registrerer også oplysninger om undtagelser (inklusive stackframes) i en lille ringbuffer, som følgende viser:

Du kan se disse ringebufferposter ved hjælp af en kommando som:

SELECT TOP (10)
    date_time = 
        DATEADD
        (
            MILLISECOND, 
            DORB.[timestamp] - DOSI.ms_ticks, 
            SYSDATETIME()
        ),
    record = CONVERT(xml, DORB.record)
FROM sys.dm_os_ring_buffers AS DORB
CROSS JOIN sys.dm_os_sys_info AS DOSI
WHERE 
    DORB.ring_buffer_type = N'RING_BUFFER_EXCEPTION'
ORDER BY 
    DORB.[timestamp] DESC;

Et eksempel på xml-posten for en dubletnøgleundtagelse følger. Bemærk stabelrammerne:

<Record id="4611442" type="RING_BUFFER_EXCEPTION" time="93079430">
  <Exception>
    <Task address="0x00000245B5E1FC28" />
    <Error>2627</Error>
    <Severity>14</Severity>
    <State>1</State>
    <UserDefined>0</UserDefined>
    <Origin>0</Origin>
  </Exception>
  <Stack>
    <frame id="0">0X00007FFAC659E80A</frame>
    <frame id="1">0X00007FFACBAC0EFD</frame>
    <frame id="2">0X00007FFACBAA1252</frame>
    <frame id="3">0X00007FFACBA9E040</frame>
    <frame id="4">0X00007FFACAB55D53</frame>
    <frame id="5">0X00007FFACAB55C06</frame>
    <frame id="6">0X00007FFACB3E3D0B</frame>
    <frame id="7">0X00007FFAC92020EC</frame>
    <frame id="8">0X00007FFACAB5B2FA</frame>
    <frame id="9">0X00007FFACABA3B9B</frame>
    <frame id="10">0X00007FFACAB3D89F</frame>
    <frame id="11">0X00007FFAC6A9D108</frame>
    <frame id="12">0X00007FFAC6AB2BBF</frame>
    <frame id="13">0X00007FFAC6AB296F</frame>
    <frame id="14">0X00007FFAC6A9B7D0</frame>
    <frame id="15">0X00007FFAC6A9B233</frame>
  </Stack>
</Record>

Alt dette baggrundsarbejde sker for enhver undtagelse. I vores test betyder det, at det sker 999.000 gange - én gang for hver række, der støder på en dublet nøgleovertrædelse.

Der er mange måder at se dette på, for eksempel ved at køre en Profiler-sporing ved hjælp af undtagelsen hændelse i Fejl og advarsler klasse. I vores testtilfælde vil dette til sidst producere 999.000 rækker med TextData elementer som dette:

Overtrædelse af UNIQUE KEY constraint 'UQ__#AC166DE__3213663B8B6E2E0E'
Kan ikke indsætte dubletnøgle i objekt 'dbo.@T'.
Duplikatnøgleværdien er (173).

At vedhæfte Profiler betyder, at hver undtagelseshåndteringshændelse får en hel del ekstra overhead, da de ekstra nødvendige data indsamles og formateres. De tidligere nævnte standarddata indsamles altid, selvom ingen aktivt bruger oplysningerne.

For at være klar:Ydeevnetallene rapporteret i denne artikel blev alle opnået uden en debugger tilknyttet, og ingen anden aktiv overvågning.

Ikke-klyngede indeksudførelsesplan

På trods af at den er så meget hurtigere, er den ikke-klyngede indeksindsættelsesplan en del mere kompleks, så jeg vil dele den op i to dele.

Det generelle tema er, at denne plan er hurtigere, fordi den eliminerer dubletter før forsøger at indsætte dem i måltabellen.

Del 1

Først højre side af den ikke-klyngede indeksplan:

Denne del af planen afviser alle rækker, der har et nøglematch i måltabellen for det unikke indeks med IGNORE_DUP_KEY sæt ON .

Du forventer måske at se en Anti Semi Join her, men SQL Server har ikke den nødvendige infrastruktur til at udsende den påkrævede duplikatnøgleadvarsel med en Anti Semi Join operatør. (Hvis det ikke allerede giver mening, skal det snart.)

I stedet får vi en plan med en række interessante funktioner:

  • Den Clustered Index Scan er Ordered:True for at give input til Merge Left Semi Join sorteret efter kolonne c1 i #Data tabel.
  • Indeksscanningen af tabelvariablen er Ordered:False
  • Sorteringen rækker rækker efter kolonne c1 i tabelvariablen. Denne ordre kunne have været leveret af en ordre scanning af tabelvariabelindekset på c1 , men optimeringsværktøjet bestemmer Sorteringen er den billigste måde at give det påkrævede niveau af Halloween-beskyttelse.
  • Tabelvariablen Indeksscanning har intern UPDLOCK og SERIALIZABLE tip anvendt for at sikre målstabilitet under planens udførelse.
  • Den Merge Left Semi Join kontrollerer for match i tabelvariablen for hver værdi af c1 returneret fra #Data bord. I modsætning til en almindelig semi-join, udsender den hver række modtaget på dens øvre input. Den sætter et flag i en probesøjle for at angive, om den aktuelle række fandt et match eller ej. Probesøjlen udsendes fra Merge Left Semi Join som et udtryk med navnet Expr1012 .
  • Den påstand operatør kontrollerer værdien af ​​probekolonnen Expr1012 . Første gang den ser en række med en probekolonneværdi, der ikke er nul (hvilket indikerer, at en indeksnøglematch blev fundet), udsender den en "Duplicate key was ignored" besked.
  • Den påstand overfører kun rækker, hvor probesøjlen er nul. Dette eliminerer indgående rækker, der ville producere en dublet nøglefejl.

Det hele kan virke komplekst, men det er i bund og grund så simpelt som at sætte et flag, hvis der findes et match, udsende en advarsel første gang flaget sættes, og kun sende rækker videre mod indsætningen, der ikke allerede findes i måltabellen .

Del 2

Den anden del af planen følger påstanden operatør:

Den forrige del af planen fjernede rækker, der havde et match i måltabellen. Denne del af planen fjerner dubletter inden for indsættelsessættet .

Forestil dig for eksempel, at der ikke er nogen rækker i måltabellen, hvor c1 = 1 . Vi kan stadig forårsage en dubletnøglefejl, hvis vi forsøger at indsætte to rækker med c1 = 1 fra kildetabellen. Det er vi nødt til at undgå for at respektere semantikken i IGNORE_DUP_KEY = ON .

Dette aspekt håndteres af Segmentet og Top operatører.

Segmentet operatør sætter et nyt flag (mærket Segment1015 ) når den støder på en række med en ny værdi for c1 . Da rækker er præsenteret i c1 ordre (takket være den ordrebevarende Flet ), kan planen stole på alle rækker med den samme c1 værdi, der kommer i en sammenhængende strøm.

Toppen operatør sender én række for hver gruppe af dubletter, som angivet af Segment flag. Hvis Top operatør støder på mere end én række for det samme Segment gruppe (c1 værdi), udsender den en "Duplikatnøgle blev ignoreret" advarsel, hvis det er første gang, planen støder på denne tilstand.

Nettoeffekten af ​​alt dette er, at kun én række sendes til indsættelsesoperatorerne for hver unik værdi af c1 , og en advarsel genereres om nødvendigt.

Udførelsesplanen har nu elimineret alle potentielle duplikerede nøgleovertrædelser, så den resterende Tabelindsæt og Indeksindsæt operatører kan sikkert indsætte rækker til heap og ikke-klyngede indeks uden frygt for en duplikatnøglefejl.

Husk at UPDLOCK og SERIALIZABLE hints, der anvendes til måltabellen, sikrer, at sættet ikke kan ændres under udførelse. Med andre ord kan en samtidig sætning ikke ændre måltabel, således at der opstår en duplikatnøglefejl ved Indsæt operatører. Det er ikke et problem her, da vi bruger en privat tabelvariabel, men SQL Server tilføjer stadig hints som en generel sikkerhedsforanstaltning.

Uden disse hints kunne en samtidig proces tilføje en række til måltabellen, der ville generere en duplikatnøgleovertrædelse, på trods af kontrollerne foretaget af del 1 af planen. SQL Server skal være sikker på, at eksistenskontrolresultaterne forbliver gyldige.

Den nysgerrige læser kan se nogle af funktionerne beskrevet ovenfor ved at aktivere sporingsflag 3604 og 8607 for at se optimeringsoutputtræet:

PhyOp_RestrRemap
    PhyOp_StreamUpdate(INS TBL: @T, iid 0x2 as IDX, Sort(QCOL: .c1, )), {
            - COL: Bmk10001013 = COL: Bmk1000 
            - COL: c11014 = QCOL: .c1} 
        PhyOp_StreamUpdate(INS TBL: @T, iid 0x0 as TBLInsLocator(COL: Bmk1000  ) REPORT-COUNT), {
                - QCOL: .c1= QCOL: [D].c1} 
            PhyOp_GbTop Group(QCOL: [D].c1,) WARN-DUP
                PhyOp_StreamCheck (WarnIgnoreDuplicate TABLE) 
                    PhyOp_MergeJoin x_jtLeftSemi M-M, Probe COL: Expr1012  ( QCOL: [D].c1) = ( QCOL: .c1)
                        PhyOp_Range TBL: #Data(alias TBL: D)(1) ASC
                        PhyOp_Sort +s -d QCOL: .c1
                            PhyOp_Range TBL: @T(2) ASC Hints( UPDLOCK SERIALIZABLE FORCEDINDEX )
                        ScaOp_Comp x_cmpIs
                            ScaOp_Identifier QCOL: [D].c1
                            ScaOp_Identifier QCOL: .c1
                    ScaOp_Logical x_lopIsNotNull
                        ScaOp_Identifier COL: Expr1012 

Sidste tanker

IGNORE_DUP_KEY indeksmulighed er ikke noget, de fleste mennesker vil bruge meget ofte. Alligevel er det interessant at se på, hvordan denne funktionalitet implementeres, og hvorfor der kan være store ydeevneforskelle mellem IGNORE_DUP_KEY på klyngede og ikke-klyngede indekser.

I mange tilfælde vil det betale sig at følge forespørgselsprocessoren og se efter at skrive forespørgsler, der eksplicit eliminerer dubletter i stedet for at stole på IGNORE_DUP_KEY . I vores eksempel ville det betyde at skrive:

DECLARE @T table 
(
    c1 integer NOT NULL
        UNIQUE CLUSTERED -- no IGNORE_DUP_KEY!
);
 
INSERT @T 
    (c1) 
SELECT DISTINCT -- Remove duplicates
    D.c1 
FROM #Data AS D;

Dette udføres på omkring 400 ms , bare for ordens skyld.


  1. Administrer transaktionssammenfald ved hjælp af låse i SQL Server

  2. Lagring af konfigurationer i Android

  3. Med henvisning til et udvalgt aggregeret kolonnealias i have-sætningen i Postgres

  4. BITAND() Funktion i Oracle