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

Korzyści z indeksowania kluczy obcych

Klucze podstawowe i obce są podstawowymi cechami relacyjnych baz danych, jak pierwotnie zauważono w artykule E.F. Codda „A Relational Model of Data for Large Shared Data Banks”, opublikowanym w 1970 roku. Często powtarzany cytat brzmi:„Klucz, cały klucz, i tylko klucz, więc pomóż mi Codd."

Tło:Klucze podstawowe

Klucz podstawowy to ograniczenie w SQL Server, które działa w celu jednoznacznej identyfikacji każdego wiersza w tabeli. Klucz może być zdefiniowany jako pojedyncza kolumna o wartości innej niż NULL lub jako kombinacja kolumn o wartości innej niż NULL, która generuje unikatową wartość i jest używana do wymuszania integralności jednostki dla tabeli. Tabela może mieć tylko jeden klucz podstawowy, a po zdefiniowaniu ograniczenia klucza podstawowego dla tabeli tworzony jest unikalny indeks. Ten indeks będzie domyślnie indeksem klastrowym, chyba że zostanie określony jako indeks nieklastrowy, gdy zdefiniowane jest ograniczenie klucza podstawowego.

Rozważ Sales.SalesOrderHeader tabela w AdventureWorks2012 Baza danych. Ta tabela zawiera podstawowe informacje o zamówieniu sprzedaży, w tym datę zamówienia i identyfikator klienta, a każda sprzedaż jest jednoznacznie identyfikowana przez SalesOrderID , który jest kluczem podstawowym tabeli. Za każdym razem, gdy do tabeli dodawany jest nowy wiersz, ograniczenie klucza podstawowego (o nazwie PK_SalesOrderHeader_SalesOrderID ) jest sprawdzane, aby upewnić się, że nie istnieje już żaden wiersz o tej samej wartości dla SalesOrderID .

Klucze obce

Oddzielone od kluczy podstawowych, ale bardzo powiązane, są klucze obce. Klucz obcy to kolumna lub kombinacja kolumn, która jest taka sama jak klucz podstawowy, ale znajduje się w innej tabeli. Klucze obce służą do definiowania relacji i egzekwowania integralności między dwiema tabelami.

Aby kontynuować korzystanie z powyższego przykładu, SalesOrderID kolumna istnieje jako klucz obcy w Sales.SalesOrderDetail tabeli, w której przechowywane są dodatkowe informacje o sprzedaży, takie jak identyfikator produktu i cena. Gdy nowa sprzedaż zostanie dodana do SalesOrderHeader tabeli, nie jest wymagane dodawanie wiersza dla tej sprzedaży do SalesOrderDetail tabela  Jednak podczas dodawania wiersza do SalesOrderDetail tabeli, odpowiedni wiersz dla SalesOrderID musi istnieją w SalesOrderHeader tabela.

I odwrotnie, podczas usuwania danych wiersz dla określonego SalesOrderID można usunąć w dowolnym momencie z SalesOrderDetail tabeli, ale w celu usunięcia wiersza z SalesOrderHeader tabela, powiązane wiersze z SalesOrderDetail należy najpierw usunąć.

W przeciwieństwie do ograniczeń klucza podstawowego, gdy ograniczenie klucza obcego jest zdefiniowane dla tabeli, indeks nie jest tworzony domyślnie przez SQL Server. Jednak często zdarza się, że programiści i administratorzy baz danych dodają je ręcznie. Klucz obcy może być częścią złożonego klucza podstawowego tabeli, w którym to przypadku indeks klastrowy będzie istniał z kluczem obcym jako częścią klucza klastrowego. Alternatywnie zapytania mogą wymagać indeksu zawierającego klucz obcy i co najmniej jednej dodatkowej kolumny w tabeli, więc indeks nieklastrowany zostanie utworzony do obsługi tych zapytań. Co więcej, indeksy kluczy obcych mogą zapewniać korzyści w zakresie wydajności łączenia tabel z kluczem podstawowym i obcym, a także mogą wpływać na wydajność, gdy wartość klucza podstawowego zostanie zaktualizowana lub jeśli wiersz zostanie usunięty.

W AdventureWorks2012 baza danych, jest jedna tabela, SalesOrderDetail , z SalesOrderID jako klucz obcy. Dla SalesOrderDetail tabela, SalesOrderID i SalesOrderDetailID połączyć, tworząc klucz podstawowy, obsługiwany przez indeks klastrowy. Jeśli SalesOrderDetail tabela nie miała indeksu w SalesOrderID kolumna, to gdy wiersz zostanie usunięty z SalesOrderHeader , SQL Server musiałby sprawdzić, czy nie ma wierszy dla tego samego SalesOrderID wartość istnieje. Bez żadnych indeksów zawierających SalesOrderID kolumna, SQL Server musiałby wykonać pełne skanowanie tabeli SalesOrderDetail . Jak możesz sobie wyobrazić, im większa tabela, do której się odnosi, tym dłużej trwa usuwanie.

