sql >> Databáze >  >> RDS >> Database

Cizí klíče, blokování a konflikty aktualizací

Většina databází by měla používat cizí klíče k vynucení referenční integrity (RI), kdykoli je to možné. V tomto rozhodnutí je však více než pouhé rozhodnutí použít omezení FK a vytvořit je. Aby vaše databáze fungovala tak hladce, jak je to jen možné, je třeba se zaměřit na řadu úvah.

Tento článek pojednává o jednom takovém aspektu, kterému se nedostává příliš velké publicity:minimalizovat blokování , měli byste pečlivě zvážit indexy používané k vynucení jedinečnosti na nadřazené straně těchto vztahů cizího klíče.

To platí bez ohledu na to, zda používáte uzamykání potvrzené čtení nebo založené na verzi čtení potvrzené izolace snímku (RCSI). U obou může dojít k zablokování, když jsou vztahy cizích klíčů kontrolovány modulem SQL Server.

V rámci izolace snímku (SI) existuje další upozornění. Stejný zásadní problém může vést k neočekávaným (a pravděpodobně nelogickým) selháním transakcí kvůli zjevným konfliktům aktualizací.

Tento článek je rozdělen do dvou částí. První část se zabývá blokováním cizího klíče pod uzamčením izolace snímku potvrzeného čtení a potvrzeného čtení. Druhá část se zabývá souvisejícími konflikty aktualizací v rámci izolace snímků.

1. Blokování kontrol cizího klíče

Nejprve se podívejme, jak může návrh indexu ovlivnit, když dojde k zablokování kvůli kontrolám cizího klíče.

Následující ukázka by měla být spuštěna pod přečtením potvrzeno izolace. Pro SQL Server je výchozí zamykání potvrzeno čtení; Azure SQL Database používá RCSI jako výchozí. Můžete si vybrat, co chcete, nebo spusťte skripty jednou pro každé nastavení, abyste si sami ověřili, že chování je stejné.

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

Vytvořte dvě tabulky propojené vztahem cizího klíče:

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

Přidejte řádek do nadřazené tabulky:

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ři druhém připojení , aktualizujte neklíčový atribut nadřazené tabulky ParentValue uvnitř transakce, ale nezavázat to ještě:

DECLARE
    @ParentID integer = 1,
    @ParentNaturalKey varchar(10) = 'PNK1',
    @ParentValue integer = 200;
 
BEGIN TRANSACTION;
    UPDATE dbo.Parent 
    SET ParentValue = @ParentValue 
    WHERE ParentID = @ParentID;

Neváhejte napsat aktualizační predikát pomocí přirozeného klíče, pokud chcete, pro naše současné účely to nemá žádný vliv.

Zpět na první připojení , pokuste se přidat podřízený záznam:

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

Tento příkaz vložení se zablokuje , ať už jste zvolili uzamykání nebo správu verzí přečteno izolace pro tento test.

Vysvětlení

Plán provádění pro vložení podřízeného záznamu je:

Po vložení nového řádku do podřízené tabulky prováděcí plán zkontroluje omezení cizího klíče. Kontrola je přeskočena, pokud je vložené rodičovské id null (dosažené prostřednictvím predikátu ‚pass through‘ na levém polovičním spojení). V tomto případě není přidané rodičovské id null, takže kontrola cizího klíče je provedeno.

SQL Server ověří omezení cizího klíče vyhledáním odpovídajícího řádku v nadřazené tabulce. Modul nemůže používat verzování řádků k tomu — musí si být jisti, že data, která kontroluje, jsou poslední potvrzená data , ne nějaká stará verze. Modul to zajišťuje přidáním interního READCOMMITTEDLOCK nápověda ke kontrole cizího klíče v nadřazené tabulce.

Konečným výsledkem je, že se SQL Server pokusí získat sdílený zámek na odpovídajícím řádku v nadřazené tabulce, což blokuje protože druhá relace má nekompatibilní zámek exkluzivního režimu kvůli dosud neprovedené aktualizaci.

Aby bylo jasno, vnitřní nápověda k zamykání se vztahuje pouze na kontrolu cizího klíče. Zbytek plánu stále používá RCSI, pokud jste zvolili implementaci úrovně izolace potvrzené čtením.

Vyhýbání se blokování

Potvrďte nebo vraťte zpět otevřenou transakci ve druhé relaci a poté resetujte testovací prostředí:

DROP TABLE IF EXISTS
    dbo.Child, dbo.Parent;

