Database
 sql >> Baza danych >  >> RDS >> Database

Klucze obce, blokowanie i konflikty aktualizacji

Większość baz danych powinna korzystać z kluczy obcych w celu wymuszenia integralności referencyjnej (RI) tam, gdzie to możliwe. Jednak w tej decyzji jest coś więcej niż tylko decyzja o użyciu ograniczeń FK i ich utworzenie. Należy uwzględnić szereg kwestii, które należy uwzględnić, aby baza danych działała tak płynnie, jak to tylko możliwe.

W tym artykule omówiono jedną z takich kwestii, która nie cieszy się dużym zainteresowaniem:zminimalizowanie blokowania , należy dokładnie przemyśleć indeksy używane do wymuszania unikalności po stronie nadrzędnej tych relacji kluczy obcych.

Dotyczy to sytuacji, gdy używasz blokowania przeczytaj zatwierdzone lub oparte na wersji przeczytaj izolację zatwierdzonej migawki (RCSI). Oba mogą doświadczać blokowania, gdy relacje kluczy obcych są sprawdzane przez silnik SQL Server.

W przypadku izolacji migawki (SI) istnieje dodatkowe zastrzeżenie. Ten sam istotny problem może prowadzić do nieoczekiwanych (i prawdopodobnie nielogicznych) nieudanych transakcji z powodu widocznych konfliktów aktualizacji.

Ten artykuł składa się z dwóch części. Pierwsza część dotyczy blokowania klucza obcego w ramach blokowania odczytu zatwierdzonej i odczytu zatwierdzonej migawki migawki. Druga część obejmuje powiązane konflikty aktualizacji w przypadku izolacji migawki.

1. Blokowanie kontroli kluczy obcych

Przyjrzyjmy się najpierw, jak projekt indeksu może wpłynąć na blokowanie z powodu sprawdzania kluczy obcych.

Poniższa prezentacja powinna być uruchomiona w trybie przeczytaj zatwierdzone izolacja. Dla SQL Server domyślną wartością jest blokada odczytu popełniona; Azure SQL Database domyślnie używa RCSI. Możesz wybrać dowolne ustawienie lub uruchomić skrypty raz dla każdego ustawienia, aby samodzielnie sprawdzić, czy zachowanie jest takie samo.

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

Utwórz dwie tabele połączone relacją klucza obcego:

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

Dodaj wiersz do tabeli nadrzędnej:

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

Na drugim połączeniu , zaktualizuj atrybut tabeli nadrzędnej niebędący kluczem ParentValue wewnątrz transakcji, ale nie zatwierdzaj to jeszcze:

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

Jeśli wolisz, możesz napisać predykat aktualizacji przy użyciu naturalnego klucza, nie ma to żadnego znaczenia dla naszych obecnych celów.

Powrót na pierwsze połączenie , spróbuj dodać rekord podrzędny:

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

Ta instrukcja insert blokuje , niezależnie od tego, czy wybrałeś blokowanie czy wersjonowanie przeczytaj popełnione izolacja dla tego testu.

Wyjaśnienie

Plan wykonania wstawiania rekordu podrzędnego to:

Po wstawieniu nowego wiersza do tabeli podrzędnej, plan wykonania sprawdza ograniczenie klucza obcego. Sprawdzenie jest pomijane, jeśli wstawiony identyfikator rodzica ma wartość null (uzyskiwany przez predykat „przejścia” na lewym sprzężeniu częściowym). W tym przypadku dodany identyfikator rodzica nie jest pusty, więc sprawdzenie klucza obcego jest wykonane.

SQL Server weryfikuje ograniczenie klucza obcego, szukając pasującego wiersza w tabeli nadrzędnej. Wyszukiwarka nie może używać wersji wierszy w tym celu — musi mieć pewność, że sprawdzane dane są ostatnimi zatwierdzonymi danymi , a nie jakaś stara wersja. Silnik zapewnia to, dodając wewnętrzny READCOMMITTEDLOCK wskazówka tabeli do sprawdzenia klucza obcego w tabeli nadrzędnej.

Wynik końcowy jest taki, że SQL Server próbuje uzyskać wspólną blokadę w odpowiednim wierszu w tabeli nadrzędnej, która blokuje ponieważ druga sesja posiada niekompatybilną blokadę trybu wyłączności z powodu jeszcze niezatwierdzonej aktualizacji.

Dla jasności, wewnętrzna wskazówka dotycząca blokowania dotyczy tylko sprawdzania klucza obcego. Pozostała część planu nadal używa RCSI, jeśli wybierzesz tę implementację odczytu zatwierdzonego poziomu izolacji.

Unikanie blokowania

