Indizieren einer PK-GUID in SQL Server 2012

13

Meine Entwickler haben ihre Anwendung so eingerichtet, dass sie GUIDs als PK für fast alle ihre Tabellen verwendet. Standardmäßig hat SQL Server den Clustered-Index für diese PKs eingerichtet.

Das System ist relativ jung und unsere größten Tabellen haben etwas mehr als eine Million Zeilen. Wir werfen jedoch einen Blick auf unsere Indizierung und möchten in der Lage sein, schnell zu skalieren, wenn dies in naher Zukunft erforderlich sein könnte.

Daher bestand meine erste Neigung darin, den Clustered-Index in das erstellte Feld zu verschieben, das eine große Repräsentation einer DateTime ist. Die einzige Möglichkeit, die CX eindeutig zu machen, besteht darin, die GUID-Spalte in diese CX einzuschließen, die Reihenfolge wird jedoch zuerst erstellt.

Würde dies den Clustering-Schlüssel zu breit machen und die Leistung für Schreibvorgänge steigern? Lesen ist ebenfalls wichtig, aber Schreiben ist an dieser Stelle wahrscheinlich ein größeres Problem.

njkroes
quelle
1
Wie werden die GUIDs generiert? NEWID oder NEWSEQUENTIALID?
Swasheck
6
Clustered Guid und Insert Performance sollten nur in einem Satz stehen, wenn das Wort unmittelbar vor "Performance" minimiert ist
billinkc 31.10.13
2
Nehmen Sie diese Entwickler zum Mittagessen mit und erklären Sie ihnen, dass sie für die schlechte Leistung verantwortlich sind, wenn sie NEWID () erneut als Primärschlüssel verwenden. Sie werden Sie sehr schnell fragen, was zu tun ist, um dies zu verhindern. An diesem Punkt verwenden Sie stattdessen IDENTITY (1,1). (vielleicht eine leichte Vereinfachung, aber 9 mal von 10 wird das funktionieren).
Max Vernon
3
Grund für unseren Guid-Hass ist, dass sie breit sind (16 Bytes) und, wenn sie nicht mit erstellt newsequentialidwerden, zufällig sind. Clustered Keys sind am besten geeignet, wenn sie schmal und ansteigend sind. Eine GUID ist das Gegenteil: fett und zufällig. Stellen Sie sich ein Bücherregal vor, das fast voller Bücher ist. In kommt die OED und wegen der Zufälligkeit der Guids, fügt es in der Mitte des Regals. Um die Ordnung zu halten, muss die rechte Hälfte der Bücher an einen neuen Ort verschoben werden, was eine zeitintensive Aufgabe ist. Das ist es, was die GUID mit Ihrer Datenbank macht und die Leistung tötet.
billinkc
7
Um das Problem der Verwendung eindeutiger Kennungen zu beheben, kehren Sie zum Zeichenbrett zurück und verwenden keine eindeutigen Kennungen . Sie sind nicht schrecklich, wenn das System klein ist, aber wenn Sie mindestens ein paar Millionen Zeilentabellen (oder eine größere Tabelle) haben, werden Sie mit eindeutigen Schlüsselkennungen regelrecht überfordert sein.
Jon Seigel

Antworten:

20

Die Hauptprobleme bei GUIDs, insbesondere bei nicht sequentiellen, sind:

  • Größe des Schlüssels (16 Bytes gegenüber 4 Bytes für einen INT): Dies bedeutet, dass Sie die vierfache Datenmenge in Ihrem Schlüssel zusammen mit dem zusätzlichen Speicherplatz für Indizes speichern, wenn dies Ihr Clustered-Index ist.
  • Indexfragmentierung: Es ist praktisch unmöglich, eine nicht sequentielle GUID-Spalte defragmentiert zu halten, da die Schlüsselwerte völlig zufällig sind.