Przykład

Możemy to zobaczyć w poniższym przykładzie, który wykorzystuje kopie wyżej wymienionych tabel z AdventureWorks2012 bazy danych, które zostały rozbudowane za pomocą skryptu, który można znaleźć tutaj. Skrypt został opracowany przez Jonathana Kehayiasa (blog | @SQLPoolBoy) i tworzy SalesOrderHeaderEnlarged tabela z 1 258 600 wierszami i SalesOrderDetailEnlarged stół z 4 852 680 rzędami. Po uruchomieniu skryptu ograniczenie klucza obcego zostało dodane za pomocą poniższych instrukcji. Zauważ, że ograniczenie jest tworzone za pomocą ON DELETE CASCADE opcja. Dzięki tej opcji, gdy aktualizacja lub usunięcie zostanie wydane względem SalesOrderHeaderEnlarged tabela, wiersze w odpowiednich tabelach – w tym przypadku po prostu SalesOrderDetailEnlarged – są aktualizowane lub usuwane.

Ponadto domyślny indeks klastrowy dla SalesOrderDetailEnglarged został usunięty i odtworzony, aby po prostu mieć SalesOrderDetailID jako klucz podstawowy, ponieważ reprezentuje typowy projekt.

USE [AdventureWorks2012];
GO
 
/* remove original clustered index */
ALTER TABLE [Sales].[SalesOrderDetailEnlarged] 
  DROP CONSTRAINT [PK_SalesOrderDetailEnlarged_SalesOrderID_SalesOrderDetailID];
GO
 
/* re-create clustered index with SalesOrderDetailID only */
ALTER TABLE [Sales].[SalesOrderDetailEnlarged] 
  ADD CONSTRAINT [PK_SalesOrderDetailEnlarged_SalesOrderDetailID] PRIMARY KEY CLUSTERED
  (
    [SalesOrderDetailID] ASC
  )
  WITH
  (
     PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, 
     IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON
  ) ON [PRIMARY];
GO
 
/* add foreign key constraint for SalesOrderID */
ALTER TABLE [Sales].[SalesOrderDetailEnlarged] WITH CHECK 
  ADD CONSTRAINT [FK_SalesOrderDetailEnlarged_SalesOrderHeaderEnlarged_SalesOrderID] 
  FOREIGN KEY([SalesOrderID])
  REFERENCES [Sales].[SalesOrderHeaderEnlarged] ([SalesOrderID])
  ON DELETE CASCADE;
GO
 
ALTER TABLE [Sales].[SalesOrderDetailEnlarged] 
  CHECK CONSTRAINT [FK_SalesOrderDetailEnlarged_SalesOrderHeaderEnlarged_SalesOrderID];
GO

Z ograniczeniem klucza obcego i brakiem indeksu pomocniczego wydano jedno usunięcie względem SalesOrderHeaderEnlarged tabeli, co spowodowało usunięcie jednego wiersza z SalesOrderHeaderEnlarged i 72 wiersze z SalesOrderDetailEnlarged :

SET STATISTICS IO ON;
SET STATISTICS TIME ON;
 
DBCC DROPCLEANBUFFERS;
DBCC FREEPROCCACHE;
 
USE [AdventureWorks2012];
GO
 
DELETE FROM [Sales].[SalesOrderHeaderEnlarged] WHERE [SalesOrderID] = 292104;

Statystyki IO i informacje o czasie przedstawiały się następująco:

Czas parsowania i kompilacji SQL Server:

Czas procesora =8 ms, upływ czasu =8 ms.

Tabela „Szczegóły zamówienia sprzedaży powiększone”. Liczba skanów 1, odczyty logiczne 50647, odczyty fizyczne 8, odczyty z wyprzedzeniem 50667, odczyty logiczne modułu 0, odczyty fizyczne modułu 0, odczyty z wyprzedzeniem modułu 0.
Tabela „Tabela robocza”. Liczba skanów 2, odczyty logiczne 7, odczyty fizyczne 0, odczyty z wyprzedzeniem 0, odczyty logiczne lobu 0, odczyty fizyczne lobu 0, odczyty z wyprzedzeniem lobu 0.
Tabela „SalesOrderHeaderEnlarged”. Liczba skanów 0, odczyty logiczne 15, odczyty fizyczne 14, odczyty z wyprzedzeniem 0, odczyty logiczne lobu 0, odczyty fizyczne lobu 0, odczyty lobu z wyprzedzeniem 0.

Czasy wykonania serwera SQL:

Czas procesora =1045 ms,  upływ czasu =1898 ms.

Używając Eksploratora planów SQL Sentry, plan wykonania pokazuje klastrowane skanowanie indeksu względem SalesOrderDetailEnlarged ponieważ nie ma indeksu w SalesOrderID :


Plan zapytań bez indeksu klucza obcego

Indeks nieklastrowy do obsługi SalesOrderDetailEnlarged został następnie utworzony za pomocą następującego oświadczenia:

USE [AdventureWorks2012];
GO
 
/* create nonclustered index */
CREATE NONCLUSTERED INDEX [IX_SalesOrderDetailEnlarged_SalesOrderID] ON [Sales].[SalesOrderDetailEnlarged]
(
  [SalesOrderID] ASC
)
WITH
(
  PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, 
  ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON
)
ON [PRIMARY];
 
GO

Wykonano kolejne usunięcie dla SalesOrderID który wpłynął na jeden wiersz w SalesOrderHeaderEnlarged i 72 wiersze w SalesOrderDetailEnlarged :

SET STATISTICS IO ON;
SET STATISTICS TIME ON;
 
DBCC DROPCLEANBUFFERS;
DBCC FREEPROCCACHE;
 
USE [AdventureWorks2012];
GO
 
DELETE FROM [Sales].[SalesOrderHeaderEnlarged] WHERE [SalesOrderID] = 697505;

Statystyki IO i informacje o czasie wykazały znaczną poprawę:

Czas parsowania i kompilacji SQL Server:

Czas procesora =0 ms, upływ czasu =7 ms.

Tabela „Szczegóły zamówienia sprzedaży powiększone”. Liczba skanów 1, odczyty logiczne 48, odczyty fizyczne 13, odczyty z wyprzedzeniem 0, odczyty logiczne lobu 0, odczyty fizyczne lobu 0, odczyty z wyprzedzeniem lobu 0.
Tabela „Tabela robocza”. Liczba skanów 2, odczyty logiczne 7, odczyty fizyczne 0, odczyty z wyprzedzeniem 0, odczyty logiczne lobu 0, odczyty fizyczne lobu 0, odczyty z wyprzedzeniem lobu 0.
Tabela „SalesOrderHeaderEnlarged”. Liczba skanów 0, odczyty logiczne 15, odczyty fizyczne 15, odczyty z wyprzedzeniem 0, odczyty logiczne modułu 0, odczyty fizyczne modułu 0, odczyty modułu z wyprzedzeniem 0.

Czasy wykonania serwera SQL:

Czas procesora =0 ms,  upływ czasu =27 ms.

A plan zapytań pokazał wyszukiwanie indeksu indeksu nieklastrowego na SalesOrderID , zgodnie z oczekiwaniami:


Plan zapytań z indeksem klucza obcego

Czas wykonania zapytania spadł z 1898 ms do 27 ms – redukcja o 98,58% i odczyty dla SalesOrderDetailEnlarged tabela spadła z 50647 do 48 – poprawa o 99,9%. Odkładając na bok procenty, weź pod uwagę same operacje we/wy generowane przez usunięcie. SalesOrderDetailEnlarged w tym przykładzie tabela ma tylko 500 MB, a dla systemu z 256 GB dostępnej pamięci tabela zajmująca 500 MB w buforze nie wydaje się straszną sytuacją. Ale tabela z 5 milionami wierszy jest stosunkowo niewielka; większość dużych systemów OLTP ma tabele z setkami milionów wierszy. Ponadto często zdarza się, że istnieje wiele odwołań do klucza obcego dla klucza podstawowego, gdzie usunięcie klucza podstawowego wymaga usunięcia z wielu powiązanych tabel. W takim przypadku można zobaczyć wydłużony czas trwania usuwania, co jest nie tylko problemem z wydajnością, ale także z blokowaniem, w zależności od poziomu izolacji.

Wniosek

Generalnie zaleca się utworzenie indeksu prowadzącego do kolumny (kolumn) klucza obcego, aby obsługiwać nie tylko połączenia między kluczem podstawowym i obcym, ale także aktualizacje i usunięcia. Należy zauważyć, że jest to ogólne zalecenie, ponieważ istnieją skrajne scenariusze, w których dodatkowy indeks klucza obcego nie został użyty z powodu bardzo małego rozmiaru tabeli, a dodatkowe aktualizacje indeksu faktycznie negatywnie wpłynęły na wydajność. Podobnie jak w przypadku wszelkich modyfikacji schematu, dodawanie indeksów powinno być testowane i monitorowane po wdrożeniu. Ważne jest, aby dodatkowe indeksy dawały pożądane efekty i nie wpływały negatywnie na wydajność rozwiązania. Warto również zauważyć, ile dodatkowej przestrzeni zajmują indeksy dla kluczy obcych. Należy to wziąć pod uwagę przed utworzeniem indeksów, a jeśli przynoszą one korzyści, należy wziąć pod uwagę przy planowaniu wydajności w przyszłości.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. UŻYJ PODPOWIEDZI i DISABLE_OPTIMIZED_NESTED_LOOP

  2. T-SQL a SQL

  3. Znajdowanie korzyści związanych z wydajnością dzięki partycjonowaniu

  4. Rozwiązania wyzwań generatora serii liczb – Część 3

  5. ETL kontra ELT:zakładamy, ty sędzio