Zatwierdź lub wycofaj otwartą transakcję w drugiej sesji, a następnie zresetuj środowisko testowe:

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

Ponownie utwórz tabele testowe, ale tym razem zamiast akceptować wartości domyślne, wybieramy klucz podstawowy nieklastrowy i unikalne ograniczenie zgrupowane:

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

Dodaj wiersz do tabeli nadrzędnej, jak poprzednio:

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

W drugiej sesji , uruchom aktualizację bez ponownego jej zatwierdzania. Tym razem używam naturalnego klucza tylko dla odmiany — nie ma to znaczenia dla wyniku. Jeśli wolisz, użyj ponownie klucza zastępczego.

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

Teraz uruchom wstawkę podrzędną z powrotem w pierwszej sesji :

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

Tym razem wstawka podrzędna nie blokuje . Dzieje się tak niezależnie od tego, czy pracujesz w trybie odczytu zatwierdzonej izolacji opartej na blokowaniu lub wersji. To nie jest literówka ani błąd:RCSI nie ma tutaj znaczenia.

Wyjaśnienie

Tym razem plan wykonania wstawiania rekordu podrzędnego jest nieco inny:

Wszystko jest takie samo jak wcześniej (w tym niewidoczny READCOMMITTEDLOCK wskazówka) z wyjątkiem sprawdzanie klucza obcego używa teraz nieklastrowanego unikalny indeks wymuszający klucz podstawowy tabeli nadrzędnej. W pierwszym teście ten indeks był pogrupowany.

Dlaczego więc tym razem nie blokujemy się?

Jeszcze niezatwierdzona aktualizacja tabeli nadrzędnej w drugiej sesji ma wyłączną blokadę w indeksie klastrowym wiersz, ponieważ modyfikowana jest tabela podstawowa. Zmiana w ParentValue kolumna nie wpływają na nieklastrowany klucz podstawowy w ParentID , aby wiersz indeksu nieklastrowanego nie był zablokowany .

Sprawdzenie klucza obcego może zatem uzyskać niezbędną blokadę współdzieloną na nieklastrowanym indeksie klucza podstawowego bez rywalizacji, a wstawienie tabeli podrzędnej od razu powiodło się .

Gdy podstawowy był klastrowany, sprawdzanie klucza obcego wymagało wspólnej blokady na tym samym zasobie (wiersz indeksu klastrowanego), który został zablokowany wyłącznie przez instrukcję aktualizacji.

Zachowanie może być zaskakujące, ale nie jest to błąd . Zapewnienie kontroli klucza obcego własnej zoptymalizowanej metody dostępu pozwala uniknąć logicznie niepotrzebnej rywalizacji o blokadę. Nie ma potrzeby blokowania wyszukiwania klucza obcego, ponieważ ParentID współbieżna aktualizacja nie ma wpływu na atrybut.

2. Konflikty aktualizacji, których można uniknąć

Jeśli uruchomisz poprzednie testy na poziomie Snapshot Isolation (SI), wynik będzie taki sam. Wiersz podrzędny wstawia bloki gdy przywoływany klucz jest wymuszany przez indeks klastrowy i nie blokuje gdy wymuszanie kluczy używa nieklastrowanego unikalny indeks.

Istnieje jednak jedna ważna potencjalna różnica podczas korzystania z SI. W przypadku izolacji zatwierdzonej do odczytu (blokowanie lub RCSI), wstawienie wiersza podrzędnego ostatecznie się powiedzie po aktualizacji w drugiej sesji zatwierdzenie lub wycofanie. Korzystając z SI, istnieje ryzyko przerwania transakcji z powodu widocznego konfliktu aktualizacji.

Jest to trochę trudniejsze do zademonstrowania, ponieważ transakcja migawki nie zaczyna się od BEGIN TRANSACTION oświadczenie — zaczyna się od pierwszego dostępu do danych użytkownika po tym punkcie.

Poniższy skrypt konfiguruje demonstrację SI, z dodatkową fikcyjną tabelą używaną tylko w celu upewnienia się, że transakcja migawki naprawdę się rozpoczęła. Wykorzystuje odmianę testową, w której przywoływany klucz podstawowy jest wymuszany przy użyciu unikalnego zgrupowania indeks (domyślny):

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

Wstawianie wiersza nadrzędnego:

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

Nadal w pierwszej sesji , rozpocznij transakcję migawki:

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

W drugiej sesji (działa na dowolnym poziomie izolacji):

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

Próba wstawienia wiersza podrzędnego w pierwszej sesji bloków zgodnie z oczekiwaniami:

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

