Database
 sql >> Datenbank >  >> RDS >> Database

Die Vorteile der Indizierung von Fremdschlüsseln

Primär- und Fremdschlüssel sind grundlegende Merkmale relationaler Datenbanken, wie ursprünglich in E. F. Codds 1970 veröffentlichtem Artikel „A Relational Model of Data for Large Shared Data Banks“ erwähnt. und nichts als den Schlüssel, also hilf mir, Codd."

Hintergrund:Primärschlüssel

Ein Primärschlüssel ist eine Einschränkung in SQL Server, die dazu dient, jede Zeile in einer Tabelle eindeutig zu identifizieren. Der Schlüssel kann als einzelne Nicht-NULL-Spalte oder als Kombination von Nicht-NULL-Spalten definiert werden, die einen eindeutigen Wert generiert, und wird verwendet, um die Entitätsintegrität für eine Tabelle zu erzwingen. Eine Tabelle kann nur einen Primärschlüssel haben, und wenn eine Primärschlüsseleinschränkung für eine Tabelle definiert wird, wird ein eindeutiger Index erstellt. Dieser Index ist standardmäßig ein geclusterter Index, sofern er nicht als nicht geclusterter Index angegeben wird, wenn die Primärschlüsseleinschränkung definiert wird.

Betrachten Sie den Sales.SalesOrderHeader Tabelle in AdventureWorks2012 Datenbank. Diese Tabelle enthält grundlegende Informationen zu einem Verkaufsauftrag, einschließlich Bestelldatum und Kunden-ID, und jeder Verkauf wird durch eine SalesOrderID eindeutig identifiziert , das ist der Primärschlüssel für die Tabelle. Jedes Mal, wenn der Tabelle eine neue Zeile hinzugefügt wird, wird die Primärschlüsseleinschränkung (mit dem Namen PK_SalesOrderHeader_SalesOrderID ) wird überprüft, um sicherzustellen, dass keine Zeile mit demselben Wert für SalesOrderID bereits vorhanden ist .

Fremdschlüssel

Von Primärschlüsseln getrennt, aber sehr verwandt, sind Fremdschlüssel. Ein Fremdschlüssel ist eine Spalte oder Kombination von Spalten, die mit dem Primärschlüssel identisch ist, sich jedoch in einer anderen Tabelle befindet. Fremdschlüssel werden verwendet, um eine Beziehung zu definieren und die Integrität zwischen zwei Tabellen zu erzwingen.

Um das oben genannte Beispiel weiter zu verwenden, die SalesOrderID Spalte existiert als Fremdschlüssel in Sales.SalesOrderDetail Tabelle, in der zusätzliche Informationen zum Verkauf gespeichert sind, z. B. Produkt-ID und Preis. Wenn ein neuer Verkauf zum SalesOrderHeader hinzugefügt wird Tabelle ist es nicht erforderlich, eine Zeile für diesen Verkauf zu SalesOrderDetail hinzuzufügen table  Allerdings beim Hinzufügen einer Zeile zu SalesOrderDetail Tabelle, eine entsprechende Zeile für die SalesOrderID muss existieren im SalesOrderHeader Tabelle.

Umgekehrt beim Löschen von Daten eine Zeile für eine bestimmte SalesOrderID kann jederzeit aus dem SalesOrderDetail gelöscht werden Tabelle, sondern damit eine Zeile aus dem SalesOrderHeader gelöscht wird Tabelle, zugehörige Zeilen aus SalesOrderDetail müssen zuerst gelöscht werden.

Im Gegensatz zu Primärschlüsseleinschränkungen wird bei der Definition einer Fremdschlüsseleinschränkung für eine Tabelle standardmäßig kein Index von SQL Server erstellt. Es ist jedoch nicht ungewöhnlich, dass Entwickler und Datenbankadministratoren sie manuell hinzufügen. Der Fremdschlüssel kann Teil eines zusammengesetzten Primärschlüssels für die Tabelle sein, in diesem Fall würde ein gruppierter Index mit dem Fremdschlüssel als Teil des Gruppierungsschlüssels existieren. Alternativ können Abfragen einen Index erfordern, der den Fremdschlüssel und eine oder mehrere zusätzliche Spalten in der Tabelle enthält, sodass ein nicht gruppierter Index erstellt wird, um diese Abfragen zu unterstützen. Darüber hinaus können Indizes für Fremdschlüssel Leistungsvorteile für Tabellenverknüpfungen mit Primär- und Fremdschlüssel bieten und die Leistung beeinträchtigen, wenn der Primärschlüsselwert aktualisiert oder die Zeile gelöscht wird.