Vytvořte testovací tabulky znovu, ale tentokrát namísto akceptování výchozích hodnot zvolíme primární klíč nonclustered a jedinečné omezení seskupené:

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

Přidejte řádek do nadřazené tabulky jako dříve:

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

V druhé relaci , spusťte aktualizaci, aniž byste ji znovu potvrzovali. Přirozený klíč tentokrát používám jen pro zpestření – pro výsledek to není důležité. Pokud chcete, použijte znovu náhradní klíč.

DECLARE
    @ParentID integer = 1,
    @ParentNaturalKey varchar(10) = 'PNK1',
    @ParentValue integer = 200;
 
BEGIN TRANSACTION 
    UPDATE dbo.Parent 
    SET ParentValue = @ParentValue 
    WHERE ParentNaturalKey = @ParentNaturalKey;

Nyní spusťte podřízenou vložku zpět na první relaci :

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

Tentokrát podřízená vložka neblokuje . To platí bez ohledu na to, zda používáte izolaci pro čtení potvrzenou na základě zamykání nebo verzování. To není překlep nebo chyba:RCSI zde nehraje žádnou roli.

Vysvětlení

Plán provádění pro vložení podřízeného záznamu je tentokrát mírně odlišný:

Vše je stejné jako předtím (včetně neviditelného READCOMMITTEDLOCK nápověda) kromě kontrola cizího klíče nyní používá nonclustered jedinečný index vynucující primární klíč nadřazené tabulky. V prvním testu byl tento index seskupený.

Proč tedy tentokrát nezablokujeme?

Dosud neodsouhlasená aktualizace nadřazené tabulky ve druhé relaci má exkluzivní zámek na sdruženém indexu řádek, protože se základní tabulka upravuje. Změna na ParentValue sloupec není ovlivnit neklastrovaný primární klíč na ParentID , takže řádek nonclustered indexu není uzamčen .

Kontrola cizího klíče tedy může získat nezbytný sdílený zámek na indexu primárního klíče bez klastrů bez sporů a vložení podřízené tabulky okamžitě uspěje .

Když byl primární klíč klastrován, kontrola cizího klíče potřebovala sdílený zámek na stejném prostředku (řádek klastrovaného indexu), který byl výhradně uzamčen příkazem aktualizace.

Chování může být překvapivé, ale nejedná se o chybu . Poskytnutí kontroly cizího klíče vlastní optimalizované přístupové metody zabrání logicky zbytečným sporům o zámek. Není potřeba blokovat vyhledávání cizího klíče, protože ParentID atribut není ovlivněn souběžnou aktualizací.

2. Konflikty aktualizací, kterým se lze vyhnout

Pokud provedete předchozí testy na úrovni izolace snímku (SI), výsledek bude stejný. bloky pro vložení podřízeného řádku když je odkazovaný klíč vynucený shlukovaným indexem a neblokuje když vynucení klíče používá nonclustered jedinečný index.

Při použití SI však existuje jeden důležitý potenciální rozdíl. V rámci izolace potvrzené čtení (uzamykání nebo RCSI) je vložení podřízeného řádku nakonec úspěšné poté, co se aktualizace ve druhé relaci potvrdí nebo vrátí zpět. Při použití SI existuje riziko zrušení transakce kvůli zjevnému konfliktu aktualizací.

Demonstrace je trochu složitější, protože transakce snímku nezačíná BEGIN TRANSACTION výpis — začíná prvním přístupem k uživatelským datům po tomto bodě.

Následující skript nastaví demonstraci SI s další fiktivní tabulkou, která se používá pouze k zajištění toho, aby transakce snímku skutečně začala. Používá variantu testu, kde je odkazovaný primární klíč vynucován pomocí jedinečného shluku index (výchozí):

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

Vložení nadřazeného řádku:

DECLARE
    @ParentID integer = 1,
    @ParentNaturalKey varchar(10) = 'PNK1',
    @ParentValue integer = 100;
 
INSERT dbo.Parent 
(
    ParentID, 
    ParentNaturalKey, 
    ParentValue
) 
VALUES 
(
    @ParentID, 
    @ParentNaturalKey, 
    @ParentValue
);

Stále v první relaci , spusťte transakci snímku:

-- Session 1
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
 
-- Ensure snapshot transaction is started
SELECT COUNT_BIG(*) FROM dbo.Dummy AS D;

V druhé relaci (běží na jakékoli úrovni izolace):

