De fleste databaser bør gøre brug af fremmednøgler til at håndhæve referentiel integritet (RI), hvor det er muligt. Der er dog mere i denne beslutning end blot at beslutte at bruge FK-begrænsninger og oprette dem. Der er en række overvejelser at tage fat på for at sikre, at din database fungerer så gnidningsløst som muligt.
Denne artikel dækker en sådan overvejelse, som ikke får meget omtale:At minimere blokering , bør du tænke grundigt over de indekser, der bruges til at håndhæve unikhed på forældresiden af disse udenlandske nøglerelationer.
Dette gælder uanset om du bruger låsning læst forpligtet eller den versionsbaserede læse committed snapshot isolation (RCSI). Begge kan opleve blokering, når fremmednøgleforhold kontrolleres af SQL Server-motoren.
Under snapshot isolation (SI) er der en ekstra advarsel. Det samme væsentlige problem kan føre til uventede (og velsagtens ulogiske) transaktionsfejl på grund af tilsyneladende opdateringskonflikter.
Denne artikel er i to dele. Den første del ser på fremmednøgleblokering under låsning læst begået og læst begået snapshot-isolation. Den anden del dækker relaterede opdateringskonflikter under snapshot-isolering.
1. Blokering af udenlandske nøglechecks
Lad os først se på, hvordan indeksdesign kan påvirke, når blokering sker på grund af kontrol af fremmednøgle.
Følgende demo skal køres under læs committed isolation. For SQL Server er standarden låsning, læst committed; Azure SQL Database bruger RCSI som standard. Du er velkommen til at vælge, hvad du kan lide, eller kør scripts én gang for hver indstilling for selv at bekræfte, at adfærden er den samme.
-- Use locking read committed ALTER DATABASE CURRENT SET READ_COMMITTED_SNAPSHOT OFF; -- Or use row-versioning read committed ALTER DATABASE CURRENT SET READ_COMMITTED_SNAPSHOT ON;
Opret to tabeller forbundet med en fremmednøglerelation:
CREATE TABLE dbo.Parent ( ParentID integer NOT NULL, ParentNaturalKey varchar(10) NOT NULL, ParentValue integer NOT NULL, CONSTRAINT [PK dbo.Parent ParentID] PRIMARY KEY (ParentID), CONSTRAINT [AK dbo.Parent ParentNaturalKey] UNIQUE (ParentNaturalKey) ); CREATE TABLE dbo.Child ( ChildID integer NOT NULL, ChildNaturalKey varchar(10) NOT NULL, ChildValue integer NOT NULL, ParentID integer NULL, CONSTRAINT [PK dbo.Child ChildID] PRIMARY KEY (ChildID), CONSTRAINT [AK dbo.Child ChildNaturalKey] UNIQUE (ChildNaturalKey), CONSTRAINT [FK dbo.Child to dbo.Parent] FOREIGN KEY (ParentID) REFERENCES dbo.Parent (ParentID) );
Tilføj en række til den overordnede tabel:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; DECLARE @ParentID integer = 1, @ParentNaturalKey varchar(10) = 'PNK1', @ParentValue integer = 100; INSERT dbo.Parent ( ParentID, ParentNaturalKey, ParentValue ) VALUES ( @ParentID, @ParentNaturalKey, @ParentValue );
På en anden forbindelse , skal du opdatere den ikke-nøgle overordnede tabel-attribut ParentValue
inde i en transaktion, men forpligt dig ikke det lige endnu:
DECLARE @ParentID integer = 1, @ParentNaturalKey varchar(10) = 'PNK1', @ParentValue integer = 200; BEGIN TRANSACTION; UPDATE dbo.Parent SET ParentValue = @ParentValue WHERE ParentID = @ParentID;
Du er velkommen til at skrive opdateringsprædikatet ved hjælp af den naturlige nøgle, hvis du foretrækker det, det gør ikke nogen forskel for vores nuværende formål.
Tilbage til den første forbindelse , forsøg at tilføje en underordnet post:
DECLARE @ChildID integer = 101, @ChildNaturalKey varchar(10) = 'CNK1', @ChildValue integer = 999, @ParentID integer = 1; INSERT dbo.Child ( ChildID, ChildNaturalKey, ChildValue, ParentID ) VALUES ( @ChildID, @ChildNaturalKey, @ChildValue, @ParentID );
Denne indsættelseserklæring vil blokere , uanset om du valgte låsning eller versionering læs engageret isolation til denne test.
Forklaring
Udførelsesplanen for den underordnede postindsættelse er:
Efter at have indsat den nye række i den underordnede tabel, kontrollerer eksekveringsplanen den fremmede nøglebegrænsning. Kontrollen springes over, hvis det indsatte overordnede id er null (opnås via et 'pass through' prædikat på venstre semi join). I dette tilfælde er det tilføjede overordnede id ikke null, så fremmednøglekontrollen er udført.
SQL Server verificerer den fremmede nøgle-begrænsning ved at lede efter en matchende række i den overordnede tabel. Motoren kan ikke bruge rækkeversionering for at gøre dette - det skal være sikker på, at de data, den kontrollerer, er de senest forpligtede data , ikke en gammel version. Motoren sikrer dette ved at tilføje en intern READCOMMITTEDLOCK
tabeltip til fremmednøgletjek på den overordnede tabel.
Slutresultatet er, at SQL Server forsøger at erhverve en delt lås på den tilsvarende række i den overordnede tabel, som blokerer fordi den anden session har en inkompatibel eksklusiv-tilstandslås på grund af den endnu ikke-forpligtede opdatering.
For at være klar, gælder det interne låsetips kun for kontrollen af fremmednøgle. Resten af planen bruger stadig RCSI, hvis du valgte implementeringen af det læste forpligtede isolationsniveau.
Undgå blokeringen
Bekræft eller rollback den åbne transaktion i den anden session, og nulstil derefter testmiljøet:
DROP TABLE IF EXISTS dbo.Child, dbo.Parent;
Opret testtabellerne igen, men denne gang i stedet for at acceptere standardindstillingerne, vælger vi at gøre den primære nøgle ikke-klynget og den unikke begrænsning i grupper:
CREATE TABLE dbo.Parent ( ParentID integer NOT NULL, ParentNaturalKey varchar(10) NOT NULL, ParentValue integer NOT NULL, CONSTRAINT [PK dbo.Parent ParentID] PRIMARY KEY NONCLUSTERED (ParentID), CONSTRAINT [AK dbo.Parent ParentNaturalKey] UNIQUE CLUSTERED (ParentNaturalKey) ); CREATE TABLE dbo.Child ( ChildID integer NOT NULL, ChildNaturalKey varchar(10) NOT NULL, ChildValue integer NOT NULL, ParentID integer NULL, CONSTRAINT [PK dbo.Child ChildID] PRIMARY KEY NONCLUSTERED (ChildID), CONSTRAINT [AK dbo.Child ChildNaturalKey] UNIQUE CLUSTERED (ChildNaturalKey), CONSTRAINT [FK dbo.Child to dbo.Parent] FOREIGN KEY (ParentID) REFERENCES dbo.Parent (ParentID) );
Tilføj en række til den overordnede tabel som før:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; DECLARE @ParentID integer = 1, @ParentNaturalKey varchar(10) = 'PNK1', @ParentValue integer = 100; INSERT dbo.Parent ( ParentID, ParentNaturalKey, ParentValue ) VALUES ( @ParentID, @ParentNaturalKey, @ParentValue );
I den anden session , kør opdateringen uden at begå den igen. Jeg bruger den naturlige nøgle denne gang kun for variation - det er ikke vigtigt for resultatet. Brug surrogatnøglen igen, hvis du foretrækker det.
DECLARE @ParentID integer = 1, @ParentNaturalKey varchar(10) = 'PNK1', @ParentValue integer = 200; BEGIN TRANSACTION UPDATE dbo.Parent SET ParentValue = @ParentValue WHERE ParentNaturalKey = @ParentNaturalKey;
Kør nu barneindsatsen tilbage på den første session :
DECLARE @ChildID integer = 101, @ChildNaturalKey varchar(10) = 'CNK1', @ChildValue integer = 999, @ParentID integer = 1; INSERT dbo.Child ( ChildID, ChildNaturalKey, ChildValue, ParentID ) VALUES ( @ChildID, @ChildNaturalKey, @ChildValue, @ParentID );
Denne gang blokerer barnet ikke . Dette gælder uanset om du kører under låse- eller versionsbaseret læseforpligtet isolation. Det er ikke en tastefejl eller fejl:RCSI gør ingen forskel her.
Forklaring
Udførelsesplanen for den underordnede postindsættelse er lidt anderledes denne gang:
Alt er det samme som før (inklusive den usynlige READCOMMITTEDLOCK
tip) undtagen kontrol af fremmednøgle bruger nu ikke-klyngede unikt indeks, der håndhæver den overordnede tabel primære nøgle. I den første test blev dette indeks grupperet.
Så hvorfor blokerer vi ikke denne gang?
Den endnu ikke-forpligtede overordnede tabelopdatering i den anden session har en eksklusiv lås på det klyngede indeks række, fordi basistabellen bliver ændret. Ændringen til ParentValue
kolonne ikke påvirke den ikke-klyngede primærnøgle på ParentID
, så rækken i ikke-klyngede indeks er ikke låst .
Fremmednøglekontrollen kan derfor erhverve den nødvendige delte lås på det ikke-klyngede primærnøgleindeks uden tvivl, og underordnet tabelindsættelse lykkes med det samme .
Når den primære var klynget, krævede kontrol af fremmednøgle en delt lås på den samme ressource (klyngede indeksrække), som udelukkende var låst af opdateringssætningen.
Opførselen kan være overraskende, men den er ikke en fejl . Ved at give den fremmede nøglecheck sin egen optimerede adgangsmetode undgås logisk unødvendig låsestrid. Der er ingen grund til at blokere den fremmede nøgleopslag, fordi ParentID
attribut påvirkes ikke af den samtidige opdatering.
2. Undgåelige opdateringskonflikter
Hvis du kører de tidligere test under Snapshot Isolation (SI) niveauet, vil resultatet være det samme. Den underordnede række indsætter blokke når den refererede nøgle håndhæves af et klynget indeks , og blokerer ikke når nøglehåndhævelse bruger en ikke-klyngede unikt indeks.
Der er dog en vigtig potentiel forskel, når du bruger SI. Under læst committed (låsning eller RCSI) isolation, lykkes den underordnede rækkeindsættelse i sidste ende efter opdateringen i den anden session forpligter eller ruller tilbage. Ved at bruge SI er der risiko for, at en transaktion afbrydes på grund af en tilsyneladende opdateringskonflikt.
Dette er lidt sværere at demonstrere, fordi en øjebliksbilledetransaktion ikke begynder med BEGIN TRANSACTION
sætning — den begynder med den første brugerdataadgang efter det punkt.
Følgende script opsætter SI-demonstrationen med en ekstra dummy-tabel, der kun bruges til at sikre, at snapshot-transaktionen virkelig er begyndt. Den bruger testvarianten, hvor den refererede primærnøgle håndhæves ved hjælp af en unik clustered indeks (standard):
ALTER DATABASE CURRENT SET ALLOW_SNAPSHOT_ISOLATION ON; GO DROP TABLE IF EXISTS dbo.Dummy, dbo.Child, dbo.Parent; GO CREATE TABLE dbo.Dummy ( x integer NULL ); CREATE TABLE dbo.Parent ( ParentID integer NOT NULL, ParentNaturalKey varchar(10) NOT NULL, ParentValue integer NOT NULL, CONSTRAINT [PK dbo.Parent ParentID] PRIMARY KEY (ParentID), CONSTRAINT [AK dbo.Parent ParentNaturalKey] UNIQUE (ParentNaturalKey) ); CREATE TABLE dbo.Child ( ChildID integer NOT NULL, ChildNaturalKey varchar(10) NOT NULL, ChildValue integer NOT NULL, ParentID integer NULL, CONSTRAINT [PK dbo.Child ChildID] PRIMARY KEY (ChildID), CONSTRAINT [AK dbo.Child ChildNaturalKey] UNIQUE (ChildNaturalKey), CONSTRAINT [FK dbo.Child to dbo.Parent] FOREIGN KEY (ParentID) REFERENCES dbo.Parent (ParentID) );
Indsættelse af den overordnede række:
DECLARE @ParentID integer = 1, @ParentNaturalKey varchar(10) = 'PNK1', @ParentValue integer = 100; INSERT dbo.Parent ( ParentID, ParentNaturalKey, ParentValue ) VALUES ( @ParentID, @ParentNaturalKey, @ParentValue );
Stadig i den første session , start øjebliksbilledetransaktionen:
-- Session 1 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; BEGIN TRANSACTION; -- Ensure snapshot transaction is started SELECT COUNT_BIG(*) FROM dbo.Dummy AS D;
I den anden session (kører på ethvert isolationsniveau):
-- Session 2 DECLARE @ParentID integer = 1, @ParentNaturalKey varchar(10) = 'PNK1', @ParentValue integer = 200; BEGIN TRANSACTION; UPDATE dbo.Parent SET ParentValue = @ParentValue WHERE ParentID = @ParentID;
Forsøg på at indsætte den underordnede række i den første session blokke som forventet:
-- Session 1 DECLARE @ChildID integer = 101, @ChildNaturalKey varchar(10) = 'CNK1', @ChildValue integer = 999, @ParentID integer = 1; INSERT dbo.Child ( ChildID, ChildNaturalKey, ChildValue, ParentID ) VALUES ( @ChildID, @ChildNaturalKey, @ChildValue, @ParentID );
Forskellen opstår, når vi afslutter transaktionen i anden session. Hvis vi ruller det tilbage , den første sessions underordnede rækkeindsættelse fuldføres korrekt .
Hvis vi i stedet forpligter den åbne transaktion:
-- Session 2 COMMIT TRANSACTION;
Den første session rapporterer en opdateringskonflikt og ruller tilbage:
Forklaring
Denne opdateringskonflikt opstår på trods af fremmednøglen bliver valideret blev ikke ændret ved den anden sessions opdatering.
Årsagen er stort set den samme som i det første sæt af tests. Når det klyngede indeks bruges til referencenøglehåndhævelse, støder snapshottransaktionen på en række der er blevet ændret siden det startede. Dette er ikke tilladt under snapshot-isolering.
Når nøglen håndhæves ved hjælp af et ikke-klynget indeks , øjebliksbillede-transaktionen ser kun den umodificerede ikke-klyngede indeksrække, så der er ingen blokering, og der registreres ingen 'opdateringskonflikt'.
Der er mange andre omstændigheder, hvor snapshot-isolering kan rapportere uventede opdateringskonflikter eller andre fejl. Se min tidligere artikel for eksempler.
Konklusioner
Der er mange overvejelser at tage i betragtning, når du vælger det klyngede indeks til en række-butikstabel. Problemerne beskrevet her er blot en anden faktor at evaluere.
Dette gælder især, hvis du vil bruge snapshot-isolering. Ingen nyder en afbrudt transaktion , især en, der nok er ulogisk. Hvis du skal bruge RCSI, er blokering ved læsning at validere fremmede nøgler kan være uventet og kan føre til dødvande.
standard for en PRIMARY KEY
begrænsning er at oprette dets understøttende indeks som clustered , medmindre et andet indeks eller begrænsning i tabeldefinitionen er eksplicit om at blive grupperet i stedet for. Det er en god vane at være eksplicit om din designhensigt, så jeg vil opfordre dig til at skrive CLUSTERED
eller NONCLUSTERED
hver gang.
Dublerede indekser?
Der kan være tidspunkter, hvor du seriøst overvejer, af gode grunde, at have et klynget indeks og ikke-klynget indeks med samme nøgle(r) .
Hensigten kan være at give optimal læseadgang til brugerforespørgsler via den klyngede indeks (undgå nøgleopslag), mens det også muliggør minimalt blokerende (og opdateringskonfliktende) validering for fremmede nøgler via den kompakte ikke-klyngede indeks som vist her.
Dette er muligt, men der er et par problemer at passe på:
-
Givet mere end ét passende målindeks giver SQL Server ikke en måde at garantere på hvilket indeks vil blive brugt til håndhævelse af udenlandsk nøgle.
Dan Guzman dokumenterede sine observationer i Secrets of Foreign Key Index Binding, men disse kan være ufuldstændige, og under alle omstændigheder er udokumenterede, og kan derfor ændre sig .
Du kan omgå dette ved at sikre dig, at der kun er et mål indeks på det tidspunkt, hvor den fremmede nøgle oprettes, men det komplicerer tingene og inviterer til fremtidige problemer, hvis den fremmede nøgle-begrænsning nogensinde bliver droppet og genskabt.
-
Hvis du bruger den stenografiske udenlandske nøglesyntaks, vil SQL Server kun binde begrænsningen til den primære nøgle , uanset om det er ikke-klynget eller klynget.
Det følgende kodestykke viser den sidste forskel:
CREATE TABLE dbo.Parent ( ParentID integer NOT NULL UNIQUE CLUSTERED ); -- Shorthand (implicit) syntax -- Fails with error 1773 CREATE TABLE dbo.Child ( ChildID integer NOT NULL PRIMARY KEY NONCLUSTERED, ParentID integer NOT NULL REFERENCES dbo.Parent ); -- Explicit syntax succeeds CREATE TABLE dbo.Child ( ChildID integer NOT NULL PRIMARY KEY NONCLUSTERED, ParentID integer NOT NULL REFERENCES dbo.Parent (ParentID) );
Folk er blevet vant til stort set at ignorere læse-skrive-konflikter under RCSI og SI. Forhåbentlig har denne artikel givet dig noget ekstra at tænke over, når du implementerer det fysiske design for tabeller relateret til en fremmednøgle.