Bieten natürliche Schlüssel in SQL Server eine höhere oder niedrigere Leistung als ganzzahlige Ersatzschlüssel?

25

Ich bin ein Fan von Ersatzschlüsseln. Es besteht die Gefahr, dass meine Ergebnisse von der Bestätigung abhängen.

Viele Fragen, die ich hier und auf http://stackoverflow.com gesehen habe, verwenden natürliche Schlüssel anstelle von Ersatzschlüsseln, die auf IDENTITY()Werten basieren .

Mein Hintergrund in Computersystemen zeigt, dass das Durchführen von Vergleichsoperationen mit einer Ganzzahl schneller ist als das Vergleichen von Zeichenfolgen.

Dieser Kommentar hat mich dazu gebracht, meine Überzeugungen in Frage zu stellen, und ich dachte, ich würde ein System erstellen, um meine These zu untersuchen, dass Ganzzahlen schneller sind als Zeichenfolgen, die als Schlüssel in SQL Server verwendet werden.

Da es bei kleinen Datenmengen wahrscheinlich nur einen sehr geringen Unterschied gibt, dachte ich sofort an eine Konfiguration mit zwei Tabellen, bei der die Primärtabelle 1.000.000 Zeilen und die Sekundärtabelle 10 Zeilen für jede Zeile in der Primärtabelle für insgesamt 10.000.000 Zeilen enthält der Nebentisch. Bei meinem Test gehe ich davon aus, dass ich zwei Sätze von Tabellen wie diese erstelle, einen mit natürlichen Schlüsseln und einen mit ganzzahligen Schlüsseln, und Timing-Tests mit einer einfachen Abfrage wie der folgenden durchführe:

SELECT *
FROM Table1
    INNER JOIN Table2 ON Table1.Key = Table2.Key;

Folgendes ist der Code, den ich als Testumgebung erstellt habe:

USE Master;
IF (SELECT COUNT(database_id) FROM sys.databases d WHERE d.name = 'NaturalKeyTest') = 1
BEGIN
    ALTER DATABASE NaturalKeyTest SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
    DROP DATABASE NaturalKeyTest;
END
GO
CREATE DATABASE NaturalKeyTest 
    ON (NAME = 'NaturalKeyTest', FILENAME = 
        'C:\SQLServer\Data\NaturalKeyTest.mdf', SIZE=8GB, FILEGROWTH=1GB) 
    LOG ON (NAME='NaturalKeyTestLog', FILENAME = 
        'C:\SQLServer\Logs\NaturalKeyTest.mdf', SIZE=256MB, FILEGROWTH=128MB);
GO
ALTER DATABASE NaturalKeyTest SET RECOVERY SIMPLE;
GO
USE NaturalKeyTest;
GO
CREATE VIEW GetRand
AS 
    SELECT RAND() AS RandomNumber;
GO
CREATE FUNCTION RandomString
(
    @StringLength INT
)
RETURNS NVARCHAR(max)
AS
BEGIN
    DECLARE @cnt INT = 0
    DECLARE @str NVARCHAR(MAX) = '';
    DECLARE @RandomNum FLOAT = 0;
    WHILE @cnt < @StringLength
    BEGIN
        SELECT @RandomNum = RandomNumber
        FROM GetRand;
        SET @str = @str + CAST(CHAR((@RandomNum * 64.) + 32) AS NVARCHAR(MAX)); 
        SET @cnt = @cnt + 1;
    END
    RETURN @str;
END;
GO
CREATE TABLE NaturalTable1
(
    NaturalTable1Key NVARCHAR(255) NOT NULL 
        CONSTRAINT PK_NaturalTable1 PRIMARY KEY CLUSTERED 
    , Table1TestData NVARCHAR(255) NOT NULL 
);
CREATE TABLE NaturalTable2
(
    NaturalTable2Key NVARCHAR(255) NOT NULL 
        CONSTRAINT PK_NaturalTable2 PRIMARY KEY CLUSTERED 
    , NaturalTable1Key NVARCHAR(255) NOT NULL 
        CONSTRAINT FK_NaturalTable2_NaturalTable1Key 
        FOREIGN KEY REFERENCES dbo.NaturalTable1 (NaturalTable1Key) 
        ON DELETE CASCADE ON UPDATE CASCADE
    , Table2TestData NVARCHAR(255) NOT NULL  
);
GO