Was bedeutet das für Ihre Situation? Es kommt auf Ihr Design an. Wenn es in Ihrem System nur um Schreibvorgänge geht und Sie keine Bedenken hinsichtlich des Datenabrufs haben, ist der von Thomas K skizzierte Ansatz korrekt. Sie müssen jedoch berücksichtigen, dass durch die Verfolgung dieser Strategie viele potenzielle Probleme beim Lesen und Speichern dieser Daten entstehen. Wie Jon Seigel hervorhebt , werden Sie auch mehr Platz einnehmen und im Wesentlichen Gedächtnisschwund haben.

Die Hauptfrage bei GUIDs ist, wie notwendig sie sind. Entwickler mögen sie, weil sie für globale Einzigartigkeit sorgen, aber es kommt selten vor, dass diese Art von Einzigartigkeit notwendig ist. Beachten Sie jedoch, dass Sie möglicherweise nicht den richtigen Datentyp für Ihren Schlüssel verwenden, wenn Ihre maximale Anzahl von Werten weniger als 2.147.483.647 beträgt (der maximale Wert einer 4-Byte-Ganzzahl mit Vorzeichen). Selbst bei Verwendung von BIGINT (8 Byte) beträgt Ihr Maximalwert 9.223.372.036.854.775.807. Dies ist in der Regel für alle nicht-globalen Datenbanken (und für viele globale Datenbanken) ausreichend, wenn Sie einen Wert für die automatische Inkrementierung für einen eindeutigen Schlüssel benötigen.

Was schließlich die Verwendung eines Heapspeichers im Vergleich zu einem Clustered-Index betrifft, ist ein Heapspeicher beim reinen Schreiben von Daten am effizientesten, da Sie den Aufwand für Einfügungen minimieren. Heaps in SQL Server sind jedoch für das Abrufen von Daten äußerst ineffizient. Ich habe die Erfahrung gemacht, dass ein Clustered-Index immer wünschenswert ist, wenn Sie die Möglichkeit haben, einen zu deklarieren. Ich habe gesehen, dass das Hinzufügen eines Clustered-Index zu einer Tabelle (4 Milliarden + Datensätze) die Gesamtselektionsleistung um den Faktor 6 verbessert hat.

Zusätzliche Information:

Mike Fal
quelle
13

Die GUID als Schlüssel und Cluster in einem OLTP-System ist nicht fehlerhaft (es sei denn, Sie haben viele Indizes in der Tabelle, die unter der erhöhten Größe des Clusters leiden). Tatsächlich sind sie viel skalierbarer als IDENTITY-Spalten.

Es ist weit verbreitet, dass GUIDs in SQL Server ein großes Problem darstellen - im Großen und Ganzen ist dies ganz einfach falsch. Tatsächlich kann GUID auf Boxen mit mehr als 8 Kernen deutlich skalierbarer sein:

Es tut mir leid, aber Ihre Entwickler haben Recht. Sorgen Sie sich um andere Dinge, bevor Sie sich um GUID sorgen.

Oh, und zum Schluss: Warum möchten Sie überhaupt einen Cluster-Index? Wenn es sich bei Ihrem Unternehmen um ein OLTP-System mit vielen kleinen Indizes handelt, sind Sie mit einem Haufen wahrscheinlich besser dran.

Betrachten wir nun, was die Fragmentierung (die die GUID einführt) für Ihre Lesevorgänge bewirkt. Es gibt drei Hauptprobleme bei der Fragmentierung:

  1. Seitenteilung kostet Festplatten-E / A
  2. Halb volle Seiten sind nicht so speichereffizient wie volle Seiten
  3. Es führt dazu, dass Seiten nicht in der richtigen Reihenfolge gespeichert werden, wodurch sequentielle E / A weniger wahrscheinlich werden

Da es bei Ihrer Frage um Skalierbarkeit geht, die wir als "Hinzufügen von mehr Hardware beschleunigt das System" definieren können, sind dies die geringsten Probleme. Um jeden der Reihe nach anzusprechen

Ad 1) Wenn Sie skalieren möchten, können Sie es sich leisten, I / O zu kaufen. Selbst eine billige Samsung / Intel 512 GB SSD (für ein paar USD / GB) bringt Ihnen weit über 100.000 IOPS. Bei einem 2-Socket-System werden Sie das so schnell nicht verbrauchen. Und wenn Sie darauf stoßen sollten, kaufen Sie noch einen und Sie sind bereit

