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:
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 .
Antworten:
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
UNIQUE
Clustered-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.
quelle
Ich glaube, dass das Beste in der Mitte liegt .
Übersicht über natürliche Schlüssel:
CHAR(4)
undCHAR(20)
) sparen einige zusätzliche Bytes, aber Sie müssen auf ihre Konsistenz achten (diesON UPDATE CASCADE
wird kritisch für die Schlüssel, die möglicherweise geändert werden).Vorteile: 1 und 2.
Watchouts: 3, 4 und 5.
Übersicht über künstliche Identitätsschlüssel:
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 CASCADE
möglicherweise nicht berücksichtigt, da sich die Schlüsselwerte nicht ändern.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.
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 wieINT 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:
quelle
ON UPDATE CASCADE
nicht verwendet, solange die Schlüssel nicht aktualisiert wurden. Wenn dies jedoch der Fall ist, kann dies ein Problem sein, wennON UPDATE NO ACTION
es konfiguriert ist. Ich meine, dass DBMS es nie benutzt, während sich die Schlüsselspaltenwerte nicht geändert haben.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.
Zu seiner logischen Entsprechung, wenn das NaturalTable1Key-Attribut in Tabelle2 durch das Ersatz-IDTable1Key ersetzt wird:
B.
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.
quelle