/* insert 1,000,000 rows into NaturalTable1 */
INSERT INTO NaturalTable1 (NaturalTable1Key, Table1TestData) 
    VALUES (dbo.RandomString(25), dbo.RandomString(100));
GO 1000000 

/* insert 10,000,000 rows into NaturalTable2 */
INSERT INTO NaturalTable2 (NaturalTable2Key, NaturalTable1Key, Table2TestData)
SELECT dbo.RandomString(25), T1.NaturalTable1Key, dbo.RandomString(100)
FROM NaturalTable1 T1
GO 10 

CREATE TABLE IDTable1
(
    IDTable1Key INT NOT NULL CONSTRAINT PK_IDTable1 
    PRIMARY KEY CLUSTERED IDENTITY(1,1)
    , Table1TestData NVARCHAR(255) NOT NULL 
    CONSTRAINT DF_IDTable1_TestData DEFAULT dbo.RandomString(100)
);
CREATE TABLE IDTable2
(
    IDTable2Key INT NOT NULL CONSTRAINT PK_IDTable2 
        PRIMARY KEY CLUSTERED IDENTITY(1,1)
    , IDTable1Key INT NOT NULL 
        CONSTRAINT FK_IDTable2_IDTable1Key FOREIGN KEY 
        REFERENCES dbo.IDTable1 (IDTable1Key) 
        ON DELETE CASCADE ON UPDATE CASCADE
    , Table2TestData NVARCHAR(255) NOT NULL 
        CONSTRAINT DF_IDTable2_TestData DEFAULT dbo.RandomString(100)
);
GO
INSERT INTO IDTable1 DEFAULT VALUES;
GO 1000000
INSERT INTO IDTable2 (IDTable1Key)
SELECT T1.IDTable1Key
FROM IDTable1 T1
GO 10

Der obige Code erstellt eine Datenbank und 4 Tabellen und füllt die Tabellen mit Daten, die zum Testen bereit sind. Der Testcode, den ich ausgeführt habe, ist:

USE NaturalKeyTest;
GO
DECLARE @loops INT = 0;
DECLARE @MaxLoops INT = 10;
DECLARE @Results TABLE (
    FinishedAt DATETIME DEFAULT (GETDATE())
    , KeyType NVARCHAR(255)
    , ElapsedTime FLOAT
);
WHILE @loops < @MaxLoops
BEGIN
    DBCC FREEPROCCACHE;
    DBCC FREESESSIONCACHE;
    DBCC FREESYSTEMCACHE ('ALL');
    DBCC DROPCLEANBUFFERS;
    WAITFOR DELAY '00:00:05';
    DECLARE @start DATETIME = GETDATE();
    DECLARE @end DATETIME;
    DECLARE @count INT;
    SELECT @count = COUNT(*) 
    FROM dbo.NaturalTable1 T1
        INNER JOIN dbo.NaturalTable2 T2 ON T1.NaturalTable1Key = T2.NaturalTable1Key;
    SET @end = GETDATE();
    INSERT INTO @Results (KeyType, ElapsedTime)
    SELECT 'Natural PK' AS KeyType, CAST((@end - @start) AS FLOAT) AS ElapsedTime;

    DBCC FREEPROCCACHE;
    DBCC FREESESSIONCACHE;
    DBCC FREESYSTEMCACHE ('ALL');
    DBCC DROPCLEANBUFFERS;
    WAITFOR DELAY '00:00:05';
    SET @start = GETDATE();
    SELECT @count = COUNT(*) 
    FROM dbo.IDTable1 T1
        INNER JOIN dbo.IDTable2 T2 ON T1.IDTable1Key = T2.IDTable1Key;
    SET @end = GETDATE();
    INSERT INTO @Results (KeyType, ElapsedTime)
    SELECT 'IDENTITY() PK' AS KeyType, CAST((@end - @start) AS FLOAT) AS ElapsedTime;

    SET @loops = @loops + 1;