Ad 2) Wenn Sie in Ihrer Tabelle löschen, haben Sie trotzdem halb volle Seiten. Und selbst wenn Sie dies nicht tun, ist Speicher billig und für alle außer den größten OLTP-Systemen geeignet - die aktuellen Daten sollten dort passen. Der Versuch, mehr Daten in Seiten zu packen, führt zu einer Suboptimierung, wenn Sie nach Skalierung suchen.

Zu 3) Eine Tabelle, die aus häufig seitenweise aufgeteilten, stark fragmentierten Daten besteht, führt zufällige E / A-Vorgänge mit genau der gleichen Geschwindigkeit aus wie eine sequentiell gefüllte Tabelle

In Bezug auf das Beitreten gibt es zwei Haupt-Join-Typen, die Sie wahrscheinlich in einer OLTP-ähnlichen Workload sehen werden: Hash und Schleife. Schauen wir uns diese der Reihe nach an:

Hash-Join: Bei einem Hash-Join wird davon ausgegangen, dass der kleine Tisch gescannt wird und in der Regel der größere gesucht wird. Es ist sehr wahrscheinlich, dass kleine Tabellen im Arbeitsspeicher vorhanden sind, daher ist die E / A hier nicht Ihr Anliegen. Wir haben bereits darauf hingewiesen, dass Suchanfragen im fragmentierten Index dieselben Kosten verursachen wie im nicht fragmentierten Index

Loop Join: Der äußere Tisch wird gesucht. Gleiche Kosten

Möglicherweise werden auch viele schlechte Tabellen gescannt - aber die GUID ist wiederum nicht Ihr Problem, sondern die richtige Indizierung.

Möglicherweise werden jetzt einige legitime Bereichsüberprüfungen durchgeführt (insbesondere beim Verknüpfen von Fremdschlüsseln). In diesem Fall sind die fragmentierten Daten im Vergleich zu den nicht fragmentierten Daten weniger "gepackt". Aber lassen Sie uns überlegen, welche Verknüpfungen Sie wahrscheinlich in gut indizierten 3NF-Daten sehen werden:

  1. Ein Join aus einer Tabelle, der einen Fremdschlüsselverweis auf den Primärschlüssel der Tabelle enthält, auf die er verweist

  2. Umgekehrt

Anzeige 1) In diesem Fall führen Sie eine einzelne Suche zum Primärschlüssel durch - Verbinden von n mit 1. Fragmentierung oder nicht, gleiche Kosten (eine Suche)

Zu 2) In diesem Fall verbinden Sie sich mit demselben Schlüssel, können jedoch mehr als eine Zeile abrufen (Bereichssuche). Der Join ist in diesem Fall 1 bis n. Bei der von Ihnen gesuchten Fremdtabelle wird jedoch nach dem gleichen Schlüssel gesucht, der sich in einem fragmentierten Index mit der gleichen Wahrscheinlichkeit auf derselben Seite wie auf einer nicht fragmentierten Seite befindet.

Betrachten Sie diese Fremdschlüssel für einen Moment. Selbst wenn Sie unsere Primärschlüssel "perfekt" sequentiell gelegt hätten - alles, was auf diesen Schlüssel zeigt, ist immer noch nicht sequentiell.

Natürlich können Sie auf einer virtuellen Maschine in einem SAN in einer Bank laufen, die wenig Geld und viel Prozess hat. Dann geht all dieser Rat verloren. Aber wenn das Ihre Welt ist, ist Skalierbarkeit wahrscheinlich nicht das, wonach Sie suchen - Sie suchen Leistung und hohe Geschwindigkeit / Kosten -, beides verschiedene Dinge.

Thomas Kejser
quelle
1
Kommentare sind nicht für längere Diskussionen gedacht. Diese Unterhaltung wurde in den Chat verschoben .
Paul White 9.
5