Im AdventureWorks2012 Datenbank gibt es eine Tabelle, SalesOrderDetail , mit SalesOrderID als Fremdschlüssel. Für SalesOrderDetail Tabelle, SalesOrderID und SalesOrderDetailID kombinieren, um den Primärschlüssel zu bilden, der von einem gruppierten Index unterstützt wird. Wenn das SalesOrderDetail Tabelle hatte keinen Index auf SalesOrderID Spalte, dann wenn eine Zeile aus SalesOrderHeader gelöscht wird , müsste SQL Server überprüfen, ob keine Zeilen für dieselbe SalesOrderID vorhanden sind Wert bestehen. Ohne Indizes, die die SalesOrderID enthalten -Spalte, müsste SQL Server einen vollständigen Tabellenscan von SalesOrderDetail durchführen . Wie Sie sich vorstellen können, dauert das Löschen umso länger, je größer die referenzierte Tabelle ist.

Ein Beispiel

Wir können dies im folgenden Beispiel sehen, das Kopien der oben genannten Tabellen aus AdventureWorks2012 verwendet Datenbank, die mit einem Skript erweitert wurden, das hier zu finden ist. Das Skript wurde von Jonathan Kehayias (Blog | @SQLPoolBoy) entwickelt und erstellt einen SalesOrderHeaderEnlarged Tabelle mit 1.258.600 Zeilen und einem SalesOrderDetailEnlarged Tabelle mit 4.852.680 Zeilen. Nachdem das Skript ausgeführt wurde, wurde die Fremdschlüsseleinschränkung mithilfe der folgenden Anweisungen hinzugefügt. Beachten Sie, dass die Einschränkung mit ON DELETE CASCADE erstellt wird Möglichkeit. Mit dieser Option, wenn eine Aktualisierung oder Löschung für den SalesOrderHeaderEnlarged ausgegeben wird Tabelle, Zeilen in der/den entsprechenden Tabelle(n) – in diesem Fall nur SalesOrderDetailEnlarged – aktualisiert oder gelöscht werden.

Außerdem der standardmäßige, gruppierte Index für SalesOrderDetailEnglarged wurde gelöscht und neu erstellt, um nur noch SalesOrderDetailID zu haben als Primärschlüssel, da er ein typisches Design darstellt.

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

Mit der Fremdschlüsseleinschränkung und keinem unterstützenden Index wurde eine einzelne Löschung für SalesOrderHeaderEnlarged ausgegeben Tabelle, was dazu führte, dass eine Zeile aus SalesOrderHeaderEnlarged entfernt wurde und 72 Zeilen aus SalesOrderDetailEnlarged :

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

Die Statistik-E/A- und Timing-Informationen zeigten Folgendes:

Analyse- und Kompilierzeit von SQL Server:

CPU-Zeit =8 ms, verstrichene Zeit =8 ms.

Tabelle 'SalesOrderDetailEnlarged'. Scan-Zähler 1, logische Lesevorgänge 50647, physische Lesevorgänge 8, Read-Ahead-Lesevorgänge 50667, Lob-Logik-Reads 0, Lob-Physische Lesevorgänge 0, Lob-Read-Ahead-Reads 0.
Tabelle 'Arbeitstabelle'. Scan-Anzahl 2, logische Lesevorgänge 7, physische Lesevorgänge 0, Read-Ahead-Lesevorgänge 0, Lob-Logik-Reads 0, Lob-Physische Lesevorgänge 0, Lob-Read-Ahead-Reads 0.
Tabelle 'SalesOrderHeaderEnlarged'. Scan-Anzahl 0, logische Lesevorgänge 15, physische Lesevorgänge 14, Read-Ahead-Lesevorgänge 0, Lob-Logik-Reads 0, Lob-Physical-Reads 0, Lob-Read-Ahead-Reads 0.

SQL Server-Ausführungszeiten:

CPU-Zeit =1045 ms, verstrichene Zeit =1898 ms.

Unter Verwendung von SQL Sentry Plan Explorer zeigt der Ausführungsplan einen Clustered-Index-Scan gegen SalesOrderDetailEnlarged da es keinen Index auf SalesOrderID gibt :


Abfrageplan ohne Index auf dem Fremdschlüssel