END
SELECT KeyType, FORMAT(CAST(AVG(ElapsedTime) AS DATETIME), 'HH:mm:ss.fff') AS AvgTime 
FROM @Results
GROUP BY KeyType;

Das sind die Ergebnisse:

Bildbeschreibung hier eingeben

Mache ich hier etwas falsch oder sind INT-Tasten dreimal schneller als natürliche 25-stellige Tasten?

Beachten Sie , ich habe eine Folgefrage geschrieben hier .

Max Vernon
quelle
1
Nun, das INT ist 4 Bytes und das effektive NVARCHAR (25) ist ungefähr 14-mal länger (einschließlich Systemdaten wie Länge). Allein in Bezug auf den Index glaube ich, dass Sie einen wesentlich breiteren und tieferen PK-Index und damit mehr I haben würden / O wird benötigt, was sich auf die Verarbeitungszeit auswirkt. Allerdings wäre eine natürliche Ganzzahl (vielleicht sogar ein Häkchen) so ziemlich dieselbe INT, die wir für eine Ersatz-Identitätsspalte verwenden möchten. Also, der "natürliche Schlüssel" ist vielleicht ein INT, BIGINT, CHAR, NVARCHAR und das ist alles wichtig.
RLF
7
Ich denke, der Leistungsgewinn bei MikeSherrill "Catcall" war, dass Sie den Join gegen die "Lookup" -Tabelle nicht wirklich benötigen, wenn Sie einen natürlichen Schlüssel verwenden. Vergleichen Sie eine Abfrage, um den Suchwert mit einem Join abzurufen, mit einer Abfrage, bei der der Wert bereits in der Haupttabelle gespeichert ist. Abhängig von der natürlichen Schlüssellänge und der Anzahl der Zeilen in der Nachschlagetabelle erhalten Sie möglicherweise einen anderen "Gewinner".
Mikael Eriksson
3
Was @MikaelEriksson gesagt hat, plus die Fälle, in denen Sie einen Join zwischen mehr als 2 Tabellen haben (sagen wir 4), bei denen Sie mit den Ersatztabellen die Tabellen A bis D bis B und C verbinden müssen, während Sie mit natürlichen Schlüsseln A bis D direkt verbinden können
ypercubeᵀᴹ

Antworten:

18

Im Allgemeinen verwendet SQL Server B + Trees für Indizes. Der Aufwand für eine Indexsuche steht in direktem Zusammenhang mit der Länge des Schlüssels in diesem Speicherformat. Daher übertrifft ein Ersatzschlüssel normalerweise einen natürlichen Schlüssel bei der Indexsuche.

SQL Server gruppiert standardmäßig eine Tabelle auf dem Primärschlüssel. Der gruppierte Indexschlüssel wird zum Identifizieren von Zeilen verwendet, sodass er jedem anderen Index als eingeschlossene Spalte hinzugefügt wird. Je breiter dieser Schlüssel, desto größer ist jeder Sekundärindex.

Schlimmer noch, wenn die Sekundärindizes nicht explizit als UNIQUEClustered-Index-Schlüssel definiert sind, werden sie automatisch Teil des Schlüssels für jeden dieser Indizes . Dies gilt normalerweise für die meisten Indizes, da Indizes normalerweise nur dann als eindeutig deklariert werden, wenn die Eindeutigkeit erzwungen werden soll.

Wenn also die Frage lautet, ob es sich um einen gruppierten natürlichen Index oder einen Ersatzindex handelt, gewinnt der Ersatz fast immer.

Auf der anderen Seite fügen Sie diese Ersatzspalte der Tabelle hinzu, wodurch die Tabelle an sich größer wird. Dadurch werden Clustered-Index-Scans teurer. Wenn Sie also nur über sehr wenige Sekundärindizes verfügen und Ihre Workload häufig alle (oder die meisten) Zeilen durchsuchen muss, ist es möglicherweise besser, wenn ein natürlicher Schlüssel diese wenigen zusätzlichen Bytes speichert.