-- Session 2
DECLARE
    @ParentID integer = 1,
    @ParentNaturalKey varchar(10) = 'PNK1',
    @ParentValue integer = 200;
 
BEGIN TRANSACTION;
    UPDATE dbo.Parent 
    SET ParentValue = @ParentValue 
    WHERE ParentID = @ParentID;

Pokus o vložení podřízeného řádku do první relace blokuje podle očekávání:

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

Rozdíl nastane, když ukončíme transakci ve druhém sezení. Pokud to vrátíme zpět , vložení podřízeného řádku první relace dokončeno úspěšně .

Pokud se místo toho zavážeme otevřená transakce:

-- Session 2
COMMIT TRANSACTION;

První relace hlásí konflikt aktualizací a vrátí se zpět:

Vysvětlení

Ke konfliktu aktualizací dochází navzdory skutečnosti, že cizí klíč ověřovaný nebyl změněn aktualizací druhé relace.

Důvod je v podstatě stejný jako u první sady testů. Když je sdružený index se používá k vynucení odkazovaného klíče, transakce snímku narazí na řádek který byl od začátku upraven. Toto není povoleno při izolaci snímků.

Když je klíč vynucen pomocí nonclustered index , transakce snímku vidí pouze neupravený řádek indexu bez seskupení, takže nedochází k blokování a není detekován žádný „konflikt aktualizací“.

Existuje mnoho dalších okolností, kdy může izolace snímku hlásit neočekávané konflikty aktualizací nebo jiné chyby. Příklady viz můj předchozí článek.

Závěry

Při výběru seskupeného indexu pro tabulku úložiště řádků je třeba vzít v úvahu mnoho aspektů. Zde popsané problémy jsou pouze dalším faktorem vyhodnotit.

To platí zejména v případě, že budete používat izolaci snímků. Přerušená transakce nikoho nepotěší , zvláště ten, který je pravděpodobně nelogický. Pokud budete používat RCSI, blokování při čtení ověření cizích klíčů může být neočekávané a může vést k uváznutí.

Výchozí pro PRIMARY KEY omezení je vytvořit svůj podpůrný index jako shlukovaný , pokud jiný index nebo omezení v definici tabulky výslovně neuvádí, že má být místo toho seskupováno. Bývá dobrým zvykem být explicitní o vašem záměru designu, proto bych vám doporučil napsat CLUSTERED nebo NONCLUSTERED pokaždé.

Duplicitní indexy?

Mohou nastat situace, kdy z rozumných důvodů vážně zvažujete mít seskupený index a neshlukovaný index se stejnými klíči .

Záměrem může být poskytnutí optimálního přístupu ke čtení pro uživatelské dotazy prostřednictvím seskupení index (vyhýbá se vyhledávání klíčů) a zároveň umožňuje minimální blokování (a konfliktní aktualizace) ověřování cizích klíčů prostřednictvím kompaktního nonclustered index, jak je znázorněno zde.

Toho lze dosáhnout, ale je tu pár zádrhelů na co si dát pozor:

  1. Vzhledem k více než jednomu vhodnému cílovému indexu SQL Server neposkytuje způsob, jak zaručit který index bude použit pro vynucení cizího klíče.

    Dan Guzman zdokumentoval svá pozorování v Secrets of Foreign Key Index Binding, ale ty mohou být neúplné a v každém případě nezdokumentované, a tak se mohou změnit .

    Tento problém můžete obejít tím, že zajistíte, že existuje pouze jeden cíl index v době vytvoření cizího klíče, ale komplikuje to věci a vyvolává budoucí problémy, pokud bude omezení cizího klíče někdy zrušeno a znovu vytvořeno.

  2. Pokud použijete zkrácenou syntaxi cizího klíče, SQL Server bude pouze svázat omezení s primárním klíčem , ať už je neklastrovaný nebo klastrovaný.

Následující fragment kódu ukazuje druhý rozdíl:

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

Lidé si zvykli do značné míry ignorovat konflikty čtení a zápisu pod RCSI a SI. Doufejme, že vám tento článek dal něco navíc, na co byste měli myslet při implementaci fyzického návrhu pro tabulky související s cizím klíčem.


  1. AKTUALIZUJTE více tabulek v MySQL pomocí LEFT JOIN

  2. Příklad jednoduchého příkazu sloučení v SQL Server

  3. Zabezpečte své Mongo Clusters pomocí SSL

  4. Jak pokračovat ve zpracování smyčky kurzoru po výjimce v Oracle