Różnica pojawia się, gdy zakończymy transakcję w drugiej sesji. Jeśli cofniemy to , wstawianie wiersza podrzędnego w pierwszej sesji pomyślnie kończy się .

Jeśli zamiast tego zobowiązujemy się otwarta transakcja:

-- Session 2
COMMIT TRANSACTION;

Pierwsza sesja zgłasza konflikt aktualizacji i cofa się:

Wyjaśnienie

Ten konflikt aktualizacji występuje pomimo klucza obcego sprawdzanie poprawności nie zostało zmienione przed aktualizacją drugiej sesji.

Powód jest zasadniczo taki sam jak w pierwszym zestawie testów. Gdy indeks klastrowy służy do wymuszania przywoływania klucza, transakcja migawki napotyka wiersz który został zmodyfikowany od początku. Nie jest to dozwolone w przypadku izolacji zrzutów.

Gdy klucz jest wymuszany przy użyciu indeksu nieklastrowego , transakcja migawki widzi tylko niezmodyfikowany nieklastrowany wiersz indeksu, więc nie ma blokowania ani „konfliktu aktualizacji”.

Istnieje wiele innych okoliczności, w których izolacja migawki może zgłaszać nieoczekiwane konflikty aktualizacji lub inne błędy. Zobacz mój poprzedni artykuł, aby zobaczyć przykłady.

Wnioski

Przy wyborze indeksu klastrowego dla tabeli magazynu wierszy należy wziąć pod uwagę wiele kwestii. Opisane tutaj problemy to tylko kolejny czynnik do oceny.

Jest to szczególnie ważne, jeśli będziesz używać izolacji migawek. Nikt nie cieszy się przerwaną transakcją , zwłaszcza taki, który jest prawdopodobnie nielogiczny. Jeśli będziesz używać RCSI, blokowanie podczas czytania sprawdzanie poprawności kluczy obcych może być nieoczekiwane i może prowadzić do zakleszczeń.

Domyślny dla PRIMARY KEY ograniczeniem jest utworzenie indeksu pomocniczego jako zgrupowanego , chyba że inny indeks lub ograniczenie w definicji tabeli wyraźnie dotyczy klastrowania. Dobrym zwyczajem jest bycie wyraźnym o Twoich zamiarach projektowych, więc zachęcam Cię do napisania CLUSTERED lub NONCLUSTERED za każdym razem.

Zduplikowane indeksy?

Może się zdarzyć, że z uzasadnionych powodów poważnie rozważysz posiadanie indeksu klastrowego i indeksu nieklastrowego z tym samym kluczem(-ami) .

Intencją może być zapewnienie optymalnego dostępu do odczytu zapytań użytkowników za pośrednictwem klastrowego indeks (unikanie wyszukiwania kluczy), jednocześnie umożliwiając minimalną blokadę (i konflikty aktualizacji) dla kluczy obcych za pośrednictwem kompaktowego nieklastrowego indeks, jak pokazano tutaj.

Jest to osiągalne, ale jest kilka szkodliwych uważać na:

  1. Biorąc pod uwagę więcej niż jeden odpowiedni indeks docelowy, SQL Server nie zapewnia sposobu gwarantowania który indeks będzie używany do wymuszania klucza obcego.

    Dan Guzman udokumentował swoje obserwacje w Secrets of Foreign Key Index Binding, ale mogą one być niekompletne, a w każdym razie nieudokumentowane, więc mogą się zmienić .

    Możesz to obejść, upewniając się, że jest tylko jeden cel indeks w momencie tworzenia klucza obcego, ale komplikuje to i powoduje przyszłe problemy, jeśli ograniczenie klucza obcego zostanie kiedykolwiek usunięte i ponownie utworzone.

  2. Jeśli użyjesz skróconej składni klucza obcego, SQL Server będzie tylko powiąż ograniczenie z kluczem podstawowym , niezależnie od tego, czy jest nieklastrowany, czy klastrowany.

Poniższy fragment kodu demonstruje tę drugą różnicę:

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

Ludzie przyzwyczaili się w dużej mierze do ignorowania konfliktów odczytu i zapisu w RCSI i SI. Mam nadzieję, że ten artykuł dał ci coś więcej do przemyślenia podczas wdrażania fizycznego projektu tabel powiązanych za pomocą klucza obcego.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. SQL Wybierz Wyraźne

  2. Jak sprawdzić, czy T-SQL UDF jest powiązany ze schematem (nawet jeśli jest zaszyfrowany)

  3. KLUCZ OBCY SQL

  4. Minimalne rejestrowanie za pomocą INSERT…SELECT i szybkiego ładowania kontekstu

  5. Zrozumienie, co naprawdę aktualizuje sp_updatestats