Der nicht gruppierte Index zur Unterstützung von SalesOrderDetailEnlarged wurde dann mit der folgenden Anweisung erstellt:

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

Für eine SalesOrderID wurde ein weiterer Löschvorgang ausgeführt die eine Zeile in SalesOrderHeaderEnlarged betraf und 72 Zeilen in SalesOrderDetailEnlarged :

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

Die statistischen E/A- und Timing-Informationen zeigten eine dramatische Verbesserung:

Analyse- und Kompilierzeit von SQL Server:

CPU-Zeit =0 ms, verstrichene Zeit =7 ms.

Tabelle 'SalesOrderDetailEnlarged'. Scan-Zähler 1, logische Lesevorgänge 48, physische Lesevorgänge 13, Read-Ahead-Lesevorgänge 0, Lob-Logik-Reads 0, Lob-Physische Lesevorgänge 0, Lob-Read-Ahead-Reads 0.
Tabelle 'Worktable'. Scan-Anzahl 2, logische Lesevorgänge 7, physische Lesevorgänge 0, Read-Ahead-Lesevorgänge 0, Lob-Logik-Reads 0, Lob-Physische Lesevorgänge 0, Lob-Read-Ahead-Reads 0.
Tabelle 'SalesOrderHeaderEnlarged'. Scan-Anzahl 0, logische Lesevorgänge 15, physische Lesevorgänge 15, Read-Ahead-Lesevorgänge 0, Lob-Logik-Reads 0, Lob-Physical-Reads 0, Lob-Read-Ahead-Reads 0.

SQL Server-Ausführungszeiten:

CPU-Zeit =0 ms, verstrichene Zeit =27 ms.

Und der Abfrageplan zeigte eine Indexsuche des Nonclustered-Index auf SalesOrderID , wie erwartet:


Abfrageplan mit Index auf dem Fremdschlüssel

Die Ausführungszeit der Abfrage sank von 1898 ms auf 27 ms – eine Reduzierung um 98,58 %, und gilt für SalesOrderDetailEnlarged Tabelle von 50647 auf 48 gesunken – eine Verbesserung von 99,9 %. Abgesehen von den Prozentzahlen, betrachten Sie allein die E/A, die durch das Löschen generiert werden. Das SalesOrderDetailEnlarged Tabelle ist in diesem Beispiel nur 500 MB groß, und für ein System mit 256 GB verfügbarem Speicher scheint eine Tabelle, die 500 MB im Puffercache belegt, keine schreckliche Situation zu sein. Aber eine Tabelle mit 5 Millionen Zeilen ist relativ klein; Die meisten großen OLTP-Systeme haben Tabellen mit Hunderten von Millionen Zeilen. Darüber hinaus ist es nicht ungewöhnlich, dass mehrere Fremdschlüsselreferenzen für einen Primärschlüssel vorhanden sind, wobei ein Löschen des Primärschlüssels das Löschen aus mehreren verknüpften Tabellen erfordert. In diesem Fall ist es möglich, längere Dauern für Löschungen zu sehen, was nicht nur ein Leistungsproblem, sondern je nach Isolationsstufe auch ein Blockierungsproblem darstellt.

Schlussfolgerung

Es wird allgemein empfohlen, einen Index zu erstellen, der zu den Fremdschlüsselspalten führt, um nicht nur Verknüpfungen zwischen Primär- und Fremdschlüsseln, sondern auch Aktualisierungen und Löschungen zu unterstützen. Beachten Sie, dass dies eine allgemeine Empfehlung ist, da es Grenzfälle gibt, in denen der zusätzliche Index für den Fremdschlüssel aufgrund einer extrem kleinen Tabellengröße nicht verwendet wurde und die zusätzlichen Indexaktualisierungen die Leistung tatsächlich negativ beeinflussten. Wie alle Schemaänderungen sollten Indexergänzungen nach der Implementierung getestet und überwacht werden. Es ist wichtig sicherzustellen, dass die zusätzlichen Indizes die gewünschten Effekte erzielen und sich nicht negativ auf die Leistung der Lösung auswirken. Beachtenswert ist auch, wie viel zusätzlichen Platz die Indizes für die Fremdschlüssel benötigen. Dies ist vor dem Erstellen der Indizes unbedingt zu berücksichtigen, und wenn sie einen Vorteil bieten, muss dies für die zukünftige Kapazitätsplanung berücksichtigt werden.