Thomas: Einige deiner Punkte sind absolut sinnvoll und ich stimme ihnen allen zu. Wenn Sie sich auf SSDs befinden, ändert sich das Gleichgewicht zwischen dem, wofür Sie optimieren. Random vs Sequential ist nicht die gleiche Diskussion wie Spinning Disk.

Insbesondere stimme ich zu, dass eine reine DB-Sichtweise fürchterlich falsch ist. Ihre Anwendung langsam und unbezwingbar machen zu verbessern einfach die DB-Leistung kann dies zu .

Das große Problem mit der IDENTITÄT (oder der Sequenz oder irgendetwas anderem) das in der DB generiert wurde) ist, dass es schrecklich langsam ist, da es einen zur DB erfordert, um einen Schlüssel zu erstellen. Dies führt automatisch zu einem Engpass in Ihrer DB und erzwingt, dass Anwendungen erforderlich sind einen DB-Aufruf tätigen, um die Verwendung einer Taste zu starten. Das Erstellen einer GUID löst dieses Problem, indem die Anwendung zum Erstellen des Schlüssels verwendet wird. Es ist garantiert (per Definition) global eindeutig, und die Anwendungsebenen können damit den Datensatz weitergeben, bevor ein DB-Roundtrip durchgeführt wird.

Ich verwende jedoch eher eine Alternative zu GUIDs. Meine persönliche Präferenz für einen Datentyp ist ein global eindeutiger BIGINT, der von der App generiert wird. Wie geht man dabei vor? Im einfachsten Beispiel fügen Sie Ihrer App eine kleine, SEHR leichte Funktion hinzu, um eine GUID zu hashen. Angenommen, Ihre Hash-Funktion ist schnell und relativ schnell (siehe CityHash von Google für ein Beispiel: http://google-opensource.blogspot.in/2011/04/introducing-cityhash.html - stellen Sie sicher, dass Sie alle Kompilierungsschritte richtig ausführen. oder die FNV1a-Variante von http://tools.ietf.org/html/draft-eastlake-fnv-03 für einfachen Code) Ihnen den Vorteil, dass sowohl anwendungsgenerierte eindeutige Bezeichner als auch ein 64-Bit-Schlüsselwert, mit dem CPUs besser arbeiten, zur Verfügung stehen .

Es gibt andere Möglichkeiten, BIGINTs zu generieren, und in beiden Algen besteht die Möglichkeit von Hash-Kollisionen - lesen Sie und treffen Sie bewusste Entscheidungen.

Mark Stacey
quelle
2
Ich schlage vor, Sie bearbeiten Ihre Antwort als Antwort auf die Frage des OP und nicht (wie jetzt) ​​als Antwort auf die Antwort von Thomas. Sie können immer noch die Unterschiede zwischen Thomas (, MikeFals) und Ihrem Vorschlag hervorheben.
Ypercubeᵀᴹ
2
Bitte adressieren Sie Ihre Antwort auf die Frage. Wenn Sie dies nicht tun, entfernen wir es für Sie.
JNK
2
Danke für die Kommentare Mark. Wenn Sie Ihre Antwort bearbeiten (was meiner Meinung nach einen sehr guten Kontext bietet), würde ich eines ändern: IDENTITY erfordert keinen zusätzlichen Roundtrip zum Server, wenn Sie mit dem INSERT vorsichtig sind. Sie können SCOPE_IDENTITY () immer in dem Stapel zurückgeben, der das INSERT aufruft.
Thomas Kejser
1
In Bezug auf "es ist schrecklich langsam, da es eine Hin- und Rückfahrt zur DB erfordert, um einen Schlüssel zu erstellen" - Sie können so viele wie Sie brauchen in einer Rückfahrt ergreifen.
AK
In Bezug auf "Sie können in einem Roundtrip so viele abrufen, wie Sie benötigen" - Sie können dies nicht mit IDENTITY-Spalten oder einer anderen Methode tun, bei der Sie DEFAULT grundsätzlich auf Datenbankebene verwenden.
Avi Cherry