Schließlich erleichtern natürliche Schlüssel häufig das Verständnis des Datenmodells. Bei größerem Speicherplatz führen natürliche Primärschlüssel zu natürlichen Fremdschlüsseln, was wiederum die lokale Informationsdichte erhöht.

Wie so oft in der Datenbankwelt lautet die eigentliche Antwort "es kommt darauf an". Und - testen Sie immer in Ihrer eigenen Umgebung mit realistischen Daten.

Sebastian Meine
quelle
10

Ich glaube, dass das Beste in der Mitte liegt .

Übersicht über natürliche Schlüssel:

  1. Sie machen das Datenmodell offensichtlicher, weil sie aus dem Themenbereich stammen und nicht aus dem Kopf eines anderen.
  2. Einfache Schlüssel (eine Spalte zwischen CHAR(4)und CHAR(20)) sparen einige zusätzliche Bytes, aber Sie müssen auf ihre Konsistenz achten (dies ON UPDATE CASCADEwird kritisch für die Schlüssel, die möglicherweise geändert werden).
  3. In vielen Fällen, in denen natürliche Schlüssel komplex sind, besteht sie aus zwei oder mehr Spalten. Wenn ein solcher Schlüssel als Vorschlüssel zu einer anderen Entität migriert werden kann, erhöht sich der Datenaufwand (Indizes und Datenspalten können groß werden) und die Leistung nimmt ab.
  4. Wenn der Schlüssel eine große Zeichenfolge ist, geht er wahrscheinlich immer in einen Ganzzahlschlüssel über, da eine einfache Suchbedingung zu einem Byte-Array-Vergleich in einem Datenbankmodul führt, der in den meisten Fällen langsamer ist als ein Ganzzahlvergleich.
  5. Wenn der Schlüssel eine mehrsprachige Zeichenfolge ist, müssen Sie auch die Kollatierungen beobachten.

Vorteile: 1 und 2.

Watchouts: 3, 4 und 5.


Übersicht über künstliche Identitätsschlüssel:

  1. Sie müssen sich (in den meisten Fällen) nicht um deren Erstellung und Verarbeitung kümmern, da diese Funktion vom Datenbankmodul verwaltet wird. Sie sind standardmäßig eindeutig und nehmen nicht viel Platz ein. Benutzerdefinierte Vorgänge wie werden ON UPDATE CASCADEmöglicherweise nicht berücksichtigt, da sich die Schlüsselwerte nicht ändern.

  2. Sie sind (oft) die besten Kandidaten für die Migration als Fremdschlüssel, weil:

    2.1. besteht aus einer Spalte;

    2.2. Verwenden eines einfachen Typs, der ein geringes Gewicht hat und für Vergleichsoperationen schnell ist.

  3. Bei einer Zuordnungsentität, deren Schlüssel nirgendwo migriert werden, kann dies zu einem reinen Datenaufwand führen, da die Nützlichkeit verloren geht. Komplexer natürlicher Primärschlüssel (wenn dort keine Zeichenfolgenspalten vorhanden sind) ist nützlicher.

Vorteile: 1 und 2.

Watchouts: 3.


FAZIT:

Künstliche Schlüssel sind wartungsfreundlicher, zuverlässiger und schneller, da sie für diese Funktionen entwickelt wurden. Aber in einigen Fällen sind nicht erforderlich. Beispielsweise CHAR(4)verhält sich ein einzelner Spaltenkandidat in den meisten Fällen wie INT IDENTITY. Es gibt also auch hier eine andere Frage: Wartbarkeit + Stabilität oder Offensichtlichkeit ?

Frage "Soll ich einen künstlichen Schlüssel injizieren oder nicht?" hängt immer von der natürlichen Schlüsselstruktur ab:

  • Wenn es eine große Zeichenfolge enthält, ist es langsamer und erhöht den Datenaufwand, wenn es als fremd auf eine andere Entität migriert wird.
  • Wenn es aus mehreren Spalten besteht, ist es langsamer und erhöht den Datenaufwand, wenn es als fremd auf eine andere Entität migriert wird.
