In LinkedIn wurde eine interessante Frage diskutiert, die in ihrem Ursprung zwar nicht unmittelbar mit dem Titel dieses Artikels zusammenhängt, mich aber auf Grund der abgebildeten Datenstruktur zu der Fragestellung dieses Artikels anregte. Dieser Artikel beschreibt, warum ein Clustered Index nach Möglichkeit immer einen Datentypen mit fester Länge besitzen sollte.
Der Clustered Key ist das Ordnungskriterium für einen Clustered Index. Der Schlüssel kann aus einem oder mehreren Attributen einer Tabelle bestehen. Bei der Definition des Index können bis zu 16 Spalten in einem einzigen zusammengesetzten Indexschlüssel kombiniert werden. Die maximal zulässige Größe der Werte des zusammengesetzten Index beträgt 900 Byte. Die Datentypen ntext, text, varchar(max), nvarchar(max), varbinary(max), xml oder image dürfen nicht als Schlüsselspalten für einen Index angegeben werden.
Struktur eines Datensatzes
Um zu verstehen, welche Nachteile ein Index mit variabler Datenlänge besitzt, muss man wissen, wie ein Datensatz in Microsoft SQL Server “strukturiert” ist. Der nachfolgende Code erstellt eine Tabelle mit einem Clustered Index auf “Customer_No”.
Die Tabelle wird anschließend mit 1.000 Testdatensätzen für weitere Analysen der Datenstruktur befüllt. Um die Struktur eines Datensatzes zu analysieren, werden Informationen zur gespeicherten Position benötigt.
SELECT sys.fn_PhysLocFormatter(%%physloc%%), * FROM dbo.Customer;
Auf Datenseite 156 werden sieben Datensätze gespeichert. Um die Datenseite – und damit die Struktur der Datensätze – einzusehen, wird mit DBCC PAGE der Inhalt der Datenseite untersucht. Wichtig bei der Verwendung von DBCC PAGE ist die vorherige Aktivierung des Traceflags 3604, um die Ausgabe vom Fehlerprotokoll in SQL Server Management Studio umzuleiten.
In der Abbildung wird der erste Datensatz der Datenseite angezeigt. Der blau gerahmte Bereich repräsentiert die tatsächlich auf der Datenseite gespeicherten Informationen. Es ist erkennbar, dass der Datensatz eine Länge von 1.017 Bytes besitzt; berechnet man jedoch die Länge des Datensatzes basierend auf der Tabellendefinition, wird man bemerken, dass die Länge eigentlich nur 1.010 Bytes sein müssten. Die zusätzlichen Bytes werden für die Definition der Metadaten eines Datensatzes benötigt. Die Anzahl der zusätzlichen Informationen (Overhead) ist von den Datentypen einer Tabelle abhängig.
Datensatz-Metadaten
Die ersten zwei Bytes (0x10 00) speichern Informationen über die Eigenschaften eines Datensatzes. Da es sich um einen “gewöhnlichen” Datensatz handelt, spricht man von einem “PRIMARY RECORD”. Die nächsten zwei Bytes (0xf6 03) bestimmen das Offset zur Information über die Anzahl der Spalten in einem Datensatz. Zwischen dem Header (4 Bytes) und dem Offset befinden sich die Daten aller Attribute mit festen Datentypen.
Fixed Length Bereich
Der Hexadezimalwert 0xf6 03 ergibt umgerechnet den Dezimalwert 1.014. 4 Bytes für den Header + 10 Bytes für das Attribut [Customer_No] + 1.000 Bytes für das Attribut [Customer_Name] ergeben den Wert von 1.014 Bytes.
Die obige Abbildung zeigt den Datenbereich ab dem Byte 1.014 (rot).
Anzahl der Attribute (Spalten)
Die nächsten zwei Bytes (0x02 00) geben Aufschluss über die Anzahl der Spalten.
NULL-Bitmap
Nach den Informationen über die Struktur des Datensatzes folgen die Informationen über über Attribute, die NULL Werte enthalten dürfen (NULL Bitmap). Hierbei gilt, dass pro 8 Attribute einer Tabelle jeweils 1 Byte belegt wird. Erlaubt eine Spalte einen NULL Wert so wird das entsprechende Bit gesetzt. Da für das obige Beispiel kein Attribut einen NULL Wert zulässt, ist dieser Wert 0 (0x00).
Variable Length Bereich
Das Ende der Datensatzstruktur ist der Datenbereich für Attribute mit variabler Länge. Spalten mit variabler Länge sind mit einem deutlich höheren Aufwand von Microsoft SQL Server zu verwalten; ein Datensatz wird – bedingt durch die zusätzlichen Informationen – deutlich größer (siehe nächstes Beispiel). Für JEDES Attribut mit einem variablen Datentypen werden zwei Bytes für das Offset gespeichert, an dem der Wert für das Attribut endet. Erst zum Schluss werden die Daten selbst gespeichert. Das obige Beispiel besitzt KEINE Attribute mit variablen Datentypen; Informationen über variable Datentypen müssen nicht gespeichert werden.
Clustered Index mit variablem Datentyp
Um die Datenstruktur eines Clustered Index mit variabler Datenmenge zu untersuchen, werden die Daten der bestehenden Tabelle [dbo].[Customer] in eine neue Tabelle übertragen.
IF OBJECT_ID('dbo.Customer_Variable', 'U') ISNOTNULL
Mit der folgenden Abfrage werden aus jeder Tabelle der jeweils erste Datensatz ausgegeben. Zusätzlich werden Informationen zur Datenseite ausgegeben, auf der die Datensätze gespeichert wurden.
SELECTTOP 1 'Fixed length'AS ClusterType, sys.fn_PhysLocFormatter(%%physloc%%), * FROM dbo.Customer
UNIONALL
SELECTTOP 1 'Variable length'AS ClusterType, sys.fn_PhysLocFormatter(%%physloc%%), * FROM dbo.Customer_Variable;
Der “identische” Datensatz ist um 4 Bytes “gewachsen”. Statt 1.017 Bytes benötigt der Datensatz 4 weitere Bytes. Diese vier weiteren Bytes resultieren aus der geänderten Datensatzstruktur. Die nächste Abbildung zeigt den betroffenen Ausschnitt vergrößert an.
Die Abbildung zeigt das Ende der Datensatzstruktur. Die ersten zwei markierten Bytes repräsentieren die Anzahl der Spalten des Datensatzes. Das nächste Byte repräsentiert das NULL Bitmap. Der anschließende Bereich (2 Bytes) ist eine der Ursachen für das Wachstum der Datensatzgröße.
Der Hexadezimalwert 0x01 00 bestimmt die Anzahl der Spalten mit variablen Datensätzen. Die nächsten zwei Bytes (0xfd 03) bestimmen das Offset für das Ende des Datenbereichs für die erste Spalte mit variabler Datenlänge. Da keine weiteren Spalten vorhanden sind, beginnen unmittelbar im Anschluss die Daten des Clustered Keys.
Bedingt durch die Struktur eines Datensatzes wächst die Datenlänge um maximal vier weitere Bytes. Hat die Tabelle bereits Attribute mit variabler Datenlänge gehabt, sind es lediglich zwei weitere Bytes für das Offset.
Neben der Vergrößerung des Datensatzes haben variable Datentypen noch andere Nachteile als Clustered Index:
Der Overhead von maximal 4 Bytes ist – auf den einzelnen Datensatz reduziert – unerheblich. Die zusätzlichen 4 Bytes können aber dazu führen, dass weniger Datensätze auf einer Datenseite gespeichert werden können.
CPU Zeit für die Evaluierung des Datenwertes, da erst das Offset für die variablen Bereiche und anschließend das Offset für den Datenbereich errechnet werden muss. Es ist sicherlich kein nennenswerter Zeitverlust aber – wie in vielen Dingen – ist die Menge der Datensätze für den Overhead entscheidend.
Das Schlüsselattribut eines Clustered Index wird in jedem non clustered Index gespeichert. Somit verteilt sich der Overhead auch auf non clustered Indexe.
Herzlichen Dank fürs Lesen!
Hinweis
Wer Kalen Delaney live erleben möchte, dem sei die SQL Konferenz in Darmstadt vom 03.02.2015 – 05.02.2015 sehr ans Herz gelegt. Die Agenda der SQL Konferenz liest sich wie das Who ist Who der nationalen und internationalen SQL Community.
Dankenswerter Weise wurde ich durch einen Kommentar des von mir sehr geschätzten Kollegen Torsten Strauss darauf aufmerksam gemacht, dass der Artikel einige Voraussetzungen “unterschlagen” hat und somit zu Fehlinterpretationen führen kann.
Die Grundvoraussetzung zur Idee zu diesem Artikel ist, dass immer mit “gleichen” Karten gespielt wird – soll heißen, dass unabhängig vom Datentypen IMMER der gleiche Datenbestand verwendet wird. Ein fairer Vergleich wäre sonst nicht möglich! Spreche ich im ersten Beispiel vom Datentypen CHAR mit einer Länge von 10 Bytes, setzt das Beispiel mit dem Datentypen VARCHAR und einer maximalen Zeichenlänge von 10 Bytes voraus, dass ebenfalls 10 Zeichen pro Datensatz im Clustered Index gespeichert werden!
Liebe SQL Gemeinde; es ist mal wieder soweit. Auch 2015 wird die im letzten Jahr ins Leben gerufene Konferenz “SQL Server Konferenz” wieder in Darmstadt ihre Tore öffnen. Ähnlich wie in meinem Artikel über den PASS Summit 2014 möchte ich mit diesem Artikel die Vor- und Nachteile eines Besuchs der SQL Server Konferenz aus meiner persönlichen Sicht beschreiben.
Was ist die SQL Server Konferenz?
Die SQL Server Konferenz fand im Jahr 2014 zum ersten Mal im darmstadtium mitten im Herzen von Darmstadt statt. Die SQL Server Konferenz 2014 war ein so großer Erfolg, dass sich die PASS Deutschland e. V. in Zusammenarbeit mit Microsoft Deutschland als Organisatoren erneut für die Durchführung der SQL Konferenz für das Jahr 2015 entschlossen hat.
Deutsche und internationale Experten präsentieren, diskutieren und erläutern Themen wie SQL Server vNext, Self-Service BI, Hochverfügbarkeit, Big Data, Continuous Integration, In-Memory Computing, Data Stewardship, Data Vault und Modern Data Warehouse, Machine Learning, das Internet der Dinge, Enterprise Information Management und viele weitere spannende Themen. In vier parallelen Tracks werden alle Neuheiten rund um den Microsoft SQL Server und die Microsoft Datenplattform von erfahrenen und bekannten Sprechern aus dem In- und Ausland angeboten.
Die Location
Die SQL Server Konferenz findet auf mehr als 18.000 qm im darmstadtium im Herzen von Darmstadt statt und ist sowohl mit öffentlichen als auch individuellen Verkehrsmitteln schnell und bequem zu erreichen. Ressourcenschonende Nachhaltigkeit und eine in Deutschland einmalige IT Infrastruktur sind neben der fantastischen Lage mitten im Rhein-Main-Gebiet Merkmale, die für das darmstadtium zählen.
Die Sprecher
So wichtig für jede Konferenz die Besucher, deren Interessen und die Themen sind; es müssen Leute da sein, die den kompliziertesten Sachverhalt in einfache und verständliche Sprache übersetzen – die Sprecher. Jede Konferenz lebt von ihren Sprechern. Ich kenne zwar nicht alle Sprecher persönlich ABER … wenn man sich mal die Liste der Sprecher hier anschaut, wird man schnell erkennen, dass man es mit hochgradig spezialisierten Experten zu tun hat, die ihr Metier in der Tiefe verstehen. Neben nationalen Sprechern sind dieses Mal sehr viele internationale Sprecher auf der SQL Server Konferenz vertreten, die auf der ganzen Welt für ihre Kenntnisse und ihr Engagement für die Community bekannt sind. Bevor ich auf einige Sprecher im Detail eingehe, möchte ich ausdrücklich darauf hinweisen, dass die Sprecher der Hauptkonferenz sich alle KOSTENLOS engagieren. Anfahrt, Hotel, Vorbereitungen – es gibt keinen Cent für die Tätigkeiten. Die Sprecher machen es aus Passion! Ich möchte hier nur einige wenige hervorheben, die ich sehr interessant finde (das soll aber keine Wertung darstellen!).
Kalen Delaney
Kalen Delaney dürfte allen DBAs durch ihre “Bibel” SQL Server Internals bekannt sein. Ich hatte das Vergüngen, Kalen im Jahre 2013 auf der SQLRally in Stockholm persönlich kennen zu lernen. Ich kann jedem nur empfehlen, ruhig mal ein paar Worte mit ihr zu wechseln. Dass Ihr – neben Paul Randal – die Storage Engine bestens bekannt ist, dürfte jeder Leser ihrer Bücher wissen. Ich habe mich mit ihren Büchern auf meine MCM Prüfungen vorbereitet.
Thomas LaRock
Thomas war bereits letztes Jahr auf der SQL Server Konferenz und hat wohl eindrucksvoll bewiesen, dass ihm Jägermeister nicht unbekannt ist. Scherz beiseite; Thomas ist der Präsident der PASS und ebenfalls einer von ca. 200 MCM weltweit. Ich habe ihn persönlich auf der SQL Server Konferenz im letzten Jahr kennengelernt; wie Kalen Delaney ein sehr erfahrener SQL Server Experte, der es mag und versteht, sein Wissen über Microsoft SQL Server in die Community zu tragen.
Steve Jones
Wer kennt nicht DIE Anlaufstelle für SQL Server Themen im Internet: http://www.sqlservercentral.com ist für jeden DBA / BI Experten immer wieder eine Anlaufstelle, wenn es darum geht, Probleme zu lösen. Es gibt eigentlich nichts, was dort nicht zu finden ist. Steve Jones ist der Betreiber der Webseite, die mittlerweile zu Red Gate gehört. Steve habe ich bei einem Vortrag in Seattle auf dem PASS Summit 2014 gehört und gesehen. Fantastisch!
Régis Baccaro
Régis Baccaro kenne ich von vielen SQL Saturday Events in Europa, die ich in 2014 als Sprecher besucht habe. Es gibt eigentlich keinen SQL Saturday, auf dem Régis nicht schon eindrucksvoll sein Wissen in Bezug auf BI-Themen von Microsoft SQL Server unter Beweis gestellt hat. Ein Franzose, der in Dänemark lebt und mit mir eine Passion teilt – gutes Essen.
Boris Hristov
Boris Hristov ist für mich das Paradebeispiel für Engagement für die Community. Boris kommt aus Bulgarien. Über seine Hangouts bin ich mit Boris zum ersten Mal in Kontakt gekommen; seitdem verbindet uns – wie übrigens alle Mitglieder von #sqlcommunity – eine Freundschaft, die – wie bei Régis auch, immer wieder “refreshed” wird, wenn wir uns auf den zahlreichen SQL Saturday Veranstaltungen treffen. Boris Hristov scheut keine Kosten, um sein exzellentes Wissen in Europa in die Community zu tragen.
Oliver Engels / Tillmann Eitelberg
Jedes PASS Mitglied in Deutschland kennt natürlich unseren Vorstand. Mich verbindet mit Oliver und Tillmann eine Freundschaft, die ich sehr zu schätzen weis. Den beiden macht so leicht niemand was in Sachen BI und SSIS vor. Obwohl ich nur einmal in einer Session der Beiden gesessen bin, war ich dennoch begeistert von den Möglichkeiten von Power BI – und das. obwohl ich kein Freund von “Clicky Bunti” (Insider) bin.
Marcel Franke
Marcel Franke habe ich das erste Mal in Stockholm auf der SQL Rally kennen gelernt. Echt beeindruckend, was Marcel in Sachen BI, “Big Data” und “Data Analytics” drauf hat – sicherlich ein sehenswerter Vortrag für alle Besucher, die viel mit diesen Themen in Berührung kommen.
Patrick Heyde
Seit dem SQL Server Bootcamp im Dezember 2014 ist Patrick für mich nur noch “Mr. Azure”. Patrick kennt als Mitarbeiter von Microsoft die Azure Welt, wie kein Zweiter. Seine Vorträge gefallen mir vor allen Dingen deswegen, da Patrick es in seiner ruhigen Art sehr gut versteht, die Komplexität von Azure für alle Zuhörer verständlich zu vermitteln.
Niko Neugebauer
Niko Neugebauer ist für mich DER Experte für Themen rund um Columnstore Indexes. Es gibt eigentlich nichts, was Niko zu diesem Thema nicht kennt. Sein Wissen ist phänomenal und mehr als 40 Blogeinträge auf seinem Blog beschäftigen sich ausschließlich mit dem Thema Columnstore Indexe; zu lesen unter http://www.nikoport.com. Ein Besuch einer Session von Niko kann ich nur empfehlen.
Natürlich sind noch viele viele andere Sprecher “am Start”, die sich in Europa für die Community engagieren. Sei es Allen Mitchel (Einer der vielen Organisatoren von http://www.sqlbits.com), Milos Radivojevic, Cathrine Wilhelmsen, …). Ein Besuch der SQL Server Konferenz ist sicherlich lohnenswert.
Die Themen
Bei der Auswahl der Themen haben sich die Organisatoren es sich sicherlich nicht leicht gemacht. Ich weiß nicht, wie viele Sessions eingereicht wurden – aber ich bin davon überzeugt, dass es weitaus mehr waren, als in den drei Tagen vorgetragen werden können. Ich bin sicher, dass unter den vielen Themen zu
Administration (DBA),
Business Intelligence (BI),
Applikationsentwicklung (DEV)
Big Data & Information Management (BD)
für jedes Interessensgebiet was dabei ist. Ich bin gespannt auf die Neuigkeiten im Bereich BI und freue mich auf die interessanten Sessions.
Warum zur SQL Server Konferenz?
Die SQL Server Konferenz ist eine Veranstaltung in Deutschland, die es in dieser Form so nicht gibt. In drei Tagen (am ersten Tag werden PreCons veranstaltet) werden viele Themen rund um den Microsoft SQL Server behandelt. Wer täglich mit Microsoft SQL Server in Themen rund um DBA, DEV, BI involviert ist, sollte sich die Zeit nehmen, die SQL Server Konferenz im darmstadtium in Darmstadt zu besuchen.
Ein – aus meiner Sicht – ganz wichtiges Thema JEDER Konferenz ist das Networking. Als ich im Jahre 2013 mit dem Besuch von Konferenzen begonnen habe, war ich natürlich noch etwas “schüchtern” in Bezug auf “große Namen”. Nie habe ich so falsch gelegen, wie mit der These der arroganten “Superstars”. Alles sehr nette Leute, die es sehr freut, wenn man sich mit Ihnen über UNSER Lieblingsthema Microsoft SQL Server austauscht. Aber man darf sich auch sehr gerne über andere Dinge unterhalten.
Ich habe über die Konferenzen viele neue Menschen getroffen; einige davon bezeichne ich als Freunde. Das Networking gibt die Möglichkeit, auch abseits von besuchten Sessions mit den Teilnehmern über das eine oder andere Problem zu sprechen – erstaunlich, wie schnell man manchmal eine Lösung zu einem Problem findet, über das man schon mehrere Tage gebrütet hat.
Kosten
Bei “Kosten” hört der Spaß in der Regel auf. Die Kosten für die SQL Server Konferenz halte ich persönlich für angemessen. Für die Teilnahme an der SQL Server Konferenz gibt es drei Kostenmodelle (Preise EXKLUSIVE MwSt.).
Besuch der PreCon: 349,00 €
Hauptkonferenz vom 04.02.2015 bis 05.02.2015: 699,00 €
Komplettpaket (PreCon und Hauptkonferenz): 949,00 €
Hört sich erst mal teuer an; ist es aber aus meiner Sicht für die Leistungen nicht. Als Mitglied in der PASS e.V. (kostenlos) gibt es noch einen Rabatt. Einen gesonderten Rabatt gibt es, wenn mehr als 2 Personen eines Unternehmens an der SQL Server Konferenz teilnehmen möchten. Weiter Informationen zu den Rabatten sind über die Emailadresse info@event-team.com zu erfahren.
Aussteller
Die IT ist im Wandel und das eigentlich stetig. Es ist auch für den SQL Experten wichtig, dass er “auf der Höhe der Zeit” bleibt. Nicht nur die neuen Versionen von Microsoft SQL Server sollten im Fokus eines jeden Besuchers stehen sondern auch, was sich auf dem Markt der Tools und der Hardware tut. Allein die Änderungen im Bereich Storage in den letzten Jahren sind gewaltig. Wie monitore ich meine SQL Server? Welche Hardware ist für meine SQL Server Anwendungen die richtige Wahl? All diese Fragen können in persönlichen Gesprächen mit den Ausstellern in Ruhe besprochen werden. Ein Besuch auf den Ausstellungsflächen der Sponsoren ist immer sinnvoll – viel Spaß beim “Stöbern”.
Was man verbessern könnte
Tatsächlich gefällt mir die SQL Server Konferenz so, wie sie ist, SEHR GUT. Lediglich ein Punkt ist aus meiner Sicht ein “must have” – Videoaufnahmen aller Sessions! Mir persönlich fällt es schwer, in manchen Slots einen “Favoriten” auszuwählen. Es sind alles exzellente Themen, die in den einzelnen Sessions behandelt werden aber ich kann mich nur für eine entscheiden.
Wie gerne würde ich mir die Session von Andreas Wolter über “In Memory OLTP” anhören; schade nur, dass zeitgleich meine eigene Session läuft. Ich glaube, dass es vielen Besuchern so geht, wenn mehrere – für den Einzelnen – interessante Themen zeitgleich behandelt werden.
Wenn es sehr komplexe Themen sind (z. B. der Vortrag von Niko Neugebauer), dann freut man sich, dass man die Session nachträglich noch einmal in Ruhe vor dem eigenen Computer anschauen kann und die Ruhe und die Zeit hat, die gezeigten Beispiele am heimischen Computer noch einmal nachzuspielen. Ein nicht zu unterschätzender Vorteil!
Ich habe auf dem PASS Summit 2014 gesehen, wie professionell dort JEDE Session aufgenommen wurde. Jeder Teilnehmer des PASS Summit hat so die Möglichkeit, nachträglich die für ihn interessanten Sessions noch einmal anzuschauen. Sollte jemand Interesse an dem Material haben, der nicht auf dem SUMMIT war, so hat er die Möglichkeit, gegen einen kleinen Obolus einen USB-Stick mit dem ganzen Material zu erwerben. Ich glaube, das wäre auch ein sehr gutes Modell für die SQL Server Konferenz – aber das ist Jammern auf sehr hohem Niveau. Es ist ja auch erst die zweite Konferenz dieser Art – es muss jedes Jahr eine kleine Verbesserung geben…
Mein Thema
Ich selbst bin ebenfalls mit einer Session auf der SQL Server Konferenz vertreten. Ich arbeite als SQL Server Architekt und SQL Server Berater in vielen Unternehmen in Deutschland und Europa. Bei den vielen hundert Einzeleinsätzen habe ich es recht häufig mit Performance-Problemen zu tun. Nicht selten sind es Architekturprobleme aber über 40% meiner Einsätze resultieren in dem Ergebnis, dass Indexe nicht richtig genutzt werden oder aber Indexe grundsätzlich fehlen. Mein Thema behandelt genau diese Szenarien aus den vielen Kundeneinsätzen, über die ich auch teilweise schon geblogged habe. “Indexing – alltägliche Performanceprobleme und Lösungen” wird mein Thema sein. Ich werde in 60 Minuten 6 – 10 gängige Probleme und deren Lösungen aufzeigen. Ich verspreche, dass es nicht langweilig wird, da die Session zu fast 90% mit Demos belegt ist. Es versteht sich von selbst, dass ich für jeden Interessierten sowohl vor als auch nach meiner Session sehr gerne für weitere Fragen rund um Microsoft SQL Server zur Verfügung stehe.
Ich freue mich auf einen regen Gedanken- und Wissensaustausch mit allen Besuchern der SQL Server Konferenz im darmstadtium in Darmstadt und wünsche allen Lesern und Besuchern viel Spaß!
Nachtrag
Mit “unserem” Vorsitzenden der PASS e. V. – Oliver Engels – habe ich auf dem SQL Boot Camp 2014 in Seeheim-Jugenheim ein kurzes Gespräch über die SQL Server Konferenz generell und im Speziellen zu meinem Thema geführt. Vielleicht macht das Gespräch ja Lust auf mehr.
Ich freue mich auf die SQL Server Konferenz! Ich freue mich sehr auf die Sprecher und ihre Themen aber am meisten freue ich mich auf die Fachbesucher und SQL Server Experten, mit denen ich über mein Lieblingsprodukt fachsimpeln kann. Es ist schön, Teil einer so interessanten und vielfältigen Community zu sein. Jede Konferenz zu Microsoft SQL Server in Deutschland ist ein Gewinn – ich werde alles dazu beitragen, dass die SQL Server Konferenz im Februar ein Gewinn für die Community wird. Ich hoffe sehr, dass auch die Besucher der Konferenz die SQL Server Konferenz wert schätzen und eine rege Teilnahme zu verzeichnen ist.
Bis zum 03.02.2015 in Darmstadt im darmstadtium auf der SQL Server Konferenz.
Einen Tag nach der SQL Konferenz in Darmstadt habe ich Zeit gehabt, meine Emails zu bearbeiten. Unter den vielen Emails war eine interessante Frage eines Teilnehmers des SQLSaturday in Slowenien, die sich mit der Größe der Protokolldatei einer Datenbank beschäftigte.
Eine Datenbank (einfache Wiederherstellung) besitzt eine Protokolldatei mit einer Größe von 76 GB. Der DBA möchte das Protokollmedium auf eine moderate Größe von 100 MB verkleinern. Trotz mehrerer Versuche lies sich die Protokolldatei auf lediglich 1.250 MB verkleinern. Warum das Protokollmedium sich nicht weiter verkleinern lies, beschreibt dieser Artikel.
Hinweis
Dieser Artikel beschreibt nicht die generelle Funktionsweise einer Transaktionsprotokolldatei sondern setzt voraus, dass der Leser die Grundlagen bereits kennt. Ein sehr gutes Dokument für den Einstieg stellt TRIVADIS bereit: “Handling der Transaction Log Files im MS SQL Server”
Transaktionsprotokoll
Die Transaktionsprotokolldatei einer Datenbank ist immer wieder ein Problem im täglichen Umfeld eines DBA. BITTE DIE EINZAHL BEACHTEN – es sollte immer nur eine Protokolldatei vorhanden sein; Microsoft SQL Server verarbeitet Transaktionen immer seriell!
Wenn ein Workload zu groß wird oder aber eine Transaktion nicht in Zwischenschritten abgearbeitet werden kann (z. B. bei ETL-Prozessen), dann wird die Protokolldatei schnell sehr groß und konsumiert einen nicht erheblichen Anteil des Storages. Selbst bei einer Datenbank im Wiederherstellungsmodus SIMPLE kann das Protokoll sehr stark wachsen, wenn auch nur eine einzelne Transaktion ein hohes Transaktionsvolumen produziert.
Da viele erfahrene DBA in der Regel bereits im Vorfeld wissen, dass – insbesondere bei der Erstinstallation – für die Importe große Transaktionsvolumen generiert werden, legt man die Datenbanken mit sehr großen Transaktionsprotokolldateien an. Dieser Umstand kann aber bei “Normalbetrieb” zu einem Problem werden wie das folgende Beispiel verdeutlicht.
Zunächst wird eine Datenbank mit einer Initialgröße von 10 GB für das Protokoll erstellt. Der nachfolgende Code erstellt eine einfache Datenbank, dessen Transaktionsprotokoll initial sehr groß gewählt wird. Je nach Initialgröße/Wachstumsgröße werden zwischen 4 und 16 sogenannter VLF (Virtual Log Files) innerhalb einer Transaktionsprotokolldatei erstellt (siehe Kimberly Tripp):
Größe / Vergrößerung: < =64 MB: 4 VLF
Größe / Vergrößerung: 64 MB – 1024 MB: 8 VLF
Größe / Vergrößerung: 1.024 MB: 16 VLF
Hinweis: Dieser Algorithmus hat sich mit Microsoft SQL Server 2014 geändert (siehe Paul Randal)!
CREATE DATABASE [demo_db]
ON PRIMARY
(
NAME = N'demo_db',
FILENAME = N'F:\DATA\demo_db.mdf',
SIZE = 100MB,
MAXSIZE = 500MB,
FILEGROWTH = 100MB
)
LOG ON
(
NAME = N'demo_log',
FILENAME = N'F:\DATA\demo_db.ldf',
SIZE = 10240MB,
MAXSIZE = 50000MB,
FILEGROWTH = 100MB
);
GO
Sobald die Datenbank angelegt wurde, werden die Informationen über die erstellten VLF mit dem folgenden Befehl angezeigt:
USE demo_db;
GO
DBCC LOGINFO();
GO
Das Ergebnis präsentiert sich wie erwartet – es sind insgesamt 16 VLF, die sich über die Transaktionsprotokolldatei erstrecken.
Wie man gut erkennen kann, erstreckt sich die Verteilung der VLF sehr gleichmäßig auf insgesamt 16 Einheiten. Jede Einheit ist ~671.023.104 Bytes (0,625 GB) groß. Soll das Transaktionsprotokoll verkleinert werden, wird mit dem nachfolgenden Befehl, versucht, eine Initialgröße von 100 MB zu erreichen:
USE demo_db;
GO
DBCC SHRINKFILE ('demo_log', 100);
GO
Das Ergebnis der Aktion sieht nicht sehr erfolgversprechend aus, da – augenscheinlich – die Größe nicht auf 1 MB reduziert wurde sondern auf 1,25 GB!
Die obigen Angaben stammen aus der Aktion DBCC SHRINKFILE. Die Größenangabe bezieht sich immer auf Datenseiten (1 Datenseite = 8.192 Bytes). 163.825 Datenseiten entsprechen 1,250 GB.
Die Ursache für dieses Verhalten versteckt sich in der Größe der VLF. Auf Grund der hohen Startgröße des Transaktionsprotokolls wurde der Initialwert gleichmäßig auf 16 VLF aufgeteilt. Beim Verkleinern verbleiben aber IMMER mindestens zwei VLF in der Protokolldatei. Schaut man sich den Inhalt des Transaktionsprotokolls nach der Verkleinerung wieder an, kann man das sehr deutlich erkennen:
Die Initialgröße des Transaktionsprotokolls sollte immer sehr gut überlegt sein. Abhängig vom Workload der Applikation kann es sicherlich sinnvoll sein, dass das Transaktionsprotokoll bereits vor der Verwendung ausreichend dimensioniert ist/wird. Wird jedoch ausschließlich für das Beladen einer neuen Datenbank das Transaktionsprotokoll initial mit großen Startwerten initialisiert, ist unter Umständen eine ausreichende Verkleinerung nicht mehr möglich. In diesem Fall bleibt nur die Möglichkeit, die Datenbank abzuhängen, die Transaktionsprotokolldatei zu löschen und die Datenbank erneut OHNE Transaktionsprotokoll wieder anzuhängen. Dabei wird dann eine neue – kleinere – Datei für das Transaktionsprotokoll erzeugt.
Links
Das Thema “Transaktionsprotokoll” ist immer wieder eine Herausforderung für DBA. Nachfolgend ein paar – aus meiner Sicht – sehr interessante Links zu diesem komplexen Thema:
Immer wieder hört oder liest man, dass ein Clustered Index möglichst fortlaufend/aufsteigend organisiert sein soll. Am besten sei immer ein Clustered Index mit möglichst kleinen Datentypen (z. B. INT); außerdem sollte ein Clustered Index nach Möglichkeit nicht aus zusammengesetzten Attributen bestehen. Die mit Abstand größte Abneigung besteht bei vielen Entwicklern gegen den Einsatz von GUID als Clustered Keys. Die generelle “Verteufelung” von GUID ist nicht gerechtfertigt – GUID sind in einigen Workloadmustern performanter als die “Heilige Kuh” IDENTITY (1, 1).
Die Vorbehalte vieler SQL Experten gegen GUID kann man z. B. in den kontroversen Foreneinträgen zu dem Artikel “^Are there that many GUIDs?” von Steve Jones auf sqlservercentral.com nachlesen.
Der nachfolgende Artikel soll zeigen, dass der schlechte Ruf, der einer GUID vorausgeht, nicht immer gerechtfertigt ist sondern seiner Nutzung immer eine sorgfältige Betrachtung des Workloads vorausgehen sollte. Pro und Contra GUID soll der Artikel etwas näher beleuchten.
Testumgebung
Für die nachfolgenden Demonstrationen und Diskussionen wird eine Datenbank mit einer Initialgröße von 1 GB erstellt. In dieser Datenbank befinden sich zwei Tabellen mit identischer Datensatzlänge sowie eine Stored Procedure, die im Beispiel von jeweils 200 Benutzern gleichzeitig ausgeführt werden wird.
CREATE DATABASE [demo_db]
ON PRIMARY
(
NAME = N'demo_db',
FILENAME = N'F:\DATA\demo_db.mdf',
SIZE = 1000MB,
MAXSIZE = 20000MB,
FILEGROWTH = 1000MB
)
LOG ON
(
NAME = N'demo_log',
FILENAME = N'F:\DATA\demo_db.ldf',
SIZE = 500MB,
MAXSIZE = 1000MB,
FILEGROWTH = 100MB
);
GO
ALTER AUTHORIZATION ON DATABASE::demo_db TO sa;
ALTER DATABASE [demo_db] SET RECOVERY SIMPLE;
GO
Die Initialgröße von 1 GB sorgt bei den Tests dafür, dass keine Abweichungen bei der Messung entstehen, weil die Datenbank auf Grund der hohen Datenmenge wachsen muss. Die Größe von 500 MB für die Protokolldatei ist ausreichend, da die Datenbank im Modus “SIMPLE” betrieben wird.
USE demo_db;
GO
-- Tabelle mit fortlaufender Nummerierung als Clustered Index
CREATE TABLE dbo.numeric_table
(
Id INT NOT NULL IDENTITY(1, 1),
c1 CHAR(400) NOT NULL DEFAULT ('just a filler'),
CONSTRAINT pk_numeric_table PRIMARY KEY CLUSTERED (Id)
);
GO
-- Tabelle mit zufälligem Clustered Index
CREATE TABLE dbo.guid_table
(
Id uniqueidentifier NOT NULL DEFAULT(NEWID()),
c1 CHAR(388) NOT NULL DEFAULT ('just a filler'),
CONSTRAINT pk_guid_table PRIMARY KEY CLUSTERED (Id)
);
GO
Beide Tabellen haben eine identische Datensatzlänge um Abweichungen bei den Messungen zu verhindern, weil unterschiedliche Datensatzlängen dazu führen können, dass mehr oder weniger Datensätze auf eine Datenseite passen. Jeder Datensatz hat eine Länge von 411 Bytes. Da ein [uniqueidentifier] eine Länge von 16 Bytes besitzt, muss lediglich der Füllbereich um 12 Bytes reduziert werden.
-- Procedure for insertion of 1,000 records in dedicated table
CREATE PROC dbo.proc_insert_data
@type varchar(10)
AS
SET NOCOUNT ON
DECLARE @i INT = 1;
IF @type = 'numeric'
BEGIN
WHILE @i <= 1000
BEGIN
INSERT INTO dbo.numeric_table DEFAULT VALUES
SET @i += 1;
END
END
ELSE
BEGIN
WHILE @i <= 1000
BEGIN
INSERT INTO dbo.guid_table DEFAULT VALUES
SET @i += 1;
END
END
SET NOCOUNT OFF;
GO
Die Prozedur hat eine triviale Aufgabe; sie soll – abhängig vom übergebenen Parameter @type – jeweils 1.000 Datensätze pro Aufruf in eine der beiden Tabellen eintragen. Wird der Prozedur der Parameter @type mit dem Wert ’numeric‘ übergeben, trägt die Prozedur entsprechende Daten in die Tabelle mit fortlaufendem Clustered Key ein während bei einem anderen Parameterwert Daten in die Tabelle mit zufälligem Clustered Key eingetragen werden.
Vor jeder Ausführung der Prozedur werden die Statistiken in sys.dm_os_wait_stats gelöscht. Dadurch sollen die während der Laufzeit registrierten Wartevorgänge analysiert werden:
DBCC SQLPERF(’sys.dm_os_wait_stats‘, ‚CLEAR‘);
GO
Während die Prozedur ausgeführt wird, wird in einer zweiten Sitzung in SQL Server Management Studio die folgende Abfrage wiederholt ausgeführt:
SELECT DOWT.session_id,
DOWT.wait_duration_ms,
DOWT.wait_type,
DOWT.resource_description,
DOWT.blocking_session_id
FROM sys.dm_exec_sessions AS DES INNER JOIN sys.dm_os_waiting_tasks AS DOWT
ON (DES.session_id = DOWT.session_id)
WHERE DES.is_user_process = 1;
GO
Um einen Workload mit mehreren Clients zu simulieren, wird das Tool “ostress.exe” verwendet. Dieses – von Microsoft kostenlos angebotene Tool – ermöglicht die Simulation von Workloads mit mehreren Clients und ist – insbesondere für PoC-Simulationen – sehr zu empfehlen.
Daten in Tabelle mit fortlaufendem Clustered Key
Zunächst wird die Prozedur für die Ausführung in die Tabelle [dbo].[numeric_table] ausgeführt:
Mit einer vertrauten Verbindung (-E) wird auf den Testserver zugegriffen und die Prozedur [dbo].[proc_insert_data] (-Q) in der Datenbank [demo_db] (-d) für 200 Clients (-n) ausgeführt. Während der Ausführung dieser Prozedur wird mittels sys.dm_os_waiting_tasks überprüft, auf welche Ressourcen Microsoft SQL Server während der Clientzugriffe warten muss. Insgesamt benötigt das Eintragen von 200.000 Datensätzen ~12,500 Sekunden.
Diese Zeitspanne mag für Datenbanken mit geringem Transaktionsvolumen ausreichend sein; jedoch gibt es Systeme, die noch deutlich mehr Datensätze in kürzerer Zeit verarbeiten müssen. Warum also dauert dieser Prozess so lange?
Die obige Abbildung zeigt die wartenden Prozesse während der Ausführung der Prozedur. Es ist deutlich zu erkennen, dass die Mehrzahl der Prozesse auf einen PAGELATCH_xx warten müssen.
PAGELATCH_XX
Als “Latch” bezeichnet man interne, nicht konfigurierbare Sperren, die von Microsoft SQL Server benötigt werden, um bei gleichzeitigem Zugriff (Konkurrierend) die im Speicher befindlichen Strukturen zu schützen. Latches werden häufig mit Locks verwechselt, da ihre Zwecke gleich gelagert aber nicht identisch sind. Ein Latch bezeichnet ein Objekt, das die Integrität der Daten anderer Objekte im Speicher von SQL Server gewährleistet. Sie sind ein logisches Konstrukt, das für einen kontrollierten Zugriff auf eine Ressource sorgt. Im Gegensatz zu Locks sind Latches ein interner SQL Server-Mechanismus. Man kann sie grob in zwei Klassen aufteilen – Buffer Latches und Non-Buffer Latches. Für den in diesem Artikel beschriebenen Fall handelt es sich um Sperren auf Datenseiten, die sich bereits im Buffer Pool befinden. Wer sich intensiver mit Latches beschäftigen möchte, dem sei das Dokument “Diagnosing and Resolving Latch Contention on SQL Server” von Ewan Fairweather und Mike Ruthruff für das weitere Studium empfohlen. Es mag zwar schon älter sein aber sein Inhalt ist auch für die neueren Versionen von Microsoft SQL Server noch relevant!
Auffällig ist, dass alle Prozesse auf die gleiche Ressource warten müssen. Die Wartevorgänge variieren zwischen SH (Shared) und EX (Exclusive) Ressourcen. Diese Wartevorgänge sind der – in diesem Fall sehr schlechten – Eigenschaft des Clustered Index geschuldet. Da die Daten in sortierter Reihenfolge in den Index eingetragen werden müssen, werden alle neuen Datensätze immer am Ende des Index eingetragen. Der Clustered Index repräsentiert die Tabelle selbst und definiert die Sortierung nach einem vorher festgelegten Schlüsselattribut. Wenn dieses Attribut mit einem fortlaufenden Wert befüllt wird, reduziert sich der Zugriffspunkt für Microsoft SQL Server immer auf die letzte Datenseite des Clustered Index im Leaf-Level.
Die Abbildung zeigt den – logischen – Aufbau eines Clustered Index. Aus Darstellungsgründen wurde auf die Beziehung der Datenseiten untereinander verzichtet. Alle Daten werden im Leaf-Level gespeichert. Da das Schlüsselattribut als fortlaufend definiert wurde, müssen ALLE Prozesse auf der letzten Seite des Index ihre Daten eintragen. Wenn – wie in diesem Beispiel gezeigt – mehrere Prozesse gleichzeitig eine INSERT-Operation durchführen, hat das den gleichen Effekt wie das Anstehen an einer Kasse im Supermarkt; man muss warten! Aber ACHTUNG: Das beschriebene Szenario gilt nicht für Workloads mit mehreren Verbindungen!
Fortlaufenden Index-Pages im Clustered Index
Sehr häufig verbindet man mit fortlaufender Nummerierung im Indexschlüssel die Vermeidung von Page Splits. Diese Aussage hat aber nur Bestand, wenn das Eintragen von neuen Datensätzen durch einen einzigen Thread geschieht. Wird jedoch – wie im aktuellen Beispiel – der Einfügevorgang von 200 Benutzern gleichzeitig gestartet, kann eine sequentielle Folge in den Eintragungen nicht mehr gewährleistet werden. Ein Blick in das Transaktionsprotokoll zeigt die Details:
SELECT [Current LSN],
Operation,
Context,
AllocUnitName,
[Page ID],
[Slot ID],
[RowLog Contents 0]
FROM sys.fn_dblog(NULL, NULL) AS FD
WHERE AllocUnitName = N'dbo.numeric_table.pk_numeric_table'
ORDER BY
[Current LSN];
Die nachfolgende Abbildung zeigt einen Extrakt aus dem Transaktionsprotokoll, der die Einfügevorgänge in chronologischer Reihenfolge ausgibt.
Hervorzuheben ist die [SLOT ID]! Aus der Slot Id ist erkennbar, dass die Datensätze nicht hintereinander eingetragen wurden, sondern nach dem 5 Eintrag (Zeile 48) ein neuer Datensatz in Slot 1 eingetragen worden ist. Das lässt nur den Rückschluss zu, dass ein kleinerer Indexwert erst später eingetragen wurde. Mit dem nachfolgenden SELECT-Statement werden die eingetragenen Indexschlüssel “entschlüsselt”:
SELECT CAST(0x14 AS INT) AS [Slot_0],
CAST(0x21 AS INT) AS [Slot 1],
CAST(0x22 AS INT) AS [Slot 2],
CAST(0x23 AS INT) AS [Slot 3],
CAST(0x24 AS INT) AS [Slot 4],
CAST(0x20 AS INT) AS [Slot w],
CAST(0x1F AS INT) AS [Slot x],
CAST(0x15 AS INT) AS [Slot y],
CAST(0x1D AS INT) AS [Slot z];
GO
Auf eine vollumfängliche Beschreibung der Strukturen eines Datensatzes kann in diesem Artikel nicht eingegangen werden! Kurz und Knapp gilt für die Berechnung des eingetragenen Wertes für die ID in der Tabelle [dbo].[numeric_table] jedoch folgende Richtlinie:
Die ersten vier Bytes in [RowLog Contents 0] sind für den Rowheader reserviert und müssen extrahiert werden
Die nächsten 4 Bytes entsprechen dem Wert, der in das Attribut [ID] (INTEGER) eingetragen wird
Diese vier Bytes werden “geshifted” und übrig bleibt der Hexadezimalwert für den eingetragenen Dezimalwert
Die Berechnung des Dezimalwertes erfolgt mittels CAST-Funktion
Das Ergebnis der obigen Abfrage sieht dann wie folgt aus:
Die ID-Werte sind in der chronologischen Reihenfolge ihres Einfügeprozesses dargestellt. Zunächst wurden die Werte 20, 33, 34, 35, 36 in die Tabelle eingetragen. Das entspricht exakt dem Auszug aus dem Transaktionsprotokoll (siehe [Slot ID]. Mit der sechsten Transaktion wurde ein neuer Datensatz mit dem Wert 32 in die Tabelle eingetragen. Der Clustered Key muss sortiert werden; also wird der Wert 32 in Slot 1 eingetragen. Anschließend wird der Wert 31 eingetragen und der Kreis schließt sich erneut!
Schaut man sich die Verteilung der eingetragenen Datensätze nach chronologischem Muster an, wird schnell klar, dass Page Splits zwangsläufig auftreten müssen. Der Grund für diese gemischte Verteilung ist schnell erkennbar, wenn man versteht, wie Microsoft SQL Server den Wert für IDENTITY ermittelt. Dieses Thema habe ich bereits ausführlich im Artikel “IDENTITY-Werte…–warum wird der Wert um <increment> erhöht, obwohl die Transaktion nicht beendet werden kann?” beschrieben.
Werden nur von einem Benutzer Daten in eine Tabelle eingetragen, wird der Indexschlüssel um jeweils 1 erhöht und der Datensatz wird eingetragen. Wir jedoch – wie im vorliegenden Beispiel – ein System mit hoher Concurrency betrieben, ist das nicht mehr gewährleistet. Die nachfolgende Abbildung zeigt den internen Sachverhalt:
Jeder Thread fordert – sobald er CPU-Zeit erhält – einen IDENTITY-Wert aus dem Pool an. Dieser Prozess ist nicht mehr umkehrbar; einmal einen Wert erhalten, wird dieser Wert von diesem Prozess für den ganzen Einfügezyklus beibehalten! Nachdem der IDENTITY-Wert vergeben wurde, kann der Thread mit diesem erhaltenen Wert den Einfügevorgang beenden. Dazu muss sich jeder Thread (mit dem ihm zugeteilten IDENTITY-Wert in eine Reihe mit anderen Threads anstellen, die ebenfalls auf die Ressource (Datenseite) warten müssen. Hierbei kann es zu Verschiebungen kommen; der Thread mit dem Wert 4 muss warten, weil die Threads mit den Werten 7 und 11 noch vor ihm ausgeführt werden müssen. Wird die Seite vollständig gefüllt, wenn der Thread mit Wert 11 seine Daten gespeichert hat, wird es unweigerlich für den Thread mit Wert 4 zu einem Page Split kommen.
Clustered Index mit zufälligem Schlüssel
Mit dem nachfolgenden Test werden ebenfalls 200.000 Datensätze in die Tabelle [dbo].[guid_table] eingetragen und erneut die Zeiten gemessen.
Während der Ausführung der Prozedur werden die wartenden Tasks überprüft und eine abschließende Abfrage über die Wartevorgänge ausgeführt. Insgesamt wird der Einfügevorgang in etwa 50% der Zeit ausgeführt. Das Eintragen in die Tabelle mit fortlaufendem Clustered Key hat: 6,833 Sekunden betragen!
Während beim Eintragen von fortlaufenden Indexwerten PAGELATCH-Wartevorgänge das Bild dominiert haben, ist beim Eintragen von zufälligen Schlüsselwerten das Schreiben in die Protokolldatei der dominierende Wartevorgang – und das zu Recht!
Wie die Abbildung zeigt, konzentriert sich der Prozess nicht mehr an das Ende des Indexes sondern verteilt sich gleichmäßig auf alle Datenseiten im Leaf-Level. Somit können mehrere Schreibvorgänge parallel durchgeführt werden. Dieser Umstand wird in den Wartevorgängen abgebildet – fast alle Prozesse warten darauf, dass die Transaktionen in die Protokolldatei geschrieben werden.
Die Abbildung zeigt einen Mitschnitt im Microsoft WIndows Performance Monitor während der Ausführung der Prozedur für die Tabelle [dbo].[numeric_table] und für die Tabelle [dbo].[guid_table]. Während für das Einfügen von Datensätzen in die Tabelle mit aufsteigendem Indexschlüssel Pagelatches zu beobachten sind, ist das Eintragen der gleichen Datenmenge in die Tabelle mit zufälligem Indexschlüssel nur eine kurze “Episode” da kaum zu messende Pagelatches auftreten.
Vor- und Nachteile der verschiedenen Varianten
Jede der oben vorgestellten Varianten hat seine Vor- und Nachteile. Die Geschwindigkeit beim Eintragen von Daten, die man sich mit einer GUID-Variante erkauft, muss unter Umständen mit teuren Table Scans und mit erhöhtem Aufwand für Index Maintenance bezahlt werden.
Fragmentierung
Wie bereits weiter oben dargestellt, können Page Splits auch bei IDENTITY-Werten für einen Clustered Key vorkommen. Dieser Umstand führt dazu, dass trotz fortlaufendem numerischen Clustered Key die logische Fragmentierung auf den Datenseiten unverhältnismäßig hoch sein kann.
SELECT OBJECT_NAME(object_id),
index_type_desc,
fragment_count,
page_count,
record_count,
avg_fragmentation_in_percent,
avg_page_space_used_in_percent
FROM sys.dm_db_index_physical_stats
(
DB_ID(),
OBJECT_ID('dbo.numeric_table', 'U'),
1,
NULL,
'DETAILED'
) AS DDIPS;
GO
Obwohl eine fortlaufende Nummerierung gewählt wurde, sind ca. 88% logische Fragmentierung zu verzeichnen. Insgesamt ist eine Datenseite zu ~65% gefüllt (Page Density). Das bedeutet für die Auslastung des Buffer Pools ca. 5.250 KB statt 8.060 Bytes.
Ein direkter Vergleich zwischen [dbo].[numeric_table] und [dbo].[guid_table] zeigt, dass es keinen nennenswerten Unterschied im Fragmentierungsgrad gibt. Beide Tabellen sind zwar hochgradig fragmentiert aber es gibt auf den ersten Blick keinen Vorteil, der für die numerische fortlaufende Variante spricht. Jedoch besitzt der fortlaufenden Clustered Keys den Vorteil, dass nach einem INDEX-REBUILD dieser Bereich des Indexes nicht mehr fragmentiert werden kann, da neue Datensätze immer an das Ende des Indexes geschrieben werden! Bei einer GUID besteht auch bei neuen Datensätzen noch das Risiko, dass der Index innerhalb von bereits bestehenden Daten erneut durch Page Splits fragmentiert wird!
Sind gleichzeitige Workloads das bestimmende Bild in Microsoft SQL Server muss sowohl bei fortlaufenden Indexschlüssel als auch bei variablen Indexschlüsseln mit Fragmentierung gerechnet werden. Grundsätzlich kann man aber behaupten, dass bei deutlich weniger Concurrency sowie nach dem Neuaufbau eines Index ein Vorteil für den fortlaufenden Indexschlüssel besteht!
Größere NONCLUSTERED Indexe
Jeder NONCLUSTERED Index muss den Clustered Key eines Clustered Index zusätzlich abspeichern, damit der Index einen Verweis zu den Daten der Datenzeile hat, die nicht im NONCLUSTERED Index hinterlegt sind. Tatsächlich ergibt sich dadurch mathematisch ein Nachteil für die Verwendung einer GUID als Clustered Index. Eine GUID hat eine Datenlänge von 16 Bytes während der Datentyp INT lediglich 4 Bytes an Speichervolumen konsumiert.
Das Ergebnis sollte nicht weiter überraschen. Es versteht sich von selbst, dass bei Verwendung eines größeren Schlüssels das Datenvolumen entsprechend wächst. Hier liegt der Vorteil eindeutig bei einem Clustered Key mit einem kleinen Datentypen (4 Bytes vs. 16 Bytes).
Ascending Key Problem
Das Problem von aufsteigenden Schlüsselattributen im Index habe ich im Artikel “Aufsteigende Indexschlüssel – Performancekiller” sehr detailliert beschrieben. Dieses Problem tritt IMMER auf, wenn ein Schlüsselattribut verwendet wird, dass beständig größere Werte in einen Index einträgt. Da die Statistiken eines Index nicht bei jedem Eintrag neu erstellt werden, kann es vorkommen, dass bei veralteten Statistiken ein schlechter Ausführungsplan generiert wird. Selbstverständlich kann auch bei einer GUID ein solches Problem auftreten, wenn die neue GUID tatsächlich am Ende des Index erstellt wird. Mit zunehmender Datenmenge wird dieses Problem jedoch immer unwahrscheinlicher! Hier liegt ein Vorteil in der Verwendung einer GUID!
Dieser Vorteil ist jedoch für einen CLUSTERED INDEX mit fortlaufender Nummerierung eher vernachlässigbar da ein Wert immer nur ein Mal als Schlüsselattribut vorkommt. Somit ist das Problem “Ascending Key” hier eher eine unbedeutende Randerscheinung!
Replikation
Manche Replikationsszenarien (z. B. MERGE) verlangen ein Attribut in jeder Tabelle, die repliziert werden soll, dass von Datentyp [uniqueidentifier] ist und die Eigenschaft ROWGUIDCOL besitzt. Plant man das Datenbanksystem in einem Replikationsumfeld, bietet sich die GUID als Clustered Key an. Will man auf IDENTITY / INT als Clustered Key nicht verzichten, muss jeder Tabelle explizit ein neues Attribut hinzugefügt werden. Das nachfolgende Codebeispiel zeigt für beide Tabellen die jeweiligen “Änderungen/Anpassungen”:
-- table with contigious numbers as clustered key
CREATE TABLE dbo.numeric_table
(
Id INT NOT NULL IDENTITY(1, 1),
c1 CHAR(400) NOT NULL DEFAULT ('just a filler'),
rowguid UNIQUEIDENTIFIER NOT NULL ROWGUIDCOL,
CONSTRAINT pk_numeric_table PRIMARY KEY CLUSTERED (Id),
CONSTRAINT uq_numeric_table UNIQUE (ROWGUID)
);
GO
-- table with random guid as clustered key
CREATE TABLE dbo.guid_table
(
Id UNIQUEIDENTIFIER NOT NULL ROWGUIDCOL DEFAULT(NEWID()),
c1 CHAR(388) NOT NULL DEFAULT ('just a filler'),
CONSTRAINT pk_guid_table PRIMARY KEY CLUSTERED (Id)
);
GO
Das Skript zeigt beide Tabellen aus den vorherigen Beispielen, wie sie für eine MERGE oder PEER-TO-PEER Replikation vorbereitet sein müssen. Während es für die Tabelle [dbo].[guid_table] lediglich der zusätzlichen Eigenschaft “ROWGUIDCOL” für den Clustered Key bedarf, ist der Aufwand (und auch die Länge eines Datensatzes) in einer Tabelle mit einem INT deutlich höher. Zunächst muss ein weiteres Attribut vom Datentypen UNIQUEIDENTIFIER angelegt werden, dass ebenfalls die Eigenschaft “ROWGUIDCOL” besitzt. Für eine bessere Performance wird mittels eines UNIQUE CONSTRAINTS ein weiterer Index hinzugefügt, der die Eindeutigkeit sicherstellt.
Hier geht der Punkt eindeutig an die GUID als Clustered Key, da sich die Datenstruktur / Metadaten nicht verändern. Während die Länge eines Datensatzes in der Tabelle [dbo].[numeric_table] um 16 Bytes erweitert werden muss, bleibt die Datensatzlänge in der Tabelle [dbo].[guid_table] unverändert bei 411 Bytes. Ebenfalls wird kein weiterer Index für die Durchsetzung der Eindeutigkeit benötigt!
Zusammenfassung
Die generelle Ablehnung von GUID als Schlüsselattribut in einem Clustered Index wird zu häufig mit pauschalen / schon mal gehörten / Behauptungen untermauert. Sie ist ungerechtfertigt, wenn man nicht den Workload berücksichtigt, der einem DML-Prozess zu Grunde liegt.
Die Verwendung von GUID macht aus der Sicht des Autors dann Sinn, wenn sehr viele Daten von sehr vielen Prozessen eingetragen werden. Handelt es sich eher um ein System, mit dem sehr wenig Benutzer arbeiten oder aber das nur “gelegentlich” neue Datensätze in der Datenbank speichert, so sollte auch weiterhin mit dem favorisierten IDENTITY / INT gearbeitet werden.
GUID sind ideal für parallele ETL-Workloads in Stagingtabellen wenn man unbedingt einen Clustered Index verwenden möchte. Grundsätzlich sollten ETL-Workloads nur in HEAPS ihre Daten speichern. Alle anderen Varianten sind aus Sicht von Durchsatz und Zeit eher eine Bremse im sonst so performanten Ladeprozess! Aber das ist ein ganz anderes Thema, auf das ich im nächsten Artikel detaillierter eingehen werde.
Die Verwendung von Indexen in Datenbanksystemen stellt einen wichtigen Schritt dar, um eine Datenbank performant zu gestalten. Die Wahl eines geeigneten Schlüsselattributs für den Clustered Index stellt bereits die Weichen für die Performance und die Größe aller weiteren – non-clustered – Indexe. Neben einem geeigneten Datentypen spielt die Eindeutigkeit der Werte bei der Definition eines Clustered Index eine entscheidende Rolle, die über die Performance entscheiden kann. Der nachfolgende Artikel beschreibt im Detail, wie Microsoft SQL Server sicherstellt, dass Werte in einem Clustered Index eindeutig identifiziert werden können, obwohl der Index nicht “UNIQUE” ist.
Testumgebung
Für die Beispiele in diesem Artikel wird eine Tabelle mit Informationen zu Kunden und deren Kostenstellenzuordnungen verwendet. Die Tabelle hat folgenden Aufbau:
CREATE TABLE [dbo].[Companies] ( [Id] int NOT NULL IDENTITY(1,1), [Name] nvarchar(128) NOT NULL, [TaxNo] varchar(24) NOT NULL, [CostCenter] char(7) NOT NULL, [UpdateBy] varchar(20) NOT NULL
)
Die Auswahl eines geeigneten Attributs für einen „clustered Index” muss man sorgfältig planen, da diese Entscheidung Auswirkungen auf das gesamte Layout der Datenstruktur hat. Der Clustered Index kann – wie ein non clustered Index auch – wahlweise als eindeutiger Index oder als Index mit redundanten Schlüsselwerten definiert werden.
Kardinalität der Schlüsselattribute
Die Kardinalität eines Attributs bestimmt, ob der Schlüssel eines Index als UNIQUE definiert werden kann. Für das erste Beispiel soll ein Clustered Index auf dem Attribut [Name] angelegt werden. Mit der folgenden Abfrage wird aus den Beispieldaten ermittelt, wie häufig der Firmenname in der Beispieltabelle [dbo].[Companies] verwendet wird:
SELECT TOP 10 [Name] AS CompanyName, COUNT_BIG(Id) AS Kardinalität FROM [dbo].[Companies] GROUP BY [Name] ORDER BY COUNT_BIG(*) DESC;
Das Ergebnis der Abfrage zeigt, dass Firmennamen in der Beispieldatenbank redundant vorkommen.
Image may be NSFW. Clik here to view.Auf Grund der Redundanzen im Firmennamen kann für das Attribut [Name] kein eindeutiger Clustered Index angewendet werden.
Ein Clustered Index ist – wie bereits oben erwähnt – nicht dadurch in seiner Anwendung begrenzt, dass er nicht eindeutig sein kann. Vielmehr reguliert ein Clustered Index die logische Sortierung von Datensätzen in einer Tabelle; respektive er repräsentiert die Tabelle.
Clustered Index mit redundanten Schlüsselattributen
Um zu zeigen, wie Microsoft SQL Server redundante Schlüsselattribute in einem Index verwaltet, wird im ersten Beispiel das Attribut [Name] mit einem Clustered Index versehen. Auf Grund der redundanten Daten darf der Index nicht eindeutig sein.
CREATE CLUSTERED INDEX [cix_companies_name] ON [dbo].[Companies] ([Name]);
Obwohl der erstellte Clustered Index nicht eindeutig ist, muss Microsoft SQL Server sicherstellen, dass der Datensatz “eindeutig identifizierbar” in der Tabelle ist. Diese “Eindeutigkeit” ist um so wichtiger, als dass diese Eindeutigkeit als Referenz in jedem non-clustered Index der Tabelle gespeichert werden muss. Es stellt sich also die Frage, wie Microsoft SQL Server die Datensätze verwaltet, um die Eindeutigkeit eines Datensatzes zu gewährleisten. Hierzu muss man tiefer in die Database Engine von Microsoft SQL Server eindringen.
Microsoft SQL Server verwaltet Datensätze in Datenseiten. Eine Datenseite hat eine feste Größe von 8.192 Bytes und kann bis zu einer Datenmenge von 8.060 Bytes vollständig für die Speicherung von Datensätzen verwendet werden. Um einen Blick auf eine Datenseite zu werfen, muss der – nicht dokumentierte – Befehl DBCC PAGE verwendet werden. Zuvor wird mit Hilfe der Funktion [sys].[fn_PhysLocCracker] ermittelt, auf welchen Datenseiten die Datensätze der Tabelle [dbo].[Companies] gespeichert wurden:
SELECT P.[file_id], P.[page_id], P.[slot_id], C.name FROM [dbo].[Companies] AS C CROSS APPLY sys.fn_PhysLocCracker(%%physloc%%) AS P;
Die Abbildung zeigt die ersten Datensätze der Beispieltabelle. Die ersten drei Spalten der Ergebnismenge verweisen auf die jeweilige Position jedes einzelnen Datensatzes der Tabelle. Der erste Datensatz befindet sich in Datei 1 der Datenbank auf Datenseite 146 in Slot 0. Der Wert des Indexschlüssels kommt in den Beispieldaten nur einmal vor. Das nächste Unternehmen wird auf der gleichen Datenseite gespeichert, kommt aber mehrmals in den Beispieldaten vor.
Mit Hilfe des nachfolgenden Befehls kann der Inhalt und die Struktur der Datenseite 146 im Management Studio von Microsoft SQL Server ausgegeben werden:
Um detaillierte Informationen zu einer Datenseite zu erhalten, muss das Traceflag 3604 aktiviert werden. Durch die Aktivierung wird die Ausgabe von DBCC PAGE nicht in das Fehlerprotokoll von Microsoft SQL geleitet sondern clientseitig ausgegeben. Die Option TABLERESULTS wurde für die bessere Darstellung des nachfolgenden Ergebnisses gewählt!
Die Abbildung zeigt die Informationen des ersten Datensatzes, der auf der Seite gespeichert wurde. Man erkennt, dass Microsoft SQL Server neben den gespeicherten Daten eine weitere Spalte VOR den eigentlichen Daten verwaltet. Hierbei handelt es sich um das Systemattribut [UNIQUIFIER]. Hierbei handelt es sich um ein internes – ausschließlich für die Verwaltung der Eindeutigkeit genutztes – Attribut, dass nicht nach außen angezeigt wird.
Ein UNIQUIFIER wird von Microsoft SQL Server dann für einen Index verwendet, wenn der Index nicht als eindeutig definiert wurde. Mit Hilfe eines UNIQUIFIER gelingt Microsoft SQL Server die “interne” Eindeutigkeit eines Datensatzes. Ein genauer Blick auf die Abbildung zeigt, dass Microsoft SQL Server trotz “zusätzlicher” Informationen sehr sparsam mit dem zur Verfügung gestellten Platz einer Datenseite umgeht. Der jeweils ERSTE Datensatz eines Schlüsselattributs bekommt zwar den Wert 0 für den UNIQUIFIER zugeteilt; dieser Wert wird aber nicht physikalisch auf der Datenseite gespeichert. Die Speicherung eines UNIQUIFIER-Wertes benötigt den Datentypen INT. Somit müssen weitere 4 Bytes zum eigentlichen Datensatz hinzu addiert werden, die ebenfalls gespeichert werden müssen.
Werden Datensätze mit redundanten Indexschlüsseln gespeichert, wird ab dem zweiten Datensatz mit gleichem Indexschlüssel der UNIQUIFIER physikalisch auf der Datenseite gespeichert.
Die Abbildung zeigt zwei Kundendatensätze, deren Indexschlüssel ([Name]) redundant in der Tabelle vorkommt. Der erste Datensatz erhält den – internen – UNIQUIFIER-Wert 0 während der zweite Eintrag den Wert 1 erhält. Ist der erste Eintrag nur eine berechnete Anzeige muss für die Speicherung des zweiten UNIQUIFIER-Werts physikalisch Speicher auf der Datenseite reserviert werden. Der Datensatz ist insgesamt um 4 Bytes gewachsen! Diese zusätzlichen 4 Bytes für den UNIQUIFIER werden zwischen den Informationen über die Struktur der variablen Spalten einer Tabelle und den variablen Daten eines Datensatzes zusätzlich gespeichert. Obwohl es sich bei dem Datentypen INT um einen festen Datentypen handelt, wird er – in diesem Fall – wie ein variabler Datentyp behandelt.
Die Abbildung zeigt – rot markiert – das Offset für die Werte jeder einzelnen Spalte mit variabler Datenlänge (jeweils 2 Bytes). So beginnt der Name des Unternehmens bei Offset 28, die Steuernummer bei Offset 72, usw.). Die Abbildung zeigt keinen Eintrag für den UNIQUIFIER, da es sich bei dem Datensatz um den ersten Eintrag mit gleichem Wert im Indexschlüssel handelt.
Die zweite Abbildung zeigt den zweiten Datensatz mit identischem Schlüsselattribut. In diesem Fall muss Microsoft SQL Server den Wert für den UNIQUIFIER physikalisch speichern. Obwohl es sich um einen Datentypen mit fester Datenlänge handelt, wird er im Bereich der variablen Daten gespeichert. Durch die zusätzlichen 4 Bytes VOR den eigentlichen Daten des Datensatzes verschieben sich die Offsets. Der Name des Unternehmens beginnt nun bei Offset 32, die Steuernummer verschiebt sich ebenfalls um 4 Bytes und beginnt nun bei Offset 76 (4C)! Insgesamt wächst der Datensatz um 4 Bytes (siehe Record Size im Header).
Clustered Index mit eindeutigen Schlüsselattributen
Wenn die Schlüsselattribute eines Clustered Index eindeutig sind, benötigt Microsoft SQL Server keine zusätzlichen Informationen, um die – interne – Eindeutigkeit eines Datensatzes zu gewährleisten. In diesem Fall kann der Index selbst die Datenintegrität gewährleisten. Neben dem fehlenden Verwaltungsaufwand ändert sich auch die Länge/Größe eines Datensatzes nicht mehr.
Die in Beispiel 1 verwendete Tabelle wird im zweiten Beispiel mit einem eindeutigen Clustered Index auf dem Feld [Id] erstellt.
CREATE UNIQUE CLUSTERED INDEX [cix_companies_id] ON [dbo].[Companies](Id);
Nach der Erstellung des Index zeigt ein erneuter Blick auf die Datenseiten des Clustered Index, dass die vormals mit UNIQUIFIER gespeicherten Datensätze nun ohne diesen Zusatz auskommen.
Die Abbildung zeigt den ersten Beispieldatensatz mit der [Id] = 2 (blaue Kennzeichnung). Auffällig ist bei der Wahl eines eindeutigen Index, dass die Anzahl der Spalten von vormals 6 auf 5 Spalten reduziert wurde, die in der Tabelle selbst definiert wurden! Die grüne Markierung zeigt die Offsets der Spalten mit variablen Datentypen. Vergleicht man die Struktur des Datensatzes mit dem zweiten – identischen – Eintrag, erkennt man, dass (bis auf den Wert für [Id]) keine Änderungen in der Struktur des Datensatzes vorgenommen wurde.
Nicht nur die Wahl der geeigneten Datentypen für einen Clustered Index entscheidet über mögliche Performanceeinbußen. Auch über die Eindeutigkeit eines Clustered Index sollte man sich bei der Erstellung Gedanken machen. Ist ein Clustered Index nicht eindeutig, muss Microsoft SQL Server selbst für die Eindeutigkeit sorgen und verwaltet diese Information mit Hilfe eines – internen – UNIQUIFIER-Attributs. Ein UNIQUIFIER belegt 4 Bytes zusätzlich im Datensatz und wird – trotz fester Datenlänge – wie ein variabler Datentyp behandelt.
Übrigens gilt die Verwendung eines UNIQUIFIER-Attributs ausschließlich für einen Clustered Index. Für non-clustered Indexe ist dieser Verwaltungsaufwand nicht erforderlich, da die Eindeutigkeit entweder durch den Clustered Key (+[UNIQUIFIER] oder die RID (Row Locator ID) eines Heap gewährleistet wird.
Beim Anlegen von Datenbankdateien (Daten, Log) werden standardmäßig die zu erstellenden Dateien beim Initialisieren mit “0” aufgefüllt, damit eventuell auf dem Datenträger verbliebende Daten von vorherigen (gelöschten) Dateien überschrieben werden. Dieses Verfahren betrifft nicht nur das Erstellen neuer Datenbanken sondern auch die Wiederherstellung von Datenbanken aus einem Backup oder die Vergrößerung einer Datenbank. Welchen Einfluss diese Vorgänge auf die Leistung von Microsoft SQL Server hat, beschreibt der nachfolgende Artikel.
Was ist Instant File Initialization?
Duch “Instant File Initialization” lässt sich der Prozess des Erstellens oder Vergrößerns von Dateien beschleunigen, indem das Überschreiben von neuem Speicher für die Datenbankdatei (Zeroing out) nicht durchgeführt wird. Microsoft SQL Server erstellt die Datei und alloziert den benötigten Speicher; aber der langwierige Prozess des Überschreibens bleibt aus. Dieser Vorteil kann aber nur auf Datenbankdateien angewendet werden, Transaktionsprotokolle können diesen Vorteil nicht nutzen. Dass Transaktionsprotokoll-Dateien diesen Vorteil nicht nutzen können, hängt damit zusammen, dass das Transaktionsprotokoll ein rotierendes Verfahren verwendet, um freie / ungenutzte VLF (Virtual Log File) wiederzuverwenden.
Vorteil von Instant File Initialization
Durch das Verhindern von “Zeroing Out” steht eine Datenbank schneller (wieder) zur Verfügung. Instant File Initialization kann bei den folgenden Prozessen einen erheblichen Geschwindigkeitsvorteil bringen:
CREATE DATABASE
ALTER DATABASE … MODIFY FILE
RESTORE DATABASE
AUTOGROWTH für Datenbanken
Alle vier genannten Prozesse haben eines gemeinsam; sie erstellen oder ändern Datenbankdateien, die von Microsoft SQL Server verwaltet werden.
Nachteil von Instant File Initialization
Da Instant File Initialization vorhandenen und allozierten Speicher nicht überschreibt, besteht die Gefahr, dass mit geeigneter Software Daten, die vorher von der Festplatte gelöscht wurden, ausgelesen werden können. Es muss vor der Aktivierung eine Sicherheitsbewertung erfolgen; wird der Speicher auch für “normale” Filesystem-Aktivitäten verwendet oder werden Datenbanken häufig gelöscht, sollte auf Instant File Initialization eventuell verzichtet werden.
Wie kann man erkennen, ob Microsoft SQL Server “Instant File Initialization” verwendet?
Instant File Initialization kann NICHT in Microsoft SQL Server konfiguriert werden, da es sich dabei nicht um eine Funktionalität von Microsoft SQL Server handelt. Es handelt sich um ein Sicherheitsprivileg, dass dem Dienstkonto der ausgeführten Instanz von Microsoft SQL Server zugewiesen werden kann. Um festzustellen, ob Instant File Initialization aktiviert ist, stehen zwei Möglichkeiten zur Verfügung:
Außerhalb von Microsoft SQL Server: Überprüfung der lokalen Sicherheitsrichtlinie
In Microsoft SQL Server: Anlegen einer neuen Datenbank bei gleichzeitiger Protokollierung in das Fehlerprotokoll
Lokale Sicherheitsrichtlinie
Instant File Initialization ist ein Sicherheitsprivileg, dass standardmäßig nur Administratoren zugewiesen ist. Die lokalen Sicherheitsrichtlinien wird durch den Start von [secpol.msc] ausgeführt. Image may be NSFW. Clik here to view.
In einem englischsprachigen System heißt die oben gezeigte Richtlinie “Perform Volume Maintenance Tasks” und findet sich in [User Rights Assignment]. Ist das Dienstkonto von Microsoft SQL Server dieser Sicherheitsrichtlinie zugeordnet, ist Instant File Initialization für Microsoft SQL Server aktiviert.
Prüfung aus Microsoft SQL Server
In einem Umfeld, in dem ein DBA keinen unmittelbaren Zugang zum Betriebssystem besitzt (Segregation of Duty), gibt es ebenfalls eine Möglichkeit, zu testen, ob das Dienstkonto von Microsoft SQL Server das Privileg besitzt. Das nachfolgende Script startet die Protokollierung und legt eine neu Datenbank mit einer Initialgröße von 1.500 MB (1 GB für Daten und 500 MB für das Protokoll) an.
/* Aktivierung der Protokollierung */
DBCC TRACEON (3004, 3605, -1);
/*
Erstellung einer neuen Datenbank mit einer initialen Größe */
von 1 GB für Daten und 500 MB für das Protokoll
*/
CREATE DATABASE Test
ON PRIMARY
(
NAME = 'Test',
FILENAME = 'S:\BACKUP\Test.mdf',
SIZE = 1000MB,
MAXSIZE = 10000MB,
FILEGROWTH = 0MB
)
LOG ON
(
NAME = 'Test_Log',
FILENAME = 'S:\BACKUP\Test.ldf',
SIZE = 500MB,
MAXSIZE = 500MB,
FILEGROWTH = 0MB
);
GO
-- Auslesen des Fehlerprotokolls von Microsoft SQL Server
EXEC xp_readerrorlog;
Nachdem die neue Datenbank angelegt wurde, zeigt, der Inhalt des ausgelesenen Fehlerprotokolls folgende Einträge: Image may be NSFW. Clik here to view.
Die Zeilen 12 – 18 zeigen, dass “Instant File Initialization” für das Dienstkonto NT Service\MSSQL$SQL_2012 nicht zur Verfügung steht; die Datendatei wurde mittels “Zeroing” mit 0 gefüllt. Für das Anlegen einer Datenbankdatei für die TEST-Datenbank benötigte das System 17 Sekunden (Zeile 12 – 13). Für das Anlegen der Protokolldatei wurden 9 Sekunden benötigt. Insgesamt werden für das Erstellen der Datenbank [Test] 26 Sekunden benötigt. Das Protokoll für das Erstellen der Datenbank bei Zuweisung des Rechts für das Dienstkonto sieht wie folgt aus: Image may be NSFW. Clik here to view.
Deutlich ist zu erkennen, dass – wie bereits oben ausgeführt – ausschließlich die Protokolldatei den Prozess des “Zeroing” über sich ergehen lassen muss. Da die Datenbankdatei unmittelbar erstellt wurde, ist die Datenbank innerhalb von 9 Sekunden betriebsbereit.
Instant File Initialization bei neuen Datenbanken
Wie die Tests demonstrieren, liegt der Vorteil bei der Anlage von neuen Datenbanken in der Bereitstellung der Datenbank innerhalb weniger Sekunden. In der Regel ist das Erstellen von neuen Datenbanken – sofern es nicht aus Applikation selbst geschieht – kein zeitkritischer Vorgang.
Instant File Initialization bei Wiederherstellung von Datensicherungen
Die Wiederherstellung einer Datenbank kann ein zeitkritisches Problem werden, wenn zum Beispiel die Produktionsdatenbank betroffen ist. Exakt dieser Umstand wurde einem Kunden zum Opfer, der eine Datenbank mit einer Dateigröße für die Daten von 750 GB wiederherstellen musste. Tragisch an dieser Situation war, dass die Datenbank selbst nur zu ca. 50% mit Daten gefüllt war. Die Wiederherstellung verzögert sich um die Zeit, die für das “Zeroing” benötigt wird. Dieser Vorgang kann jedoch eingespart werden, da die Daten unmittelbar nach der Initialisierung in die neu angelegten Datenbank-Dateien geschrieben werden.
Instant File Initialization beim Vergrößern von Datenbanken
In vielen Microsoft SQL Server Installationen ist auffällig, dass die Vergrößerung einer Datenbank entweder sehr klein gewählt wurde (1 MB) oder aber – für den angegebenen Workload – zu groß. Entscheidend für beide Szenarien ist, dass bei fehlendem Recht für “Instant File Initialization” die Anwendungen unverhältnismäßig lange warten müssen, bis das “Zeroing” abgeschlossen ist. Ist ein hoher Workload in der Applikation erkennbar, wird bei einem Vergrößerungsintervall von 1 MB zu oft die Datenbank erweitert und ein “Zeroing” initiiert. Bei einer Größe von 500 MB müsste die Applikation ca. 9 Sekunden warten, bis der Prozess abgeschlossen ist. Für eine Anwendung, die fast ausschließlich auf hohe OLTP-Vorgänge beschränkt ist, ein absolutes K.O-Kriterium.
Testmessungen
Um ein Gefühl für die Zeitunterschiede zu vermitteln, wurde eine Testdatenbank mit verschiedenen Größen und auf verschiedenen Medien erstellt. Für die Messungen wird die Testdatenbank in vier unterschiedlichen Größen (100 MB, 500 MB, 1.000 MB, 2.000 MB) mit identischer Größe für die Protokolldatei (100 MB) sowohl auf einer HDD als auch einer SDD erstellt. Eine Messung erfolgt mit jeweils aktivierter (IFI) und deaktivierter (No IFI) Berechtigung. Für die Messungen wurden als HDD eine [Toshiba MK5061GSY] mit einer Blockgröße von 64 KBytes verwendet. Für die Tests auf der SSD wurde eine [CRUCIAL CT960M500] verwendet, die ebenfalls eine Blockgröße von 64 KBytes verwendet.
Klar erkennbar ist, dass – unabhängig von HDD und/oder SDD die Erstellungszeiten fast proportional wachsen, wenn “Instant File Initialization” nicht möglich ist. Bei Aktivierung sind die Zeiten nahezu identisch, da die Größe der Protokolldatei in allen Tests identisch ist. Die Variationen rühren wohl eher aus Streuungen!
Bei einer 500 GB großen Datenbank würde die Erstellung auf einer HDD bei deaktiviertem “Instant File Initialization” nahezu 3 Stunden benötigen! Selbst auf einer SSD müssen immer noch ca. 35 Minuten vergehen, bevor die Datenbank online ist. Da in der Regel neue Datenbanken eher in kleineren Dimensionen erstellt werden, sind diese Zeiten wohl eher zu vernachlässigen. Jedoch sieht es bei der Wiederherstellung von Datenbanken ganz anders aus. Müssen erst die erstellten Datenbankdateien mittels “Zeroing out” überschrieben werden, kann so eine nicht unerhebliche Zeit für die Wiederherstellung vergehen. Ein Umstand, der in einer Produktionsumgebung bei entsprechendem SLA schnell zu Problemen führen kann.
Zeitweise Deaktivierung von Instant File Initialization
Die durchgeführten Tests kann man auf dem eigenen Datenbanksystem selbst durchspielen, ohne die Berechtigung immer wieder zu ändern (und damit einen Neustart des Dienstes durchzuführen). Sollte auf den eigenen Systemen Instant File Initialization aktiviert sein, so kann man diese Option zeitweilig mit dem Traceflag 1806 zwischenzeitlich deaktivieren. Der Vorteil bei der Verwendung dieses Traceflags besteht vor allen darin, dass Datenbanken, die einer höhere Sicherheitsstufe unterliegen, bei der Erstellung einem “Zeroing out” unterzogen werden.
Einschränkungen von Instant File Initialization
Instant File Initialization unterliegt besonderen Einschränkungen, die im Vorfeld geprüft werden müssen. So ist Instant File Initialization nur möglich, wenn die nachfolgenden Voraussetzungen erfüllt sind:
Es handelt sich nicht um eine Protokolldatei (*.ldf)
Instant File Initialization gibt dem DBA die Möglichkeit, Datenbankoperationen, die einen unmittelbaren Einfluss auf die Eigenschaften der Datenbankdateien besitzen, durch “Instant File Initialization” zu beschleunigen. Bevor man Instant File Initialization aktiviert, sollte das betriebliche Umfeld sehr genau geprüft werden. Insbesondere ein Blick auf das Dateisystem in Verbindung mit der Sensibilität der Daten ist ein wichtiges Kriterium für die Aktivierung / Deaktivierung. In größeren Unternehmen gibt es eine klare Trennung von Aufgaben (Segregation of Duty). Ein Gespräch mit dem verantwortlichen Administrator für das Betriebssystem schafft schnell Klarheit.
Insgesamt kommt die Bedeutung von Instant File Initialization nicht bei der Erstellung von “neuen” Datenbanken zum tragen. Vielmehr ist bei zeitkritischen Wiederherstellungsszenarien diese Option von größerer Bedeutung. Auch bei Applikationen, die sehr viele Daten schreiben und somit die Datenbank regelmäßig vergrößern müssen, ist Instant File Initialization ein Gewinn für die Geschwindigkeit und Stabilität der Applikation.
Sollten die Richtlinien des Unternehmens Instant File Initialization verhindern, so wäre aus meiner Sicht das Recht zu erteilen und der SQL Server Dienst mit dem Traceflag 1806 zu starten. Somit wäre Instant File Initialization grundsätzlich nicht möglich – es wäre aber für einen Administrator im Falle einer Wiederherstellung einer großen Datenbank aus einem Backup möglich, für diese Operation Instant File Initialization zeitweise zu aktivieren.
Welche Strategie ist am besten geeignet, um Daten, die temporär für weitere Aufgaben benötigt werden, zu speichern und zu verwalten? Mit Microsoft SQL Server 2000 wurden zum ersten Mal Tabellenvariablen als Erweiterung eingeführt. Die Arbeit mit temporären Tabellen war zu diesem Zeitpunkt bereits Alltag und jeder Datenbankprogrammierer hat diese bewährte Technik verwendet. Mit der Einführung von Tabellenvariablen wird alles besser – so dachte man zumindest. Dieser Artikel zeigt die Unterschiede zwischen beiden technischen Möglichkeiten.
Temporäre Tabellen
Temporäre Tabellen sind, wie der Name es bereits sagt, “temporär”. Temporäre Tabellen unterscheiden sich technisch nicht durch normale Tabellen, die sich in den Datenbanken befinden. Temporäre Tabellen können jederzeit erstellt und gelöscht werden. Daten können in temporären Tabellen beliebig hinzugefügt, geändert oder gelöscht werden. Der Unterschied zu “normalen” Relationen besteht in zwei wesentlichen Punkten, die nachfolgend etwas näher beleuchtet werden sollen.
Temporäre Tabellen befinden sich in der TEMPDB
Die Datenbank TEMPDB ist eine Systemdatenbank von SQL Server, die für JEDEN Benutzer einer Datenbank verfügbar ist. In dieser globalen Systemdatenbank werden alle temporären Benutzerobjekte angelegt. Diese Datenbank wird bei jedem Start von Microsoft SQL Server neu angelegt und beginnt somit mit einer “sauberen Arbeitsfläche”. Temporäre Relationen unterscheiden sich durch folgende Merkmale von normalen Relationen:
Temporäre Tabellen beginnen IMMER mit “#” (lokal) oder “##” (global)!
Temporäre Tabellen werden automatisch gelöscht, sobald sie nicht mehr verwendet werden!
Globale temporäre Relationen sind für ALLE Datenbanksitzungen verfügbar!
Lokale temporäre Tabellen
Lokale temporäre Tabellen sind nur innerhalb der Datenbanksitzung gültig, in der sie erstellt wurde. Lokale temporäre Tabellen beginnen immer mit einem Hashtag (“#”). Wird die Datenbankverbindung getrennt, werden lokale temporäre Objekte, die während dieser Datenbanksitzung erstellt wurden, automatisch gelöscht.
-- Gültigkeitsbereich von temporären Tabellen
USE tempdb;
GO
-- Erstellen von zwei Testbenutzern in TEMPDB
CREATE USER User1 WITHOUT LOGIN;
CREATE USER User2 WITHOUT LOGIN;
GO
-- Im Kontext von Benutzer User 1 ausführen
EXECUTE AS USER = 'User1'
CREATE TABLE #User1_Table
(
Id int NOT NULL IDENTITY,
spid int NOT NULL DEFAULT (@@spid),
myText varchar(100) NOT NULL DEFAULT (suser_sname())
)
GO
INSERT INTO #User1_Table DEFAULT VALUES
GO 10
SELECT * FROM #User1_Table
REVERT
GO
-- Ausführen als User2
EXECUTE AS USER = 'User2'
SELECT * FROM #User1_Table;
REVERT
GO
Wird der obige Code in EINEM Ausführungsfenster von Microsoft SQL Server Management Studio ausgeführt, kann die vom ersten Benutzer (“User1”) erstellte temporäre Tabelle auch von Benutzer 2 (“User2”) verwendet werden. Die Demonstration zeigt verschiedene Besonderheiten von temporären Tabellen, die bei der Wahl der geeigneten Technologie zu berücksichtigen sind:
Lokale temporäre Tabellen sind nicht auf einen Ersteller begrenzt
Lokale temporäre Tabellen sind nur auf die Verbindung beschränkt
Wird das zweite Codeelement (die Ausführung als “User2”) in einem neuen Abfragefenster von Microsoft SQL Server ausgeführt, wird eine Fehlermeldung ausgelöst, die besagt, dass eine temporäre Tabelle mit dem Namen [#User1_Table] nicht vorhanden ist.
Msg 208, Level 16, State 0,
Line 4 Ungültiger Objektname '#User1_Table'.
Globale temporäre Tabellen
Globale temporäre Tabellen weisen im Unterschied zu lokalen temporären Relationen als erste Zeichen im Namen zwei Hashtags („##“) auf. Eine globale temporäre Tabelle ist für JEDE Sitzung sichtbar. Globale temporäre Tabellen werden gelöscht, wenn ALLE Sitzungen, in denen auf diese Tabelle verwiesen wurde, die Verbindung mit der Instanz von SQL Server getrennt haben. Verwendet man obiges Script und ändert man die Definition der temporären Tabelle zu einer globalen Tabelle, kann in einer zweiten Session ohne Probleme auf diese Tabelle und deren Daten zugegriffen werden. Dieses Verhalten zeigt einen Schwachpunkt am Konzept der temporären Relationen:
Globale temporäre Tabellen können von allen Datenbankbenutzern uneingeschränkt verwendet werden.
Es gibt KEINE Möglichkeit, diese Berechtigungen mittels DENY oder REVOKE einzuschränken.
Globale temporäre Tabellen werden gelöscht, wenn keine Session mehr existiert, die auf die temporäre Tabelle zugegriffen hat
Aus diesen Gründen gilt es, bereits im Vorfeld eines Einsatzs von globale Tabellen eine Risikobewertung durchzuführen. Bei sensiblen und vertraulichen Daten sollte möglichst auf die Verwendung von globalen temporären Relationen verzichtet werden!
Temporäre Tabellen und Indexierung
Temporäre Tabellen verhalten sich wie “gewöhnliche” Datenbanktabellen. Das schließt ein, dass sie indiziert werden können und das Statistiken gepflegt werden. Der folgende Beispielcode erstellt eine temporäre Tabelle mit 1.010 Datensätzen. Anschließend wird die Tabelle indexiert und eine Abfrage auf den Index ausgeführt.
USE tempdb;
GO
-- Erstellen einer Demo-Tabelle
CREATE TABLE ##customer_country
(
customer_id INT NOT NULL IDENTITY (1, 1),
CCode CHAR(3) NOT NULL,
c1 CHAR(2000) NOT NULL DEFAULT ('just a filler')
);
-- Erstellen eines Clustered Index auf Customer_Id
CREATE UNIQUE CLUSTERED INDEX cix_customer_Id ON ##customer_country (customer_id);
-- Erstellen eines Index auf CCode
CREATE INDEX ix_CCode ON ##customer_country (CCode);
-- 1000 Datensätze für Deutschland sowie 10 Datensätze für Österreich eintragen
SET NOCOUNT ON;
GO
INSERT INTO ##customer_country(CCode) VALUES ('DE');
GO 1000
INSERT INTO ##customer_country(CCode) VALUES ('AT');
GO 10
SET NOCOUNT OFF;
GO
SELECT * FROM ##customer_country WHERE CCode = 'AT';
SELECT * FROM ##customer_country WHERE CCode = 'DE';
Image may be NSFW. Clik here to view.
Der Ausführungsplan zeigt, dass für beide Abfragen unterschiedliche Strategien (und Indexe) verwendet werden.
Vorsicht ist angeraten, wenn innerhalb einer Stored Procedure eine temporäre Tabelle erstellt und nachträglich Metadaten der Tabelle geändert werden! Das folgende Beispiel verdeutlicht das Problem. Innerhalb der Stored Procedure wird der Clustered Index mit Hilfe eines Constraints (Primary Key) erstellt und im Anschluss wird ein zusätzlicher non clustered Index erstellt.
CREATE PROC dbo.demo
AS
SET NOCOUNT ON;
DECLARE @i INT = 1;
-- Erstellen der temporären Tabelle
CREATE TABLE #master_table
(
Id int NOT NULL IDENTITY(1, 1),
Customer char(89) NOT NULL DEFAULT ('a new customer'),
c2 DATE NOT NULL DEFAULT (GETDATE())
);
-- Eintragen von 100 Datensätzen
WHILE @i <= 100
BEGIN
INSERT INTO #master_table DEFAULT VALUES;
SET @i += 1;
END
-- Provokation eines RECOMPILE, da Metadaten der Tabelle geändert werden!
ALTER TABLE #master_table ADD CONSTRAINT pk_master_table PRIMARY KEY CLUSTERED (Id);
-- Erstellung eines nonclustered Index ohne RECOMPILE!
CREATE INDEX ix_master_table ON #master_table (c2);
-- Auswahl eines beliebigen Datensatzes
SELECT * FROM #master_table AS MT WHERE Id = 10;
-- Löschen der temporären Tabelle
DROP TABLE #master_table;
SET NOCOUNT OFF;
GO
In einem Profiler Trace wird während der Ausführung der Prozedur protokolliert, wann ein RECOMPILE stattfindet. Image may be NSFW. Clik here to view.
Die Abbildung zeigt, innerhalb der Prozedur jedes Mal ein RECOMPILE ausgelöst wird, wenn die ALTER TABLE-Zeile ausgeführt wurde. Hierbei handelt es sich um eine Schemaänderung die zur Folge hat, dass ursprüngliche – mögliche – Ausführungspläne im Zusammenhang mit der temporären Tabelle verworfen werden müssen. Ebenfalls erkennbar ist, dass das Hinzufügen eines Indexes nicht automatisch zu einem RECOMPILE führt; ein neuer Index bedeutet nicht, dass sich Metadaten der Tabelle selbst ändern!
Temporäre Tabelle und Statistiken
Statistiken sind für adäquate Ausführungspläne unerlässlich. Wenn Microsoft SQL Server keine oder veraltete Statistiken zu einem Index besitzt, kann sich das negativ auf die Ausführung von Abfragen auswirken. Das folgende Beispiel zeigt den Zusammenhang zwischen – notwendigen – Statistiken und geeigneten Ausführungsplänen.
-- Erstellen der temporären Tabelle
CREATE TABLE ##customer_country
(
customer_id INT NOT NULL IDENTITY(1, 1),
CCode CHAR(3) NOT NULL,
C1 CHAR(2000) NOT NULL DEFAULT ('only a filler')
);
GO
SET NOCOUNT ON;
GO
-- 1.000 Datensätze für Deutschland eintragen
INSERT INTO ##customer_country (CCode) VALUES ('DE');
GO 1000
-- 10 Datensätze für Österreich eintragen
INSERT INTO ##customer_country (CCode) VALUES ('AT');
GO 10
-- Erstellung des Clustered Index ...
CREATE UNIQUE CLUSTERED INDEX cix_customer_id ON ##customer_country (customer_id);
-- Erstellen des non clustered index auf CCode
CREATE INDEX ix_CCode ON ##customer_country (CCode);
GO
Zunächst wird eine temporäre Tabelle erstellt und insgesamt 1.010 Datensätze mit einer unterschiedlichen Verteilung von Daten (CCode) hinzugefügt. Basierend auf Statistiken kann Microsoft SQL Server die geeigneten Ausführungspläne für die jeweiligen Abfragen nach den unterschiedlichen Länderkennzeichen ermitteln und umsetzen:
-- Auswahl aller Kunden in Österreich!
SELECT * FROM #customer_country AS CC WHERE CCode = 'AT';
-- Auswahl aller Kunden in Deutschland!
SELECT * FROM #customer_country AS CC WHERE CCode = 'DE';
Image may be NSFW. Clik here to view.
Die Abbildung zeigt verschiedene Ausführungspläne. Eine Abfrage nach dem Länderkennzeichen “AT” führt zu einem INDEX SEEK mit einer Schlüsselsuche. Für Microsoft SQL Server ist diese Variante die effektivste Variante, da Microsoft SQL Server “weiß”, dass es nur 10 Datensätze sind. Die zweite Abfrage nach dem Länderkennzeichen “DE” ist kostenmäßig für einen INDEX SEEK zu teuer. Microsoft SQL Server kann auf Grund der bekannten Zeilenzahl von 1.000 Datensätzen das erforderliche IO berechnen und entscheidet sich für einen INDEX SCAN!
Temporäre Tabellen und Transaktionen
Temporäre Tabellen verhalten sich in konkurrierenden Systemen wie herkömmliche Tabellen. Wie bereits weiter oben erwähnt, gibt es lokale und globale temporäre Tabellen. Während lokale temporäre Tabellen nur innerhalb einer Session sichtbar sind, verhält es sich mit globalen temporären Tabellen anders. Dieser Umstand birgt Gefahren in Form von Sperren, die in einer Applikation auftreten können. Im vorherigen Beispiel wurde eine globale temporäre Tabelle verwendet. Wird nun in einer Session ein Update auf Zeilen in der Tabelle ausgeführt, wird auf diese Zeilen eine Sperre gesetzt:
-- Transaktion beginnen
BEGIN TRANSACTION Demo;
-- Aktualisierung aller Länderkennzeichen von Österreich
UPDATE ##customer_country
SET CCode = 'FR'
WHERE CCode = 'AT';
-- Welche Sperren wurden auf das Objekt gesetzt
SELECT I.name AS index_name,
DTL.resource_type,
DTL.resource_description,
DTL.request_mode
FROM sys.dm_tran_locks AS DTL INNER JOIN sys.partitions AS P
ON P.hobt_id = DTL.resource_associated_entity_id INNER JOIN sys.indexes AS I
ON I.OBJECT_ID = P.OBJECT_ID AND I.index_id = P.index_id
WHERE DTL.resource_database_id = DB_ID() AND
DTL.request_session_id = @@SPID
ORDER BY
request_session_id,
resource_associated_entity_id
-- Transaktion bleibt geöffnet und in einer zweite Session wird versucht
-- auf die Ressourcen mittels SELECT zuzugreifen
ROLLBACK TRANSACTION;
Image may be NSFW. Clik here to view.
In der Abbildung ist zu erkennen, dass sowohl Teile des Clustered Index [cix_customer_id] als auch des Index auf das Feld CCode von Microsoft SQL Server blockiert werden. Auf die Datensätze selbst (KEY) wird eine eXklusive Sperre gesetzt. Die Intent eXclusive Sperren dienen nur einer möglichen Lock-Eskalation”. Wird in einer zweiten Sitzung versucht, auf die Daten der Kunden aus dem Land “AT” zuzugreifen, werden so lange keine Daten geliefert, bis die Sperren der aktualisierenden Transaktion wieder aufgehoben werden.
Temporäre Tabellen und Collation
Eine der größten Herausforderungen für Datenbankprogrammierer ist bei Verwendung von temporären Tabellen die Berücksichtigung der eingestellten Collation für Datenbanken und Tabellen. Die Herausforderung besteht darin, dass – ohne explizite Angabe einer Collation – immer die Einstellung der Datenbank TEMPDB verwendet wird. Das folgende Szenario verdeutlicht den Sachverhalt:
-- Welche Collation gibt es für TEMPDB und Server?
SELECT name AS DatabaseName,
collation_name AS DatabaseCollation,
SERVERPROPERTY('Collation') AS ServerCollation
FROM sys.databases AS D
WHERE D.database_id = 2;
Mit Hilfe des obigen Codes werden Informationen über die aktuelle Collation von Server und TEMPDB abgerufen. Da es sich bei TEMPDB um eine Systemdatenbank handelt, sind die Einstellungen identisch.
-- Erstellung einer neuen Datenbank mit unterschiedlicher Collation!
CREATE DATABASE [demo_db]
COLLATE Latin1_General_BIN;
Im Anschluss wird eine neue Datenbank [demo_db] erstellt. Diese Datenbank verwendet eine andere Collation als den Serverstandard.
-- Erstellen einer Tabelle in demo_db mit Standardeinstellungen
USE demo_db;
GO
CREATE TABLE dbo.Customers
(
Customer_Id INT NOT NULL IDENTITY (1, 1),
Customer_Number CHAR(5) NOT NULL,
Customer_Name VARCHAR(255) NOT NULL,
CONSTRAINT pk_Customers_Id PRIMARY KEY CLUSTERED (Customer_Id)
);
GO
-- Zusätzlicher Index auf Customer_Number
CREATE UNIQUE INDEX ix_Customer_Number ON dbo.Customers(Customer_Number)
INCLUDE (Customer_Name);
-- Eintragen von 3 Datensätzen
INSERT INTO dbo.Customers (Customer_Number, Customer_Name)
VALUES
('A0001', 'db Berater GmbH'),
('A0092', 'ABC GmbH'),
('B2345', 'ZYX AG');
GO
In er neuen Datenbank wird eine Tabelle mit 3 Datensätzen angelegt. Da keine explizite Collation für die Textattribute angegeben wurden, wird die Collation der Datenbank [demo_db] übernommen.
-- Information über die Metadaten der Tabelle dbo.Customers
SELECT OBJECT_NAME(C.object_id) AS Table_Name,
C.name AS Column_Name,
S.name AS Type_Name,
C.column_id,
C.max_length,
C.collation_name
FROM sys.columns AS C INNER JOIN sys.types AS S
ON (C.system_type_id = S.system_type_id)
WHERE object_id = object_id('dbo.Customers', 'U');
Image may be NSFW. Clik here to view.
Der nachfolgende Code erstellt eine temporäre Tabelle und eine Kundennummer wird als Suchkriterium eingetragen. Anschließend wird die Tabelle [dbo].[Customer] mit dieser temporären Tabelle mittels JOIN verbunden und die Abfrage ausgeführt.
-- Erstellen einer temporären Tabelle
CREATE TABLE #t (Customer_Number CHAR(5) PRIMARY KEY CLUSTERED);
GO
-- Auswahl aus dem Frontend wird in temporärer Tabelle gespeichert
INSERT INTO #t (Customer_Number) VALUES ('A0001');
GO
-- Auswahl von Datensätzen aus Kundentabelle
SELECT C.*
FROM dbo.Customers AS C INNER JOIN #t AS CN
ON (C.Customer_Number = CN.Customer_Number);
Die Auswahl führt zu einem klassischen Fehler, der darauf hinweist, dass die Collation beider Verbindungsfelder nicht übereinstimmen.
Meldung 468, Ebene 16, Status 9, Zeile 3
Cannot resolve the collation conflict between "Latin1_General_CI_AS" and "Latin1_General_BIN"
in the equal to operation.
Tabellenvariablen
Tabellenvariablen wurden mit Microsoft SQL Server 2000 erstmals eingeführt. Mit dem Einzug von Tabellenvariablen in die Programmiersprache von Microsoft SQL Server sollten viele Dinge vereinfacht.Während temporäre Tabellen während einer ganzen Session gültig sind, beschränkt sich die Gültigkeit von Tabellenvariablen immer auf den BATCH! Einige Vor- und Nachteile als auch hartnäckige Mythen rund um Tabellenvariablen werden nachfolgend beschrieben.
Gültigkeit von Tabellenvariablen
Tabellenvariablen sind nur innerhalb eines Batches gültig. Das bedeutet für die Programmierung, dass in dem Moment, in dem ein GO den Batch abschließt, die Tabellenvariable ihre Gültigkeit verliert. Hier unterscheidet sich die Tabellenvariable nicht von einer herkömmlichen Variable, wie der nachfolgende Code zeigt:
-- Definition einer Tabellenvariablen
DECLARE @customer_country TABLE
(
customer_id INT NOT NULL IDENTITY (1, 1),
CCode CHAR(3) NOT NULL,
c1 CHAR(2000) NOT NULL DEFAULT ('just a filler')
);
-- 100 Datensätze aus Deutschland
DECLARE @I INT = 1;
WHILE @I <= 100
BEGIN
INSERT INTO @customer_country(CCode) VALUES ('DE')
SET @I += 1;
END
-- Anzeige aller Datensätze IM Batch!
SELECT * FROM @customer_country AS CC;
GO
-- Batch ist beendet und Tabellenvariable ist nicht mehr gültig
SELECT * FROM @customer_country AS CC;
GO
Wenn der obige Code vollständig ausgeführt wird, läuft das erste SELECT einwandfrei, da es sich innerhalb des Batches befindet. Nach dem GO werden erneut Daten aus der Tabellenvariable angefordert. Da jedoch mittels GO der vorherige Batch abgeschlossen ist, wird die Tabellenvariable nicht erkannt.
Tabellenvariablen und Indexierung
Bis Microsoft SQL Server 2012 waren Indexe in Tabellenvariablen nicht möglich. Sofern man eine “Indexierung” wünschte, konnte man sich nur über den Umweg eines “Constraints” helfen. Der nachfolgende Code zeigt, wie innerhalb einer Tabellenvariable bis SQL Server 2012 Indexe erstellt werden konnten.
DECLARE @customer_country TABLE
(
customer_id INT NOT NULL IDENTITY (1, 1),
CCode CHAR(3) NOT NULL,
c1 CHAR(2000) NOT NULL DEFAULT ('just a filler'),
PRIMARY KEY CLUSTERED (customer_id),
UNIQUE ix_ccode (CCode)
);
Seit Microsoft SQL Server 2014 ist diese Einschränkung jedoch aufgehoben und man kann nun auch non clustered Indexe innerhalb der Tabellendefinition deklarieren.
DECLARE @customer_country TABLE
(
customer_id INT NOT NULL IDENTITY (1, 1),
CCode CHAR(3) NOT NULL,
c1 CHAR(2000) NOT NULL DEFAULT ('just a filler'),
PRIMARY KEY CLUSTERED (customer_id),
INDEX ix_ccode NONCLUSTERED (CCode)
);
Auf Grund der restriktiven Regeln für Tabellenvariablen ist kein Risiko der Neukompilierung einer Stored Procedure vorhanden. Für Tabellenvariablen gilt:
Tabellenvariablen sind nur im aktuellen Batch gültig
Die vollständige Tabelle muss im DECLARE-Block beschrieben werden
Zusätzliche Indexe oder Attribute sind nach der Deklaration nicht mehr möglich
Es wird die COLLATION der aktuellen Datenbank für Textdatentypen verwendet
TRUNCATE wird nicht unterstützt (DDL-Befehl)
Da außerhalb der Deklaration einer Tabellenvariable keine zusätzlichen Änderungen an der Tabellenvariable erlaubt sind, ist eine Rekompilierung – sofern sie nicht explizit angegeben wird – ausgeschlossen.
Tabellenvariablen und Statistiken
Statistiken werden für temporäre Tabellen NICHT gepflegt. Diese Einschränkung kann bei der Wahl von Tabellenvariablen ein echter Performancekiller werden. Als Beispiel soll folgende Prozedur (Microsoft SQL Server 2014) dienen:
-- Deklaration der Tabellenvariable
DECLARE @customer_country TABLE
(
customer_id INT NOT NULL IDENTITY (1, 1),
CCode CHAR(3) NOT NULL,
c1 CHAR(2000) NOT NULL DEFAULT ('just a filler'),
PRIMARY KEY CLUSTERED (customer_id),
INDEX ix_ccode NONCLUSTERED (CCode)
);
-- 1.000 Datensätze aus Deutschland
DECLARE @I INT = 1;
WHILE @I <= 1000
BEGIN
INSERT INTO @customer_country(CCode) VALUES ('DE')
SET @I += 1;
END
-- 10 Datensätze aus Österreich
DECLARE @I INT = 1;
WHILE @I <= 10
BEGIN
INSERT INTO @customer_country(CCode) VALUES ('AT')
SET @I += 1;
END
-- Abfrage nach Datensätzen aus Deutschland!
SELECT * FROM @customer_country AS CC WHERE CCode = 'DE';
-- Abfrage nach Datensätzen aus Österreich!
SELECT * FROM @customer_country AS CC WHERE CCode = 'AT';
Im Code wird zunächst eine Tabellenvariable deklariert, die mit 1.000 Datensätzen aus Deutschland und 10 Datensätzen aus Österreich gefüllt wird. Bei der Auswahl der Datensätze aus Deutschland – wie auch aus Österreich – werden jedoch die gleichen Ausführungspläne verwendet! Image may be NSFW. Clik here to view.
Die Abbildung zeigt, dass Microsoft SQL Server davon ausgeht, dass nur eine Zeile in der Tabelle steht. Somit ist für Microsoft SQL Server ein INDEX SCAN über den Clustered Index optimaler als ein INDEX SEEK über den Index ix_ccode und eine weitere Schlüsselsuche. Das Problem in diesem Beispiel liegt auf der Hand; Microsoft SQL Server wird NIE den Index verwenden, der auf dem Attribut CCode liegt. Microsoft SQL Server geht grundsätzlich von einem Datensatz aus, der geliefert werden soll. Somit wird einem INDEX SCAN der Vorzug gegeben. Die Ausführung der Abfrage nach Kunden aus Österreich belegt diese Vermutung. Image may be NSFW. Clik here to view.
Noch deutlicher wird die “Nichtverwendung” von Statistiken mit dem nachfolgenden Skript. Die Verwendung der Traceflags bewirkt eine Ausgabe der von Microsoft SQL Server geprüften und verwendeten Statistiken für die Abfrage.
SET NOCOUNT ON;
GO
DECLARE @t TABLE
(
Id INT NOT NULL IDENTITY(1,1),
C1 CHAR(1000) NOT NULL DEFAULT ('a filler only!'),
C2 DATE NOT NULL DEFAULT (GETDATE()),
PRIMARY KEY CLUSTERED (Id)
);
DECLARE @i INT = 1
WHILE @i <= 1000
BEGIN
INSERT INTO @t DEFAULT VALUES;
SET @i += 1;
END
SELECT * FROM @t AS T
WHERE T.Id = 100
OPTION
(
RECOMPILE,
QUERYTRACEON 3604,
QUERYTRACEON 9292,
QUERYTRACEON 9204
);
Mit Traceflag 9292 werden die Statistiken angezeigt, die für den Query Optimizer von Microsoft SQL Server als „interessant“ für den Ausführungsplan eingestuft werden; Traceflag 9204 zeigt, welche Statistiken der Query Optimizer von Microsoft SQL Server vollständig lädt, um einen Ausführungsplan zu generieren. Das Ergebnis ist LEER!
Tabellenvariablen und Transaktionen
Im Gegensatz zu Temporären Tabellen sind Tabellenvariablen nicht transaktionsgebunden. Wird der Wert einer Datenzeile in einer Tabellenvariable innerhalb einer expliziten Transaktion geändert, wird der ursprüngliche Wert bei einem möglichen ROLLBACK nicht wiederhergestellt. Tabellenvariablen speichern UPDATES unabhängig von einer expliziten Transaktion.
DECLARE @t TABLE
(
Id INT NOT NULL IDENTITY(1,1),
C1 CHAR(1000) NOT NULL DEFAULT ('a filler only!'),
C2 DATE NOT NULL DEFAULT (GETDATE()),
PRIMARY KEY CLUSTERED (Id)
);
DECLARE @i INT = 1
WHILE @i <= 10
BEGIN
INSERT INTO @t DEFAULT VALUES;
SET @i += 1;
END
BEGIN TRANSACTION
UPDATE @t
SET c1 = 'AT'
WHERE Id = 1;
ROLLBACK TRANSACTION
SELECT * FROM @t;
GO
Der Vorteil dieses Verfahrens ist natürlich naheliegend; da Tabellenvariablen nicht transaktionsgebunden sind, benötigen sie keine Sperren und müssen kein Transaktionsprotokoll führen – sie sind bei DML-Operationen schneller!
Verwendung von Tabellenvariablen
Bei den vielen Nachteilen, die Tabellenvariablen – offensichtlich – haben, stellt man sich die Frage, wozu man Tabellenvariablen verwenden sollte. Ein sehr großer Vorteil von Tabellenvariablen ist, dass man sie wie ein Array an Funktionen und Stored Procedures übergeben kann. Man kann also zunächst Werte in eine Tabellenvariable füllen um sie anschließend in einer Stored Procedure zu verarbeiten. Der folgende Code zeigt ein mögliches Szenario, das mit Hilfe von Tabellenvariablen gelöst werden kann.
“In einer Applikation können aus einer Liste von Kunden mehrere Kunden ausgewählt werden, um sie anschließend in einem Report auszugeben”
-- Zunächst wird ein neuer Datentyp TABLE angelegt
CREATE TYPE Customer_Id AS TABLE (customer_id int NOT NULL PRIMARY KEY CLUSTERED);
GO
-- Erstellung einer Prozedur für die Auswahl von Kunden
-- Der zu übergebende Datentyp ist der zuvor definierte TYPE
CREATE PROC dbo.proc_CustomerList
@C AS Customer_Id READONLY
AS
SET NOCOUNT ON;
SELECT * FROM dbo.Customer
WHERE Customer_Id IN (SELECT Customer_Id FROM @c);
SET NOCOUNT OFF;
GO
-- Erstellung einer Tabelle vom Typen [Customer_Id]
DECLARE @t AS Customer_Id;
-- Eintragen der gewünschten Kunden-Id
INSERT INTO @t(customer_id)
VALUES (1), (2), (3), (4), (5);
-- Ausführung der Prozedur
EXEC dbo.proc_CustomerList @t;
GO
Tabellenvariablen sind nur im RAM persistent?
Diese Aussage ist so nicht richtig. Selbstverständlich muss für die Erstellung einer temporären Tabelle ebenfalls Speicher in TEMPDB allokiert werden. Hier spielen jedoch andere Faktoren eine Rolle, die zu der Annahme führen könnten, dass Tabellenvariablen nur im RAM vorhanden sind – die Art und Weise, wie Microsoft SQL Server (wie übrigens auch andere RDBMS-Systeme) Transaktionsdaten behandelt. Beim Schreiben von Datensätzen werden – entgegen landläufiger Meinung – die Daten nicht sofort in die Datenbank selbst geschrieben. Vielmehr werden lediglich Transaktionen unmittelbar in das Transaktionsprotokoll geschrieben; die Datenseiten selbst verbleiben zunächst im RAM. Durch einen CHECKPOINT werden die Daten selbst (dirty pages) erst in die Datenbankdatei(en) geschrieben. Mit dem nachfolgenden Skript wird eine Tabellenvariable erzeugt und 100 Datensätze in diese Tabellenvariable geschrieben. Anschließend werden die Daten mit ihrer physikalischen Position in der Datenbank ausgegeben. Nachdem ein CHECKPOINT ausgeführt wurde, werden die Daten unmittelbar in der Datenbankdatei untersucht!
SET NOCOUNT ON;
GO
-- Was steht derzeit im Transaktionsprotokoll?
SELECT * FROM sys.fn_dblog(NULL, NULL);
GO
-- Beginn der Transaktion
BEGIN TRANSACTION
DECLARE @t TABLE
(
Id INT NOT NULL IDENTITY(1,1),
C1 CHAR(1000) NOT NULL DEFAULT ('a filler only!'),
C2 DATE NOT NULL DEFAULT (GETDATE()),
PRIMARY KEY CLUSTERED (Id)
);
-- Eintragen von 100 Datensätzen
DECLARE @i INT = 1
WHILE @i <= 100
BEGIN
INSERT INTO @t DEFAULT VALUES;
SET @i += 1;
END
-- Was wurde in das Transaktionsprotokoll geschrieben?
SELECT * FROM sys.fn_dblog(NULL, NULL);
-- Wo werden die Daten physikalisch gespeichert?
SELECT * FROM @t AS T
CROSS APPLY sys.fn_PhysLocCracker(%%physloc%%) AS FPLC
GO
COMMIT TRANSACTION;
-- Anzeigen der ersten Datenseite,
-- auf der Daten der Tabellenvariable gespeichert sind.
DBCC TRACEON (3604);
DBCC PAGE ('tempdb', 1, 127, 1);
GO
Warum Tabellenvariablen, obwohl es bereits temporäre Tabellen gibt?
Die nachfolgende – nicht abschließende – Liste gibt einen Überblick über die Besonderheiten von Tabellenvariablen.
Tabellenvariablen haben wie lokale Variablen einen definierten Bereich, an dessen Ende sie automatisch gelöscht werden.
Im Vergleich zu temporären Tabellen haben Tabellenvariablen weniger Neukompilierungen einer gespeicherten Prozedur zur Folge.
Transaktionen, dauern nur so lange wie eine Aktualisierung der Tabellenvariable. Deshalb benötigen Tabellenvariablen weniger Ressourcen für Sperren und Protokollierung.
Da Tabellenvariablen einen beschränkten Bereich haben und nicht Bestandteil der persistenten Datenbank sind, sind sie von Transaktionsrollbacks nicht betroffen.
Wann mit Tabellenvariablen oder temporärer Tabelle arbeiten?
Die Antwort auf diese Frage ist recht einfach: “It depends”. Es hängen viele Faktoren von der Entscheidung für die richtige Strategie ab:
Anzahl der Zeilen, die in die Tabelle eingefügt werden.
Anzahl der Neukompilierungen, aus denen die Abfrage gespeichert wird.
Typ der Abfragen und deren Abhängigkeit von Indizes und Statistiken bei der Leistung.
In manchen Situationen ist es nützlich, eine gespeicherte Prozedur mit temporären Tabellen in kleinere gespeicherte Prozeduren aufzuteilen, damit die Neukompilierung für kleinere Einheiten stattfindet. Im Allgemeinen werden Tabellenvariablen verwendet, wenn es um ein kleines Datenvolumen geht! Wenn große Datenmengen wiederholt verwendet werden müssen, hat die Arbeit mit temporären Tabellen deutliche Vorteile. Außerdem können Indizes für die temporäre Tabelle erstellt werden, um die Abfragegeschwindigkeit zu erhöhen. Microsoft empfiehlt zu testen, ob Tabellenvariablen für eine bestimmte Abfrage oder gespeicherte Prozedur geeigneter sind als temporäre Tabellen. Eine Aufzählung von Vor- und Nachteilen temporärer Tabellen und Tabellenvariablen hat Yogesh Kamble in einem “Quiz” gegeben.
Bei der täglichen Arbeit mit Microsoft SQL Server in mittelständischen und großen Unternehmen kommt es immer wieder mal vor, dass Programmcodes in die Testsysteme und Produktionssysteme implementiert werden mussten. Beim durchgeführten Code Review stößt man immer wieder auf die Verwendung einer einfache Notation für die Aufrufe von Prozeduren oder SQL-Abfragen. Insbesondere seit der Trennung von Schemata und Benutzern ist diese “Unart” nicht nur schwieriger zu bearbeiten (aus welchem Schema wird das Objekt aufgerufen?) sondern kann auch gravierende Auswirkungen auf den Plan Cache von Microsoft SQL Server haben.
Warum vollständig qualifizierte Objektverweise?
Aus Sicht des Suchalgorithmus von Microsoft SQL Server nach ausführbaren Objekten ist eine vollständig qualifizierte Notation auf ein Objekt sinnvoll, da Microsoft SQL Server dadurch unmittelbar auf das richtige Schema verwiesen wird, in dem sich das referenzierte Objekt befindet. Hintergrund dafür ist, dass bei unqualifizierten Objekten zunächst im Standardschema des Benutzers nach einem entsprechenden Objekt gesucht wird. Befindet sich ein Objekt im Schema [dbo], muss SQL Server zunächst im Standardschema des Benutzers suchen und – wenn das Objekt dort nicht gefunden wird – im [dbo]-Schema nach dem Objekt suchen. Diese Suchen nach referenzierten Objekten sind immer mit Zeiteinbußen verbunden.
Neben dem oben beschriebenen Effekt kommt aber noch ein anderer nicht zu unterschätzender Effekt zum Tragen; die Speicherung und Wiederverwendung von Ausführungsplänen (Prozedurcache) kann bei vielen Benutzern mit eigenen Schemata über Gebühr beansprucht werden. Die Aufgabe des Prozedurcaches von Microsoft SQL Server ist die Speicherung von Abfrageplänen für eine weitere Verwendung, sofern die gleiche Abfrage erneut aufgerufen wird. Findet sich bereits ein Ausführungsplan im Cache, muss Microsoft SQL Server keinen neuen Plan erstellen sondern kann die Abfrage unmittelbar ausführen. Weitere Informationen zum Prozedurcache finden sich z. B. hier: http://blogs.msdn.com/b/sqlserverfaq/archive/2011/12/12/procedure-cache.aspx
Das nachfolgende Beispiel zeigt den Zusammenhang zwischen der Benutzung von voll qualifizierten Objekten und der Speicherung von Abfrageplänen.
Testumgebung
Zunächst wird eine Testdatenbank alle benötigten Objekte für die Demonstration (Tabellen / Schema / User) angelegt.
-- Erstellen einer Testdatenbank
CREATE DATABASE [demo_db];
GO
-- Anlegen von 3 Benutzern in der Datenbank demo_db!
USE demo_db;
GO
CREATE USER demo_1 WITHOUT LOGIN WITH DEFAULT_SCHEMA = dbo;
CREATE USER demo_2 WITHOUT LOGIN WITH DEFAULT_SCHEMA = dbo;
CREATE USER demo_3 WITHOUT LOGIN WITH DEFAULT_SCHEMA = demo_3;
GO
-- Anlegen eines expliziten Schemas für den Benutzer demo_3!
CREATE SCHEMA [demo_3] AUTHORIZATION demo_3;
GO
-- Anlegen einer Tabelle für den gemeinsamen Zugriff!
CREATE TABLE dbo.foo
(
id int NOT NULL IDENTITY (1, 1),
FirstName nvarchar(20) NOT NULL,
LastName nvarchar(20) NOT NULL,
CONSTRAINT pk_foo PRIMARY KEY CLUSTERED (Id)
)
GO
-- Berechtigungen für ALLE Benutzer einrichten
GRANT SELECT ON dbo.foo TO public;
GO
-- Eintragen von Testdaten
INSERT INTO dbo.foo (FirstName, LastName)
VALUES
('Uwe','Ricken'),
('Max','Muster'),
('Michael','Schumacher'),
('Kimi','Räikkönen');
Abfragen ohne qualifizierte Objektnamen
Sind alle Vorbereitungen abgeschlossen, kann der Prozedurcache für die Datenbank geleert werden (BITTE NICHT IN PRODUKTIONSSYSTEMEN ANWENDEN!)
Um Informationen zum Plancache / Prozedurcache abzufragen, werden seit SQL Server 2005 Dynamic Management Objects (dmo) verwendet. Um zu überprüfen, welche Informationen der aktuellen Datenbank sich derzeit im Plancache befinden, wird die nachfolgende Abfrage verwendet.
SELECT cp.plan_handle,
cp.usecounts,
cp.size_in_bytes,
cp.cacheobjtype,
st.text
FROM sys.dm_exec_cached_plans AS cp
CROSS APPLY sys.dm_exec_sql_text(plan_handle) AS st
WHERE st.dbid=DB_ID() AND
st.text NOT LIKE '%exec_cached_plans%';
Bei der Ausführung von Ad hoc-Abfragen (z. B. konkatenierte Abfragestrings aus einer .NET-Anwendung) wird der Text von SQL Server analysiert und mit den Einträgen im Plancache verglichen. Wird der Abfragetext nicht gefunden, muss ein neuer Abfrageplan erstellt werden, der dann im Plancache abgelegt wird. Selbst kleinste Abweichungen im Text werden als “neu” interpretiert!
-- Abfrage 1
SELECT * FROM foo WHERE id = 3;
GO
-- Abfrage 2 (unterschiedlich)
SELECT * FROM foo
WHERE id = 3;
GO
-- Abfrage 3 (Leerzeichen)
SELECT * FROM foo WHERE id = 3;
GO
Alle drei Abfragen sind – scheinbar – identisch. Dennoch muss Microsoft SQL Server für jede Abfrage eine Speicherungen im Plancache vornehmen, da sich Abfrage 1 von Abfrage 2 z. B. durch den Zeilenumbruch unterscheidet während Abfrage 3 Leerzeichen zwischen dem * besitzt. Selbst Kommentare erzwingen einen neuen Abfrageplan, wenn die Kommentare Bestandteile des auszuführenden Textes sind! Die Ausführung der obigen drei Abfragen wird wie folgt im Plancache gespeichert:
Ein ähnliches Verhalten kann bei der Ausführung von identischen Abfragen unter verschiedenen Benutzerkontexten beobachtet werden.
EXECUTE AS User = 'demo_1'
SELECT * FROM foo WHERE id = 3;
REVERT
GO
EXECUTE AS User = 'demo_2'
SELECT * FROM foo WHERE id = 3;
REVERT
GO
EXECUTE AS User = 'demo_3'
SELECT * FROM foo WHERE id = 3;
REVERT
GO
Der Code wechselt vor jeder Abfrage den Kontext des Benutzers und führt anschließen die Abfrage aus. Interessant ist dabei, dass alle drei Benutzer exakt das gleiche SQL‑Statement ausführen. Ein Blick in den Plancache zeigt ein “seltsames” Verhalten. Image may be NSFW. Clik here to view.
Ein Blick auf das Ergebnis überrascht, da eine IDENTISCHE Abfrage mehrmals im Plancache steht. Während die Benutzer demo_1 und demo_2 als Standardschema [dbo] verwenden, benutzt Benutzer demo_3 sein eigenes Schema als Standard. Um mehr Informationen über die Attribute zum Plancache zu erhalten, verwendet man die Systemview sys.dm_exec_plan_attributes.
Links werden die Attribute des ersten Plans aufgezeigt; auf der rechten Seite finden sich die Informationen zum zweiten Plan. Alle Attribute (bis auf [user_id]) sind identisch. Der Eintrag [user_id] ist im Zusammenhang eher unglücklich gewählt worden. Der Eintrag repräsentiert NICHT wie bei MSDN angegeben wird, die principal_id eines Datenbankbenutzers sondern die [schema_id] aus sys.schemas. Bemerkenswert bei diesem Ergebnis ist, dass sofern man nicht voll qualifizierte Objektnamen verwendet immer das Standardschema des Benutzers Bestandteil des Plans ist. Für den Benutzer demo_3 wurde als Standardschema nicht [dbo] angegeben.
Abfragen mit qualifizierten Objektnamen
Nachdem die Ergebnisse des ersten Tests bekannt sind, wird der Prozedurcache für die Datenbank wieder geleert und die Abfrage, die von allen Benutzern ausgeführt werden soll, geringfügig geändert; es wird nicht nur der Name der Relation angegeben sondern durch die Angabe des Schemas wird das Objekt qualifiziert.
—Löschen des Prozedurcaches
DECLARE@db_id int=db_id();
DBCC FLUSHPROCINDB(@db_id);
EXECUTE AS User = 'demo_1';
SELECT * FROM dbo.foo WHERE id = 3 ;
REVERT;
GO
EXECUTE AS User = 'demo_2';
SELECT * FROM dbo.foo WHERE id = 3 ;
REVERT;
GO
EXECUTE AS User = 'demo_3';
SELECT * FROM dbo.foo WHERE id = 3;
REVERT;
GO
Die Analyse des Plancaches zeigt, dass für alle drei Abfragen der gleiche Abfrageplan verwendet worden ist. Image may be NSFW. Clik here to view.
Grund für dieses Verhalten ist, wie schon im vorherigen Beispiel gezeigt, dass alle Planattribute identisch sind. Durch die explizite Angabe des Schemas, in dem sich das Objekt befindet, kann der Abfrageplan für alle drei Benutzer verwendet werden!
Zusammenfassung
Dass die Verwendung von qualifizierten Objekten nicht nur freundlicher zu lesen ist sondern auch umständliche Suchen des SQL Servers nach dem geeigneten Objekt vermieden werden, sind nur einige Vorteile. Besonders hervorzuheben bleibt jedoch der immense Vorteil bei die Wiederverwendung von Abfrageplänen, da sie nicht mehrfach im Plancache hinterlegt werden müssen. Die Abfragen können optimiert ausgeführt werden und der Speicher von SQL Server dankt es auch noch.
Im Juli 2015 war ich als Sprecher auf dem SQL Saturday in Manchester mit dem Thema “DML deep dive” vertreten. Unter anderem wurde im Vortrag gezeigt, wie Forwarded Records entstehen und welchen Einfluss sie auf Abfragen haben. Das Thema ist recht komplex und kompliziert. Daher soll dieser Artikel die Besonderheiten von Forwarded Records detailliert – und durch Beispiele untermauert – hervorheben.
Interner Aufbau eines HEAPS
Forwarded Records können nur in Heaps auftreten. Als Heap werden Tabellen bezeichnet, die keiner logischen Ordnung nach einem Attribut der Tabelle unterworfen sind. Werden in Heaps Datensätze gespeichert, scannt Microsoft SQL Server die PFS (Page Free Space) und sucht nach Datenseiten, die von der Tabelle allokiert sind. Ist auf einer Datenseite ausreichend Platz für die Speicherung des Datensatzes vorhanden, wird der Datensatz auf der entsprechenden Datenseite gespeichert; sind keine freien Datenseiten mehr vorhanden, werden bis zu acht neue Datenseiten für die Tabelle reserviert und der Datensatz wird auf einer neuen, leeren Datenseite gespeichert.
PFS – Datenseite
PFS-Seiten (Page Free Space) zeichnen den Zuordnungsstatus sowie den belegten Speicherplatz der einzelnen Datenseiten auf. Die PFS-Seite verwaltet jede Datenseite einer Datenbank durch die Belegung von 1 Byte pro Datenseite. Somit können pro PFS-Seite 8.088 Datenseiten verwaltet werden! Ist eine Datenseite zugeordnet und es handelt sich um die Zuordnung zu einem HEAP, wird in der PFS hinterlegt, wie die Datenseite bereits prozentual gefüllt ist. Hierzu werden die ersten beiden Bits gesetzt:
Bit-Wert
Bedeutung
0x00
Die Datenseite ist leer
0x01
Die Datenseite ist bis zu 50% gefüllt
0x02
Die Datenseite ist zwischen 51% und 85% gefüllt
0x03
Die Datenseite ist zwischen 86% und 95% gefüllt
0x04
Die Datenseite ist zwischen 96% und 100% gefüllt
Die Höhe des freien Speicherplatzes einer Datenseite wird ausschließlich für Heap- und Text/Image-Seiten verwaltet. Indexe erfordern keine Verwaltung in der PFS, da die Stelle, an der eine neue Zeile eingefügt werden soll, von den Indexschlüsselwerten abhängig ist und nicht vom möglichen freien Platz auf einer Datenseite.
IAM – Datenseite
Als IAM Datenseite (Index Allocation Map) wird eine Systemdatenseite in Microsoft SQL Server bezeichnet, in der zugehörige Datenseiten EINER Tabelle oder eines Indexes verwaltet werden. Microsoft SQL Server verwendet die IAM Datenseiten für Bewegungen innerhalb eines Heaps. Die Zeilen innerhalb eines Heaps weisen keine bestimmte Reihenfolge auf und die Datenseiten sind nicht verknüpft. Die einzige logische Verbindung zwischen den Datenseiten sind die Informationen, die auf den IAM-Seiten aufgezeichnet sind!
SELECT P.index_id,
P.rows,
SIAU.type_desc,
SIAU.total_pages,
SIAU.used_pages,
SIAU.data_pages,
SIAU.first_page,
SIAU.first_iam_page
FROM sys.partitions AS P
INNER JOIN sys.system_internals_allocation_units AS SIAU
ON (P.hobt_id = SIAU.container_id)
WHERE P.object_id = OBJECT_ID('dbo.demo_table', 'U');
Die Spalte [first_iam_page] aus [sys].[system_internals_allocation_units] verweist auf die erste IAM-Datenseite in der Kette möglicher IAM-Datenseiten, die zur Verwaltung des Speicherplatzes verwendet werden, der dem Heap zugeordnet ist.
FORWARDED RECORDS?
Ein Forwarded Record ist ein Datensatz in einem HEAP, der – bedingt durch eine Aktualisierung – im Volumen so stark anwächst, dass er nicht mehr vollständig auf die ursprüngliche Datenseite passt. Microsoft SQL Server erstellt eine neue Datenseite und speichert den Datensatz auf der neu erstellten Datenseite. Auf der ursprünglichen Datenseite verbleibt ein Eintrag, der auf die neue Adresse/Datenseite verweist. Dieses Verfahren ist einem “Nachsendeantrag der Post” ähnlich. Obwohl Microsoft SQL Server den Datensatz auf einer neuen Datenseite speichert, bleibt die Originaladresse immer noch gültig und ein Update der Position in eventuell vorhandenen Non Clustered Indexes muss nicht ausgeführt werden.
Testumgebung
Für die Demonstration wird eine Tabelle angelegt, in der sich 20 Datensätze befinden. Von diesen 20 Datensätzen wird ein Datensatz durch Aktualisierungen so weit vergrößert, dass der Inhalt des Datensatzes nicht mehr auf eine Datenseite passt; die Daten müssen also auf eine neue Datenseite verschoben werden. Im Ergebnis erzielt man so einen Forwarded Record.
/* Create the demo table for 20 records */
CREATE TABLE dbo.demo_table
(
Id INT NOT NULL IDENTITY (1, 1),
C1 VARCHAR(4000) NOT NULL
);
GO
/* Now insert 20 records into the table */
INSERT INTO dbo.demo_table (C1) VALUES
(REPLICATE('A', 2000)),
(REPLICATE('B', 2000)),
(REPLICATE('C', 2000)),
(REPLICATE('D', 2000));
GO 5
/* On what pages are the records stored? */
SELECT FPLC.*,
DT.*
FROM dbo.demo_table AS DT
CROSS APPLY sys.fn_PhysLocCracker(%%physloc%%) AS FPLC;
GO
Der Code erstellt eine neue Tabelle [dbo].[demo_table] und füllt sie mit 20 Datensätzen. Da jeder Datensatz eine Satzlänge von 2.015 Bytes besitzt, sind die Datenseiten mit 8.060 Bytes zu 100% gefüllt.
Die IAM-Datenseite wird nicht als I/O gewertet, da sie nichts zur Ausgabe beiträgt. Es werden ausschließlich Datenseiten berücksichtigt, die Daten für die Ausgabe bereithalten.
FORWARDED RECORDS generieren
Welcher Prozess erstellt einen Forwarded Record? Ein Forwarded Record kann nur generiert werden, wenn ein Datensatz geändert wird und der auf der Datenseite zur Verfügung stehende Platz nicht mehr ausreicht, den vollständigen Datensatz zu speichern. In diesem Fall muss der Datensatz die ursprüngliche Datenseite verlassen und auf eine neue Datenseite “umziehen”. Der nachfolgende Code erweitert den Wert in C1 von 2.000 Bytes auf 2.500 Bytes für den Datensatz mit der [Id] = 1. Anschließend wird die Transaktion aus dem Transaktionsprotokoll ausgelesen um die einzelnen Transaktionsschritte sichtbar zu machen.
UPDATE dbo.demo_table
SET C1 = REPLICATE('Z', 2500)
WHERE Id = 1;
GO
Nachdem der Inhalt von [C1] von 2.000 Bytes auf 2.500 Bytes angewachsen ist, reicht der vorhandene Platz auf der Datenseite 119 nicht mehr aus. Microsoft SQL Server muss den Datensatz auf eine andere Datenseite verschieben, die ausreichend Platz zur Verfügung stellt, um den Datensatz zu speichern. Was genau während dieser Transaktion passiert, wird mit einem Blick in das Transaktionsprotokoll sichtbar gemacht.
-- Check the transaction log for every single step
SELECT FD.[Current LSN],
FD.Operation,
FD.Context,
FD.AllocUnitName,
FD.[Page ID],
FD.[Slot ID]
FROM sys.fn_dblog(NULL, NULL) AS FD
WHERE FD.Context <> N'LCX_NULL'
ORDER BY
FD.[Current LSN];
GO
Der Inhalt des Transaktionsprotokolls zeigt, dass zunächst Metadaten in der Datenbank angepasst wurden (Zeile 1 – 7). Diese Informationen sind für die Betrachtung irrelevant, da sie lediglich das Erstellen einer Statistik für das Attribut [Id] der Tabelle [dbo].[demo_table] protokolliert haben. Der eigentliche Prozess beginnt in Zeile 8 des Auszugs. In der SGAM-Datenseite wird eine Aktualisierung durchgeführt, da eine neue Datenseite / Extent hinzugefügt wird.
Sobald die neue Datenseite im System bekannt ist, folgt der weitere Ablauf einem fest vordefinierten Muster:
in der PFS wird die neue Seite als “leer” gekennzeichnet (Zeile 9)
Anschließend wird die neue Datenseite in die Verwaltung der IAM-Datenseite der Tabelle [dbo].[demo_table] aufgenommen (Zeile 10)
Die neue Datenseite (0x9C = 156) wird zunächst formatiert (Zeile 11)
um anschließend den ALTEN Datensatz mit der [Id] = 1 auf die neue Datenseite zu schreiben (Zeile 12)
und die PFS-Datenseite zu aktualisieren, da die Datenseite nun <= 50% gefüllt ist (Zeile 13).
Mit dem Verschieben des ALTEN Datensatzes geht einher, dass er nach der Speicherung aktualisiert werden muss (Zeile 14).
Gleiches gilt natürlich für den ursprünglichen Speicherort. Dort wird statt des ursprünglichen Datensatzes lediglich ein Verweis auf den neuen Speicherort geschrieben (Zeile 15)
Bedingt durch die Aktualisierung (aus 2.000 Bytes werden nun 8 Bytes) muss auch die PFS-Datenseite erneut aktualisiert werden; schließlich ist Seite 119 nun nicht mehr zu 100% gefüllt (Zeile 16)
FORWARDED RECORS erkennen
Forwarded Records können nur in Heaps auftreten und haben ähnliche Auswirkungen auf das I/O wie fragmentierte Indexe. Ein Forwarded Record bedeutet erhöhtes I/O, da von der Original-Datenseite, auf der der Datensatz gespeichert wurde, nur noch ein Verweis auf den tatsächlichen Speicherort zeigt. Damit wird Microsoft SQL Server gezwungen, das Lesen zunächst auf der verwiesenen Datenseite fortzusetzen. Es gilt also, rechtzeitig festzustellen, ob – und wie viele – Forwarded Records in einem Heap existieren.
SELECT DDIPS.index_id,
DDIPS.index_type_desc,
DDIPS.page_count,
DDIPS.record_count,
DDIPS.min_record_size_in_bytes,
DDIPS.max_record_size_in_bytes,
DDIPS.forwarded_record_count
FROM sys.dm_db_index_physical_stats
(
DB_ID(),
OBJECT_ID('dbo.demo_table', 'U'),
0,
NULL,
'DETAILED'
) AS DDIPS;
GO
Die Analyse des Index zeigt, dass nunmehr 6 Datenseiten im Heap vertreten sind. Besonders interessant ist, dass – obwohl kein neuer Datensatz hinzugefügt wurde – ein weiterer Datensatz hinzugekommen ist. Das liegt daran, dass der Verweis auf den neuen Speicherort innerhalb von Microsoft SQL Server wie ein normaler Datensatz behandelt wird. Das hier ein “besonderer” Datensatz gespeichert wird, erkennt man an der [min_record_size_in_bytes], die bei 9 Bytes liegt. Alle eingetragenen Datensätze haben eine Länge von 2.015 Bytes! Die Spalte [forwarded_record_count] weist darauf hin, dass es einen Datensatz gibt, der so groß ist, dass er mit seinem Volumen nicht mehr auf die ursprüngliche Datenseite passt.
FORWARDED RECORDS lesen
Ein Forwarded Record kann einen erheblichen Einfluss auf das IO für eine Abfrage haben wie das nachfolgende Beispiel zeigt. Es wird exakt die gleiche Abfrage ausgeführt wie bereits weiter oben beschrieben. Zu erwarten wäre ein IO von 6 Datenseiten wie die Statistik des Heaps in der obigen Abbildung vermuten lässt; das Ergebnis ist überraschend:
-- INDEX SCAN über 6 Datenseiten?
SET STATISTICS IO ON;
SELECT * FROM dbo.demo_table AS DT;
SET STATISTICS IO OFF;
GO
Insgesamt muss für die Abfrage auf 7 Datenseiten zugegriffen werden; und diese Zahl ist – basierend auf der internen Struktur – vollkommen in Ordnung wie die nachfolgende Abbildung demonstriert!
Wenn ein Heap gelesen wird, gibt es keinen Index, an dem sich die “Leserichtung” oder “Sortierung” orientieren kann. Zunächst wird durch einen Zugriff auf die IAM-Datenseite festgestellt, welche Datenseiten durch den Heap allokiert wurden. Durch den Forwarded Record ist – intern – eine weitere Datenseite hinzugekommen. Microsoft SQL Server “weiß” durch das Lesen der IAM-Datenseite, dass die Datenseiten 119, 121, 126, 127, 142 und 156 gelesen werden müssen. Das sind die Datenseiten, die durch den Heap belegt werden.
Mit dem ersten IO wird die Datenseite 119 gelesen. Während die Datensätze von 119 gelesen werden, trifft Microsoft SQL Server auf einen Forwarded Record und liest diesen Datensatz von der “neuen” Adresse auf Seite 156 (2. IO). Nachdem der Forwarded Record gelesen wurde, wird mit Seite 121 fortgefahren: 121 –> 126 –> 127 –> 142 -> 156! Microsoft SQL Server muss Datenseite 156 zwei Mal lesen! Beim ersten Lesevorgang ist ausschließlich der Forwarded Record betroffen. Er ist initial für den Zugriff auf Datenseiten 156. Die Reihenfolge der Lesezugriffe sieht wie folgt aus:
119 –> 156 –> 121 –> 126 –> 127 –> 142 –> 156 = 7 IO.
Die Lesevorgänge können durch den Aufruf von Sperren, die durch Microsoft SQL Server beim Lesen gesetzt werden, transparent gemacht werden. Der nachfolgende Code zeigt für jeden Zugriff die gesetzten und freigegebenen Sperren in der Tabelle.
-- make locks and releases visible!
DBCC TRACEON (3604, 1200, -1);
SELECT * FROM dbo.demo_table AS DT;
DBCC TRACEOFF(3604, 1200, -1);
GO
Die aktivierten Traceflags bewirken, dass verwendete Sperren und Freigaben in SSMS protokolliert werden. Das Ergebnis stellt sich wie folgt dar:
Process 60 acquiring IS lock on OBJECT: 6:245575913:0 (class bit0 ref1) result: OK
Process 60 acquiring IS lock on PAGE: 6:1:119 (class bit0 ref1) result: OK
Process 60 acquiring S lock on RID: 6:1:119:0 (class bit0 ref1) result: OK
Process 60 acquiring IS lock on PAGE: 6:1:156 (class bit0 ref1) result: OK
Process 60 acquiring S lock on RID: 6:1:156:0 (class bit0 ref1) result: OK
Process 60 releasing lock on RID: 6:1:156:0
Process 60 releasing lock on PAGE: 6:1:156
Process 60 releasing lock on RID: 6:1:119:0
Process 60 releasing lock on PAGE: 6:1:119
Process 60 acquiring IS lock on PAGE: 6:1:121 (class bit0 ref1) result: OK
Process 60 releasing lock on PAGE: 6:1:121
Process 60 acquiring IS lock on PAGE: 6:1:126 (class bit0 ref1) result: OK
Process 60 releasing lock on PAGE: 6:1:126
Process 60 acquiring IS lock on PAGE: 6:1:127 (class bit0 ref1) result: OK
Process 60 releasing lock on PAGE: 6:1:127
Process 60 acquiring IS lock on PAGE: 6:1:142 (class bit0 ref1) result: OK
Process 60 releasing lock on PAGE: 6:1:142
Process 60 acquiring IS lock on PAGE: 6:1:156 (class bit0 ref1) result: OK
Process 60 releasing lock on PAGE: 6:1:156
Process 60 releasing lock on OBJECT: 6:245575913:0
Nachdem eine IS-Sperre auf die Tabelle gesetzt wurde, wird die Datenseite 119 gelesen. Hierbei wird gleich beim ersten Datensatz eine Zeilensperre angewendet, um anschließend auf die neue Datenseite zu gelangen, auf der sich der Datensatz als „Forwarded Record“ befindet. Sobald der Datensatz gelesen wurde, wird die Sperre auf Datenseite 156 und anschließend auf Datenseite 119 wieder aufgehoben und der Prozess liest alle anderen Datenseiten. Die Datenseite 156 muss zwei Mal gelesen werden, da die zu lesenden Datenseiten über die IAM festgelegt waren.
Herzlichen Dank fürs Lesen!
Während der Erstellung eines Artikels für SIMPLE-TALK kam ein sehr interessanter Aspekt in Bezug auf HEAPS in Verbindung mit non-clustered Indexen in den Fokus: „Wird beim Neuaufbau eines non-clustered Index die Position des Forwarded Record im non-clustered Index gespeichert oder die ursprüngliche Adresse?. Was ein Forwarded Record ist und welchen Einfluss ein Forwarded Record auf die Performance haben kann, beschreibt der Artikel “Forwarded Records Intern”. Der nachfolgende Artikel geht auf die Verwaltung von non-clustered Indexen in einem Heap ein.
Testumgebung
Für den Artikel wird eine Tabelle [dbo].[Customer] erstellt, die mit 1.000 Datensätzen* gefüllt wird. Die Tabelle hat folgenden – einfachen – Aufbau:
CREATE TABLE dbo.Company
(
Id INT NOT NULL IDENTITY (1, 1),
Name VARCHAR(200) NOT NULL,
Street VARCHAR(100) NOT NULL,
ZIP VARCHAR(10) NOT NULL,
City VARCHAR(100) NOT NULL
);
GO
CREATE NONCLUSTERED INDEX ix_Company_ZIP ON dbo.Company (ZIP);
GO
Non-Clustered Index in einem Heap
Ein Non-Clustered Index in einem Heap folgt einer anderen Vorgehensweise als in einem Clustered Index. Während ein non-clustered Index in einem Clustered Index nur den Clusterschlüssel speichert, muss der non-clustered Index für einen Heap immer die absolute Position des Datensatzes speichern, da eine Suche über einen Schlüssel nicht möglich ist. Die nachfolgende Abbildung zeigt die unterschiedliche Indexstruktur für einen Heap und für einen Clustered Index.
Die linke Abbildung zeigt den Index [ix_company_zip], wie er in einem Heap gespeichert ist. Der ZIP-Code ist der Indexschlüssel und im Attribut [HEAP RID (key)] wird der Verweis zum Datensatz im Heap gespeichert. Da ein Heap keine Ordnungsschlüssel besitzt, kann der Datensatz nur über die absolute Position angesprochen werden. Die RID ist ein Binärwert mit einer Länge von 8 Bytes, in dem die Datenseite, Dateinummer und Slot ID gespeichert sind. Die ersten vier Bytes speichern die Datenseite während die restlichen vier Bytes für Dateinummer und Slot ID verwendet werden:
Vollkommen anders sieht es jedoch aus, wenn der non-clustered Index [ix_company_zip] in einem clustered Index implementiert wird. Statt einer Referenz zu einer absoluten Position in der Tabelle wird ein Verweis auf den Ordnungsschlüssel gespeichert. Kann der Datensatz [ZIP = 00142] in einem Heap direkt angewählt werden, so muss er in einem Clustered Index über den B-Tree ermittelt werden.
Ein großer Vorteil jedoch ist die Größe des Indexes. Er ist 4 Bytes kleiner, da der Datentyp INT lediglich 4 Bytes konsumiert.
Im Zusammenhang mit Forwarded Records kam die Frage auf, ob bei dem Neuaufbau eines non-clustered Index Microsoft SQL Server die “neue” Adresse des Datensatzes im non-clustered Index speichert. Diese Frage kann man ganz klar mit NEIN beantworten. Mit dem nachfolgenden Code wird zunächst die Datenseite ermittelt, auf dem sich der Datensatz befindet.
-- Auf welcher Datenseite liegt der Datensatz mit ZIP = 00142
SELECT FPLC.*,
C.*
FROM dbo.Company AS C
CROSS APPLY sys.fn_PhysLocCracker(%%physloc%%) AS FPLC
ORDER BY ZIP;
GO
Wird der Datensatz mit der ID = 626 erweitert, so muss es unweigerlich zu einem Forwarded Record kommen. Im Beispiel Fall wird auf der Seite 145 ein FORWARDED_STUB generiert, der auf die neue Datenseite verweist, auf der sich der Datensatz als Forwarded Record befindet.
—Aktualisierung des Datensatzes mit der ID 626
UPDATE dbo.Company
SET Street = 'Das ist eine Strasse mit einem gaaaaaaanz langen Namen'
WHERE ID = 626;
Wie die Abbildung zeigt, wurde der Datensatz mit der ID = 626 auf die Datenseite 337 in Datendatei 1 in Slot 31 verschoben. Diese Position entspricht – in Binärcode umgewandelt – der RID-Position 0x51 01 00 00 01 00 1f 00. Im nächsten Schritt wird der non-clustered Index neu aufgebaut und anschließend die Indexseiten überprüft.
-- Bestehenden Index neu aufbauen
ALTER INDEX ix_Company_ZIP ON dbo.Company REBUILD;
GO
-- Auf welcher Datenseite ist der Datensatz mit ID = 00142 gespeichert?
SELECT *
FROM dbo.Company WITH (INDEX(2))
CROSS APPLY sys.fn_PhysLocCracker(%%physloc%%) AS FPLC
-- Ein Blick auf die Datenseite...
DBCC TRACEON (3604);
DBCC PAGE (demo_db, 1, 1920, 3);
GO
Die Abbildung zeigt, dass trotz Neuaufbau des Index die Position des Datensatzes im Heap nicht geändert wurde. Dieses Ergebnis ist verständlich, wenn man versteht, wie ein Forwarded Record funktioniert.
Ein Forwarded Record gibt den allokierten Speicherplatz nicht auf sondern hinterlässt lediglich einen Verweis auf die neue Position. Tatsächlich wird der Datensatz selbst immer noch auf der Originalseite verwaltet. Ein non-clustered Index in einem Heap speichert – wie oben beschrieben – die exakte Position des Datensatzes und nicht, wie in einem Clustered Index, einen Schlüsselwert. Somit versteht sich das Ergebnis als Konsequenz daraus, dass bei einem Rebuild des Index die Position des Datensatzes im Heap zu keiner Zeit neu positioniert wurde.
Herzlichen Dank fürs Lesen!
*Achtung: Werbung
Für das Befüllen von Testtabellen verwende ich sehr gern den SQL Data Generator von RedGate. Hiermit lassen sich in wenigen Sekunden komplexe Datenmodelle mit Testdaten füllen.
Ich mag es eigentlich nicht, den eigenen Erfolg so demonstrativ in den Vordergrund zu stellen (Ausnahme war die MCM Zertifizierung und der MVP Award). Aber mit dem folgenden Artikel möchte ich nicht verhehlen, dass ein gewisser Stolz beim Schreiben “mitschwingt”. Dieser Artikel soll aber auch dem Einen oder Anderen etwas Mut machen, vielleicht selbst einmal als Sprecher vor einem interessierten Publikum zu stehen. Vielleicht macht dieser Artikel ja Lust darauf.
PASS SUMMIT 2015
Der eine oder andere hatte es sicherlich mitbekommen; ich durfte auf dem PASS Summit 2015 in Seattle (WA) zum ersten Mal mit einer Session dabei sein. Der erste Versuch im Jahre 2014 war nicht vom Erfolg gekrönt. Das es diesmal geklappt hat, lag sicherlich am Thema (ich war wohl der einzige mit diesem Topic) aber auch am Review meiner Abstracts! Hier gilt mein Dank vor allem Brent Ozar (t | w). Brent hat sich meine “Submission Abstracts” angeschaut und mir wertvolle Tipps bezüglich der Formulierung, etc. gegeben. Nun ja – es hat geklappt und das Review Board hat meine Session “Change Data Capture Case Study and Checklist” für den diesjährigen Summit ausgewählt. Am 30.10.2015 um 14:00 (PST) ging es dann auch pünktlich in “Ballroom 6A” los.
Da es sich um den letzten Tag der Konferenz handelte und die Mittagszeit schon rum war, habe ich nicht mit zu vielen Zuhörern gerechnet. Insgesamt haben sich 123 SQL-Experten eingefunden, um meinen Ausführungen zu CDC zuzuhören. Nach 75 Minuten war der “Spuk” vorbei und ich muss eingestehen, dass ich noch nie so viel Spaß auf einer Session hatte, wie diese. Ich zitiere mal aus einem deutschen Film mit Til Schweiger: “Wenn du mit dem Schlitten durch die Stadt fährst, die Fenster auf und die Anlage voll aufgedreht bis zum Anschlag, das ist mehr als nur Auto fahren, das ist ein Gefühl von Freiheit, das ist total geil!”. So ähnlich kam es mir auch vor, nachdem die ersten zwei Minuten etwas holprig auf Grund der Aufregung vergangen waren. Der deutsche Fanclub war ebenso vertreten, wie der Fanclub aus Österreich, Dänemark und England! Ein dickes DANKE an meine FREUNDE aus der SQL Community!
Evaluation
Heute kam dann die Auswertung der Teilnehmer – und die war einfach nur überwältigend. Auf einer Skala von 1 (schlecht) – 3 (sehr gut) habe ich im Durchschnitt bei einer Beteiligung von 23 Teilnehmern eine 2,97 erreicht. Für das erste Mal vor so einer großen Kulisse als “non-native” Speaker finde ich diese Bewertung wirklich super! Ein anderes Highlight im “Bewertungsmarathon” ist diese Passage: “A minimum of twenty attendance and evaluation submissions must be reached to be considered for the top ten sessions.”. Da meine Session von 23 Personen bewertet wurden, nimmt meine Evaluation auf jeden Fall an der “Competition for the TOP 10 Sessions” teil. Insgesamt war für mich weniger die Bewertung “gut” oder “schlecht” interessant als vielmehr die Kommentare der Teilnehmer. Sie geben ein viel detaillierteres Bild von meiner Session als die Auswahl zwischen drei Werten. Folgende Kommentare (Nur ein Auszug) habe ich erhalten:
“A very engaging speaker. Enjoyed thr session throughly in addition to learning CDC.”
“Excellent, fun session. I was stunned that CDC could be made fun.”
“Was a great overview and came away feeling like I can set it up right away. Did a good job showing examples before and after.”
“Great introduction to CDC. The examples were spot on and I feel comfortable now implementing it in my organization. Since I was not aware of this feature at all I am glad I found something useful to bring home from the conference that will make our lives easy”
So – genug Lobhuddelei. Mich hat an den Kommentaren im Tenor gefreut, dass die Teilnehmer neben wichtigen Informationen zum Thema auch SPASS an der Session hatten. Sie haben sich “unterhalten” gefühlt und gemerkt, dass ich die Session nicht einfach “runterspule” sondern mit Engagement und Leidenschaft auf der Bühne stehe! Genau das war mein Gedanke, während ich die Session geplant und immer wieder korrigiert hatte; wenn die Leute lachen, bemerken sie Deine Fehler nicht! :) Ich habe mich über die Bewertungen natürlich gefreut – sie haben gezeigt, dass die vielen Stunden der Vorbereitung lohnenswert waren.
An diesem Punkt möchte ich sehr gerne noch einmal den Aufruf von Andreas Wolter (t |w) ins Gedächtnis bringen: “Die SQL PASS Deutschland sucht Sprecher – Aufruf an alle SQL Server Fachleute”. Ich teile seine Auffassung, dass es schön wäre, wenn sich mehr Sprecher für die örtlichen Usergroups / lokalen Konferenzen / SQL Saturdays / SQL Rally / PASS Summit aus Deutschland finden würden. Ich kenne so viele deutsche SQL Experten von den verschiedenen Usergroups, die das Zeug zu einem genialen Sprecher haben. Leider trauen sich einige nicht oder aber sie haben zu wenig Zeit. Beides ist absolut nachvollziehbar; aber es wäre doch sehr schön, wenn man sich mal einen Ruck gibt und dann – vielleicht – auch mal in Seattle in einem der großen Konferenzräume steht und über ein SQL Thema spricht, das mehr als 100 Leute interessiert.
Das Jahr geht nun zu Ende und aktuell stehen noch zwei interessante Konferenzen zum Thema SQL Server auf meiner Agenda. Zum einen der SQL Saturday in Slovenien (nächste Woche) und die IT Tage Frankfurt mit Schwerpunkt Datenbanken. Insgesamt war das Jahr 2015 durch sehr viele Konferenzen geprägt, von denen ich nicht eine einzige missen wollte.
Deutsche SQL Konferenz
Im Februar ging es mit der “Deutsche SQL Konferenz” in Darmstadt los. Es freut mich wirklich sehr, dass diese Konferenz – in 2015 zum zweiten Mal ausgetragen – gut etabliert hat und auch im Jahr 2016 wieder in Darmstadt interessante Themen zum Microsoft SQL Server bietet. Da Darmstadt für mich quasi ein Heimspiel ist, ging es gemütlich mit dem Auto in weniger als 15 Minuten zum Veranstaltungsort.
SQLRally Nordic
Die SQLRally war auch dieses Jahr sicherlich wieder ein Highlight. Ich erwähne diese Konferenz insbesondere, weil die PASS leider dieses Format eingestellt hat. Ich weiß nicht, was dahinter steckt – aber ich bin davon überzeugt, dass dieses Format in Europa gut angekommen ist. Das beweisen immer wieder die hohen Besucherzahlen. Sehr schade!
SNEK (SQL Server + .NET-Entwicklerkonferenz)
Die SNEK ist eine von Karl Donaubauer initiierte Konferenz, die – wie es der Name schon verrät – sowohl SQL Server als auch .NET-Themen behandelt. Ich kenne Karl schon seit mehr als 20 Jahren. Karl ist seit vielen Jahren MVP für Microsoft Access und seine AEK gehen nun bereits in das 19. Jahr. Die AEK als auch die SEK/SNEK besuche ich schon seit Beginn als Sprecher. Das Format ist einzigartig; Karl veranstaltet die komplette Konferenz auf eigenes Risiko – es sind KEINE Sponsoren vor Ort. Das Konzept ist an Entwickler gerichtet und soll nicht für Marketingveranstaltungen missbraucht werden. Bei Karl habe ich – wenn man es genau nimmt – meine Karriere als Sprecher begonnen.
SQL Saturday(s)
Dieses Jahr war es ein neuer persönlicher Rekord – insgesamt bin ich als Sprecher auf mehr als 15 SQL Saturdays in Europa und USA mit einer eingereichten Session ausgewählt worden. ALLE SQL Saturdays waren außergewöhnlich. Ich kann wirklich jedem SQL Experten empfehlen, diese – für die Besucher – kostenlosen Veranstaltungen zu besuchen. Tolle Lokationen, tolle Sprecher und super nette SQL Experten all around. Einige SQL Saturdays werden mir auch noch in vielen Jahren in Erinnerung bleiben.
WOW – Was Mark Broadbent (w | t) da mit seinem Team in Cambridge organisiert hat. ist einfach der Hammer. Mich hat nachhaltig das Speaker Dinner beeindruckt. Wir hatten die einmalige Gelegenheit, in einem der vielen Colleges in einem Saal zu essen, der selbst Harry Potter hätte erblassen lassen. Dir, lieber Mark und Deinem Team, ein großes Danke für drei unvergleichliche Tage in Cambridge!
Niko Neugebauer hat in Lissabon schon vor vielen Jahren zum ersten Mal überhaupt einen SQL Saturday in Europa veranstaltet. Sein Hang zur Perfektion ist allgemein bekannt. Ich habe mich noch nie so gut “betreut” gefühlt, wie auf den SQL Saturdays, die von Niko Neugebauer veranstaltet wurden. Niko hat es dieses Jahr geschafft, neben Dejan Sarka (w | t) auch Paul White (w | t)aus Neuseeland als PreCon-Speaker zu gewinnen. Die gemeinsame Sonntagsveranstaltung wird mir immer im Gedächtnis bleiben – dort wurde der Twitter-Account-Name @sqlbambi für mich geboren :)
Portland (OR)
Vielen SQL Server Experten ist der SQL Saturday in Portland als “Vorglühen” zum SQL PASS SUMMIT sicherlich wohl bekannt; wird er doch genau eine Woche vor dem Pass Summit ausgetragen. Für mich war der SQL Saturday in Portland aus drei Gründen ein tolles Event:
Es war meine ERSTE Konferenz als Sprecher in den USA
Das Who is Who der europäischen Sprecherelite war vor Ort
Es war eine super tolle Zeit mit den deutschen und österreichischen Kollegen
SQL in the City – Redgate
Ich bin seit einigen Jahren sehr engagiert mit einem fantastischen Team von Redgate in Cambridge verbunden. Für mich persönlich ist aus dieser “Projekt-Verbindung” eine wunderbare persönliche Verbindung zu den Menschen bei Redgate geworden. Ich mag die Leute. Man trifft sie immer wieder auf den vielen SQL Server Konferenzen. Es ist für mich einfach nur schön, meine Zeit mit diesen Leuten zu verbringen. Von diesem Team kam die Anfrage, ob ich nicht bei “SQL in the City” in London und Seattle mit einem Beitrag über Wait Stats dabei sein wolle. Da mussten sie nicht lange fragen :)
In London war ich mit meinem eigenen Beitrag als auch als Ersatz für einen ausgefallenen Kollegen vertreten: Meine Vorträge wurden mit Platz 1 und Platz 2 der Konferenzbeiträge gewertet. WOW!
In Seattle ging es dann mit meinem ursprünglichen Beitrag in die zweite Runde. Auch dieser Vortrag wurde von den Teilnehmern mit Platz 1 in der Wertung belohnt.
Ein großes DANKE an das Team von Redgate dafür, dass sie so großes Vertrauen in mich haben; ein super großes DANKE geht natürlich an “the audience”. War eine tolle Erfahrung für mich und wird auch nie vergessen.
Na ja – was soll man da sagen! Ich war das zweite Mal nach 2014 auf dem PASS Summit und das ERSTE Mal als Sprecher vertreten. Man kann sich sicherlich vorstellen, dass ich ganz schön aufgeregt gewesen war. Ein großes Danke geht an meine “internationalen SQL Friends”, die mir – quasi – das Händchen während meines Vortrags gehalten haben. Mark Broadbent, Regis Baccaro, Oliver Engels, Tillmann Eitelberg, Frank Geisler, … – euch gilt mein großes DANKE. Ihr seid in meiner Session gewesen und wir hatten zwei tolle Wochen miteinander verbracht.
PASS Camp 2015
Das PASS Camp wird jedes Jahr im Lufthansa Training und Conference Center in Seeheim-Jugenheim von der deutschen PASS e.V. veranstaltet. Ich war mit einem neuen Feature von SQL Server 2016 vertreten zu dem ich auch auf der SQL Konferenz 2016 eine Menge zu sagen habe. Das PASS Camp ist aus meiner Sicht EINMALIG. Es verbindet die klassische Konferenz mit Labs, in denen jeder Teilnehmer die vorher besprochenen Topics in einem eigenen Lab ausprobieren kann. Schade, dass ich nur an dem Tag in Seeheim war, an dem mein Vortrag war – aber ich habe derzeit einfach zu viel zu tun!
IT Tage Frankfurt 2015
Bevor für das Konferenzjahr 2015 der Vorhang fällt, werde ich noch zwei Mal in Frankfurt auf den IT-Tagen mit ein paar Sessions vertreten sein. Unter anderem gibt es eine – bereits lange vorher ausverkaufte – ganztägige Veranstaltung zur Analyse von SQL Server Problemen. Veranstaltet werden die IT-Tage Frankfurt von Andrea Held, die eine leidenschaftliche ORACLE-Expertin ist. Da soll man noch mal sagen, dass sich ORACLE nicht mit MS SQL Server verträgt.
Insgesamt bin ich im Jahr 2015 mehr als 40.000 km mit dem Flugzeug, 5.000 km mit der Bahn und 20.000 km mit dem Auto unterwegs gewesen, um auf unzähligen SQL Server Konferenzen und Usergroup-Treffen zu sprechen. Das Jahr geht nun bald zu Ende; die Arbeit wird – leider – noch nicht weniger aber es ist absehbar, dass 2015 wohl zu einem der intensivsten Jahre in und für die SQL Server Community geworden ist.
Euch allen ein schönes Weihnachtsfest und ein erfolgreiches Jahr 2016.
Herzlichen Dank fürs Lesen!
Eine Anfrage im MSDN-Forum von Microsoft mit dem Titel “Issue in shrinking data file” ist der Grund für diesen Artikel. In der Anfrage ging es darum, dass der Fragesteller aus einer sehr großen Datenbank Unmengen von Datensätzen aus diversen Tabellen gelöscht hatte. Anschließend wollte er die Datenbankdatei verkleinern um so mehr Platz auf dem Storage zu schaffen. Jedoch ergaben Überprüfungen des konsumierten / allokierten Speichers, dass trotz des Löschens mehrerer Millionen Datensätze der Speicher nicht als “frei” gekennzeichnet wurde. Beim genaueren Lesen der Fragestellung kam einem Satz besondere Aufmerksamkeit zu Teil: “Also, I noticed that there is three huge tables in the db and these are non-clustered index.”. Damit war eigentlich schon klar, was das Problem des Fragestellers war. Dieser Artikel beschreibt die technischen Hintergründe, warum ein DELETE-Befehl nicht automatisch den allokierten Speicher freigibt.
Was ist ein HEAP?
Als HEAP wird eine Tabelle bezeichnet, die nicht nach Ordnungskriterien sortiert wird. Das bedeutet, dass ein HEAP die Datensätze immer da abspeichert, wo ausreichend Platz in einer Datenseite ist. Ein HEAP genießt den großen Vorteil, dass er – anders als ein Clustered Index oder Nonclustered Index – keine B-Tree-Struktur zur Verwaltung benötigt. Ein HEAP wird ausschließlich durch Datenseiten (Leafs) repräsentiert, die durch eine oder mehrere IAM-Datenseiten (Index Allocation Map) verwaltet werden.
Die obige Abbildung zeigt die Organisationsstruktur eines HEAPS. Die Datenseiten (110 – 152) haben keinen direkten Bezug zueinander und die übergeordnete IAM-Datenseite verwaltet die der Tabelle zugehörigen Datenseiten. Eine IAM-Datenseite kann immer nur EIN Datenbankobjekt (Tabelle, Indexed View) verwalten!
Testumgebung
Um die Fragestellung / Problemstellung des Autors der obigen Anfrage zu reproduzieren, wird in einer Testdatenbank eine Tabelle [dbo].[demo_table] angelegt. Diese Tabelle kann pro Datenseite maximal einen Datensatz speichern, da die Datensatzlänge 8.004 Bytes beträgt. Anschließend werden 10.000 Datensätze in die zuvor angelegte Tabelle eingetragen um die zugewiesene Speicherzuordnung auszuwerten.
-- Create the demo table
CREATE TABLE dbo.demo_table
(
Id INT NOT NULL IDENTITY (1, 1),
C1 CHAR(8000) NOT NULL DEFAULT ('only a filler')
);
GO
-- and insert 10.000 records into this table
SET NOCOUNT ON;
GO
INSERT INTO dbo.demo_table DEFAULT VALUES;
GO 10000
-- show the number of allocated data pages after the insert
SELECT DDPS.index_id,
DDPS.in_row_data_page_count,
DDPS.in_row_used_page_count,
DDPS.in_row_reserved_page_count,
DDPS.row_count
FROM sys.dm_db_partition_stats AS DDPS
WHERE DDPS.object_id = OBJECT_ID(N'dbo.demo_table', N'U');
GO
Wie die Abbildung zeigt, wurden 10.000 Datensätze ([row_count]) in die Tabelle eingetragen. Diese 10.000 Datensätze belegen insgesamt 10.000 Datenseiten ([in_row_data_page_count]).
Löschen von Datensätzen
Nachdem die Datensätze eingetragen wurden, werden im nächsten Schritt 1.000 Datensätze aus der Tabelle gelöscht und erneut der belegte Speicher überprüft. Zum Löschen von Datensätzen wird der DELETE-Befehl im “klassischen” Stil ohne weitere Hints verwendet
-- Delete the last 1,000 records...
DELETE dbo.demo_table WHERE Id >= 9001;
GO
-- and check the allocated space again!
SELECT DDPS.index_id,
DDPS.in_row_data_page_count,
DDPS.in_row_used_page_count,
DDPS.in_row_reserved_page_count,
DDPS.row_count
FROM sys.dm_db_partition_stats AS DDPS
WHERE DDPS.object_id = OBJECT_ID(N'dbo.demo_table', N'U');
GO
Das Ergebnis ist “überraschend”; obwohl 1.000 Datensätze gelöscht wurden, ist der allokierte Speicher nicht freigegeben worden. Image may be NSFW. Clik here to view.
SELECT in HEAP
Die Ursache für dieses Verhalten liegt in der Art und Weise, wie Microsoft SQL Server beim Lesen von Daten aus einem Heap vorgeht. Da ein HEAP kein Ordnungskriterium besitzt, wird auch mit eingesetztem Prädikat immer ein Table Scan ausgeführt; es muss also immer die komplette Tabelle gelesen werden.
SELECT * FROM dbo.demo_table WHERE Id = 10 OPTION (QUERYTRACEON 9130);
Der Lesevorgang in einem HEAP liest zunächst die IAM-Datenseite des betroffenen Objekts. Die IAM-Datenseite muss gelesen werden, da ansonsten Microsoft SQL Server nicht weiß, welche Datenseiten zum Objekt gehören. In einem Clustered Index / Nonclustered Index ist das nicht notwendig, da die Datenseiten Verknüpfungen zu den nachfolgenden / vorherigen Datenseiten besitzen! Hat Microsoft SQL Server die IAM-Datenseite gelesen, kann mit dem Einlesen der allgemeinen Datenseiten begonnen werden. Genau hier liegt aber der “Fehler des Designs”; da der DELETE-Vorgang nicht mit einer Tabellensperre einhergeht, muss Microsoft SQL Server alle Datenseiten auch weiterhin bereitstellen, da ansonsten ein zweiter Vorgang, der einen SELECT auf die Tabelle durchführt, die IAM-Datenseite bereits gelesen haben könnte und somit die zu lesenden Datenseiten bereits kennt. Würde Microsoft SQL Server nun bei einem DELETE-Vorgang diese Datenseiten aus der Zuordnung entfernen, würde der zweite – Lese – Vorgang eine Datenseite anfordern, die nicht mehr existiert.
Dieses „Problem“ ist auch offiziell bei Microsoft bekannt und kann hier nachgelesen werden: https://support.microsoft.com/en-us/kb/913399. Entgegen der Auffassung von Microsoft, dass es sich um einen „bekannten Bug“ handelt, stellt sich dieses Verhalten eher als „Feature“ dar!
Lösung
Um das Problem der Freigabe von allokiertem Datenspeicher zu lösen, gibt es zwei Alternativen:
Neuaufbau der Tabelle
Die erste Möglichkeit besteht darin, die Tabelle selbst mittels REBUILD neu aufzubauen. Hierzu benötigt Microsoft SQL Server – kurzfristig – eine exklusive Sperre auf die Tabelle, um die “alte” Tabelle durch die neu aufgebaute Tabelle zu ersetzen. Zuvor werden die Datenbestände in die “neue” Tabelle transferiert und somit eine neue Struktur geschaffen.
-- Rebuild the table
ALTER TABLE dbo.demo_table REBUILD;
GO
-- and check the allocated space again!
SELECT DDPS.index_id,
DDPS.in_row_data_page_count,
DDPS.in_row_used_page_count,
DDPS.in_row_reserved_page_count,
DDPS.row_count
FROM sys.dm_db_partition_stats AS DDPS
WHERE DDPS.object_id = OBJECT_ID(N'dbo.demo_table', N'U');
GO
Nach dem REBUILD sind die “leeren” Datenseiten gelöscht worden und nur noch Datenseiten mit Datensätzen sind vorhanden.
Löschen von Datensätzen mit TABLOCK-Hinweis
Eine andere – elegantere – Möglichkeit besteht darin, den Löschvorgang mit einer Tabellensperre zu verbinden. Da ein möglicher SELECT-Vorgang die IAM-Datenseite lesen muss, kann durch das Sperren der Tabelle dieser Zugriff ausgeschlossen werden. Ein möglicher SELECT-Befehl kann keine IS-Sperre (Intent Shared) auf das Tabellenobjekt legen, da während des Löschvorgangs eine X-Sperre (Exklusiv) auf dem Tabellenobjekt liegt. Somit wird der SELECT-Befehl solange gesperrt, bis der Löschvorgang abgeschlossen ist. Da in dieser Situation sichergestellt ist, dass niemand lesend auf die Tabelle zugreift, kann Microsoft SQL Server gefahrlos die leeren Datenseiten entfernen!
-- Delete 2,000 records with a TABLOCK hint
DELETE dbo.demo_table WITH (TABLOCK) WHERE Id >= 7001;
GO
-- and check the allocated space again!
SELECT DDPS.index_id,
DDPS.in_row_data_page_count,
DDPS.in_row_used_page_count,
DDPS.in_row_reserved_page_count,
DDPS.row_count
FROM sys.dm_db_partition_stats AS DDPS
WHERE DDPS.object_id = OBJECT_ID(N'dbo.demo_table', N'U');
GO
HEAPS haben eine andere interne Struktur, die viele Vor- aber auch Nachteile besitzt. Viele Aktionen, die als “selbstverständlich” gelten, bergen Tücken. Wenn man aber die Konzepte hinter den Objekten versteht, tappt man nicht in diese Fallen.
Herzlichen Dank fürs Lesen und allen Lesern ein frohes neues Jahr!
Seit Microsoft SQL Server 2012 gibt es eine neue Möglichkeit, die allokierten Datenseiten eines Objekts mit Hilfe der Systemfunktion [sys].[dm_db_database_page_allocations] zu ermitteln. Über die Verwendung habe ich bereits im Artikel “Neue DMF für Aufteilung von Datenseiten” geschrieben. Diese Systemfunktion ist keine offiziell dokumentierte Funktion. Ich benutze diese Funktion sehr gern, da sie – anders als DBCC IND die Daten als Tabelle zurück liefert, dessen Ergebnis durch Prädikate eingegrenzt werden können. Eher durch Zufall ist aufgefallen, dass diese Funktion nicht immer zuverlässig arbeitet.
Verwendung von sys.dm_db_database_page_allocations
sys.dm_db_database_page_allocation wird verwendet, um sich einen Überblick über die durch ein Objekt belegten Datenseiten zu verschaffen. Hierbei handelt es sich um eine Funktion; die grundsätzliche Filterung findet bereits durch die übergebenen Parameter statt. Der grundsätzliche Aufruf der Funktion sieht wie folgt aus:
SELECT *
FROM sys.dm_db_database_page_allocations
(
@DatabaseId SMALLINT,
@TableId INT = NULL,
@IndexId INT = NULL,
@PartitionId BIGINT = NULL,
@Mode NVARCHAR(64) = 'LIMITED'
);
Das Ergebnis dieser Funktion ist eine Tabelle mit allen durch ein Objekt belegten Datenseiten; oder sollte es sein. Der “Fehler” ist mir in einer Demo-Datenbank aufgefallen, die ich als Grundlage eines zu produzierenden Video-Workshops verwende. Diese Datenbank – mit den Fehlern – kann hier heruntergeladen werden: http://1drv.ms/1ZNxEXH. Die betroffene Tabelle ist [dbo].[Customers]. Diese Tabelle – wie alle anderen auch – ist ein HEAP und besitzt 75.000 Datensätze.
Demonstration
Mit den nachfolgenden Skripten wird zunächst ermittelt, wie viele Datenseiten durch die Tabelle [dbo].[Customers] belegt sind. Die einfachste Art der Feststellung ist das gemessene IO für einen TABLE SCAN.
SET STATISTICS IO ON
USE CustomerOrders;
GO
SET STATISTICS IO ON;
GO
SELECT * FROM dbo.Customers AS C;
GO
SET STATISTICS IO OFF;
GO
Ein direkter Vergleich mit den allokierten Datenseiten bestätigt, dass diese Tabelle 682 Datenseiten belegt.
sys.system_internals_allocation_units
Die Systemsicht sys.system_internals_allocation_units ist nur für die interne Verwendung durch Microsoft SQL Server reserviert. Jede Partition einer Tabelle, eines Indexes oder einer indizierten Sicht hat eine Zeile in sys.system_internals_allocation_units, die eindeutig durch eine Container-ID (container_id) identifiziert ist. Die Container-ID besitzt eine 1:1-Zuordnung zur [partition_id] in der Katalogsicht [sys].[partitions], mit der die Beziehung zwischen den in einer Partition gespeicherten Daten der Tabelle, des Indexes oder der indizierten Sicht und den Zuordnungseinheiten bestimmt wird, die zum Verwalten der Daten in der Partition verwendet werden.
SELECT IAU.total_pages,
IAU.used_pages,
IAU.data_pages
FROM sys.partitions AS P INNER JOIN sys.system_internals_allocation_units AS IAU
ON (P.partition_id = IAU.container_id)
WHERE P.object_id = OBJECT_ID(N'dbo.Customers', N'U');
Insgesamt belegt die Tabelle 689 Datenseiten; davon sind 688 REINE Datenseiten und eine IAM-Datenseite belegt. Dass Datenseiten belegt sind, obwohl sie nicht verwendet werden, liegt an dem Umstand, dass Microsoft SQL Server automatisch vollständige Extents belegt, wenn eine Tabelle mehr als 8 Datenseiten belegt! Von den 688 Datenseiten sind 682 Datenseiten mit Daten belegt. Somit stimmt die Ausgabe des SELECT-Befehls. Insgesamt müssen 682 Datenseiten ausgegeben werden.
sys.dm_db_database_page_allocations
Eine Abfrage auf die Systemfunktion sys.dm_db_database_page_allocations zeigt jedoch ein anderes Ergebnis:
SELECT DDDPA.allocation_unit_type_desc,
DDDPA.allocated_page_page_id,
DDDPA.page_free_space_percent
FROM sys.dm_db_database_page_allocations
(
DB_ID(),
OBJECT_ID(N'dbo.Customers', N'U'),
NULL,
NULL,
N'DETAILED'
) AS DDDPA
WHERE DDDPA.is_allocated = 1 AND
DDDPA.page_type = 1
ORDER BY
DDDPA.page_type DESC,
DDDPA.allocated_page_page_id ASC;
GO
Wie man an der obigen Abbildung deutlich erkennen kann, werden NICHT die erwarteten 682 Datenseiten angezeigt sondern es fehlt offensichtlich eine Datenseite. Wird jedoch die Zuordnung der Datenseiten mit DBCC IND überprüft, stimmt die Zuordnung wieder:
Die Differenz von +1 hängt damit zusammen, dass DBCC IND nicht nur die reinen Datenseiten ausgibt sondern zusätzlich die IAM-Datenseiten im Resultat auswirft. Zieht man die IAM-Datenseite vom Ergebnis ab, so verbleiben 682 Datenseiten für die Ausgabe der Datensätze von [dbo].{Customers]. Bei genauerer Betrachtung war im Anschluss erkennbar, dass die allokierte Datenseite 40447 nicht von sys.dm_db_database_page_allocation berücksichtigt wurde.
Die seit Microsoft SQL Server 2012 zur Verfügung gestellte Funktion ist eine große Hilfe für DBA, wenn es darum geht, gezielt eine Liste der allokierten Datenseiten mit Hilfe von Filtern und Sortierungen ausgeben zu lassen. Ist man auf die exakte Anzal der allokierten Datenseiten angewiesen, sollte besser weiterhin mit DBCC IND die Ausgabe gesteuert werden. Im konkreten Fall half der Neuaufbau der Tabelle mit Hilfe eines REBUILDs.
Ein von mir sehr geschätzter Kollege – der SQL Paparazzi der PASS Deutschland – Dirk Hondong (t | w) – hat mich während meines Vortrags bei der PASS Usergroup in Köln gefragt, inwieweit man belegen kann, ob die Änderung der Sortierung einer Spalte eine reine Metadaten-Operation ist oder ob eine solche Änderung auch die Änderung von Daten nach sich zieht. Die Frage fand ich so interessant, dass ich mich gleich an die Untersuchung gemacht hatte, um selbst festzustellen, welche Auswirkungen die Änderung der Sortierung auf die entstehenden Transaktionen haben.
Warum eine Datenbank oder ein Objekt eine Sortierung braucht.
Sobald mit einem Datenbanksystem gearbeitet wird, werden Tabellen angelegt, Daten gespeichert und mit Hilfe von Indexen organisiert. Für die Reihenfolge der Daten in einem Index gibt es zwei grobe Richtungen. Handelt es sich um numerische Werte, ist die Sortierung einer Datenbank oder einer Spalte irrelevant. Sobald jedoch ein Index auf ein nicht numerisches Feld angewendet wird, muss Microsoft SQL Server für die richtige Sortierung der Werte im Index die Datenbank- oder Spaltensortierung berücksichtigen. Das nachfolgende Beispiel zeigt eine Tabelle mit zwei Textspalten. Beide Textspalten haben unterschiedliche Sortierungen und werden mit einem dedizierten Index pro Spalte versehen.
/* Create the demo table with different collations */
CREATE TABLE dbo.demo_table
(
Id INT NOT NULL,
c1 CHAR(1) COLLATE Latin1_General_CI_AS NOT NULL,
c2 CHAR(1) COLLATE Latin1_General_BIN NOT NULL
);
/* Create an index on each different column */
CREATE INDEX ix_demo_table_c1 ON dbo.demo_table (c1);
CREATE INDEX ix_demo_table_c2 ON dbo.demo_table (c2);
GO
/* fill the table with A-Z and a-z */
DECLARE @i INT = 65;
WHILE @i <= 90
BEGIN
INSERT INTO dbo.demo_table (Id, C1, C2)
SELECT @i, CHAR(@i), CHAR(@i)
UNION ALL
SELECT @i + 32, CHAR(@i + 32), CHAR(@i + 32)
SET @i += 1;
END
GO
/* Select the different columns by usage of it's index */
SELECT c1 FROM dbo.demo_table;
SELECT c2 FROM dbo.demo_table;
GO
Für die beiden Abfragen werden die auf den Spalten befindlichen Indexe verwendet und man kann im Ergebnis deutlich erkennen, dass beide Spalten nach unterschiedlichen Mustern sortiert werden.
Die nebenstehende Abbildung zeigt auf der linken Seite die Spalte [c1]. Diese Spalte verwendet eine Sortierung, die Groß- und Kleinschreibung nicht unterscheidet (“_CI_” = Case Insensitive) während die Spalte [c2] eine binäre Sortierung anwendet. Bei der binären Sortierung werden die Werte in der Spalte nach ihrem Binärwert sortiert. Da die Kleinbuchstaben einen höheren Binärwert haben (Großbuchstaben beginnen bei 0x41 und Kleinbuchstaben beginnen bei 0x5B) werden sie erst NACH den Großbuchstaben einsortiert.
Die einstellte Sortierung für eine Spalte, die alphanumerische Werte enthält ist beim Einsatz eines Indexes (egal ob Clustered Index oder Nonclustered Index) ein ausschlaggebendes Moment für die Einordnung der Werte, die in diese Spalte gespeichert werden sollen.
Ändern einer Sortierung
Man kann jederzeit für einzelne Attribute, eine Datenbank oder für den Server die Sortierung ändern. Während die Änderung für Datenbanken und Tabellen nachträglich mit ertragbarem Aufwand verbunden ist, ist die Änderung der Sortierung für den Server mit deutlich mehr Aufwand verbunden. Weitere Informationen zum Ändern der Sortierungen finden sich hier:
Sobald eine Datenbank Daten enthält und es sollen nachträglich Änderungen an der Sortierung vorgenommen werden, dann ist es nur recht, dass man sich Gedanken darüber macht, wie hoch wohl das Transaktionsvolumen dieser Transaktion ist. Sofern es sich nur um Änderungen am Schema handelt (Schemaänderungen), wird das Transaktionsvolumen in einem verträglichen Rahmen bleiben; sollten jedoch die gespeicherten Daten betroffen sein, muss man sich eine entsprechende Strategie zurechtlegen, um die Änderungen sorgfältig zu planen.
Beispielszenario
Um zu prüfen, welche Ressourcen bei der Änderung der Sortierung an der Spalte einer Tabelle beteiligt sind, wird zunächst eine Beispieltabelle mit 1.000 Datensätzen erstellt. Diese Tabelle besitzt – im ersten Beispiel – keine Indexe; es handelt sich also um einen HEAP.
/* Create a HEAP with a few demo data and default collation */
CREATE TABLE dbo.demo_table
(
Id INT NOT NULL IDENTITY(1, 1),
C1 CHAR(3) NOT NULL DEFAULT ('DE'),
C2 CHAR(5) NOT NULL DEFAULT ('12345')
);
GO
/* what is the collation of the string attributes? */
SELECT C.name AS column_name,
S.name AS type_name,
C.max_length AS data_length,
C.collation_name AS collation_name
FROM sys.tables AS T INNER JOIN sys.columns AS C
ON (T.object_id = C.object_Id) INNER JOIN sys.types AS S
ON (
C.user_type_id = S.user_type_id AND
C.system_type_id = S.system_type_id
)
WHERE T.name = 'demo_table'
ORDER BY
C.column_id;
GO
Zunächst wird die Tabelle mit der Sortierung der Datenbank angelegt. Die Abfrage zeigt, welche Sortierung für die einzelnen Spalten verwendet werden (in meinem Beispiel ist es Latin1_General_CI_AS).
Anschließend wird diese Tabelle mit 1.000 Datensätzen gefüllt und die Test können beginnen.
Änderung der Sortierung in HEAP
Die Tabelle besitzt keine Indexe – sie ist ein HEAP. Um für eine Spalte eine Eigenschaft zu ändern, muss mit Hilfe von ALTER TABLE … ALTER COLUMN die Eigenschaft angepasst werden. Das nachfolgende Beispiel verwendet eine explizite Transaktion für diese Anpassungen. Diese explizite Transaktion muss verwendet werden, um nach der Aktion festzustellen, welche Ressourcen durch die Aktion gesperrt/verwendet werden. Gleichfalls kann mit Hilfe einer benannten Transaktion der entsprechende Eintrag im Transaktionsprotokoll gefunden werden (siehe Code).
/* to monitor the behavior of the transaction we wrap it in a named transaction */
BEGIN TRANSACTION ChangeCollation;
GO
ALTER TABLE demo_table ALTER COLUMN C1 CHAR(3) COLLATE Latin1_General_BIN NOT NULL;
GO
-- what resources are blocked by the transaction?
SELECT DTL.resource_type,
DTL.resource_description,
DTL.request_mode,
DTL.request_type,
DTL.request_status,
CASE WHEN DTL.resource_type = N'OBJECT'
THEN OBJECT_NAME(DTL.resource_associated_entity_id)
ELSE NULL
END AS resource_Object_Name
FROM sys.dm_tran_locks AS DTL
WHERE DTL.request_session_id = @@SPID AND
DTL.resource_type != N'DATABASE';
-- what has happend in the transaction log?
SELECT Operation,
Context,
AllocUnitName,
[Page ID],
[Slot ID]
FROM sys.fn_dblog(NULL, NULL)
WHERE [TRANSACTION ID] IN
(
SELECT [Transaction ID]
FROM sys.fn_dblog(NULL, NULL)
WHERE [Transaction Name] = N'ChangeCollation'
) AND
Context != N'LCX_NULL'
ORDER BY
[Current LSN],
[Transaction ID];
COMMIT TRANSACTION ChangeCollation;
GO
Zunächst wird die Sortierung der Spalte [C1] umgestellt. Um festzustellen, welche Ressourcen dabei von Microsoft SQL Server verwendet werden, hilft die Funktion [sys].[dm_tran_locks]. Sie zeigt, welche Ressourcen aktuell von offenen Transaktionen verwendet werden.
Die obige Abbildung zeigt, welche Objekte durch die Transaktion gesperrt sind. Die Beispieltabelle [dbo].[demo_table] wird mit einer SCH-M-Sperre versehen. Hierbei handelt es sich um eine Sperre, die gesetzt werden muss, um Änderungen an den Objekten (Schemata) vornehmen zu können. Ebenfalls ist zu erkennen, dass X-Sperren (Exklusivsperren) auf Datensätzen (KEY) liegen. Da es sich nicht um die Benutzertabelle handelt (die ist mit einer SCH-M-Sperre versehen), kann es sich nur um die drei Systemtabellen handeln, die mit einer IX-Sperre versehen wurden.
Ein Blick in das aktive Transaktionsprotokoll bestätigt diesen Verdacht. Tatsächlich befindet sich im Transaktionsprotokoll lediglich EIN Transaktionseintrag, der im Zusammenhang mit der Änderung der Sortierung steht!
Wie sieht es mit der Änderung der Sortierung aus, wenn die Tabelle ein Clustered Index ist und der Schlüssel selbst eine Textspalte ist? Das nachfolgende Skript erstellt eine Tabelle mit einem Clustered Index auf der Spalte [Id]. In diese Tabelle werden ein paar Datensätze eingetragen um anschließend die Sortierung anzupassen.
CREATE TABLE dbo.demo_table
(
Id CHAR(4) COLLATE Latin1_General_CS_AI NOT NULL PRIMARY KEY CLUSTERED,
C1 CHAR(300) NOT NULL,
C2 CHAR(500) NOT NULL
);
GO
/* Fill the table with a few values */
DECLARE @i INT = 65;
WHILE @i <= 90
BEGIN
INSERT INTO dbo.demo_table (Id, C1, C2)
SELECT CHAR(@i), 'Das ist ein Test', 'Ja, das ist ein Test'
UNION ALL
SELECT CHAR(@i + 32), 'Das ist ein Test', 'Ja, das ist ein Test';
SET @i += 1;
END
GO
Versucht man, nachträglich die Sortierung der Spalte [Id] zu ändern, erhält man einen “klassischen” Fehler, der eindeutig darauf hinweist, dass Sortierungen auf indexierte Spalten nicht anwendbar sind.
Diese Fehlermeldung macht im Zusammenhang mit der Effizienz einer DDL-Operation Sinn. Würde Microsoft SQL Server erlauben, dass Sortierungen in Spalten geändert werden, die von einem Index berücksichtigt werden, dann müsste Microsoft SQL Server die Schemasperre auf dem Tabellenobjekt so lange aufrecht erhalten, bis die geänderte Sortierung in jedem betroffenen Index berücksichtigt wurde. Das bedeutet für die Indexe jedoch eine vollständige Neusortierung, da sich – bedingt durch Groß-/Kleinschreibung, Akzente, etc – die Sortierung der Einträge ändert.
Damit die Schemasperre so schnell wie möglich wieder aufgehoben werden kann, sind solche lang laufenden Transaktionen in Microsoft SQL Server nicht erlaubt! Andere Prozesse können auf die Tabelle nicht zugreifen und die Applikationen müssten warten, bis der Sortiervorgang und Neuaufbau der Indexe abgeschlossen ist.
Zusammenfassung
Die Änderung der Sortierung ist ein DDL-Befehl und setzt voraus, dass die betroffenen Spalten einer Tabelle nicht von Indexen verwendet werden. Die eigentliche Operation der Änderung der Sortierung geht mit minimalem Aufwand, da ausschließlich Metadaten geändert werden.
In einem aktuellen Projekt bin ich auf eine Technik gestoßen, die – LEIDER – noch viel zu häufig von Programmierern im Umfeld von Microsoft SQL Server angewendet wird; Konkatenation von Texten zu vollständigen SQL-Befehlen und deren Ausführung mittels EXEC(). Dieser Artikel beschreibt einen – von vielen – Nachteil, der sich aus dieser Technik ergibt und zeigt einen Lösungsweg, die in den nachfolgenden Beispielen gezeigten Nachteile zu umgehen.
Dynamisches SQL
Unter “dynamischem SQL“ versteht man eine Technik, mit der man SQL-Fragmente mit variablen Werten (meistens aus zuvor deklarierten Variablen) zur Laufzeit zusammensetzt so dass sich aus den Einzelteilen am Ende ein vollständiges SQL Statement gebildet hat. Dieser “SQL-Text“ wird entweder mit EXEC() oder mit sp_executesql ausgeführt. Das nachfolgende Code-Beispiel zeigt die – generelle – Vorgehensweise:
-- Erstellen einer Demotabelle mit verschiedenen Attributen
CREATE TABLE dbo.demo_table
(
Id INT NOT NULL IDENTITY (1, 1),
KundenNo CHAR(5) NOT NULL,
Vorname VARCHAR(20) NOT NULL,
Nachname VARCHAR(20) NOT NULL,
Strasse VARCHAR(20) NOT NULL,
PLZ VARCHAR(10) NOT NULL,
Ort VARCHAR(20) NOT NULL
);
GO
/* Eintragen von 5 Beispieldatensätzen */
SET NOCOUNT ON;
GO
INSERT INTO dbo.demo_table (KundenNo, Vorname, Nachname, Strasse, PLZ, Ort)
VALUES
('00001', 'Uwe', 'Ricken', 'Musterweg 10', '12345', 'Musterhausen'),
('00002', 'Berthold', 'Meyer', 'Parkstrasse 5', '98765', 'Musterburg'),
('00003', 'Beate', 'Ricken', 'Badstrasse 15', '87654', 'Monopoly'),
('00004', 'Emma', 'Ricken', 'Badstrasse 15', '87654', 'Monopoly'),
('00005', 'Udo', 'Lohmeyer', 'Brühlgasse 57', '01234', 'Irgendwo');
GO
Der Code erstellt eine Beispieltabelle und füllt sie mit 5 Datensätzen. Dynamisches SQL wird anschließend wie folgt angewendet:
DECLARE @stmt NVARCHAR(4000);
DECLARE @col NVARCHAR(100);
DECLARE @Value NVARCHAR(100);
SET @Col = N'Nachname';
SET @Value = N'Ricken';
SET @stmt = N'SELECT * FROM dbo.demo_table ' +
CASE WHEN @Col IS NOT NULL
THEN N'WHERE ' + @Col + N' = ' + QUOTENAME(@Value, '''') + N';'
ELSE N''
END
SELECT @stmt;
EXEC sp_executesql @stmt;
SET @Col = N'PLZ';
SET @Value = N'87654';
SET @stmt = N'SELECT * FROM dbo.demo_table ' +
CASE WHEN @Col IS NOT NULL
THEN N'WHERE ' + @Col + N' = ' + QUOTENAME(@Value, '''') + N';'
ELSE N''
END
SELECT @stmt;
EXEC sp_executesql @stmt;
Den deklarierten Variablen werden Parameterwerte zugewiesen und anschließend wird aus diesen Parametern ein SQL-Statement generiert. Dieses SQL-Statement wird im Anschluss ausgeführt und das Ergebnis ausgegeben.
Basierend auf Statistiken generiert Microsoft SQL Server einen Ausführungsplan für die Durchführung einer Abfrage. Wenn Statistiken nicht akkurat/aktuell sind, kann im Ergebnis die Abfrage unperformant sein, da Microsoft SQL Server zum Beispiel zu wenig Speicher für die Durchführung reserviert hat. Wie unterschiedlich Ausführungspläne sein können, wenn Microsoft SQL Server weiß, wie viele Datensätze zu erwarten sind, zeigt das nächste Beispiel:
CREATE TABLE dbo.Addresses
(
Id INT NOT NULL IDENTITY (1, 1),
Strasse CHAR(500) NOT NULL DEFAULT ('Einfach nur ein Füller'),
PLZ CHAR(5) NOT NULL,
Ort VARCHAR(100) NOT NULL
);
GO
CREATE UNIQUE CLUSTERED INDEX cix_Addresses_Id ON dbo.Addresses (Id);
GO
/* 5000 Adressen aus Frankfurt */
INSERT INTO dbo.Addresses (PLZ, Ort) VALUES ('60313', 'Frankfurt am Main');
GO 5000
/* 1000 Adressen aus Darmstadt */
INSERT INTO dbo.Addresses (PLZ, Ort) VALUES ('64283', 'Darmstadt');
GO 1000
/* 100 Adressen aus Hamburg */
INSERT INTO dbo.Addresses (PLZ, Ort) VALUES ('20095', 'Hamburg');
GO 100
/* 100 Adressen aus Erzhausen */
INSERT INTO dbo.Addresses (PLZ, Ort) VALUES ('64390', 'Erzhausen');
GO 100
/* Erstellung eines Index auf ZIP*/
CREATE NONCLUSTERED INDEX ix_Addresses_ZIP ON dbo.Addresses (PLZ);
GO
Der Code erstellt eine Tabelle mit dem Namen [dbo].[Addresses] und füllt sie mit unterschiedlichen Mengen verschiedener Adressen. Während für eine Großstadt wie Frankfurt am Main sehr viele Adressen in der Tabelle vorhanden sind, sind das für ein Dorf nur wenige Datensätze. Sobald alle Datensätze in die Tabelle eingetragen wurden, wird zu Guter Letzt auf dem Attribut [ZIP] ein Index erstellt, um effizient nach der PLZ zu suchen.
/* Beispiel für viele Datensätze */
SELECT * FROM dbo.Addresses WHERE PLZ = '60313';
GO
/* Beispiel für wenige Datensätze */
SELECT * FROM dbo.Addresses WHERE PLZ = '64390';
GO
Die beiden Abfragen erzeugen unterschiedliche Abfragepläne, da – je nach Datenmenge – die Suche durch die gesamte Tabelle effizienter sein kann, als jeden Datensatz einzeln zu suchen.
Die Abbildung zeigt, dass für eine große Datenmenge (5.000 Datensätze) ein Suchmuster über die komplette Tabelle für den Query Optimizer die schnellste Möglichkeit ist, die gewünschten Daten zu liefern. Bei einer – deutlich – kleineren Datenmenge entscheidet sich der Query Optimizer für eine Strategie, die den Index auf [ix_Adresses_ZIP] berücksichtigt aber dafür in Kauf nimmt, dass fehlende Informationen aus der Tabelle entnommen werden müssen (Schlüsselsuche/Key Lookup).
Die entsprechende Abfragestrategie wird unter Zuhilfenahme von Statistiken realisiert. Microsoft SQL Server überprüft die Verteilung der Daten im Index [ix_Addresses_ZIP] und entscheidet sich – basierend auf dem Ergebnis – anschließend für eine geeignete Abfragestrategie.
DBCC SHOW_STATISTICS ('dbo.Addresses', 'ix_Adresses_ZIP') WITH HISTOGRAM;
Im der Testumgebung wird eine Tabelle mit dem Namen [dbo].[Orders] angelegt. Diese Tabelle besitzt 10.000.000 Datensätze, die pro Handelstag die Orders aus einem Internetportal speichert. Dazu werden die Käufe jede Nacht von der Produktionsdatenbank in die Reporting-Datenbank übertragen. Insgesamt sind Bestellungen vom 01.01.2015 bis zum 10.01.2016 in der [dbo].[Orders] gespeichert. Pro Tag kommen 25.000 – 30.000 Bestellungen dazu. Die Tabelle hat folgende Struktur:
CREATE TABLE dbo.Orders
(
Order_Id INT NOT NULL IDENTITY (1, 1),
Customer_No CHAR(5) NOT NULL,
OrderDate DATE NOT NULL,
ShippingDate DATE NULL,
Cancelled BIT NOT NULL DEFAULT (0)
);
GO
CREATE UNIQUE CLUSTERED INDEX cix_Orders_Order_Id ON dbo.Orders(Order_ID);
CREATE NONCLUSTERED INDEX ix_Orders_Customer_No ON dbo.Orders (Customer_No);
CREATE NONCLUSTERED INDEX ix_Orders_OrderDate ON dbo.Orders (OrderDate);
GO
Die – aktuellen – Statistiken für das Bestelldatum (OrderDate) sind bis zum 10.01.2016 gepflegt!
Für die Abfrage(n) aus dieser Tabelle wird eine Stored Procedure mit dem folgenden Code programmiert:
CREATE PROC dbo.proc_SearchOrders
@Search_Shipping BIT,
@Search_Date DATE,
@Additional_Column NVARCHAR(64),
@Additional_Value VARCHAR(64)
AS
BEGIN
SET NOCOUNT ON
DECLARE @stmt NVARCHAR(4000) = N'SELECT * FROM dbo.Orders'
IF @Search_Shipping = 1
SET @stmt = @stmt + N' WHERE ShippingDate = ''' + CONVERT(CHAR(8), @Search_Date, 112) + N''''
ELSE
SET @stmt = @stmt + N' WHERE OrderDate = ''' + CONVERT(CHAR(8), @Search_Date, 112) + N''''
IF @Additional_Column IS NOT NULL
SET @stmt = @stmt + N' AND ' + @Additional_Column + N' = ''' + @Additional_Value + ''''
EXEC sp_executesql @stmt;
SET NOCOUNT OFF;
END
GO
Der Code verwendet dynamisches SQL, um einen ausführbaren Abfragebefehl zu konkatenieren. Dabei werden nicht nur die zu verwendenden Spalten konkateniert sondern auch die abzufragenden Werte werden dynamisch dem SQL-String hinzugefügt. Somit ergibt sich bei der Ausführung der Prozedur je nach Parameter immer ein unterschiedlicher Abfragebefehl wie die folgenden Beispiele zeigen:
Abhängig von den Parametern werden unterschiedliche Abfragebefehle konkateniert. Microsoft SQL Server kann – bedingt durch die unterschiedlichen Kombinationen aus abzufragenden Spalten und abzufragenden Werten – keinen einheitlichen Ausführungsplan für die Abfrage erstellen. Sobald ein ausführbarer SQL Code geringster Abweichungen (Kommentare, Leerzeichen, Werte) besitzt, behandelt Microsoft SQL Server den Ausführungstext wie eine NEUE Abfrage und erstellt für die auszuführende Abfrage einen neuen Ausführungsplan.
Microsoft SQL Server prüft die Daten für den 05.01.2016 im Histogramm und schätzt, dass ca. 26.600 Datensätze zurückgeliefert werden. Diese “Schätzung“ ist sehr nah an den realen Daten und die Abfrage wird mittels INDEX SCAN durchgeführt. Je nach Datum werden immer wieder NEUE Ausführungspläne generiert und im Prozedur Cache abgelegt.
Statistiken werden von Microsoft SQL Server automatisch aktualisiert, wenn mindestens 20% der Daten in einem Index geändert wurden. Sind sehr viele Datensätze in einem Index, dann kann diese Aktualisierung recht lange auf sich warten lassen.
Wenn Microsoft SQL Server mit einem Abfragewert konfrontiert wird, der NICHT in den Statistiken vorhanden ist, dann „schätzt“ Microsoft SQL Server immer, dass sich 1 Datensatz in der Tabelle befindet. Dieses Problem kommt im obigen Beispiel zum tragen. Jeden Tag werden die aktuellsten Orders in die Tabelle eingetragen. In der Tabelle befinden sich 10.000.000 Datensätze. Insgesamt müssten nun 2.000.000 Datenänderungen durchgeführt werden, um die Statistiken automatisch zu aktualisieren.
Die Stored Procedure wird im nächsten Beispiel für den 11.01.2016 aufgerufen. Wie aus der Abbildung erkennbar ist, sind Werte nach dem 10.01.2016 noch nicht in der Statistik vorhanden. Sollten also Werte in der Tabelle sein, dann verarbeitet Microsoft SQL Server die Anfrage wie folgt:
Da der Parameter für das OrderDate im Abfragetext konkateniert wird, wird ein NEUER Abfrageplan erstellt
Bei der Erstellung des Plans schaut Microsoft SQL Server in die Statistiken zum OrderDate und stellt fest, dass der letzte Eintrag vom 10.01.2016 ist
Microsoft SQL Server geht davon aus, dass für den 11.01.2016 lediglich 1 Datensatz in der Datenbank vorhanden ist
Der Ausführungsplan wird für 1 Datensatz geplant und gespeichert
Die Abbildung zeigt den Ausführungsplan in Microsoft SQL Server, wie er für den 11.01.2016 geplant wurde. Es ist erkennbar, dass die geschätzte Anzahl von Datensätzen DEUTLICH unter dem tatsächlichen Ergebnis liegt. Solche Fehleinschätzungen haben in einem Ausführungsplan Seiteneffekte:
Der geplante Speicher für die Ausführung der Abfrage wird niedriger berechnet als er tatsächlich benötigt wird. Da nachträglich kein Speicher mehr allokiert werden kann, werden einige Operatoren die Daten in TEMPDB zwischenspeichern (SORT / HASH Spills)
Nested Loops sind ideal für wenige Datensätze. Ein Nested Loop geht für jeden Datensatz aus der “OUTER TABLE“ in die “INNER TABLE“ und fragt dort über das Schlüsselattribut Informationen ab. Im obigen Beispiel hat sich Microsoft SQL Server für einen Nested Loop entschieden, da das geschätzte IO für einen Datensatz deutlich unter einem INDEX SCAN liegt. Tatsächlich müssen aber nicht 1 Datensatz aus dem Clustered Index gesucht werden sondern 2.701 Datensätze!
Konkatenierte SQL Strings verhalten sich wie Ad Hoc Abfragen. Sie führen dazu, dass der Prozedur/Plan Cache übermäßig gefüllt wird. Ebenfalls geht wertvolle Zeit verloren, da für jede neue Konkatentation ein neuer Plan berechnet und gespeichert werden muss!
Die nächste Abfrage ermittelt – aus den obigen Beispielen – die gespeicherten Ausführungspläne.
SELECT DEST.text,
DECP.usecounts,
DECP.size_in_bytes,
DECP.cacheobjtype
FROM sys.dm_exec_cached_plans AS DECP
CROSS APPLY sys.dm_exec_sql_text (DECP.plan_handle) AS DEST
WHERE DEST.text LIKE ‚%Orders%‘ AND
DEST.text NOT LIKE ‚%dm_exec_sql_text%‘;
Das Ergebnis zeigt, dass die Prozedur zwei Mal aufgerufen wurde. Dennoch musste für JEDEN konkatenierten Ausführungstext ein eigener Plan erstellt und gespeichert werden.
Lösung
Nicht immer kann man auf dynamisches SQL verzichten. Es wird immer Situationen geben, in denen man mit den Herausforderungen von dynamischen SQL konfrontiert wird. In diesen Situationen ist es wichtig, zu verstehen, welchen Einfluss solche Entscheidungen auf die Performance der Abfragen haben. Im gezeigten Fall sind – unter anderem – Statistiken ein Problem. Es muss also eine Lösung geschaffen werden, die darauf baut, dass Pläne wiederverwendet werden können – Parameter! Es muss eine Lösung gefunden werden, bei der zwei wichtige Voraussetzungen erfüllt werden:
Der auszuführende Befehl darf sich nicht mehr verändern
Ein einmal generierter Plan muss wiederverwendbar sein
Beide Voraussetzungen kann man mit leichten Modifikationen innerhalb der Prozedur schnell und einfach erfüllen.
ALTER PROC dbo.proc_SearchOrders
@Search_Shipping BIT,
@Search_Date DATE,
@Additional_Column NVARCHAR(64),
@Additional_Value VARCHAR(64)
AS
BEGIN
SET NOCOUNT ON
DECLARE @stmt NVARCHAR(4000) = N'SELECT * FROM dbo.Orders';
DECLARE @vars NVARCHAR(1000) = N'@Search_Date DATE, @Additional_Value VARCHAR(64)';
IF @Search_Shipping = 1
SET @stmt = @stmt + N' WHERE ShippingDate = @Search_Date';
ELSE
SET @stmt = @stmt + N' WHERE OrderDate = @Search_Date';
IF @Additional_Column IS NOT NULL
SET @stmt = @stmt + N' AND ' + QUOTENAME(@Additional_Column) + N' = @Additional_Value';
SET @stmt = @stmt + N';'
SELECT @stmt;
EXEC sp_executesql @stmt, @vars, @Search_Date, @Additional_Value;
SET NOCOUNT OFF;
END
GO
Nachdem der Prozedur Cache gelöscht wurde, wird die Prozedur erneut in verschiedenen Varianten ausgeführt und die Abfragepläne werden analysiert:
Obwohl nun ein Datum verwendet wird, dass nachweislich noch nicht in den Statistiken erfasst ist, wird dennoch ein identischer Plan verwendet. Ursächlich für dieses Verhalten ist, dass Microsoft SQL Server für beide Ausführungen auf ein identisches Statement verweisen kann – somit kann ein bereits im ersten Durchlauf verwendeter Ausführungsplan angewendet werden. Dieses “Phänomen“ wird Parameter Sniffing genannt. Auch dieses Verfahren hat seine Vor- und Nachteile, die ich im nächsten Artikel beschreiben werde.
Die nächsten zwei Beispiele zeigen die Ausführungspläne mit jeweils unterschiedlichen – zusätzlichen – Kriterien. Es ist erkennbar, dass Microsoft SQL Server nun für beide Ausführungen jeweils unterschiedliche Ausführungspläne verwendet. Unabhängig von dieser Tatsache werden nun aber nicht mehr für jeden unterschiedlichen Kunden EINZELNE Pläne gespeichert sondern der bei der ersten Ausführung gespeicherte Ausführungsplan wiederverwendet. Die nachfolgende Abfrage zeigt im Ergebnis die gespeicherten Ausführungspläne, die für 10 weitere – unterschiedliche – Kundennummern verwendet wurden:
SELECT DEST.text,
DECP.usecounts,
DECP.size_in_bytes,
DECP.cacheobjtype
FROM sys.dm_exec_cached_plans AS DECP
CROSS APPLY sys.dm_exec_sql_text (DECP.plan_handle) AS DEST
WHERE DEST.text LIKE '%Orders%' AND
DEST.text NOT LIKE '%dm_exec_sql_text%';
GO
Statt – wie bisher – für jede Ausführung mit unterschiedlichen Kundennummern einen eigenen Plan zu speichern, kann der einmal generierte Plan verwendet werden.
Zusammenfassung
Dynamisches SQL wird recht häufig verwendet, um mit möglichst einer – zentralen – Prozedur mehrere Möglichkeiten abzudecken. So legitim dieser Ansatz ist, so gefährlich ist er aber, wenn man dynamisches SQL und Konkatenation wie “gewöhnlichen“ Code in einer Hochsprache verwendet. Microsoft SQL Server muss dann für JEDE Abfrage einen Ausführungsplan generieren. Dieser Ausführungsplan beruht auf Statistiken, die für einen idealen Plan benötigt werden. Sind die Statistiken veraltet, werden unter Umständen schlechte Pläne generiert. Sind die Daten regelmäßig verteilt, bietet es sich an, mit Parametern statt konkatenierten SQL Statements zu arbeiten. Durch die Verwendung von Parametern wird einerseits der Plan Cache entlastet und andererseits muss Microsoft SQL Server bei wiederholter Ausführung mit anderen Werten nicht erneut einen Ausführungsplan erstellen und eine Prüfung der Statistiken entfällt.
Wo Licht ist, ist natürlich auch Schatten! Die oben beschriebene Methode bietet sich nur dann an, wenn die Daten regelmäßig verteilt sind. Wenn die abzufragenden Daten in der Anzahl ihrer Schlüsselattribute zu stark variieren, ist auch diese Lösung mangelhaft! Für das Projekt konnten wir mit dieser Methode sicherstellen, dass NEUE Daten geladen werden konnten und Statistiken nicht notwendiger Weise aktualisiert sein mussten. Die Verteilung der Daten ist für jeden Tag nahezu identisch!
Herzlichen Dank fürs Lesen!
Der von mir sehr geschätzte Kollege und Kenner der SQL Server Engine Torsten Strauss kam mit einer sehr interessanten Beobachtung auf mich zu. Dabei ging es um die Frage, wann Statistiken aktualisiert werden, wenn für die Datenbank die entsprechende Option aktiviert ist. Dieser Artikel zeigt, dass es bestimmte Situationen gibt, in denen eine automatische Aktualisierung der Statistiken nicht durchgeführt wird.
Statistiken
Der Abfrageoptimierer verwendet Statistiken zum Erstellen von Abfrageplänen, die die Abfrageleistung verbessern. In den meisten Fällen generiert der Abfrageoptimierer automatisch die erforderlichen Statistiken; in anderen Fällen müssen weitere Statistiken erstellen werden, um optimale Ergebnisse zu erzielen. Statistiken können veraltet sein, wenn die Datenverteilung in der Tabelle durch Datenänderungsvorgänge geändert wird.
Wenn die Option „AUTO_UPDATE_STATISTICS“ aktiviert ist, prüft der Abfrageoptimierer, wann Statistiken veraltet sein könnten, und aktualisiert diese Statistiken, sobald sie von einer Abfrage verwendet werden. Der Abfrageoptimierer stellt fest, wann Statistiken veraltet sein könnten, indem er die Anzahl der Datenänderungen seit des letzten Statistikupdates ermittelt und sie mit einem Schwellenwert vergleicht. Der Schwellenwert basiert auf der Anzahl von Zeilen in der Tabelle oder indizierten Sicht. Pauschal gilt eine Statistik als veraltet, wenn mehr als 20% + 500 Datenänderungen durchgeführt wurden. Weitere Informationen zu den Schwellenwerten finden sich hier: https://support.microsoft.com/de-de/kb/195565.
Hinweis
Im nachfolgenden Artikel werden Traceflags verwendet, die nicht von Microsoft dokumentiert sind. Es wird darauf hingewiesen, dass eigene Beispiele nicht in einer Produktionsumgebung ausgeführt werden. Folgende Traceflags werden in den Codes verwendet:
3604: Aktiviert die Ausgabe von Meldungen in den Client statt ins Fehlerprotokoll
9204: Zeigt die für den Abfrageoptimierer „interessanten“ Statistiken, die geladen werden
9292: Zeigt die Statistiken an, die der Abfrageoptimierer in der Kompilephase für „interessant“ hält
8666: Speichert Informationen über verwendete Statistiken im Ausführungsplan
Testumgebung
Das die obige Aussage bezüglich der Aktualisierung von Statistiken nicht pauschal angewendet werden kann, zeigt das nachfolgende Beispiel. Dazu wird eine Tabelle [dbo].[Customer] angelegt und mit ~10.500 Datensätzen gefüllt. Die Tabelle [dbo].[Customer] besitzt zwei Indexe; zum einen wird ein eindeutiger Clustered Index auf dem Attribut [Id] verwendet und zum anderen wird das Attribut [ZIP] mit einem nonclustered Index versehen.
-- Create the demo table
IF OBJECT_ID(N'dbo.Customer', N'U') IS NOT NULL
DROP TABLE dbo.Customer;
GO
CREATE TABLE dbo.Customer
(
Id INT NOT NULL IDENTITY (1, 1),
Name VARCHAR(100) NOT NULL,
Street VARCHAR(100) NOT NULL,
ZIP CHAR(5) NOT NULL,
City VARCHAR(100) NOT NULL
);
GO
-- and fill it with ~10,000 records
INSERT INTO dbo.Customer WITH (TABLOCK)
(Name, Street, ZIP, CIty)
SELECT 'Customer ' + CAST(message_id AS VARCHAR(10)),
'Street ' + CAST(severity AS VARCHAR(10)),
severity * 1000,
LEFT(text, 100)
FROM sys.messages
WHERE language_id = 1033;
GO
-- than we create two indexes for accurate statistics
CREATE UNIQUE INDEX ix_Customer_ID ON dbo.Customer (Id);
CREATE NONCLUSTERED INDEX ix_Customer_ZIP ON dbo.Customer (ZIP);
GO
-- what statistics will be used by different queries
-- result of implemented statistics
SELECT S.object_id,
S.name,
DDSP.last_updated,
DDSP.rows,
DDSP.modification_counter
FROM sys.stats AS S
CROSS APPLY sys.dm_db_stats_properties(S.object_id, S.stats_id) AS DDSP
WHERE S.object_id = OBJECT_ID(N'dbo.Customer', N'U');
GO
Die Abbildung zeigt, dass für die Tabelle zwei Statistik-Objekte existieren. Insgesamt sind 10.557 Datensätze in der Tabelle und es wurden noch keine weiteren Modifikationen an den Daten vorgenommen. Da der zweite Index nicht eindeutig ist, gilt das Augenmerk der Verteilung der Daten in diesem Index. Dazu wird der folgende T-SQL-Befehl ausgeführt:
-- show the distribution of data in the statistics
DBCC SHOW_STATISTICS ('dbo.Customer', 'ix_Customer_ZIP') WITH HISTOGRAM;
GO
Die Verteilung der Schlüsselwerte ist sehr heterogen. Während für den ZIP-Code „12000“ lediglich ein Eintrag vorhanden ist, sind es für den ZIP-Code „16000“ mehr als 7.500 Datensätze. Abhängig vom zu suchenden ZIP-Code besteht zusätzlich die Gefahr von „Parameter Sniffing“; das soll aber in diesem Beitrag nicht weiter thematisiert werden.
Abfragen
Sobald die Tabelle erstellt wurde, kann mit den Abfragen begonnen werden. Es werden zwei Abfragen auf die Tabelle ausgeführt, die jeweils unterschiedliche Indexe adressieren. Bei den Abfragen gilt die besondere Beachtung dem Umstand, dass sie hoch selektiv sind; sie verwenden einen „=“-Operator für die Suche nach Datensätzen.
DBCC TRACEON (3604, 9204, 9292, 8666);
GO
DECLARE @stmt NVARCHAR(1000) = N'SELECT * FROM dbo.Customer WHERE Id = @Id;';
DECLARE @parm NVARCHAR(100) = N'@Id INT';
EXEC sp_executesql @stmt, @parm, 10;
GO
DECLARE @stmt NVARCHAR(1000) = N'SELECT * FROM dbo.Customer WHERE ZIP = @ZIP;';
DECLARE @parm NVARCHAR(100) = N'@ZIP CHAR(5)';
EXEC sp_executesql @stmt, @parm, '18000';
GO
Die erste Abfrage verwendet den eindeutigen Index [ix_Customer_Id] während die zweite Abfrage einen performanten INDES SEEK auf den Index [ix_Customer_ZIP] ausführt. Die aus den Abfragen resultierenden Ausführungspläne stellen sich wie folgt dar:
Die Abfrage auf eine bestimmte ID in der Tabelle führt IMMER zu einem INDEX SEEK auf dem Index [ix_Customer_ID]. Durch den „=“-Operator in Verbindung mit dem eindeutigen Index ist gewährleistet, dass immer nur ein Datensatz geliefert werden kann.
Nicht eindeutiger Index
Die Abfrage auf einen bestimmten ZIP-Code kann zu unterschiedlichen Ausführungsplänen führen. Welcher Ausführungsplan verwendet wird, hängt von der Distribution der Kardinalitäten ab. Wenn es sich nur um sehr wenige Datensätze handelt, wird ein INDEX SEEK verwendet; sind jedoch die mit einem INDEX SEEK einhergehenden Lookups zu hoch, wird sich der Abfrageoptimierer für einen TABLE SCAN entscheiden. Man kann also beim ZIP-Code von einem „instabilen“ und „nicht vorhersehbaren“ Ausführungsplan sprechen.
Manipulation der Daten
Basierend auf den Statistiken entscheidet sich der Abfrageoptimierer von Microsoft SQL Server für eine entsprechende Ausführungsstrategie. Werden mehr als 20% der Daten einer Statistik (+500) geändert, so wird eine Statistik invalide.
Das nachfolgende Skript fügt weitere 4.000 Datensätze zur Tabelle hinzu. Bei 10.557 bereits in der Tabelle vorhandenen Datensätzen müssen mindestens 2.612 Datensätze geändert / hinzugefügt werden, damit die Statistiken als veraltet gekennzeichnet werden (10.557 * 20% + 500). Mit den hinzugefügten 4.000 Datensätzen ist dieser Schwellwert auf jeden Fall überschritten.
-- now additional 4,000 records will be filled into the table
-- to make the stats invalid!
INSERT INTO dbo.Customer WITH (TABLOCK)
(Name, Street, ZIP, City)
SELECT TOP 4000
'Customer ' + CAST(message_id AS VARCHAR(10)),
'Street ' + CAST(severity AS VARCHAR(10)),
severity * 1000,
LEFT(text, 100)
FROM sys.messages
WHERE language_id = 1033;
GO
Wie in der Abbildung zu erkennen ist, wurden die 4.000 Datenmanipulationen registriert; diese Aktualisierungen verbleiben so lange in den Statistiken, bis sie erneut abgerufen werden und ggfls. aktualisiert werden.
In der Online-Dokumentation von Microsoft SQL Server heißt es: „Bevor der Abfrageoptimierer eine Abfrage kompiliert und einen zwischengespeicherten Abfrageplan ausführt, sucht er nach veraltetenStatistiken. … Vor dem Ausführen eines zwischengespeicherten Abfrageplans überprüft die Database Engine, ob der Abfrageplan auf aktuelle Statistiken verweist.“.
Folgt man der Beschreibung aus der Online-Dokumentation, so müsste bei erneuter Ausführung der zuvor erstellten Abfragen eine Prüfung der Statistiken durchgeführt werden und die Statistiken – auf Grund der Änderungsquote von mehr als 20% – aktualisiert werden.
DBCC TRACEON (3604, 9204, 9292, 8666);
GO
DECLARE @stmt NVARCHAR(1000) = N'SELECT * FROM dbo.Customer WHERE Id = @Id;';
DECLARE @parm NVARCHAR(100) = N'@Id INT';
EXEC sp_executesql @stmt, @parm, 10;
GO
DECLARE @stmt NVARCHAR(1000) = N'SELECT * FROM dbo.Customer WHERE ZIP = @ZIP;';
DECLARE @parm NVARCHAR(100) = N'@ZIP CHAR(5)';
EXEC sp_executesql @stmt, @parm, '18000';
GO
DBCC TRACEOFF (3604, 9204, 9292, 8666);
GO
Die Abbildung zeigt, dass für die erste Abfrage auf die [ID] die Statistiken nicht erneut überprüft wurden. Für die zweite Abfrage wurden die Statistiken erneut überprüft. Im Ergebnis zeigt dieses Verhalten auch die Abfrage nach den Zuständen der Statistiken der betroffenen Tabelle.
SELECT S.object_id,
S.name,
DDSP.last_updated,
DDSP.rows,
DDSP.modification_counter
FROM sys.stats AS S
CROSS APPLY sys.dm_db_stats_properties(S.object_id, S.stats_id) AS DDSP
WHERE S.object_id = OBJECT_ID(N'dbo.Customer', N'U');
GO
Die Statistiken für den Index [ix_Customer_ZIP] wurden aktualisiert und die 4.000 neuen Datensätze sind in der Statistik enthalten. Für den eindeutigen Index [ix_Customer_ID] wurde diese Aktualisierung jedoch nicht vorgenommen. Der Grund für dieses Verhalten ist relativ einfach zu erklären:
Begründung für das Verhalten
Eindeutiger Index
Wen ein eindeutiger Index auf dem Schlüsselattribut abgefragt wird, muss Microsoft SQL Server keine Statistiken bemühen, da IMMER davon ausgegangen werden kann, dass ein gesuchter Wert nur einmal in im Index erscheint. Bei der ersten Ausführung der Abfrage wurde ein NEUER Ausführungsplan generiert. Insofern stimmt die Aussage aus der Online-Dokumentation. Bevor die Abfrage kompiliert und ein Plan generiert werden kann, müssen die Statistiken überprüft werden. Bei der zweiten Ausführung dieser Abfrage lag der Plan bereits vor; warum sollte Microsoft SQL Server hier die Strategie ändern? Da auf Grund der Eindeutigkeit der Indexwerte niemals mehr als ein Datensatz im Ergebnis erscheinen kann, muss der Plan nicht erneut überprüft werden – er ist „stabil“
Nichteindeutiger Index
Bei der zweiten Abfrage sieht die Stabilität des Plans etwas anders aus. Der Index ist nicht als UNIQUE erstellt worden; es können also pro Schlüsselwert mehrere Daten im Index vorhanden sein. Wenn tatsächlich die Anzahl der Datensätze zu einem Schlüsselattribut variieren, dann ist der Plan „instabil“; er ist abhängig von der Anzahl der vorhandenen Datensätze. In diesem Fall trifft die zweite Aussage aus der Online-Dokumentation zu – der Plan muss auf Validität überprüft werden. Dazu gehört das Überprüfen der veralteten Statistiken. Nun stellt Microsoft SQL Server fest, dass die Statistiken Änderungen erfahren haben, die über dem Schwellwert liegen und somit werden die Statistiken vor der Erstellung des Plans aktualisiert.
Zusammenfassung
Statistiken sind bei Performance-Problemen immer ein Punkt, der überprüft werden sollte. Statistiken werden aber – entgegen der Aussage von Microsoft – nicht grundsätzlich aktualisiert, sobald der definierte Schwellwert überschritten ist. Statistiken werden auch nicht durch einen Background-Task aktualisiert. Die Aktualisierung von Statistiken beruht darauf, wie stabil / instabil ein gespeicherter Plan ist. Wird – auf Grund der Stabilität – bei der Ermittlung der Datensätze erkannt, dass sich die Datenmenge nicht verändern kann, kann es passieren, dass Statistiken so lange nicht aktualisiert werden, bis entweder ein bestehender Plan aus dem Cache gelöscht wird oder aber eine Abfrage mit RECOMPILE dazu gezwungen wird, einen neuen Plan zu verwenden.
Während meines Vortrags über “Temporal Tables” auf dem SQL Saturday Rheinland 2016 wurden einige Fragen gestellt, die ich nicht “ad hoc” beantworten konnte, da ich zu den Fragen noch keine ausreichenden Tests gemacht hatte. Mit diesem Artikel beginne ich eine Artikelreihe über “System versioned Temporal Tables”, zu der mich insbesondere Thomas Franz inspiriert hat. Ihm danke ich sehr herzlich für die vielen Fragen, die er mir per Email zugesendet hat.
Hinweis
Diese Artikelreihe befasst sich nicht mit den Grundlagen von “System Versioned Temporal Tables”! Die grundsätzliche Funktionsweise über “System versioned Temporal Tables” kann im Artikel “Temporal Tables” (english) bei Microsoft nachgelesen werden.
NULL oder NOT NULL
Frage: “… kann ein Attribut mit einer NULL-Einschränkung nachträglich eine NOT NULL-Einschränkung erhalten?”
Diese Frage kann man ganz eindeutig beantworten: “It depends!”
Wenn Attribute in einer Tabelle NULL-Werte zulassen, dann muss die abhängige “System Versioned Temporal Table” die gleichen Einschränkungen besitzen. Sollte diese Einschränkung in beiden Tabellen unterschiedlich sein, können Informationen eventuell nicht abgespeichert werden und die Historisierung ist unvollständig. Das folgende Skript erstellt eine Tabelle [dbo].[Customers] sowie die korrespondierende Tabelle für die Speicherung der historischen Daten mit gleichem Namen im Schema [history].
-- Create a dedicated schema for the history data
IF SCHEMA_ID(N'history') IS NULL
EXEC sp_executesql N'CREATE SCHEMA [history] AUTHORIZATION dbo;';
GO
-- Create the base table for the application data
IF OBJECT_ID(N'dbo.Customers', N'U') IS NOT NULL
DROP TABLE dbo.Customers;
GO
CREATE TABLE dbo.Customers
(
Id INT NOT NULL IDENTITY (1, 1),
Name VARCHAR(100) NOT NULL,
Street VARCHAR(100) NOT NULL,
ZIP CHAR(5) NOT NULL,
City VARCHAR(100) NOT NULL,
Phone VARCHAR(20) NULL,
Fax VARCHAR(20) NULL,
EMail VARCHAR(255) NULL,
ValidFrom DATETIME2(0) GENERATED ALWAYS AS ROW START NOT NULL DEFAULT ('2016-01-01T00:00:00'),
ValidTo DATETIME2(0) GENERATED ALWAYS AS ROW END NOT NULL DEFAULT ('9999-12-31T23:59:59'),
CONSTRAINT pk_Customers_ID PRIMARY KEY CLUSTERED (Id),
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
);
GO
-- Create the System Versioned Temporal Table for history data
CREATE TABLE history.Customers
(
Id INT NOT NULL,
Name VARCHAR(100) NOT NULL,
Street VARCHAR(100) NOT NULL,
ZIP CHAR(5) NOT NULL,
City VARCHAR(100) NOT NULL,
Phone VARCHAR(20) NULL,
Fax VARCHAR(20) NULL,
EMail VARCHAR(255) NULL,
ValidFrom DATETIME2(0) NOT NULL,
ValidTo DATETIME2(0) NOT NULL
);
GO
-- Activate System Versioning
ALTER TABLE dbo.Customers
SET
(
SYSTEM_VERSIONING = ON
(HISTORY_TABLE = History.Customers)
);
GO
In der Tabelle können die Attribute [Phone], [Fax] und [Email] leer sein (NULL). Anschließend wird ein Datensatz in die Tabelle [dbo].[Customers] eingetragen. Der einzutragende Datensatz besitzt eine Emailadresse aber Phone und Fax bleiben leer!
INSERT INTO [dbo].[Customers]
(Name, Street, ZIP, City, Phone, Fax, EMail)
VALUES
('db Berater GmbH', 'Bahnstrasse 33', '64390', 'Erzhausen', NULL, NULL, 'info@db-berater.de');
GO
Testszenarien
Die Tabellen- und Datenstruktur lässt unterschiedliche Testszenarien zu, mit denen die Fragestellung analysiert werden kann. Für die Fragestellung werden vier verschiedene Szenarien untersucht:
NULL wird zu NOT NULL in einer leeren Tabelle
NULL wird zu NOT NULL in einer gefüllten Tabelle
NOT NULL wird zu NULL in einer gefüllten Tabelle
NOT NULL wird zu NULL
NULL wird zu NOT NULL in leerer Tabelle
Im ersten Beispiel wird die Einschränkung ohne Inhalt in der Tabelle geändert
BEGIN TRANSACTION;
GO
ALTER TABLE dbo.Customers
ALTER COLUMN [Phone] VARCHAR(20) NOT NULL;
GO
SELECT SCHEMA_NAME(o.schema_id) + N'.' + O.name,
DTL.request_mode,
DTL.request_type,
DTL.request_status
FROM sys.dm_tran_locks AS DTL
INNER JOIN sys.objects AS O
ON (DTL.resource_associated_entity_id = O.object_id)
WHERE DTL.request_session_id = @@SPID
AND DTL.resource_type = N'OBJECT'
GO
COMMIT TRANSACTION;
GO
Der Code wickelt die Änderung innerhalb einer expliziten Transaktion ab, um so die gesetzten Sperren nach der Änderung nachverfolgen zu können. Tatsächlich ist eine Änderung von Einschränkungen in der Tabelle ohne Weiteres möglich, wenn sich noch keine Daten in der “System versioned Temporal Table” befinden.
Die Abbildung zeigt, dass für die Anpassungen in Systemtabellen Änderungen vorgenommen werden müssen. Die beiden Benutzertabellen werden mit einer LCK_M_SCH_M-Sperre versehen. Hierbei handelt es sich um Schemasperren, die verhindern, dass während eines DDL-Prozesses auf die betroffenen Objekte zugegriffen wird. Ohne Fehler wird die Änderung implementiert. Für eine Anpassung muss “System Versioning” nicht deaktiviert werden!
Die Abbildung zeigt, dass die Einschränkungen nicht nur auf [dbo].[Customers] angewendet wurde sondern ebenfalls auf die Tabelle [history].[Customers] übertragen wurde. Identische Metadaten sind essentiell für “System Versioned Temporal Tables”! Dieser Test lässt sich auch in die andere Richtung wiederholen. Solange noch KEINE DATEN in der “System Versioned Temporal Table” vorhanden sind, können NULL-Einschränkungen ohne Probleme auf die Objekte angewendet werden.
NULL wird zu NOT NULL in einer gefüllten Tabelle
Das nächste Szenario muss differenziert betrachtet werden. Für das Beispiel soll das Attribut [Email] verwendet werden. Zunächst wird ein Datensatz in die Tabelle [dbo].[Customers] eingetragen, der eine EMail-Adresse besitzt.
INSERT INTO [dbo].[Customers]
(Name, Street, ZIP, City, Phone, Fax, EMail)
VALUES
('db Berater GmbH', 'Bahnstrasse 33', '64390', 'Erzhausen', NULL, NULL, 'info@db-berater.de');
GO
Anschließend wird versucht, die NULL-Einschränkung für das Attribut [Email] zu ändern:
ALTER TABLE dbo.Customers
ALTER COLUMN [Email] VARCHAR(255) NOT NULL;
GO
Tatsächlich läuft die Änderung ohne Probleme durch und für das Attribut [Email] sind keine NULL-Werte mehr erlaubt. Nachdem für das Attribut [Email] die NULL-Einschränkung erneut geändert wurde (NULL-Werte erlaubt) , wird ein weiterer Datensatz eingetragen, der keine Email-Adresse besitzt:
ALTER TABLE dbo.Customers
ALTER COLUMN [Email] VARCHAR(255) NULL;
GO
INSERT INTO [dbo].[Customers]
(Name, Street, ZIP, City, Phone, Fax, Email)
VALUES
('Microsoft GmbH', 'Musterstrasse 33', '12345', 'Musterhausen', NULL, NULL, NULL);
GO
Wird nun erneut versucht, für das Attribut [Email] die NULL-Einschränkung zu ändern, schlägt die Änderung fehl. Die Fehlermeldung lässt sehr schnell erkennen, worin die Ursache dafür liegt:
Meldung 515, Ebene 16, Status 2, Zeile 155
Der Wert NULL kann in die Email-Spalte, temporal.dbo.Customers-Tabelle nicht eingefügt werden. Die Spalte lässt NULL-Werte nicht zu. Fehler bei UPDATE.
Microsoft SQL Server überprüft vor der Konfiguration der NULL-Einschränkung zunächst das vorhandene Datenmaterial. Befinden sich Datensätze in der Tabelle, die einen NULL-Wert im Attribut besitzen, dann können diese Datensätze bei einer Änderung die vorherige Version nicht abspeichern. Für das Beispiel des zweiten Datensatzes würde eine Änderung an der Adresse dazu führen, dass der ursprüngliche Datensatz (mit einen NULL-Wert in [Email]) nicht in der “System Versioned Temporal Table” eingetragen werden kann, da bei erfolgreicher Anpassung dieses Attribut keine NULL-Werte zulassen würde!
NOT NULL wird zu NULL in einer gefüllten Tabelle
Wie sieht es aber aus, wenn ein Attribut bereits bei der Erstellung eine NOT NULL-Einschränkung besitzt die nachträglich geändert werden soll? Diese Frage wird mit dem folgenden Szenario untersucht und beantwortet:
ALTER TABLE dbo.Customers
ALTER COLUMN [Name] VARCHAR(100) NULL;
GO
Nicht ganz so überraschend ist das Ergebnis – es funktioniert einwandfrei ohne dabei “System Versioning” zu unterbrechen. Die Erklärung für dieses Verhalten liegt im gleichen Verhalten wie bereits im vorherigen Test. In diesem Szenario muss Microsoft SQL Server keine Validierungen durchführen, da die Restriktion NOT NULL zu einem NULL wird. Somit sind leere Werte erlaubt. Unabhängig davon, ob bereits Werte im Attribut stehen, verletzen sie keine NULL-Einschränkung.
Ausblick
Dieser Artikel ist der erste Artikel in einer Reihe von unterschiedlichen Artikeln zum Thema “System Versioned Temporal Tables”. Ich bin sehr an Fragen zu diesem Thema interessiert und sofern ein Leser Fragen zu diesem Thema hat, möchte ich sie sehr gerne aufgreifen und darüber bloggen. Fragen können jederzeit über das Kontakt-Formular gesendet werden.
Während meines Vortrags über “Temporal Tables” auf dem SQL Saturday Rheinland 2016 wurden einige Fragen gestellt, die ich nicht “ad hoc” beantworten konnte, da ich zu den Fragen noch keine ausreichenden Tests gemacht hatte. Dieser Artikel ist der zweite Artikel in einer Artikelreihe über “System versioned Temporal Tables” Dieser Artikel beschäftigt sich mit der Frage, ob man mit [sp_rename] Tabellen / Spalten von System Versioned Temporal Tables umbenennen kann.
Hinweis
Diese Artikelreihe befasst sich nicht mit den Grundlagen von “System Versioned Temporal Tables”! Die grundsätzliche Funktionsweise über “System Versioned Temporal Tables” kann im Artikel “Temporal Tables” (englisch) bei Microsoft nachgelesen werden.
Umbenennung von Metadaten
Frage: “…funktioniert sp_rename und wird die Umbenennung durchgereicht?” Eigentlich besteht diese Frage aus zwei Elementen. Die Antwort ist “Ja” und “Nein”.
JA – Objekte können mit der Systemprozedur sp_rename jederzeit umbenannt werden. Das Umbenennen von Objekten wird nicht dadurch blockiert, dass eine Tabelle als “System Versioned Temporal Table” gekennzeichnet und eingebunden ist.
NEIN – wenn eine Tabelle, die als System Versioned Temporal Table dient, umbenannt wird, wird nicht automatisch die History-Tabelle mit umbenannt. Jedoch muss man beim Umbenennen zwei wichtige Aspekte beachten; mit [sp_rename] können nicht nur Tabellen umbenannt werden sondern auch Spaltennamen! Wie unterschiedlich [sp_rename] auf beide Objekttypen reagiert, zeigen die die folgenden Beispiele und Erklärungen.
Wie funktioniert sp_rename?
Bei [sp_rename] handelt es sich um eine Systemprozedur, die von Microsoft bereitgestellt wird. Mit [sp_rename] kann der Name eines benutzerdefinierten Objekts in der aktuellen Datenbank geändert werden. Bei diesem Objekt kann es sich um eine Tabelle, einen Index, eine Spalte, einen Aliasdatentyp oder einen CLR-benutzerdefinierten Typ (Common Language Runtime) von Microsoft .NET Framework handeln.
Umbenennen von Tabellennamen
Interne Verwaltung von Tabellenobjekten
Obwohl ein Objekt immer eindeutig benannt werden muss, verwaltet Microsoft SQL Server alle Objekte intern mittels einer OBJECT_ID. Dieses Verfahren gilt sowohl für Tabellen als auch für Indexe, Einschränkungen, usw. Wir lesen und adressieren Objekte nach ihren Namen aber intern verwenden viele Funktionen und Prozeduren für den Zugriff die interne OBJECT_ID.
Mit dem folgenden Code wird eine Tabelle [dbo].[Customers] erzeugt. Ebenfalls wird für die Speicherung der historischen Daten eine entsprechende Tabelle mit gleichen Schemaeigenschaften erstellt.
USE [temporal];
GO
-- Create a dedicated schema for the history data
IF SCHEMA_ID(N'history') IS NULL
EXEC sp_executesql N'CREATE SCHEMA [history] AUTHORIZATION dbo;';
GO
IF OBJECT_ID(N'dbo.Customers', N'U') IS NOT NULL
DROP TABLE dbo.Customers;
GO
CREATE TABLE dbo.Customers
(
Id INT NOT NULL IDENTITY (1, 1),
Name VARCHAR(100) NOT NULL,
Street VARCHAR(100) NOT NULL,
ZIP CHAR(5) NOT NULL,
City VARCHAR(100) NOT NULL,
Phone VARCHAR(20) NULL,
Fax VARCHAR(20) NULL,
EMail VARCHAR(255) NULL,
ValidFrom DATETIME2(0) GENERATED ALWAYS AS ROW START NOT NULL DEFAULT ('2016-01-01T00:00:00'),
ValidTo DATETIME2(0) GENERATED ALWAYS AS ROW END NOT NULL DEFAULT ('9999-12-31T23:59:59'),
CONSTRAINT pk_Customers_ID PRIMARY KEY CLUSTERED (Id),
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
);
GO
ALTER TABLE dbo.Customers SET(SYSTEM_VERSIONING = ON); GO
Ein Blick hinter die Kulissen zeigt, wie die beiden Objekte in der Datenbank verwaltet werden:
SELECT object_id,
QUOTENAME(SCHEMA_NAME(schema_id)) + N'.' + QUOTENAME(name) AS TableName,
temporal_type,
temporal_type_desc,
history_table_id
FROM sys.tables
WHERE object_id = OBJECT_ID(N'dbo.Customers', N'U')
UNION ALL
SELECT object_id,
QUOTENAME(SCHEMA_NAME(schema_id)) + N'.' + QUOTENAME(name) AS TableName,
temporal_type,
temporal_type_desc,
history_table_id
FROM sys.tables
WHERE object_id = OBJECT_ID(N'history.Customers', N'U');
GO
In der Abbildung sind die Abhängigkeiten der beiden Tabellen untereinander gut zu erkennen. Wichtig für die Fragestellung ist, dass beide Tabellen unterschiedliche [object_id] besitzen! Den für die Umbenennung von “Objekten” verantwortliche Code aus [sp_rename] zeigt die folgende Abbildung.
Der Codeausschnitt zeigt, dass die Umbenennung einer Tabelle (%%object) auf der korrespondierenden object_id (@objid) basiert. Das Objekt mit der ID = @objid erhält den Namen @newname. Sollte die Umbenennung einen Fehler verursachen, wird als Fehlermeldung ausgegeben, dass ein Objekt mit gleichem Namen bereits in der Datenbank existiert! Unabhängig von Art der Programmierung sollte jedoch klar sein, dass [sp_rename] für Objekte nur auf die ObjektId verweist und keine weiteren Tabellenabhängigkeiten überprüft und/oder anpasst.
EXEC sp_rename
@objname = N'dbo.Customers',
@newname = N'NewCustomers';
GO
Für das Beispiel wird die Tabelle [dbo].[Customers] in [dbo].[NewCustomers] umbenannt. Diese Operation wird ohne Fehlermeldungen durchgeführt und die Überprüfung der Tabellennamen zeigt, dass ausschließlich die „System Versioned Temporal Table“ umbenannt wurde; die „History Table“ blieb unberührt!
Der Name hat sich geändert aber die [object_id] bleibt von einer Neubenennung unberührt. Der Name einer Tabelle ist für eine “System Versioned Temporal Table” nicht wichtig. Die Verwaltung erfolgt über die [object_id].
Umbenennung von Spaltennamen
Die Systemprozedur [sp_rename] wird nicht nur für die Umbenennung von Tabellen verwendet sondern kann auch verwendet werden, um Attribute einer Tabelle umzubenennen. Bei den Tests müssen zwei Situationen berücksichtigt werden:
Umbenennen von Attributen bei aktivierter “System Versioned Temporal Table”
Umbenennen von Attribut aus “System Versioned Temporal Table”
Im ersten Beispiel wird versucht, das Attribut [Name] aus der Tabelle [dbo].[Customers] neu zu benennen. Dabei bleibt die Systemversionierung aktiviert. Das Ergebnis sollte nicht überraschen – die Umbenennung funktioniert einwandfrei und ohne Fehler.
Microsoft SQL Server kann das Attribut in der „System Versioned Temporal Table“ ohne Probleme ändern und wendet die Anpassungen automatisch auf die „History Table“ an.
Umbenennen von Attribut aus “History Table”
Im nächsten Beispiel soll versucht werden, das geänderte Attribut [CustomerName] wieder in [Name] umzubenennen. Diesmal wird die Prozedur auf die “History Table” angewendet. Auch dieses Ergebnis sollte nicht überraschen; der Versuch schlägt fehl mit dem Hinweis, dass Änderungen an der “History Table” nicht erlaubt sind!
Die Fehlermeldung zeigt, dass eine Änderung an der aktivierten “History Table” nicht erlaubt ist. Die Fehlermeldung ist korrekt, wenn man in die Sicherheitsprinzipien von “System Versioned Temporal Tables” kennt. Hier heißt es: “When SYSTEM_VERSIONING is ON users cannot alter history data regardless of their actual permissions on current or the history table. This includes both data and schema modifications.”
Somit ist klar, warum weder Datensätze noch Schemamodifikationen möglich sind – die Sicherheitsrichtlinien von Microsoft SQL Server lassen Manipulationen an der “History Table” nicht zu, so lange die die Systemversionierung aktiviert ist.
Umbenennen von Attributen bei deaktivierter “System Versioned Temporal Table”
Mit dem letzten Beispiel wird geprüft, ob eine Änderung der Attribute ohne aktivierter Systemversionierung möglich ist. Hierzu wird der folgende Code ausgeführt:
-- Deactivate System Versioning and change the attributes
ALTER TABLE dbo.Customers
SET (SYSTEM_VERSIONING = OFF);
GO
-- Now we can change the attributes in the history table
EXEC sp_rename
@objname = N'history.Customers.Name',
@newname = N'CustomerName',
@objtype = N'COLUMN';
GO
-- can we now activate System Versioning?
ALTER TABLE dbo.Customers
SET
(
SYSTEM_VERSIONING = ON
(HISTORY_TABLE = History.Customers)
);
GO
Zunächst wird die Systemversionierung deaktiviert und anschließend das Attribut [Name] umbenannt. Dieser Schritt ist nur möglich, da keine Systemversionierung mehr aktiviert ist. Versucht man anschließend, die Systemversionierung wieder zu aktivieren, erhält man den folgenden Fehler:
Die Systemversionierung kann nicht mehr aktiviert werden, da nach der Änderung des Attributs die Metadaten beider Tabellen unterschiedlich sind. Um die Systemversionierung wieder erfolgreich aktivieren zu können, muss das geänderte Attribut auch in der “System Versioned History Table” geändert werden.
Nachdem die Namen der Attribute in beiden Tabellen wieder identische Namen besitzen, lässt sich die Tabelle erneut als “System Versioned Temporal Table” konfigurieren.
Zusammenfassung
Das Umbenennen von “System Versioned Temporal Tables” sowie deren “History Tables” ist mit [sp_rename] möglich, da für die interne Verwaltung nicht der Name der Tabelle entscheidend ist sondern die ObjektId. Sofern Attribute in einer Relation umbenannt werden sollen, können nur Attribute der “System Versioned Temporal Table” ohne Deaktivierung umbenannt werden. Eine Neubenennung von Attributen in der „History Table“ kann nur durchgeführt werden, wenn die Systemversionierung aufgelöst wird.
Bisher erschienen Artikel zu System Versioned Temporal Tables
Im Kommentar zu meinem Artikel “Temporal Tables – Umbenennen von Metadaten” hat ein von mir sehr geschätzter Kollege aus meiner Access-Zeit – Philipp Stiefel (w) – angemerkt, dass eine Gegenüberstellung von Programmierung und Systemlösung interessant wäre. Das finde ich auch – also wurde der Urlaub dazu genutzt, sich mit den unterschiedlichen Lösungsansätzen zu beschäftigen.
Temporal Tables mit Hilfe einer Eigenlösung
Wer noch keine Möglichkeit hat, mit Microsoft SQL Server 2016 zu arbeiten, wird nicht umhin kommen, eine Implementierung von “Temporal Tables” in Eigenregie zu realisieren. Hierzu gibt es drei mögliche Lösungsansätze:
Implementierung in der Clientsoftware
Implementierung durch Stored Procedures
Implementierung durch Trigger
Die Optionen 1 und 2 fallen in diesem Artikel aus der Betrachtung heraus, da diese Lösungen eine Protokollierung verhindern, wenn unmittelbar oder durch Drittsoftware (z. B. Access) Zugang zu den Tabellen zu erwarten ist. Meine Erfahrung hat gezeigt, dass bisher KEINE Software konsequent eine Abstraktionsschicht verwendet (Views / Stored Procedures), um den unmittelbaren Zugang zu den Tabellen zu verhindern. Aus diesem Grund betrachte ich in diesem Artikel ausschließlich die Implementierung durch Trigger.
Grundsätzliche Einschränkungen bei einer programmierten Lösung
Unabhängig von der gewählten Lösung gibt es in einer programmierten Lösung generelle Probleme, die nicht zu lösen sind:
Die Tabelle für die Historisierung besitzt KEINE unmittelbare Abhängigkeit zur “Temporal Table”!
Die Tabelle für die Historisierung kann ohne Berücksichtigung der “Temporal Table” in ihrer Struktur manipuliert werden und vice versa!
Die Tabelle für die Historisierung ist eine gewöhnliche Tabelle. Somit besteht Raum für direkte Manipulationen des Dateninhalts!
Möglichkeiten des “Verbergens” von Attributen für die Versionierung bestehen nicht – es muss über Views eine Möglichkeit geschaffen werden.
Sofern nicht mit der Enterprise-Edition gearbeitet wird, ist PAGE-Compression (Standard für die History Table) nicht möglich!
Szenario für Trigger
Wie schon in den bisher erstellten Artikeln wird eine Tabelle [dbo].[Customer] für die Beispiele verwendet. Für die Historisierung wird die Tabelle [history].[Customers] verwendet.
IF SCHEMA_ID(N'history') IS NULL
EXEC sp_executesql N'CREATE SCHEMA [history] AUTHORIZATION dbo;';
GO
IF OBJECT_ID(N'dbo.Customers', N'U') IS NOT NULL
DROP TABLE dbo.Customers;
GO
CREATE TABLE dbo.Customers
(
Id INT NOT NULL IDENTITY (1, 1),
Name VARCHAR(100) NOT NULL,
Street VARCHAR(100) NOT NULL,
ZIP CHAR(5) NOT NULL,
City VARCHAR(100) NOT NULL,
ValidFrom DATETIME2(0) NOT NULL DEFAULT ('2016-01-01T00:00:00'),
ValidTo DATETIME2(0) NOT NULL DEFAULT ('9999-12-31T23:59:59'),
CONSTRAINT pk_Customers_ID PRIMARY KEY CLUSTERED (Id)
);
GO
CREATE TABLE history.Customers
(
Id INT NOT NULL,
Name VARCHAR(100) NOT NULL,
Street VARCHAR(100) NOT NULL,
ZIP CHAR(5) NOT NULL,
City VARCHAR(100) NOT NULL,
ValidFrom DATETIME2(0) NOT NULL,
ValidTo DATETIME2(0) NOT NULL
);
GO
Für die Implementierung von “Temporal Tables” mit Triggern müssen zwei Situationen/Ereignisse in der Tabelle berücksichtigt werden.
UPDATE-Trigger
Der UPDATE-Trigger wird ausgeführt, sobald ein bestehender Datensatz manipuliert wird. Der Trigger besitzt eine simple Struktur. Zunächst muss ein Zeitstempel für die Manipulation generiert werden um ihn anschließend in beiden Tabellen für die Gültigkeit zu verwenden. Während der Zeitstempel in der “Temporal Table” für den Beginn der Validierung verwendet wird, muss der in die “History Table” einzufügende Datensatz diesen Zeitstempel für das Ende der Gültigkeit erhalten.
CREATE TRIGGER dbo.trg_Customers_Update
ON dbo.Customers
FOR UPDATE
AS
SET NOCOUNT ON;
DECLARE @Timestamp DATETIME2(0) = GETDATE();
-- in the first step we insert the "old" record with a validation stamp
-- into the history tables
INSERT INTO history.Customers (Id, Name, Street, ZIP, City, ValidFrom, ValidTo)
SELECT Id, Name, Street, ZIP, City, ValidFrom, @TimeStamp
FROM deleted;
-- now we have to update the original row in the ValidFrom attribute
UPDATE C
SET C.ValidFrom = @TimeStamp
FROM dbo.Customers AS C INNER JOIN inserted AS I
ON (C.Id = I.Id);
SET NOCOUNT OFF;
GO
DELETE-Trigger
Wird ein Datensatz aus der Tabelle gelöscht, muss der Datensatz in die “History Table” übertragen werden. Ebenfalls muss protokolliert werden, bis wann dieser Datensatz im der “Temporal Table” vorhanden war.
CREATE TRIGGER dbo.trg_Customers_Delete
ON dbo.Customers
FOR DELETE
AS
SET NOCOUNT ON;
DECLARE @TimeStamp DATETIME2(0) = GETDATE();
INSERT INTO history.Customers
SELECT Id, Name, Street, ZIP, City, ValidFrom, @TimeStamp
FROM deleted;
SET NOCOUNT OFF;
GO
Welche Ressourcen werden bei einem Update blockiert?
Die Verwendung von Triggern ist mit Vorsicht zu genießen – insbesondere in Umgebungen mit hohem Transaktionsvolumen. Die folgende Abbildung zeigt den Prozessaufruf für ein UPDATE.
Insgesamt unterteilt sich die Aktualisierung in drei Phasen. In Phase 1 wird der Wert für das Attribut [Street] geändert und der Trigger aktiviert. In der zweiten Phase wird der ursprüngliche Datensatz in der Tabelle [history].[Customers] gespeichert. In diesem Abschnitt wird der zuvor ermittelte Zeitstempel verwendet, um das Gültigkeitsende des Datensatzes zu bestimmen. In der letzten Phase muss erneut der geänderte Datensatz aktualisiert werden, da der ermittelte Zeitstempel nun als neues Startdatum für die Gültigkeit des Datensatzes verwendet wird.
In der Praxis sieht der Prozess wie folgt aus:
-- Activation of output of the locked resources to the client
DBCC TRACEON (3604, 1200, -1);
GO
Die Traceflags sorgen dafür, dass die Sperren, die während der Aktualisierung gesetzt werden, am Client sichtbar gemacht werden. Anschließend wird in einer expliziten Transaktion der Datensatz geändert. Die Transaktion bleibt für weitere Untersuchungen geöffnet!
BEGIN TRANSACTION;
GO
UPDATE dbo.Customers
SET Street = 'Musterweg 1'
WHERE Id = 33906;
Die Ausgabe der Ressourcen zeigt, in welcher Reihenfolge die Tabellen/Datensätze blockiert werden. Im ersten Abschnitt wird in der Tabelle [dbo].[Customers] (OBJECT: 6:565577053:0) der Datensatz mit der Id = 33906 exklusiv blockiert. Hierzu wird hierarchisch zunächst die Tabelle und dann die Datenseite mit einem “Intent Exclusive Lock” blockiert. Anschließend wird der Datensatz selbst mit einem eXclusive-Lock blockiert.
Der nächste Abschnitt ist aus Performance-Sicht sehr interessant. Wie man erkennen kann, wird die Datenbank 2 in der Transaktion benutzt. Die Datenbank mit der ID = 2 ist TEMPDB! Bei der Verwendung von Triggern werden zwei Systemtabellen innerhalb eines Triggers benötigt. In einem UPDATE-Prozess sind es die Tabellen [inserted] und [deleted]. Diese Objekte werden in TEMPDB angelegt und verwaltet. Nachdem die Aktualisierung des Datensatzes abgeschlossen wurde, wird der Datensatz wieder freigegeben. Anschließend muss die Tabelle [history].[Customers] (OBJECT: 6:645577338:0) verwendet werden, da der Trigger den vorherigen Datensatz in diese Tabelle einträgt. Abschließend wird erneut eine Aktualisierung (U-Lock –> X-Lock) der Tabelle [dbo].[Customers] durchgeführt, um das Attribut [ValidFrom] neu zu setzen. Neben den eXklusiven Sperren der beiden Tabellen führt insbesondere der Zugriff auf TEMPDB zu einem nicht unerheblichen Einfluss auf die Performance, wenn TEMPDB nicht richtig konfiguriert ist!
Ein Blick in das Transaktionsprotokoll zeigt die Aufrufreihenfolge aus Sicht der durchgeführten Transaktion
SELECT [Current LSN],
[Operation],
[Log Record Length] + [Log Reserve] AS LogVolume,
AllocUnitName,
[Page ID],
[Slot ID]
FROM sys.fn_dblog(NULL, NULL);
Sobald die Transaktion beginnt, wird eine Aktualisierung ([LOP_MODIFY_ROW]) auf die Tabelle [dbo].[Customers] durchgeführt. Anschließend wird der “alte” Datensatz in die Tabelle [history].[Customers] eingetragen ([LOP_INSERT_ROWS]). Da der Trigger jedoch erneut die Tabelle [dbo].[Customers] aktualisieren muss, wird diese Tabelle erneut in der Transaktion bearbeitet ([LOP_MODIFY_ROW]).
Trigger = Deadlock
Wie die Aufrufkette in der obigen Prozessbeschreibung zeigt, werden zwei Ressourcen in einer Wechselwirkung zueinander blockiert. Dieses Verhalten kann dazu führen, dass es vermehrt zu Deadlock-Problemen kommt. Die Situation wird durch den Trigger initiiert. Das folgende Beispiel zeigt eine Situation, in der ein Deadlock eine Transaktion beendet.
In Transaktion 1 (T1) wird ein SELECT auf [history].[Customers] ausgeführt, dem unmittelbar danach ein SELECT auf [dbo].[Customers] folgt. Wenn zwischen beiden Aufrufen eine Aktualisierung auf [dbo].[Customers] ausgeführt wird, versucht der Trigger eine X-Sperre auf [history].[Customers]. Diese X-Sperre kann nicht gesetzt werden, da T1 die Tabelle noch im Zugriff hat. Gleichwohl kann T1 nicht auf [dbo].[Customers] zugreifen, da sie von T2 exklusiv blockiert ist.
Das nachfolgende Script wird in einem neuen Abfragefenster gestartet. Um einen Deadlock zu erzwingen wurde eine restriktive Isolationsstufe gewählt: (SERIALIZABLE).
SET TRAN ISOLATION LEVEL SERIALIZABLE;
GO
BEGIN TRANSACTION;
GO
SELECT * FROM History.Customers AS H
WHERE H.Id = 10;
GO
In einem zweiten Abfragefenster wird die folgende Transaktion gestartet:
UPDATE dbo.Customers
SET Name = 'db Berater GmbH'
WHERE Id = 10;
Diese Transaktion wird begonnen aber nicht beendet. Grund dafür sind Blockaden auf der Ressource [history].[Customers].
SELECT DTL.request_session_id,
CASE WHEN DTL.resource_type = N'OBJECT'
THEN SCHEMA_NAME(T.schema_id) + N'.' + OBJECT_NAME(DTL.resource_associated_entity_id)
ELSE DTL.resource_description
END AS object_resource,
DTL.request_mode,
DTL.request_type,
DTL.request_status
FROM sys.dm_tran_locks AS DTL LEFT JOIN sys.tables AS T
ON (DTL.resource_associated_entity_id = T.object_id)
WHERE DTL.resource_database_id = DB_ID()
AND DTL.resource_type != N'DATABASE'
ORDER BY
DTL.request_session_id;
Die Abbildung zeigt, dass der Prozess 57 eine IX-Sperre auf die Tabelle [history].[Customers] setzen möchte aber nicht erhält, weil Prozess 54 bereits eine S-Sperre auf die Ressource gesetzt hat. Prozess 57 hat aber bereits eine IX-Sperre auf [dbo].[Customers] gesetzt. Nun versucht Prozess 54 ein SELECT auf [dbo].[Customers]:
SELECT * FROM dbo.Customers AS C
WHERE C.Id = 10;
Nach einigen Sekunden wird der Prozess als DEADLOCK-Opfer beendet!
Mit “System Versioned Temporal Tables” ist es innerhalb einer Abfrage möglich, für einen bestimmten Zeitpunkt den Status des gewünschten Datensatzes zu ermitteln. Diese Möglichkeit besteht für eine “Eigenlösung” natürlich nicht, da die Syntax ohne “System Versioned Temporal Tables” nicht funktioniert. In diesem Fall bleibt nur die Möglichkeit einer programmierten Lösung, die – basierend auf der Annahme, dass ein bestimmter Zeitpunkt angegeben wird – mit Hilfe einer INLINE-Funktion implementiert wird.
CREATE FUNCTION dbo.if_Customers
(
@Id int,
@TimeStamp datetime2(0)
)
RETURNS TABLE
AS
RETURN
(
SELECT *
FROM dbo.Customers
WHERE (
@Id = 0 OR
Id = @Id
) AND
ValidFrom <= @TimeStamp AND
ValidTo >= @TimeStamp
UNION ALL
SELECT *
FROM history.Customers
WHERE (
@Id = 0 OR
Id = @Id
) AND
ValidFrom <= @TimeStamp AND
ValidTo >= @TimeStamp
);
GO
Die Funktion muss beide Tabellen abfragen und den jeweiligen Zeitraum berücksichtigen. Mit “System Versioned Temporal Tables” gibt es weitere Abfragemöglichkeiten, die in einer programmierten Version selbst erstellt werden müssten. Bei der Erstellung der Funktionen ist zu berücksichtigen, dass es sich immer um Inline-Funktionen handelt. Ansonsten könnte es Probleme bei Abfragen geben, die diese Funktion mittels JOIN oder CROSS APPLY verwenden, da in solchen Fällen immer von einer Rückgabemenge von 1 Datensatz ausgegangen wird!
Szenario für “System Versioned Temporal Tables”
Das gleiche Szenario wird nun für “System Versioned Temporal Tables” durchgeführt. Hierbei interessiert primär, welche Ressourcen belegt/blockiert werden und wie sich das Transaktionsvolumen im Verhältnis zur Triggerlösung verhält.
Welche Ressourcen werden bei einem Update blockiert?
Erneut wird ein Update auf einen Datensatz in der “System Versioned Temporal Table” durchgeführt, um zu prüfen, welche Ressourcen gesperrt werden.
Die Ausgabe der blockierten Ressourcen zeigt die Reihenfolge, in der die Tabellen/Datensätze blockiert werden. Hervorzuheben sind die ersten beiden Sperren. Anders als bei der “programmierten” Version sperrt Microsoft SQL Server bereits zu Beginn der Transaktion BEIDE Tabellen! Zu Beginn wird ein Intent Exclusive Lock auf die Tabelle [dbo].[Customers] (OBJECT: 6:565577053:0) gesetzt um unmittelbar im Anschluss die Tabelle [history].[Customers] (OBJECT: 6:629577281:0) zu sperren. Durch die IX-Sperren wird signalisiert, dass in tieferen Hierarchien eine X-Sperre gesetzt werden soll. Sobald ein IX-Sperre auf die Datenseite (PAGE: 6:1:818) gesetzt wurde, kann eine X-Sperre auf den Datensatz in [dbo].[Customers] angewendet werden.
Erst, wenn die exklusive Sperre auf de Datensatz angewendet werden kann, wird in der Tabelle für den neu hinzuzufügenden Datensatz eine entsprechende Datenseite nebst Slot gesperrt. Dieser Teil der Transaktion beschreibt das Hinzufügen des ursprünglichen Datensatzes in die Tabelle [history].[Customers].
Ein Blick in das Transaktionsprotokoll zeigt die Aufrufreihenfolge aus Sicht der durchgeführten Transaktion.
Zu Beginn wird die Zeile in [dbo].[Customers] aktualisiert um anschließend in [history].[Customers] den vorherigen Datensatz einzutragen. Die Zeilen 6 – 10 sind für die Bewertung der Transaktion irrelevant; sie zeigen lediglich, dass in [history].[Customers] vor dem Eintragen eines neuen Datensatzes eine neue Datenseite allokiert wurde.
Deadlock-Szenarien
Sicherlich sind auch in diesem Szenario DEADLOCK-Gefahren vorhanden. Sie entsprechen aber anderer Natur und liegen eher im Design der Indexe. Ist es in der programmierten Version notwendig, die Objekte “sequentiell” und getrennt voneinander zu sperren, so lässt eine Systemlösung von “Temporal Tables” dieses Szenario nicht zu. Microsoft SQL Server sperrt immer BEIDE Tabellen vor einer Manipulation von Datensätzen. Damit kann sich kein weiterer Prozess zwischen die Transaktion schieben; ein Deadlock ist – bezüglich der beschriebenen Version – nicht mehr möglich!
Abfragen auf Basis von Zeitstempeln
Ganz klar liegt hier der große Vorteil von “System Versioned Temporal Tables”. Statt – wie in der programmierten Version – mit eigenen Funktionen die Funktionalität von “Temporal Tables” nachzubauen, bedient man sich im neuen Feature von Microsoft SQL Server 2016 lediglich der erweiterten Syntax von System Versioned Temporal Tables.
SELECT * FROM dbo.Customers
FOR SYSTEM_TIME AS OF '2016-07-05T16:00:00' AS C
WHERE Id = 33906;
Statt komplizierter Abfragen reicht der Hinweis “FOR SYSTEM_TIME…” um verschiedene Abfragemöglichkeiten basierend auf Zeitstempeln zu generieren. Ob diese Abfragen performant sind oder Verbesserungspotential haben, soll in einem anderen Artikel detailliert untersucht werden.
Bisher veröffentlichte Artikel zu System Versioned Temporal Tables