BlitZ
quelle
5
"Benutzerdefinierte Operationen wie ON UPDATE CASCADE werden möglicherweise nicht ausgeführt, da sich die Schlüsselwerte nicht ändern." Die Wirkung von Ersatzschlüsseln besteht darin, dass jeder Fremdschlüsselverweis dem Äquivalent von "ON UPDATE CASCADE" entspricht. Der Schlüssel ändert sich nicht, aber der Wert, den er darstellt, ändert sich .
Mike Sherrill 'Cat Recall'
@ MikeSherrill'Catcall 'Ja, natürlich. Wird jedoch ON UPDATE CASCADEnicht verwendet, solange die Schlüssel nicht aktualisiert wurden. Wenn dies jedoch der Fall ist, kann dies ein Problem sein, wenn ON UPDATE NO ACTIONes konfiguriert ist. Ich meine, dass DBMS es nie benutzt, während sich die Schlüsselspaltenwerte nicht geändert haben.
BlitZ
4

Ein Schlüssel ist ein logisches Merkmal einer Datenbank, während die Leistung immer durch die physische Implementierung im Speicher und durch physische Vorgänge bestimmt wird, die für diese Implementierung ausgeführt werden. Es ist daher ein Fehler, den Schlüsseln Leistungsmerkmale zuzuweisen.

In diesem speziellen Beispiel werden jedoch zwei mögliche Implementierungen von Tabellen und Abfragen miteinander verglichen. Das Beispiel beantwortet nicht die Frage, die hier im Titel gestellt wird. Der Vergleich besteht aus Joins mit zwei verschiedenen Datentypen (Ganzzahl und Zeichen) und nur einem Indextyp (B-Baum). Ein "offensichtlicher" Punkt ist, dass bei Verwendung eines Hash-Index oder eines anderen Indextyps möglicherweise kein messbarer Leistungsunterschied zwischen den beiden Implementierungen besteht. Es gibt jedoch grundlegendere Probleme mit dem Beispiel.

Zwei Abfragen werden auf Leistung verglichen, aber die beiden Abfragen sind nicht logisch äquivalent, da sie unterschiedliche Ergebnisse liefern! Ein realistischerer Test würde zwei Abfragen vergleichen, die dieselben Ergebnisse liefern, aber unterschiedliche Implementierungen verwenden.

Der wesentliche Punkt bei einem Ersatzschlüssel ist, dass es sich um ein zusätzliches Attribut in einer Tabelle handelt, in der die Tabelle auch "aussagekräftige" Schlüsselattribute enthält, die in der Geschäftsdomäne verwendet werden. Es sind die Nicht-Ersatzattribute, die für Abfrageergebnisse von Interesse sind, um nützlich zu sein. Eine realistische Test würde daher Tabellen vergleicht nur natürlichen Schlüssel mit einer alternativen Implementierung unter Verwendung von sowohl natürliche als mit und Ersatzschlüssel in der gleichen Tabelle. Ersatzschlüssel erfordern in der Regel zusätzlichen Speicher und Index und per Definition zusätzliche Eindeutigkeitsbeschränkungen. Ersatzzeichen erfordern eine zusätzliche Verarbeitung, um die externen natürlichen Schlüsselwerte auf ihre Ersatzzeichen abzubilden und umgekehrt.

Vergleichen Sie nun diese mögliche Abfrage:

EIN.

SELECT t2.NaturalTable2Key, t2.NaturalTable1Key
FROM Table2 t2;

Zu seiner logischen Entsprechung, wenn das NaturalTable1Key-Attribut in Tabelle2 durch das Ersatz-IDTable1Key ersetzt wird:

B.

SELECT t2.NaturalTable2Key, t1.NaturalTable1Key
FROM Table2 t2
INNER JOIN Table1 t1
ON t1.IDTable1Key = t2.IDTable1Key;

Abfrage B erfordert eine Verknüpfung. Abfrage A nicht. Dies ist eine bekannte Situation in Datenbanken, in denen (zu) viele Ersatzdaten verwendet werden. Abfragen werden unnötig komplex und viel schwieriger zu optimieren. Es wird schwieriger, Geschäftslogik (insbesondere Datenintegritätsbeschränkungen) zu implementieren, zu testen und zu überprüfen.

nvogel
quelle