2030-4.book Page 1 Monday, February 8, 2010 4:26 PM
Robert Panther SQL Server Performance-Ratgeber
2030-4.book Page 2 Monday, February 8, 2010 4:26 PM
2030-4.book Page 3 Monday, February 8, 2010 4:26 PM
Robert Panther
SQL Server Performance-Ratgeber Datenbankoptimierung für Architekten, Entwickler und Administratoren
2030-4.book Page 4 Monday, February 8, 2010 4:26 PM
Robert Panther: SQL Server Performance-Ratgeber Datenbankoptimierung für Architekten, Entwickler und Administratoren ISBN: 978-3-86802-232-2
© 2010 entwickler.press Ein Imprint der Software & Support Verlag GmbH
Bibliografische Information der Deutschen Nationalbibliothek Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar.
Ihr Kontakt zum Verlag und Lektorat: Software & Support Verlag GmbH entwickler.press Geleitsstraße 14 60599 Frankfurt am Main Tel: +49(0) 69 63 00 89 - 0 Fax: +49(0) 69 63 00 89 - 89
[email protected] http://www.entwickler-press.de
Projektleitung: Sebastian Burkart Lektorat und Korrektorat: Rüdiger Glaum Layout: SatzWERK, Siegen (www.satz-werk.com) Umschlaggestaltung: Maria Rudi Belichtung, Druck & Bindung: M.P. Media-Print Informationstechnologie GmbH, Paderborn Alle Rechte, auch für Übersetzungen, sind vorbehalten. Reproduktion jeglicher Art (Fotokopie, Nachdruck, Mikrofilm, Erfassung auf elektronischen Datenträgern oder andere Verfahren) nur mit schriftlicher Genehmigung des Verlags. Jegliche Haftung für die Richtigkeit des gesamten Werks kann, trotz sorgfältiger Prüfung durch Autor und Verlag, nicht übernommen werden. Die im Buch genannten Produkte, Warenzeichen und Firmennamen sind in der Regel durch deren Inhaber geschützt.
2030-4.book Page 5 Monday, February 8, 2010 4:26 PM
Inhaltsverzeichnis V
Vorwort
13
E
Einleitung
15
E.1
Warum dieses Buch?
15
E.2
Für wen ist dieses Buch gedacht?
16
E.3
Erforderliche Kenntnisse und Voraussetzungen
17
E.4
Aufbau des Buches
18
E.5
Schreibweisen
18
E.6
Website zum Buch
19
E.7
Über den Autor/Kontakt zum Autor
20
Teil A: Optimierung für Administratoren
21
1
Vorbereitung der Systemumgebung
1.1
Hardwareumgebung 1.1.1 Prozessor 1.1.2 Hauptspeicher 1.1.3 Festplatten 1.1.4 Netzwerk
23 23 24 25 32
1.2
Betriebssystem 1.2.1 Auswahl des richtigen Betriebssystems 1.2.2 Konfiguration des Betriebssystems
33 33 34
1.3
Datenbank-Management-System 1.3.1 Auswahl der richtigen Edition 1.3.2 Installation 1.3.3 Konfiguration
35 35 37 38
1.4
Zusammenfassung
45
SQL Server Performance-Ratgeber
23
5
2030-4.book Page 6 Monday, February 8, 2010 4:26 PM
Inhaltsverzeichnis
2
Einrichten und Warten von Datenbanken
47
2.1
Anlegen und Konfigurieren einer Datenbank 2.1.1 Datenbank- und Protokolldatei 2.1.2 Sonstige Konfigurationseinstellungen
47 47 50
2.2
Regelmäßige Wartungsarbeiten 2.2.1 Datenbank verkleinern 2.2.2 Indizes aktuell halten 2.2.3 Sicherungen erstellen 2.2.4 Statistiken aktualisieren 2.2.5 Prüfung der Systemauslastung
52 54 54 57 58 58
2.3
Ressourcenkontrolle mit dem Resource Governor 2.3.1 Ressourcenpools und Arbeitsauslastungsgruppen 2.3.2 Klassifizierungsfunktionen erstellen und zuordnen
61 62 64
2.4
Zusammenfassung
65
Teil B: Optimierung für Designer und Architekten
6
67
3
Modellierung einer Datenbank
69
3.1
Normalisierung & Denormalisierung 3.1.1 Die ersten drei Normalformen 3.1.2 Bewusste Denormalisierung
69 70 72
3.2
Datentypen sinnvoll nutzen 3.2.1 Unicode oder nicht? 3.2.2 Alphanumerische Datentypen 3.2.3 Numerische Datentypen 3.2.4 Binäre Datentypen 3.2.5 Sonstige Datentypen 3.2.6 Spalten mit geringer Dichte 3.2.7 Identitätsspezifikation oder Uniqueidentifier?
74 75 76 77 78 78 79 80
3.3
Zusammenfassung
81
2030-4.book Page 7 Monday, February 8, 2010 4:26 PM
Inhaltsverzeichnis
4
Indizes
83
4.1
Grundlegende Funktionsweise von Indizes
83
4.2
Realisierung von Indizes bei SQL Server
84
4.3
Besondere Indexarten 4.3.1 Kombinierte Indizes 4.3.2 Abdeckende Indizes 4.3.3 Gefilterte Indizes 4.3.4 Indizierte Sichten
86 86 87 87 88
4.4
Index oder nicht? 4.4.1 Indexhinweise bei Anzeige des Ausführungsplans 4.4.2 Dynamische Management-Sichten zur Erkennung fehlender Indizes 4.4.3 Dynamische Management-Sichten zur Erkennung überflüssiger Indizes 4.4.4 Indexempfehlungen des Datenbankoptimierungsratgebers 4.4.5 Gruppiert oder nicht?
90 91 92 93 95 97
4.5
Volltextindizes 4.5.1 Funktionsweise von Volltextindizes 4.5.2 Verwendung von Volltextindizes 4.5.3 Anlegen eines Volltextindexes
98 98 99 101
4.6
Zusammenfassung
102
5
Optimale Verteilung der Daten
103
5.1
Manuelle Aufteilung von Datenzeilen 5.1.1 Archivierung von Altdaten 5.1.2 Historisierung
103 103 106
5.2
Partitionierung 5.2.1 Erstellen einer Partitionierungsfunktion 5.2.2 Erstellen eines Partitionierungsschemas 5.2.3 Verwenden von Partitionen 5.2.4 Hinzufügen und Entfernen von Partitionen 5.2.5 Verschieben von Partitionen
108 109 109 110 111 111
5.3
Zusammenfassung
112
SQL Server Performance-Ratgeber
7
2030-4.book Page 8 Monday, February 8, 2010 4:26 PM
Inhaltsverzeichnis
Teil C: Optimierung für Entwickler
8
113
6
Performancegrundlagen für Entwickler
115
6.1
Interne Verarbeitung von Abfragen 6.1.1 Ablauf der Abfrageverarbeitung 6.1.2 Ausführungspläne 6.1.3 Abfragestatistiken 6.1.4 Tabellenstatistiken 6.1.5 Wiederverwendung von Ausführungsplänen 6.1.6 Parametrisierung von Abfragen
115 115 116 120 123 126 129
6.2
Sperren und Transaktionen 6.2.1 Locking und Blocking 6.2.2 Transaktionen 6.2.3 Isolationsstufen (Isolation Level) 6.2.4 Tabellenhinweise 6.2.5 Deadlocks
133 133 134 137 139 140
6.3
Zusammenfassung
144
7
Abfrageoptimierung
145
7.1
Optimierung einzelner Abfragen 7.1.1 Voll qualifizierte Bezeichner verwenden 7.1.2 Datenvolumen so schnell wie möglich reduzieren 7.1.3 Indexverwendung ermöglichen 7.1.4 Unterabfragen 7.1.5 Ausführungspläne beeinflussen 7.1.6 Parametrisierung von Abfragen
145 145 147 150 153 156 162
7.2
Abfrageübergreifende Optimierung 7.2.1 Sperren und Transaktionen 7.2.2 Abfragen zusammenfassen 7.2.3 Zwischenergebnisse speichern
163 163 163 166
7.3
SQL-Cursor 7.3.1 Funktionsweise eines SQL-Cursors 7.3.2 Optimierungsmöglichkeiten 7.3.3 Sinnvolle Verwendung eines SQL-Cursors
169 169 171 175
7.4
Zusammenfassung
177
2030-4.book Page 9 Monday, February 8, 2010 4:26 PM
Inhaltsverzeichnis
8
Optimierung des Datenzugriffs
179
8.1
Datenzugriffstechnologien 8.1.1 Microsoft Data Access Components 8.1.2 SQL Native Client 8.1.3 ADO.NET
179 179 181 182
8.2
Varianten des Datenzugriffs unter .NET 8.2.1 ADO.NET DataReader & Execute 8.2.2 ADO.NET DataSets 8.2.3 LINQ to SQL 8.2.4 ADO.NET Entity Framework 8.2.5 Sonstige O/R-Mapper
183 185 190 193 199 207
8.3
Datenbanklogik auf den Server verlagern 8.3.1 Sichten 8.3.2 Funktionen und gespeicherte Prozeduren 8.3.3 SQL Server .NET CLR
207 208 208 209
8.4
Zusammenfassung
210
Teil D: Optimieren einer bestehenden Datenbank
213
9
Strukturierte Performanceanalyse
215
9.1
Zieldefinition
215
9.2
Überblick des Gesamtablaufs
216
9.3
Allgemeine Prüfung des Systems 9.3.1 Prüfung der wichtigsten Systemressourcen mit dem Windows Task-Manager 9.3.2 Prüfung des SQL-Server-Zustands mit dem Activity Monitor
218
9.4
Zusammenfassung
223
10
Schwachstellen identifizieren durch Performancetests
219 220
225
10.1 Performance Counter und dynamische Management-Sichten 10.1.1 Der Windows Performance Monitor 10.1.2 Dynamische Management-Sichten
225 225 228
10.2 SQL Server Profiler & Datenbankoptimierungsratgeber 10.2.1 SQL Server Profiler 10.2.2 Der Datenbankoptimierungsratgeber
230 231 236
SQL Server Performance-Ratgeber
9
2030-4.book Page 10 Monday, February 8, 2010 4:26 PM
Inhaltsverzeichnis
10.3 Management Data Warehouse 10.3.1 Funktionsweise des Management Data Warehouse 10.3.2 Verwendung des Management Data Warehouse 10.3.3 Einrichten von eigenen Datenauflistungen
239 240 241 245
10.4 Zusammenfassung
247
11
Durchführung der Optimierung
11.1 Maßnahmen definieren
249
11.2 Gewichtung der Optimierungsansätze
250
11.3 Gezielt optimieren und Erfolge messen
251
11.4 Zusammenfassung
253
Teil E: Anhang
10
249
255
A
Tools & Features
257
A.1
Performance-Tools von Windows und SQL Server A.1.1 Windows Task-Manager A.1.2 Windows Performance Monitor A.1.3 SQL Server Profiler A.1.4 Datenbankoptimierungsratgeber A.1.5 SQL Server Management Studio A.1.6 Management Data Warehouse
257 257 257 258 258 258 258
A.2
Kostenfreie Messwerkzeuge und Optimierungstools A.2.1 SQLIO A.2.2 SQLIOSim A.2.3 SQL Stress A.2.4 SQL Server 2005 Performance Dashboard A.2.5 PAL – Performance Analysis of Logs
259 259 259 260 260 261
A.3
Kommerzielle Tools von Drittanbietern A.3.1 Quest Software A.3.2 Redgate A.3.3 Idera
262 262 263 263
2030-4.book Page 11 Monday, February 8, 2010 4:26 PM
Inhaltsverzeichnis
B
Informationsquellen im Internet
B.1
Websites zum Buch
265
B.2
Websites zur SQL Server Performance
265
B.3
Allgemeine Websites zum SQL Server
265
B.4
Foren & Newsgroups zum SQL Server
266
C
Checklisten
C.1
Checkliste für Administratoren
267
C.2
Checkliste für Designer und Architekten
269
C.3
Checkliste für Entwickler
270
C.4
Checkliste zum Optimieren einer bestehenden Anwendung
271
D
Glossar
273
Stichwortverzeichnis
285
SQL Server Performance-Ratgeber
265
267
11
2030-4.book Page 12 Monday, February 8, 2010 4:26 PM
2030-4.book Page 13 Monday, February 8, 2010 4:26 PM
V
Vorwort
Die Vorgeschichte dieses Buches ist eine sehr lange, die ich hier – um nicht zu sehr ins Detail zu gehen – kurz zusammenfassen möchte: Bereits im Jahre 1996, als ich meine Diplomarbeit zum Thema Optimierung von Datenbankanwendungen fertig stellte, hatte ich den Plan, dieses Thema auch mal in Form eines Buches zu verarbeiten. Es folgten Verhandlungen mit diversen Verlagen, die meist darin endeten, dass dieses Thema als zu speziell angesehen wurde. Offensichtlich war die Zeit dafür noch nicht gekommen. Mein erstes Buch schrieb ich dann Jahre später und zu einem völlig anderen Thema, nämlich über die Programmierung von Pocket-PCs. Das positive Feedback hierzu ließ mich noch ein weiteres Buch sowie zahlreiche Fachartikel in diesem Themenumfeld schreiben, wobei ich hauptberuflich eigentlich immer primär mit der Administration und Programmierung relationaler Datenbanksysteme – insbesondere MS SQL Server – zu tun hatte. Inzwischen waren auch einige englischsprachige Bücher zur Performanceoptimierung derselben erschienen, aber nur sehr wenige in deutscher Sprache. Es scheint also, dass die Zeit nun endlich reif ist für dieses Buch, das Sie – liebe Leser – gerade in den Händen halten. Bevor ich Sie nun dem eigentlichen Buchtext überlasse, möchte ich noch einigen Menschen danken, die dazu beigetragen haben, dass dieses Buch etwa 14 Jahre nach der ursprünglichen Idee dann doch noch erschienen ist: Der erste Dank geht an die Mitarbeiter von entwickler.press und dem Software & Support Verlag. Hier möchte ich insbesondere Sebastian Meyen hervorheben, der mir durch konstruktive Gespräche in der Konzeptionsphase des Buches geholfen hat, dem Buch eine klarer definierte Zielrichtung zu geben. Aber auch meinem Lektor Sebastian Burkart, der mich während des Schreibens betreut hat, bin ich zu Dank verpflichtet. Dies insbesondere dafür, dass er mehrere Terminverschiebungen gegenüber dem Verlag erfolgreich vertreten hat, die notwendig waren, um das doch recht tief gehende Thema in der richtigen Qualität aufbereiten zu können. Dazu gilt mein Dank allen anderen Leuten, die im Hintergrund mitgeholfen haben, seien es weitere Mitarbeiter des Software & Support Verlags, aber auch Freunde und Kollegen, die mich in verschiedenen Diskussionen mit weiteren Anregungen und Detailideen versorgt haben. Hier möchte ich insbesondere Thomas Küppers nennen, der mir viele hilfreiche Anregungen zu den ersten beiden Kapiteln gegeben hat, sowie Wolfgang Braun, der für mich das Kapitel acht qualitätsgesichert hat.
SQL Server Performance-Ratgeber
13
2030-4.book Page 14 Monday, February 8, 2010 4:26 PM
V – Vorwort
Ein weiterer Dank geht an Erik Franz, der mich als ehemaliger Verlagsleiter von entwickler.press im Vorfeld darin unterstützt hat, das Thema bei diesem Verlag zu platzieren. Ach ja, und wie es so schön heißt: „last but not least“ ein großes Dankeschön an meine Frau Birgit, die im vergangenen Jahr sicherlich zu wenig Aufmerksamkeit von mir bekam, weil ich neben meinem „normalen“ Job als IT-Berater gleich zwei Buchprojekte im SQL-Server-Umfeld zu bewältigen hatte. Aber nun wünsche ich viel Spaß beim Lesen des SQL-Server-Performanceratgebers.
Robert Panther Königstein im Januar 2010
14
2030-4.book Page 15 Monday, February 8, 2010 4:26 PM
E E.1
Einleitung
Warum dieses Buch?
Datenbanken bilden das zentrale Rückgrat von fast jeder Businessanwendung. Ein Thema, was dabei oft vernachlässigt wird – oder zumindest zu spät Beachtung findet –, ist die Performanceoptimierung. Es gibt mittlerweile ja zumindest in englischer Sprache einiges an Literatur zum Thema Datenbankperformance. Der wesentliche Unterschied an dem Buch, das Sie gerade in den Händen halten, ist aber – abgesehen von der deutschen Sprache – die bewusste Aufteilung nach Zielgruppen. Bei immer größeren und komplexeren Anwendungen kommt es nur noch selten vor, dass ein und dieselbe Person den Server aufsetzt, die Datenbank sowie dazugehörige Software entwirft, programmiert und später auch noch wartet. Daher ist die Struktur – insbesondere der ersten drei Abschnitte – dieses Buches konsequent so gegliedert, dass IT-Profis, die eine bestimmte Rolle ausüben, schnell die für sie relevanten Informationen finden, ohne sich diese umständlich aus verschiedenen Quellen zusammensuchen zu müssen. Aber auch im Rest des Buches steht die Praxisorientierung im Vordergrund, denn dort werden praxiserprobte Vorgehensweisen geschildert, mit denen Sie eine gute Performance Ihrer Datenbank bzw. Datenbankanwendung erreichen oder wiederherstellen können. Was Sie von diesem Buch nicht erwarten sollten, sind vollständige Auflistungen aller möglichen Detaileinstellungen, um noch die letzten 0,02% Performanceverbesserung zu erreichen. Dafür gibt es sicherlich andere Quellen und dies würde auch dem pragmatischen Ansatz des Textes widersprechen. Schließlich sollen hier auf möglichst knappem Raum die wesentlichen Informationen untergebracht werden, anstatt sich in endlosen Details zu verlieren, die hunderte von Seiten füllen, dabei aber nur noch marginale Performancevorteile erzielen. Man kann dieses Buch also eher als eine Art SQL-Server-Performance-Basiswerk sehen, im Sinne von: Was jeder SQL-Server-Datenbankadministrator, -architekt oder -entwickler über Performanceoptimierung wissen sollte. Dabei werden bewusst auch einige Grundlagenthemen (wie beispielsweise die Funktionsweise von Indizes) angesprochen, um sicherzustellen, dass diese Grundlagen nicht nur vage verstanden, sondern auch wirklich verinnerlicht wurden. Oft habe ich in der Praxis erlebt, dass Leute über ein oberflächliches Wissen von sehr vielen Details verfügen, die wirklichen Grundlagen – und damit auch Zusammenhänge – aber nie ganz verstanden haben. Aus falsch verstandener „Professionalität“ kann es dann vor-
SQL Server Performance-Ratgeber
15
2030-4.book Page 16 Monday, February 8, 2010 4:26 PM
E – Einleitung
kommen, dass dieser Zustand dauerhaft anhält, weil man nur ungern die Schwäche zeigt, eventuell eine Wissenslücke in einem Grundlagenthema zu offenbaren. In diesem Sinne kann man das vorliegende Buch auch unter dem folgenden Aspekt sehen: Was Sie schon immer über SQL-Server-Performance wissen wollten, aber nie zu fragen gewagt haben! Ich hoffe, Sie lassen sich von dieser – zugegebenermaßen etwas provokanten – Aussage nicht zu sehr abschrecken, denn diese trifft natürlich (hoffentlich) nur auf einige wenige zu und die Grundlagenthemen füllen daher auch nur einen Teil des gesamten Textes. Auch für erfahrene SQL-Server-Profis sollten zahlreiche interessante Informationen und Anregungen im Text zu finden sein. Dies bringt mich zur wichtigen Frage:
E.2
Für wen ist dieses Buch gedacht?
Dieses Buch richtet sich an alle, die direkt oder indirekt mit SQL-Server-Datenbanken zu tun haben. Damit sind neben Datenbankadministratoren, Datenbankarchitekten und Datenbankentwicklern auch Anwendungsentwickler und Systemadministratoren angesprochen. Die Begriffe Administratoren, Architekten und Entwickler werden jedoch in jedem Unternehmen im Detail etwas anders interpretiert und damit auch gelebt. Oft wird sogar noch eine feinere Unterteilung vorgenommen, sodass sich folgende Rollen ergeben können: 쮿
Systemadministratoren
쮿
Datenbankadministratoren
쮿
Softwarearchitekten
쮿
Datenbankarchitekten
쮿
Software-/Anwendungsentwickler
쮿
Datenbankentwickler
Zwischen den Softwarearchitekten und den -entwicklern ist mit den Software- bzw. Anwendungsdesignern sogar noch eine weitere Rolle möglich. Der Einfachheit halber unterscheide ich hier jedoch lediglich zwischen drei grundlegenden Rollen, die jeweils für einen bestimmten Aspekt der Optimierung zuständig sind. 쮿
Administratoren: Optimierung der Systemumgebung
쮿
Architekten: Optimierung der Datenhaltung
쮿
Entwickler: Optimierung des Datenzugriffs
Dabei sind die Grenzen der Zuständigkeiten oft fließend (so sind Datenbankadministratoren beispielsweise auch für die Datenhaltung mit verantwortlich, allerdings auf einer anderen Ebene als die Datenbankarchitekten, die das Datenmodell entwerfen). Dadurch wird aber auch klar, dass eine enge Zusammenarbeit der verschiedenen Rollen ein wesentlicher Faktor für den Gesamterfolg einer guten (und performanten) Datenbankanwendung ist.
16
2030-4.book Page 17 Monday, February 8, 2010 4:26 PM
Erforderliche Kenntnisse und Voraussetzungen
In diesem Sinne können die bereits erwähnten Grundlagenthemen auch dazu genutzt werden, sich zumindest die performancerelevanten Grundlagen der jeweils anderen Rollen anzueignen. Dies erleichtert die Kommunikation und damit auch die Zusammenarbeit ungemein, da auf diesem Wege Administratoren, Architekten und Entwickler dieselbe Sprache sprechen.
E.3
Erforderliche Kenntnisse und Voraussetzungen
Dieses Buch soll bewusst keine Einführung in Administration oder Programmierung des Microsoft SQL Servers darstellen. Dazu gibt es einerseits bereits genügend andere gute Bücher und andererseits würde dies auch den inhaltlichen Rahmen sprengen und vom eigentlichen Thema ablenken. Es werden daher Grundkenntnisse in der Nutzung des Microsoft SQL Servers vorausgesetzt. Je nach Rolle (Administrator, DB-Entwickler etc.) sollten Sie in der Lage sein, einen SQL Server einzurichten, Datenbanken mit Tabellen und Indizes anzulegen und grundlegende SQL-Statements (vor allem SELECT, INSERT, UPDATE, DELETE) zu formulieren. Auch die Verwendung der wichtigsten Tools sollte Ihnen dabei geläufig sein: SQL Server Enterprise Manager (bis SQL Server 2000) bzw. SQL Server Management Studio (ab SQL Server 2005). Eine Kenntnis der spezielleren (performancerelevanten) Zusatztools wie beispielsweise SQL Server Profiler oder SQL-Server-Optimierungsratgeber ist dagegen nicht erforderlich, da die Verwendung dieser Tools im Buch erklärt wird. Generell spielt es keine Rolle, ob Sie auf einem einzelnen Arbeitsplatz mit der kostenfreien Express Edition von SQL Server oder in einem großen Netzwerk mit mehreren Enterprise Editionen von SQL Server arbeiten, denn die meisten Optimierungsansätze treffen für alle Varianten des Produkts zu. Dort, wo sich einzelne Themen nur auf einige Editionen von SQL Server beziehen, wird explizit darauf hingewiesen. Dasselbe gilt für die verwendete Version von SQL Server. Dabei wird im Zweifelsfall von der aktuellsten Produktversion (SQL Server 2008) ausgegangen, jedoch wird an den entsprechenden Stellen auch auf die Besonderheiten von SQL Server 2005 (und teilweise auch SQL Server 2000) hingewiesen. Um wirklich alle hier vorgestellten Tools verfügbar zu haben, verwenden Sie idealerweise die Developer oder Enterprise Edition von SQL Server 2008. Dabei ist die Developer Edition eine – insbesondere im Vergleich zur Enterprise Edition – sehr günstige Variante, die allerdings nur für Entwicklungszwecke lizenziert ist. Von der Enterprise Edition bietet Microsoft aber auch eine 180-Tage-Testversion an, die Sie im Internet kostenfrei herunterladen können. Damit die gezeigten Abfragen und Beispiele möglichst gut nachzuvollziehen sind, wird in den meisten Fällen die AdventureWorks-Beispieldatenbank (in der 2008er-Version) verwendet. Diese wird seit einiger Zeit nicht mehr mit dem SQL Server ausgeliefert, ist aber als kostenloser Download über die Codeplex-Website verfügbar. Downloadlink: http://www.codeplex.com/SqlServerSamples
SQL Server Performance-Ratgeber
17
2030-4.book Page 18 Monday, February 8, 2010 4:26 PM
E – Einleitung
E.4
Aufbau des Buches
Der gesamte Text des Buches ist – abgesehen vom Anhang – in vier große Abschnitte unterteilt. Davon tragen die ersten drei Abschnitte des Buches den bereits erwähnten Zielgruppen Rechnung, da hier für jede Gruppe ein Abschnitt zu finden ist, der die wichtigsten Faktoren beschreibt, die aus dem jeweiligen Blickwinkel zu beachten sind. 쮿
Teil A: Optimierung für Administratoren
쮿
Teil B: Optimierung für Architekten
쮿
Teil C: Optimierung für Entwickler
쮿
Teil D: Optimieren einer bestehenden Datenbank(-anwendung)
Durch diese Unterteilung nach Zielgruppen können Sie schneller den Teil des Buches finden, der für Sie in Ihrer alltäglichen Arbeit die größte Bedeutung hat. Wenn Sie also beispielsweise Datenbankanwendungen entwerfen, starten Sie doch gleich mit den Kapiteln für Datenbankarchitekten. Als Anwendungsprogrammierer können Sie sich stattdessen gezielt auf die Kapitel für Entwickler stürzen. Und wenn Sie tatsächlich alle diese Rollen in einem Projekt ausüben sollten (was gerade bei kleineren Projekten leicht der Fall sein kann), so können Sie natürlich auch alle Kapitel lesen. Dabei orientiert sich die Reihenfolge der Kapitel so weit wie möglich an einem üblichen Software-Entwicklungszyklus. Erst wird der entsprechende Datenbankserver aufgesetzt, anschließend wird die Datenbank entworfen. Im nächsten Schritt erfolgt die Implementierung durch die Programmierer, bevor die Anwendung letzten Endes in den Produktivbetrieb übergeht. Lediglich das Kapitel, in dem die regelmäßige Wartung einer Datenbank beschrieben wird, weicht hiervon ab und ist bereits in Teil A zu finden, da hier auch die anderen für Administratoren relevanten Themen behandelt sind. Der vierte Teil des Buches widmet sich der Optimierung von bestehenden Anwendungen. Hier erfahren Sie, wie man am besten vorgeht, um aus einer bereits bestehenden und produktiv eingesetzten Datenbankanwendung durch möglichst geringe Änderungen das Maximum an Performancegewinn zu erzielen. Dabei werden die verschiedenen Zielgruppen wieder zusammengeführt, da hier eine Zusammenarbeit von Administratoren, Architekten und Entwicklern gefragt ist, um möglichst effektive Ergebnisse zu erzielen. Zum Schluss folgt dann noch ein Anhang, in dem ein Überblick über die wichtigsten Tools zur Performanceoptimierung zu finden ist. Außerdem sind hier auch eine Auflistung von Quellen zu finden, aus denen man weitere Informationen zum Thema Datenbankperformance erhalten kann, sowie ein kurzes Glossar, in dem die wichtigsten Begriffe und Abkürzungen aufgelistet sind.
E.5
Schreibweisen
Damit der Buchtext besser lesbar ist, werden verschiedene typografische Konventionen für bestimmte Inhalte verwendet. Diese möchte ich hier kurz auflisten: Verweise auf andere Kapitel werden in dieser Form dargestellt: siehe Einleitung – Schreibweisen
18
2030-4.book Page 19 Monday, February 8, 2010 4:26 PM
Website zum Buch
Verweise auf Webseiten werden wie folgt dargestellt: http://www.PantherPhotoPress.de Für Zitate ist die folgende Schreibweise vorgesehen! Eine ähnliche Schreibweise wird für Texte verwendet, die auf dem Bildschirm zu sehen sind, aber auch für allgemeine Hervorhebungen von Begriffen, die im Text neu eingeführt werden. Quelltextausschnitte werden in dieser Schrift wiedergegeben. Dieselbe Schrift wird auch für Datenbankobjekte (Tabellen, gespeicherte Prozeduren etc.) sowie Datei- und Verzeichnisnamen verwendet. SELECT * FROM Person.Address SELECT DISTINCT * FROM Person.Address -- Hin und wieder sind Teile von Listings durch Fettdruck hervorgehoben SELECT * FROM Tabellenname -- Platzhalter, die durch die eigentlichen Objektnamen zu ersetzen sind, -- werden kursiv dargestellt Listing E.1: Ganze Quelltexte werden in der oben gezeigten Schreibweise dargestellt
i
Gelegentlich findet sich auch ein Kasten, in dem ein Exkurs (z.B. eine Hintergrundinformation), ein besonders hervorzuhebender Hinweis oder Tipp dargestellt ist.
Bezüglich der Sprachwahl für Fachbegriffe aus dem SQL-Server-Umfeld versuche ich möglichst, sowohl die deutschsprachige als auch die englischsprachige Bezeichnung zu erwähnen, da in vielen – teilweise selbst deutschsprachigen – Texten oft die englischen Bezeichnungen bevorzugt verwendet werden. In vielen deutschen Unternehmen gibt es sogar generelle Richtlinien, dass auf Servern ausschließlich die englischsprachigen Softwareversionen zu installieren sind, da für diese Updates und Patches oft etwas früher verfügbar sind. Beim SQL Server war dies in der letzten Zeit zwar weniger der Fall (hier erschienen die wichtigsten Sprachvarianten gleichzeitig und lediglich exotische Sprachen kamen später dazu). Wenn Sie aber zu einem bestimmten Begriff nach Informationen im Internet suchen, finden Sie definitiv deutlich mehr Treffer, wenn Sie nach den englischen Bezeichnungen suchen. Von daher schadet es also sicher nicht, auch diese zu kennen, selbst wenn Sie mit der deutschen Software arbeiten.
E.6
Website zum Buch
Auf meiner Website http://www.PantherComputing.de ist ein Unterbereich zu diesem Buch zu finden. Dort werden Ergänzungen und Fehlerkorrekturen (falls jemand einen Fehler findet, wovon ich ausgehe, denn „Nobody is perfect!“), auch weitere Tipps und Verweise auf zusätzliche Informationen (sowohl im Internet als auch Buchempfehlungen) zu finden sein. Dieser Bereich ist momentan noch recht überschaubar, da ich mit dem Schrei-
SQL Server Performance-Ratgeber
19
2030-4.book Page 20 Monday, February 8, 2010 4:26 PM
E – Einleitung
ben dieses Buches beschäftigt war, wird im Laufe der Zeit aber sicherlich deutlich mehr Inhalt bekommen, also schauen Sie öfter mal vorbei!
E.7
Über den Autor/Kontakt zum Autor
Damit Sie wissen, wer für diesen Text verantwortlich ist, möchte ich mich kurz vorstellen: Ich beschäftige mich mit dem Microsoft SQL Server bereits, seit im Jahre 1995 mit der Version 6.0 die erste echte Microsoft-Version des Produkts veröffentlicht wurde. Seit dieser Zeit war auch immer wieder die Datenbank-Performanceoptimierung ein zentrales Thema für mich. So kam es auch, dass ich dies zum Thema meiner Diplomarbeit an der FH-Darmstadt wählte, die ich im Jahre 1996 erfolgreich abschloss. In den Jahren danach kamen zahlreiche Projekte mit verschiedenen Versionen des SQL Servers (6.0, 6.5, 7.0, 2000, 2005 und 2008), bei denen ich alle möglichen Rollen (DBAdministrator, DB-Architekt, DB-Entwickler, Anwendungsentwickler, Projektleiter und Trainer) einnahm und so das Produkt aus verschiedenen Blickwinkeln besser kennen lernen konnte. Inzwischen bin ich als Senior Consultant für die Logica Deutschland GmbH & Co. KG mit dem Schwerpunkt SQL Server tätig. Daneben schreibe ich Fachartikel und Bücher zu meinen Spezialgebieten SQL Server und Anwendungsentwicklung für Mobile Devices. Hin und wieder bin ich zu diesen Themen auch als Sprecher auf Konferenzen wie beispielsweise BASTA! und SQLCON aktiv. Für Fragen oder Feedback zu diesem Buch können Sie mit mir über folgende Adresse in Kontakt treten:
[email protected] Auch wenn es manchmal projektbedingt etwas länger dauern kann, versuche ich jede Mail zu beantworten. Erwarten Sie aber nicht, zu jedem Performanceproblem eine allumfassende Antwort zu bekommen, mit der Sie das Problem im Nu beheben können. Dies ist erfahrungsgemäß in den wenigsten Fällen möglich, ohne das Gesamtsystem aus Server und Anwendung ausführlich unter die Lupe zu nehmen und im Detail zu analysieren. Bei der Performance von SQL-Datenbanken spielen so viele Faktoren eine Rolle, dass sich zwar leicht exakte Regeln finden lassen, was man nicht tun sollte, aber nur selten solche, die besagen, wie man die optimale (also maximal mögliche) Performance erreicht. Und letzten Endes bin auch ich – obwohl ich glaube, den SQL Server mittlerweile recht gut zu kennen – sicherlich nicht allwissend. Denn das Produkt ist inzwischen so komplex geworden, dass ich auch nach jahrelanger Beschäftigung damit immer wieder ein paar neue Details entdecke.
!
20
Sollten Sie mal auf jemanden treffen, der von sich behauptet, über den SQL Server alles zu wissen, sollten Sie diesem sehr skeptisch begegnen. Mit größter Wahrscheinlichkeit kennt er das Produkt so wenig, dass er nicht einmal weiß, was er alles nicht weiß. Und sollte sich wider Erwarten doch herausstellen, dass er tatsächlich alles zu diesem Produkt weiß, schicken Sie mir bitte seine Adresse. Ich hätte da sicherlich ein paar Fragen, die schon lange auf eine Antwort warten. ;-)
2030-4.book Page 21 Monday, February 8, 2010 4:26 PM
Teil A Optimierung für Administratoren Dieser Teil des Buches fasst die Optimierungsansätze zusammen, die sowohl für Systemadministratoren als auch für Datenbankadministratoren relevant sind. Je nach Unternehmens- bzw. Projektgröße können sich diese Rollen auch stark überschneiden oder sogar von denselben Personen wahrgenommen werden. Dabei richtet sich Kapitel 1 – Vorbereitung der Systemumgebung primär an Systemadministratoren, während Kapitel 2 Regelmäßige Wartungsarbeiten vor allem für Datenbankadministratoren interessant sein dürfte.
SQL Server Performance-Ratgeber
21
2030-4.book Page 22 Monday, February 8, 2010 4:26 PM
2030-4.book Page 23 Monday, February 8, 2010 4:26 PM
1 1.1
Vorbereitung der Systemumgebung Hardwareumgebung
Es ist weit verbreiteter Irrglaube, dass sich Datenbank-Performanceprobleme am einfachsten durch banale Hardwareaufrüstung lösen lassen. Insbesondere dann, wenn es darum geht, eine bestehende Umgebung zu optimieren, sollte dies eigentlich das letzte Mittel sein. Das setzt natürlich voraus, dass bei der Ersteinrichtung des Servers eine angemessene Hardware zur Verfügung stand. Daher wird dieses Thema auch gleich zu Beginn des Buches behandelt.
i
Neben den in diesem Kapitel folgenden Informationen empfehle ich vor einer Neubestellung von Hardware auch einen kurzen Blick in den Windows-Server-Katalog. Hier sind alle zugelassenen Server für den Betrieb aufgeführt, wodurch auch gewährleistet wird, dass diese im Problemfall von Microsoft supported werden: http://www.windowsservercatalog.com
1.1.1
Prozessor
Lange Zeit war die Angabe der Taktfrequenz das wesentliche Merkmal, um die Leistungsfähigkeit eines Prozessors zu beschreiben. Im Laufe der Jahre wurden die Frequenzen der Prozessoren immer weiter gesteigert, so dass diese von den 4,77 MHz der ersten PCs bis hin zu 2–3 GHz nahezu um den Faktor 1.000 gestiegen sind. Inzwischen sind die derzeit bekannten physikalischen Möglichkeiten zur Steigerung des Prozessortaktes jedoch weitgehend ausgeschöpft. Stattdessen wird die Entwicklung von parallelisierten Systemen bzw. Prozessoren mit höherer Transistordichte vorangetrieben, um eine weitere Leistungssteigerung zu erreichen. So kommt es, dass bei typischen Unternehmensservern normalerweise mehrere Prozessoren verfügbar sind und sogar viele aktuelle Arbeitsplatzrechner – aufgrund moderner Prozessortechnologie – oft über bis zu vier Prozessorkerne verfügen, die in einem einzigen Prozessor enthalten sind. Dabei macht es keinen Unterschied, ob zwei Prozessoren mit jeweils zwei Kernen oder ein Prozessor mit vier Kernen im System arbeiten, entscheidend ist die Gesamtzahl der Prozessorkerne (engl. Cores). Ob diese Prozessorkerne dann auch effektiv genutzt werden können, hängt davon ab, ob dies auch vom verwendeten Betriebssystem und der darauf laufenden Software unterstützt wird. Eine Software, die nicht multiprozessorfähig ist, wird auch auf einem System mit acht Prozessorkernen nicht schneller laufen als mit einem Prozessorkern desselben Typs.
SQL Server Performance-Ratgeber
23
2030-4.book Page 24 Monday, February 8, 2010 4:26 PM
1 – Vorbereitung der Systemumgebung
Im Falle von SQL Server muss man sich darum allerdings weniger Sorgen machen. Lediglich die Anzahl der nutzbaren Prozessorkerne ist je nach verwendeter SQL Server Edition eventuell begrenzt. So nutzt die Express Edition nur einen Prozessorkern, während die Enterprise Edition mit beliebig vielen Prozessorkernen umgehen kann. (Einen genauen Überblick zu diesen Einschränkungen finden Sie weiter unten beim Abschnitt zur Auswahl der richtigen SQL Server Edition.) In der Praxis ist es generell sinnvoll und üblich, ein Mehrprozessorsystem für die Serverhardware zu nutzen. Im einfachsten Fall können Betriebssystem und SQL-Server-Dienst auf verschiedenen Prozessorkernen laufen, aber auch die parallele Abarbeitung von verschiedenen Datenbankabfragen auf verschiedenen Prozessorkernen ist möglich. Der Geschwindigkeitszuwachs, der durch weitere Prozessorkerne erzielbar ist, steigt jedoch nicht linear, da durch die Verteilung von Prozessen auf Prozessorkerne auch ein zusätzlicher Verwaltungsaufwand entsteht. Man sollte sich also keine Hoffnung machen, dass eine Datenbank auf einem Server mit acht Kernen doppelt so schnell läuft als auf einem vergleichbaren System mit vier Kernen. In welchem Ausmaß sich die Leistung durch zusätzliche Prozessorkerne steigern lässt, hängt hauptsächlich davon ab, wie rechenintensiv die Datenbankanwendungen sind und wie viele Prozesse normalerweise gleichzeitig auf dem Server laufen. Ein weiterer wichtiger Aspekt bei der Wahl des richtigen Prozessors ist die Größe des prozessorinternen Cache-Speichers. So kann es vorkommen, dass ein Prozessor mit einem größeren internen Cache eine bessere Gesamtleistung bringt als ein vergleichbarer Prozessor mit einer höheren Taktfrequenz, aber kleinerem CPU-Cache.
1.1.2
Hauptspeicher
Die besten Aussichten auf einen spürbaren Performancegewinn durch Hardwareaufrüstung hat man beim Hauptspeicher (oder kurz: RAM für Random Access Memory). Das belegt schon ein beliebter Running Joke unter Datenbankadministratoren: Frage: »How much memory does SQL Server need?« Antwort: »MORE!« Das fängt schon bei der Nutzung durch das Betriebssystem an, denn wenn zu wenig physikalischer Hauptspeicher vorhanden ist, nutzt Windows eine Auslagerungsdatei auf der Festplatte, um auf diesem Weg zusätzlichen Speicher bereitzustellen. Da Festplatten natürlich weit langsamer sind als RAM-Speicher, bremst dies die Gesamtleistung des Systems natürlich deutlich aus. Deutlich wird dieser Geschwindigkeitsunterschied schon, wenn man sich die Einheiten ansieht, in denen die typischen Zugriffszeiten für RAM-Speicher und Festplatten angegeben werden. Während die Zugriffszeiten für Festplatten in Millisekunden (ms) angegeben sind, bewegen sich die Zugriffszeiten von RAM-Speicher im Nanosekunden-Bereich (ns).(1 ms = 1 000 000 ns)
24
2030-4.book Page 25 Monday, February 8, 2010 4:26 PM
Hardwareumgebung
Diese Tatsache kann man natürlich auch zum Vorteil nutzen, indem man durch großzügig bemessenen Hauptspeicher ein Puffern (Caching) von oft verwendeten Daten ermöglicht, die sonst durch wesentlich langsamere Festplattenzugriffe gelesen werden müssten. So ist es bei gut konfigurierten Datenbankservern durchaus normal, dass über 90 % der Daten aus dem Hauptspeicher gelesen werden. Diese Kette lässt sich sogar noch fortsetzen, wenn man den weiter oben bereits erwähnten prozessorinternen CPU-Cache berücksichtigt. Dieser ist durch seine Integration in den Prozessor wiederum um ein Vielfaches schneller ansprechbar als der normale Hauptspeicher, sodass man auch hier von einem möglichst großen Cache profitieren kann. Allerdings lässt sich der Prozessorcache im Gegensatz zum Hauptspeicher natürlich nicht mehr nachträglich aufrüsten. Für eine Aufrüstung des Hauptspeichers spricht außerdem, dass die RAM-Preise seit einigen Jahren so niedrig sind, dass Hauptspeicher auch eine der günstigsten Varianten für eine Hardwareaufrüstung darstellen. Allerdings müssen – je nach Gesamtgröße des Hauptspeichers – auch einige Voraussetzungen bei Betriebssystem und SQL Server erfüllt sein, damit dieser auch genutzt werden kann. Diese werden in Kapitel 1.3.1 – Auswahl der richtigen Edition behandelt.
1.1.3
Festplatten
Da der langsame Zugriff auf Festplatten einer der typischen Flaschenhälse in der Gesamtperformance eines Datenbankservers darstellt, gibt es auch in diesem Bereich einige Ansätze, dieses Übel so weit wie möglich zu minimieren.
Parallele Zugriffe durch Verteilung auf mehrere Platten ermöglichen Wie bei den Prozessoren basieren diese meist auf Ausnutzung von parallelen Zugriffen. Dies bedeutet im einfachsten Fall, dass Betriebssystem und SQL-Server-Datenbanken auf verschiedenen Festplatten (nicht Partitionen, sondern physikalisch getrennten Platten) untergebracht werden. Dadurch können die Daten nahezu parallel gelesen und geschrieben werden. Gleichzeitig wird beim Lesen von größeren Datenblöcken die Notwendigkeit minimiert, den Schreib-/Lesekopf häufig neu zu positionieren. Dieses Prinzip lässt sich nahezu beliebig fortsetzen, sodass für eine optimale Konfiguration separate Datenträger für folgende Dateien verwendet werden: 쮿
System (Betriebssystem & SQL Server)
쮿
Windows-Auslagerungsdatei
쮿
Datenbankdateien
쮿
Protokolldateien
쮿
TempDB (Datenbank & Protokoll)
쮿
Backupdateien
SQL Server Performance-Ratgeber
25
2030-4.book Page 26 Monday, February 8, 2010 4:26 PM
1 – Vorbereitung der Systemumgebung
Wenn weitere Platten verfügbar sind, lassen sich auch noch Datenbank- und Protokolldatei der TempDB trennen sowie verschiedene Datenbanken auf separate Platten verteilen. Sollten Filestreams verwendet werden, können auch diese auf eine weitere Platte gelegt werden. Für kleinere Abteilungsserver stehen in der Regel aber eher deutlich weniger Plattensysteme zur Verfügung, sodass man sich hier darauf beschränken muss, die Dateien voneinander zu trennen, die am häufigsten gemeinsam benötigt werden. Dies sind vor allem System-, Datenbank-, Protokolldateien und TempDB. Oft bringt es schon einen spürbaren Performancegewinn, die eher leselastigen Dateien (Systemdateien und Datenbank) von den schreiblastigeren (Protokoll und TempDB) zu trennen. Die Verteilung der Dateien wird natürlich durch den Datenbankadministrator vorgenommen, der Systemadministrator muss aber die entsprechenden Plattensysteme zur Verfügung stellen, sodass auch hier eine enge Zusammenarbeit erforderlich ist.
Einsatz von RAID-Systemen für Sicherheit und Performance Ursprünglich primär entwickelt, um die Datensicherheit zu erhöhen, sind heute in fast allen produktiven Serverumgebungen sogenannte RAID-Systeme im Einsatz. Die Abkürzung RAID stand ursprünglich für Redundant Array of Inexpensive Disks (inzwischen gilt die marketingtauglichere Variante Redundant Array of Independent Disks) und bezeichnet damit den Zusammenschluss von mehreren preiswerten (oder unabhängigen) Festplatten, auf denen die Daten redundant gespeichert werden, um beispielsweise den Komplettausfall einer einzigen Platte ohne Datenverlust abfangen zu können. Durch die redundante Speicherung kommt als positiver Nebeneffekt hinzu, dass zumindest Lesezugriffe mitunter deutlich beschleunigt werden, da die Daten nun parallel gelesen werden können. Je nach verwendeter RAID-Variante kann sich dies allerdings negativ auf die Schreib-Performance auswirken, da die redundanten Daten ja auch zusätzlich geschrieben werden müssen. In den RAID-Implementierungen ist zwischen einem Software- und einem Hardware-RAID zu unterscheiden. Das Software-RAID wird durch eine Software (z.B. das Betriebssystem) gebildet. Im Falle des Hardware-RAID ist ein spezieller Controller für die Bildung des RAID verantwortlich. Auf dieser Controller Card ist ebenfalls Software in einem Speicherbaustein abgelegt, die jedoch nicht durch das Betriebssystem gesteuert wird. Allen RAID-Varianten gemeinsam ist die Tatsache, dass alle Laufwerke eines RAID-Verbundes zusammen als ein logisches Laufwerk im Betriebssystem erscheinen. Die verschiedenen RAID-Varianten werden als RAID-Level bezeichnet, von denen ich die gängigsten drei kurz beschreiben will. RAID 0 (Striping) – Geschwindigkeit ohne Netz und doppelten Boden RAID 0 verdient eigentlich nur den Namen AID (oder auch „0-redundant AID“), denn der Aspekt der redundanten Datenspeicherung wurde hier weggelassen. Eigentlich werden hierbei lediglich zwei physikalische Datenträger zu einem logischen Datenträger vereint. Eine erhöhte Ausfallsicherheit ist hier also nicht gegeben. Man bedient sich dafür allerdings eines einfachen Tricks, um zumindest einen Performancegewinn zu erhalten: Und zwar werden die Daten nicht fortlaufend auf den Platten abgelegt, sondern blockweise abwechselnd – quasi im Reißverschlussverfahren. Dadurch können sowohl beim Lesen als auch beim Schreiben beide Platten parallel genutzt werden. Nachteilig ist dabei aller-
26
2030-4.book Page 27 Monday, February 8, 2010 4:26 PM
Hardwareumgebung
dings die Tatsache, dass bei einem Plattendefekt gleich alle Daten verloren sind, da jede Datei ihre Daten auf beide Platten verteilt hat. (Lediglich dann, wenn eine Datei klein genug ist, um komplett in einen der – meist 64 KB großen – Blöcke zu passen, wäre diese rein theoretisch noch zu retten, wenn die andere Platte ausfällt.)
RAID 0 A1
A2
A3
A4
A5
A6
A7
A8
Disk 0
Disk 1
Abbildung 1.1: Aufbau eines RAID-0-Systems
Der typische Einsatz eines RAID-0-Systems ist also ein Szenario, in dem viele Daten schnell gelesen oder geschrieben werden (optimalerweise sequenziell) und die Ausfallsicherheit eher eine untergeordnete Rolle spielt. Bei einem SQL Server wäre dies beispielsweise geeignet, um die TempDB zu speichern, da diese nach einem Neustart des SQL-Server-Dienstes ohnehin geleert wird und daher keine dauerhaft relevanten Daten enthält. RAID 1 (Mirroring) – Spieg'lein, Spieg'lein an der Wand ... RAID 1 ist ebenso einfach erklärt wie RAID 0, handelt es sich hierbei lediglich um zwei (oder mehr) Platten, die den exakt identischen Inhalt speichern. Somit ist automatisch eine gewisse Ausfallsicherheit gegeben, denn bei einem Plattendefekt kann jede Platte alle anderen jederzeit vollends ersetzen. Allerdings geht dies auch stark auf Kosten der Kapazität, denn die gesamte nutzbare Kapazität entspricht der Speicherkapazität der kleinsten Platte im Verbund. Daher werden bei RAID 1 meist auch nur zwei (möglichst gleich große) Festplatten verwendet, um möglichst wenig Platz zu verschwenden. Dennoch wird selbst in dieser Konfiguration natürlich die doppelte Menge der eigentlich benötigten Kapazität erforderlich.
SQL Server Performance-Ratgeber
27
2030-4.book Page 28 Monday, February 8, 2010 4:26 PM
1 – Vorbereitung der Systemumgebung
In Bezug auf Performance ist RAID 1 besonders beim Lesen schnell, denn auch hier kann von allen beteiligten Platten parallel gelesen werden. Da alle Platten dieselben Informationen vorhalten, ist es sogar egal, welche Daten woher gelesen werden, sodass die schnelleren Platten nicht warten müssen, bis die langsameren fertig sind, sondern stattdessen weitere Daten lesen können. Beim Schreiben ist dagegen kein Performancegewinn zu erhoffen, da alle Schreiboperationen gleichzeitig auf allen Platten ausgeführt werden müssen. Aufgrund der notwendigen Synchronisierung ist hierbei sogar mit einem minimalen Performanceverlust zu rechnen. Hier begegnen wir also zum ersten Mal einem immer wiederkehrenden Prinzip der Performanceoptimierung: „Redundante Datenhaltung erhöht die Lesegeschwindigkeit auf Kosten der Schreibgeschwindigkeit!“ (Wobei der negative Einfluss auf die Schreibgeschwindigkeit in diesem Fall wirklich vernachlässigbar klein ist.)
RAID 1 A1
A1
A2
A2
A3
A3
A4
A4
Disk 0
Disk 1
Abbildung 1.2: Aufbau eines RAID-1-Systems
RAID 1 eignet sich also für Daten, die vor allem schnell gelesen werden sollen und dabei eine gewisse Ausfallsicherheit benötigen. Bei einem Datenbankserver trifft dies am ehesten auf die Systempartition zu, auf der das Betriebssystem und die SQL-Server-Software gespeichert sind. RAID 5 – Performant und redundant Das RAID-Level 5 bietet sowohl Ausfallsicherheit als auch Performance und das bei vergleichsweise geringen Eingeständnissen bezüglich der nutzbaren Gesamtkapazität. Das verwendete Prinzip basiert darauf, dass bei einer bestimmten Anzahl von Festplatten jeweils eine zusätzliche Platte Paritätsinformationen (quasi eine Prüfsumme, basierend auf einer XOR-Verknüpfung) speichert. Dadurch ist beim Ausfall einer beliebigen Platte im Verbund der fehlende Wert durch die Paritätsinformation rekonstruierbar. Um die Platten möglichst gleichmäßig auszulasten, wird – je nach Position des Blocks auf der Platte – jeweils eine andere Platte zur Speicherung der Paritätsinformationen verwendet. Beim Lesen der Daten kann teilweise ein deutlicher Performancegewinn erzielt werden, da auf mehrere Platten parallel zugegriffen werden kann. Dafür muss man beim Schreiben mit geringen Performanceeinbußen rechnen, da neben dem Schreiben des eigentlichen Werts
28
2030-4.book Page 29 Monday, February 8, 2010 4:26 PM
Hardwareumgebung
auch alle entsprechenden Blöcke auf den anderen Platten gelesen und die Paritätsinformation aktualisiert werden muss. Im Gegensatz zu RAID 1 ergibt sich ein Performancenachteil im Fehlerfall. Denn sobald eine Platte ausgefallen ist, müssen die Blöcke auf allen verbleibenden Platten gelesen werden, um die Daten des fehlenden Blocks zu rekonstruieren. Bei RAID 1 kann dagegen die verbleibende Platte mit normaler Geschwindigkeit weiter genutzt werden. Erst wenn die defekte Platte ausgetauscht wird, entstehen zusätzliche Performanceeinbußen, da diese dann erst einmal mit den richtigen Daten versorgt werden muss (dies gilt dann aber sowohl für RAID 1 als auch für RAID 5).
RAID 5 A1
A2
A3
Ap
B1
B2
Bp
B3
C1
Cp
C2
C3
Dp
D1
D2
D3
Disk 0
Disk 1
Disk 2
Disk 3
Abbildung 1.3: Aufbau eines RAID-5-Systems
Aufgrund der Kombination von Ausfallsicherheit und Performancegewinn ist RAID 5 normalerweise die bevorzugte Variante zum Speichern von Datenbank-Backups. Für das Speichern von Datenbank- und Protokolldateien ist RAID 5 ebenfalls gut geeignet, hier gibt es jedoch noch Alternativen, die eine deutlich bessere Performance bieten. RAID 01 und RAID 10 – „gespiegelte Streifen“ und „gestreifte Spiegel“ Bei diesen beiden Varianten handelt es sich um die Kombination aus RAID 0 und RAID 1. Dabei werden bei RAID 01 mehrere RAID 0-Systeme zu einem RAID 1-System zusammengeschlossen. Bei RAID 10 dagegen werden mehrere RAID-1-Systeme zu einem RAID-0System zusammengefügt. Beide Varianten bieten sowohl die Ausfallsicherheit von RAID 1 als auch die gesteigerte Performance (sowohl für schreibende als auch für lesende Zugriffe) von RAID 0. Auf der anderen Seite kommt hier natürlich auch der RAID-1-typische Nachteil zum Tragen, dass die doppelte Plattenkapazität benötigt wird. Wenn man mit diesem Manko leben kann, bieten diese beiden Varianten eine hervorragende Basis für Ausfallsicherheit in Kombination mit optimaler Performance. Damit eignen sich diese kombinierten Varianten bestens zum Speichern von Datenbank- und Protokolldateien.
SQL Server Performance-Ratgeber
29
2030-4.book Page 30 Monday, February 8, 2010 4:26 PM
1 – Vorbereitung der Systemumgebung
RAID 10 RAID 0
RAID 1
RAID 1
A1
A1
A2
A2
A3
A3
A4
A4
A5
A5
A6
A6
A7
A7
A8
A8
Disk 0
Disk 1
Disk 2
Disk 3
Abbildung 1.4: Aufbau eines RAID-10-Systems
Welcher von beiden Varianten man den Vorzug gibt, ist letzten Endes Geschmackssache. Bei RAID 10 dürfte es problemloser möglich sein, eine einzelne defekte Platte auszutauschen, weshalb dies wohl auch die weiter verbreitete Variante ist.
i
Für die weiter oben beschriebene Verteilung auf mehrere Plattensysteme ist ein RAID-System als eine Festplatte anzusehen. Aus diesem Grund habe ich in der Beschreibung auch meist den Begriff Plattensystem anstelle von Festplatte verwendet.
SAN Insbesondere bei System- bzw. Netzwerkadministratoren sehr beliebt sind die so genannten Storage Area Networks (oder kurz: SAN). Diese stellen eine große Menge an Plattenspeicher zur Verfügung, der dann beliebigen Servern dynamisch zugeordnet werden kann. Während das beispielsweise für die Archivierung von Daten eine wunderbare Lösung sein kann, da der Platz leicht dynamisch zugeteilt werden kann, wo er gerade benötigt wird, ist dies für einen Datenbankserver eine sehr schlechte Wahl. Durch die transparente Zuweisung des Speicherplatzes ist hierbei keine sinnvolle Zugriffsoptimierung mehr durchführbar. Daher sollten SAN-Systeme im Zusammenhang mit Datenbankservern lediglich für weniger performancekritische Aufgaben (z.B. zur Archivierung von Backups) eingesetzt werden. Falls ein SAN verwendet wird, sollte dies exklusiv für den SQL Server zur Verfügung stehen, damit dieser nicht durch andere Server ausgebremst wird, die ebenfalls dasselbe SAN verwenden.
30
2030-4.book Page 31 Monday, February 8, 2010 4:26 PM
Hardwareumgebung
SATA, SCSI & SSD Natürlich gibt es auch bei der Geschwindigkeit der Festplatten selbst große Unterschiede. Früher wurden für Server bevorzugt SCSI-Festplatten verwendet, da diese – in Kombination mit dem passenden Controller – eine deutlich bessere Leistung boten als die günstigeren IDE-Platten. Daneben sind SCSI-Festplatten auf einen Dauerbetrieb in Servern ausgerichtet, d.h., im Gegensatz zu IDE-Festplatten können diese nach Herstellerangaben in einem 24x7Betrieb (also ununterbrochen 24 Stunden am Tag, 7 Tage die Woche) laufen. Die Drehzahl von SCSI-Festplatten und die damit verbundenen Zugriffszeiten sind ebenfalls höher als bei IDE-Festplatten. Spätestens mit Einführung der aktuellen SATA II-Platten spielt dies aber keine Rolle mehr, sodass nun auch in Servern oftmals wieder dieselben „herkömmlichen“ Festplatten verwendet werden, die auch in Arbeitsplatzrechnern zu finden sind. Sowohl SATA- als auch SCSI-Festplatten können – im Gegensatz zu den älteren IDE-Platten – mehrere I/O-Anfragen pro Kanal austauschen, sodass diese beiden Technologien in einem Server eine deutlich bessere Performance bieten. Die meisten Festplatten – sowie auch entsprechende Controller – verfügen auch über einen eigenen Cache zum Puffern von Daten. Problematisch ist dabei, dass der SQL Server über diese Art von Cache keine Kontrolle hat. Wenn es zu einem Systemabsturz kommt, weiß der SQL Server normalerweise genau, welche Daten schon geschrieben wurden und für welche Transaktionen ein Rollback ausgeführt werden muss. Wenn die Datenänderung aber noch im Pufferspeicher der Festplatte oder des Controllers lag und dort durch den Absturz verloren ging, geht der SQL Server fälschlicherweise davon aus, dass die Daten korrekt geschrieben wurden und es kommt zu Inkonsistenzen. Aus diesem Grund sollte zumindest der Schreibcache von Festplatten und Controllern nicht verwendet werden (es sei denn, der Cache ist batteriegepuffert und es ist dadurch sichergestellt, dass die Daten auch bei einem Systemausfall noch geschrieben werden können). Eine Möglichkeit, die Geschwindigkeit beim Plattenzugriff weiter zu erhöhen, liegt darin, statt klassische Festplatten Flash-Speicher (so genannte Solid State Disks, oder kurz: SSD) zu verwenden, die inzwischen deutlich schneller sind als herkömmliche Platten, da sie nicht auf eine komplizierte Mechanik angewiesen sind und damit nicht denselben physikalischen Beschränkungen unterliegen. Allerdings sind diese SSD-Platten noch sehr teuer, sodass aus dem RAID schnell ein „RAED“ (Redundant Array of Expensive Disks) werden kann. Jedoch fallen die Preise ständig, sodass SSDs eine immer interessanter werdende Alternative zu herkömmlichen Plattensystemen darstellen. Oftmals werden neue Serversysteme zumindest für das Betriebssystem mit SSDs ausgestattet, sodass die Schreib-/Lesezugriffe hier entsprechend schnell funktionieren.
Blockgröße Bei der Formatierung von Festplatten stellt sich die Frage der richtigen Blockgröße (Clustergröße). Bei älteren Dateisystemen war diese teilweise fest vorgegeben, bei den aktuelleren Dateisystemen – wie beispielsweise NTFS – ist diese innerhalb gewisser Grenzen einstellbar. Um den optimalen Wert zu finden, muss man wissen, dass die SQL Server Storage Engine intern mit 8 KB großen Speicherseiten arbeitet. Acht dieser Seiten werden wiederum zu einem Block (oder engl. Extent) zusammengefasst. Diese 64 KB großen Blöcke sind normalerweise die Einheiten, die physikalisch geschrieben oder gelesen werden. Daher sollte man die NTFS-Blockgröße ebenfalls auf diesen Wert festlegen. Das ist natürlich nur für Platten prak-
SQL Server Performance-Ratgeber
31
2030-4.book Page 32 Monday, February 8, 2010 4:26 PM
1 – Vorbereitung der Systemumgebung
tikabel, die überwiegend SQL-Datenbank- und/oder Protokolldateien speichern. Für die Systempartition dagegen wäre dies reine Platzverschwendung, da jede noch so kleine Datei (und Windows beinhaltet Unmengen von kleinen Dateien) mindestens 64 KB belegen würde. Für die Partitionen, auf denen Systemdateien und/oder die Windows-Auslagerungsdatei abgelegt sind, wird im Allgemeinen eine Blockgröße von 4 KB empfohlen.
Fragmentierung Die Wahl von 64 KB großen Blöcken für SQL-Daten-Partitionen verringert die Gefahr einer übermäßigen Fragmentierung der Dateien. Dennoch sollte man als regelmäßige Wartungsaufgabe eine explizite Defragmentierung aller Plattensysteme durchführen, auf denen viel geschrieben wird. Dafür kann man einerseits die vom Betriebssystem verwendeten Tools nutzen. Für gesperrte Dateien – wie beispielsweise Datenbankdateien, wenn die Datenbank online ist – eignen sich stattdessen besser Zusatz-Tools – wie beispielsweise PageDefrag von Mark Russinovich, das im Rahmen der Sysinternals Suite (aber wahlweise auch als separater Download) kostenfrei erhältlich ist. Weitere Informationen dazu hier: http://technet.microsoft.com/de-de/sysinternals/bb897426.aspx
1.1.4
Netzwerk
Da die Datenbankanwendung in den wenigsten Fällen direkt auf dem SQL Server laufen wird, ist auch eine performante Netzwerkanbindung ein wichtiger Faktor für die Gesamtperformance eines Datenbankservers. Die Notwendigkeit der Verwendung einer schnellen Netzwerkkarte ist sicherlich offensichtlich. Wichtig ist hier aber natürlich auch die gesamte Anbindung. Plump gesagt nutzt es wenig, wenn der Server zwar über eine Gigabit-Netzwerkkarte verfügt, damit aber an einem 10-Mbit-Hub hängt. In vielen Fällen kann es auch sinnvoll sein, den Datenbankserver mit mehreren Netzwerkkarten auszustatten. Beispielsweise dann, wenn er über eine Netzwerkkarte mit dem Firmennetz und über eine weitere mit einem Internet-Server verbunden ist, auf dem Webanwendungen laufen, die den SQL Server nutzen. Ich will an dieser Stelle auch nicht tiefer in das Netzwerkthema einsteigen, denn das ist – offen gestanden – nicht wirklich mein Spezialgebiet und würde auch sicherlich den Rahmen dieses Buches sprengen. Auf eine kleine Einstellung im Betriebssystem möchte ich aber dennoch hinweisen, denn diese kann leicht dazu führen, dass eine schnelle Netzwerkkarte nur langsam genutzt wird. Unter den Eigenschaften der Netzwerkverbindung ist ein Punkt zu finden, über den die Netzwerkkarte konfiguriert werden kann. Hier ist bei den meisten Netzwerkkarten unter den erweiterten Eigenschaften eine Einstellung für Verbindungsgeschwindigkeit und Duplex-Modus aufgelistet. Sollte hier die Einstellung Auto stehen, sollten Sie dies in eine explizite Einstellung (z.B. 100 Full Mode) ändern, da Auto oft dafür sorgt, dass ein zu langsamer Modus verwendet wird, obwohl eine deutlich schnellere Verbindungsgeschwindigkeit möglich wäre. Diese Einstellung muss natürlich mit dem Port, an dem das Netzkabel angeschlossen ist, korrespondieren. Hier kann im Zweifelsfall der zuständige Netzwerkadministrator mit Rat und Tat zur Seite stehen.
32
2030-4.book Page 33 Monday, February 8, 2010 4:26 PM
Betriebssystem
Abbildung 1.5: Die Eigenschaften der Netzwerkkarte
1.2
Betriebssystem
1.2.1
Auswahl des richtigen Betriebssystems
Die Auswahl des richtigen Betriebssystems hängt einerseits von der Anzahl der User ab, die mit diesem arbeiten (bzw. sich mit diesem verbinden), sollte aber auch zur entsprechenden SQL Server Edition passen. Sofern es sich um eine Test-/Entwicklungsmaschine für einen Benutzer oder einen Arbeitsgruppenserver handelt, kann man durchaus SQL Server Compact, Express, aber auch die Developer Edition in Kombination mit einer Arbeitsplatz-Variante von Windows (wie beispielsweise Windows XP, Windows Vista1 oder auch Windows 7) nutzen. Bei produktiven Servern, die eine der anderen SQL Server Editionen nutzen, ist auch ein Server-Betriebssystem (Windows 2003 Server, Windows 2008 Server) erforderlich. Der Windows Small Business Server 2008 und Windows Essential Business Server 2008 werden in der Premium-Variante sogar mit einem SQL Server (SQL Server 2008 Standard for Small Business bzw. SQL Server 2008 Standard) ausgeliefert. Für alle Varianten gilt, dass – sofern ein 64-Bit-Prozessor verwendet wird – natürlich auch die 64-Bit-Variante des gewählten Betriebssystems installiert werden sollte. Dies ermöglicht unter anderem die Nutzung von mehr Hauptspeicher, da ein 32-Bit-Prozessor auf normalem
1.
Windows Vista ist aus Performancesicht eher nicht zu empfehlen. Verwenden Sie stattdessen besser Windows XP oder das neue Windows 7.
SQL Server Performance-Ratgeber
33
2030-4.book Page 34 Monday, February 8, 2010 4:26 PM
1 – Vorbereitung der Systemumgebung
Weg lediglich 4 GB RAM adressieren kann (wovon in der Standardkonfiguration 2 GB für das Betriebssystem und 2 GB für SQL Server zur Verfügung stehen2). Für extrem leistungsfähige Systeme mit mehr als 64 Prozessorkernen sollte Windows Server 2008 R2 verwendet werden, da diese überarbeitete Variante des Betriebssystems bis zu 256 Prozessorkerne unterstützt. Die ursprüngliche Version von Windows Server 2008 ist dagegen auf maximal 64 CPU-Kerne beschränkt, was für die meisten Anwendungszwecke allerdings auch ausreichen sollte. Nichtsdestotrotz ist auch aus anderen Gründen der Einsatz von Windows Server 2008 R2 zu empfehlen, soweit die Beschaffungsrichtlinie Ihres Unternehmens dies zulässt.
1.2.2
Konfiguration des Betriebssystems
Windows-Auslagerungsdatei Auch bei der Konfiguration des Betriebssystems gibt es einige Punkte zu beachten. Einer davon ist die Windows-Auslagerungsdatei (engl. Pagefile). Hier lässt sich betriebssystemseitig ein Wert für die minimale und maximale Größe festlegen. Da ein häufiges Vergrößern und Verkleinern der Auslagerungsdatei eine hohe Last erzeugt und für zusätzliche Fragmentierung der Festplatte sorgt, sollte hier für beide Einstellungen derselbe Wert verwendet werden. Microsoft empfiehlt als Einstellung die zweieinhalbfache Größe des installierten Arbeitsspeichers. Erfahrungsgemäß sollte die Größe einer Auslagerungsdatei aber 4095 MB nicht übersteigen, selbst wenn mehr Hauptspeicher verfügbar ist. Wie bereits weiter oben erwähnt, ist aus Performancegründen die Nutzung einer separaten Festplatte für die Auslagerungsdatei zu empfehlen. Hier kann beispielsweise eine kostengünstige SATA II-Platte genutzt werden (gerne auch ohne RAID oder mit RAID-Level 0). Vor dem Ändern der Größe und/oder des Speicherortes der Auslagerungsdatei sollte eine Defragmentierung des entsprechenden Laufwerks durchgeführt werden, damit die neue Auslagerungsdatei mit konstanter Größe möglichst unfragmentiert gespeichert werden kann.
Dienste Der andere Ansatz liegt darin, alle nicht benötigten Dienste auf dem Server zu beenden, da diese nur unnötige Ressourcen verbrauchen. So wird – insbesondere bei älteren Versionen von Windows Server – oft automatisch der Internet Information Service Dienst mit installiert, der sicherlich nicht benötigt wird, wenn der Rechner als reiner Datenbankserver genutzt werden soll. Nutzen Sie auf dem Server aber gleichzeitig auch die SQL Server Reporting Services, so wird wiederum ein Webserver-Dienst benötigt. Aber auch in vielen kleineren Umgebungen wird manchmal bewusst ein Server sowohl als Datenbank- als auch als Webserver genutzt. Hier sollte man sich zumindest der Tatsache bewusst sein, dass hier noch ein gewisses Optimierungspotenzial liegt, indem man die Dienste auf zwei Server verteilt.
2.
34
Diese Verteilung lässt sich auch ändern, sodass beispielsweise 1 GB für das Betriebssystem und 3 GB für SQL Server zur Verfügung stehen.
2030-4.book Page 35 Monday, February 8, 2010 4:26 PM
Datenbank-Management-System
Auch im Rahmen von Sicherheitsüberlegungen ist die Analyse von aktivierten Diensten und Applikationen ein wichtiger Aspekt. Microsoft verfolgt bei den neueren Betriebssystemen ohnehin die Strategie, standardmäßig nur benötigte Dienste zu aktivieren, sodass der Performancegewinn direkt am Anfang im System etabliert ist. Da SQL-Administratoren ihre Systeme nicht im luftleeren Raum betreiben, ist ebenfalls der Bereich der Drittapplikationen wie Antivirussoftware und Datensicherungssoftware aus Performancesicht kritisch zu betrachten. Die Antivirenhersteller sowie Microsoft geben regelmäßig Empfehlungen raus, wie SQL-Systeme mit ihren Softwareprodukten zu konfigurieren sind, sodass es einerseits keinen unnötigen Performanceverlust und andererseits auch keine korrupten Datenbanken gibt. Hier sollten Sie die entsprechenden Dokumente Ihres Herstellers kennen. Eine Datensicherungssoftware wiederum „belastet“ ein System mit seinen regelmäßig durchgeführten Sicherungsprozeduren. Oftmals wird im Rahmen eines Server-Sizings die entsprechende Arbeitslast dieser beiden Softwarekategorien nicht beachtet und erst im laufenden Betrieb nachgerüstet.
1.3
Datenbank-Management-System
Die optimale Konfiguration des Datenbank-Management-Systems ist definitiv eine Aufgabe des Datenbankadministrators. Dieser bekommt in der Regel vom Systemadministrator einen Server bereitgestellt, bei dem das Betriebssystem installiert und konfiguriert ist, und ist nun für Installation und Konfiguration des SQL Servers verantwortlich. Ein wesentlicher Aspekt liegt dabei darin, die vorgegebene Hardware auch effektiv nutzbar zu machen. Zuvor gilt es jedoch, erst einmal die passende Edition von SQL Server auszuwählen.
1.3.1
Auswahl der richtigen Edition
SQL Server ist mittlerweile in sieben verschiedenen Editionen verfügbar, die – von der Compact Edition einmal abgesehen – alle auf derselben Basis aufbauen. Dadurch können Sie dieselbe Anwendung auf jeder dieser Editionen laufen lassen, je nachdem, welches Software-Budget Sie zur Verfügung haben. Allerdings macht es auch bei großzügig bemessenem Budget nicht immer Sinn, auch die größte Edition zu verwenden, denn mit dem Funktionsumfang steigen auch die Anforderungen an die Systemvoraussetzung. So ist die Compact Edition als einzige auch auf Mobile Devices wie Windows Mobilebasierten Pocket PCs und Smartphones einsetzbar. Während die Express Edition auf jeder Windows-Variante für Desktoprechner läuft, ist für die hauptsächlich in größeren Unternehmen genutzten Standard- und Enterprise-Editionen eine Server-Variante des Windows-Betriebssystems (2003 oder 2008) erforderlich.
SQL Server Performance-Ratgeber
35
2030-4.book Page 36 Monday, February 8, 2010 4:26 PM
1 – Vorbereitung der Systemumgebung
kostenfreie Editionen Spezialeditionen
Haupteditionen
Edition
max. CPUs
max. RAM
max. DB-Größe
Compact Edition
unbegrenzt
unbegrenzt
4 GB
Express Edition
1
1 GB
4 GB
Web Edition
4
unbegrenzt
unbegrenzt
Workgroup Edition
2
4 GB
unbegrenzt
Developer Edition
unbegrenzt
unbegrenzt
unbegrenzt
Standard Edition
4
unbegrenzt
unbegrenzt
Enterprise Edition
unbegrenzt
unbegrenzt
unbegrenzt
Tabelle 1.1: Beschränkungen der verschiedenen SQL-Server-Editionen (SQL Server 2008)
Dabei spielt die Beschränkung der Datenbankgröße für die Performance eine eher untergeordnete Rolle und wirkt sich lediglich indirekt aus, falls Sie die Daten bei Verwendung einer Compact oder Express Edition auf mehrere Datenbanken verteilen müssen. Durch den damit verbundenen zusätzlichen Verwaltungsaufwand wird die Gesamt-Performance sicherlich negativ beeinflusst. Abgesehen von der Compact Edition gibt es alle SQL-Server-Editionen sowohl in einer 32- als auch in einer 64-Bit-Variante. Dies sollte wiederum mit Prozessortyp und Betriebssystem abgestimmt sein. Wenn Sie also einen 64-Bit-Prozessor mit dazu passendem 64-Bit-Windows 2008 Server verwenden, wäre es sicherlich wenig sinnvoll, eine 32-Bit-Variante von SQL Server zu installieren. Ein weiterer Unterschied der verschiedenen Editionen liegt darin, dass viele ZusatzTools und Features erst in größeren Versionen enthalten sind. Darunter sind auch einige Features enthalten, die für die Datenbankperformance eine wichtige Rolle spielen: Feature
Enterprise
Standard
Workgroup
Web
Express
SQL Server Profiler
ja
ja
ja
ja
nein
Datenbankoptimierungsratgeber
ja
ja
ja
ja
nein
Ressource Governor
ja
nein
nein
nein
nein
Partitionierung
ja
nein
nein
nein
nein
Online-Indizierung
ja
nein
nein
nein
nein
Parallele Indexoperationen
ja
nein
nein
nein
nein
SQL Merge-Befehl („Upsert“)
ja
ja
ja
ja
ja
Dynamic AWE
ja
ja
nein
nein
nein
Tabelle 1.2: Performance-Features für verschiedene SQL Server 2008 Editionen
Man sieht schnell, dass für viele Performance-Features die Enterprise (bzw. Developer) Edition erforderlich ist. Das soll natürlich nicht heißen, dass die anderen Editionen langsam sind. Jedoch ist die Enterprise Edition bewusst so platziert, dass sie vor allem zusätzliche Hochverfügbarkeits- und Performance-Features beinhaltet.
36
2030-4.book Page 37 Monday, February 8, 2010 4:26 PM
Datenbank-Management-System
1.3.2
Installation
Bei der Installation des SQL Servers sollte die bereits erwähnte Aufteilung auf verschiedene Plattensysteme berücksichtigt werden. Bei SQL Server 2008 kann man bereits bei der Installation verschiedene Standardverzeichnisse für bestimmte Dateitypen (Datenbankdateien, Protokolldateien, TempDB etc.) definieren.
Abbildung 1.6: Die Auswahl der Verzeichnisse während der Installation
Für ältere Versionen von SQL Server konnte man lediglich einen Installationspfad angeben und musste später selbst dafür sorgen, dass beispielsweise Protokolldateien auf einem anderen Laufwerk abgelegt werden. Ähnlich wie beim Betriebssystem sollten Sie auch hier alle nicht benötigten Dienste deaktivieren. So können Sie bei der Featureauswahl zwar zusätzliche Dienste wie beispielsweise die SQL Server Integration Services, Analysis Services und Reporting Services problemlos installieren, sollten diese aber – sofern sie nicht genutzt werden – deaktivieren, damit sie außer dem Plattenplatz, den sie belegen, keine weiteren Ressourcen beanspruchen. Dies lässt sich bereits während der Installation bei der Serverkonfiguration einstellen, indem die entsprechenden Dienste den Starttyp Manuell oder Deaktiviert erhalten. Wenn Sie diese Einstellung nach erfolgter Installation vornehmen (oder aber revidieren) wollen, geht dies einerseits – wie für jeden anderen Dienst auch – über die Windows-Systemsteuerung. Komfortabler ist dies aber über den SQL-Server-Konfigurationsmanager zu bewerkstelligen.
SQL Server Performance-Ratgeber
37
2030-4.book Page 38 Monday, February 8, 2010 4:26 PM
1 – Vorbereitung der Systemumgebung
Abbildung 1.7: Die Ansicht der SQL-Server-Dienste im SQL-Server-Konfigurationsmanager
Hier erhalten Sie übersichtlich nur diejenigen Dienste aufgelistet, die sich auf SQL Server beziehen (inklusive anderer Instanzen, Editionen und Versionen). Nutzen Sie den SQL-Server-Konfigurationsmanager – gerade dann, wenn mehrere Instanzen auf einem Server installiert sind –, um sich einen Überblick zu verschaffen, welche davon gerade laufen. Nur zu häufig wird beispielsweise nach einem Side-by-side Upgrade vergessen, die alte SQL-Server-Instanz irgendwann zu deaktivieren, so dass diese weiterhin unnötig Ressourcen verbraucht.
1.3.3
Konfiguration
Auswahl von Netzwerkprotokollen Der SQL-Server-Konfigurationsmanager bringt mich auch gleich zu einem weiteren wichtigen Punkt, denn hierüber werden die zu verwendenden Netzwerkprotokolle definiert. Dies geschieht einerseits für den Server, indem eines oder mehrere der folgenden Protokolle aktiviert werden: 쮿
Shared Memory
쮿
TCP/IP
쮿
Named Pipes
쮿
VIA (Virtual Interface Architecture)
Dabei bietet Shared Memory zwar den direktesten und damit sicherlich schnellsten Weg. Diese Variante ist im Normalfall aber dennoch uninteressant, da hierbei ein gemeinsamer Speicherbereich zur Kommunikation genutzt wird, was folglich nur für die Kommunikation innerhalb eines Servers verwendet werden kann. (Der einzige halbwegs sinnvolle Einsatzzweck liegt damit im Zusammenhang mit einer Webanwendung, wenn Webserver und SQL Server auf demselben Rechner liegen, was aber alleine aus SecurityAspekten nicht zu empfehlen ist.)
38
2030-4.book Page 39 Monday, February 8, 2010 4:26 PM
Datenbank-Management-System
VIA dagegen lässt sich nur mit entsprechender VIA-Hardware einsetzen und spielt daher lediglich eine Außenseiterrolle. Bleiben also eigentlich nur Named Pipes und TCP/ IP als sinnvolle Varianten übrig. Diese unterscheiden sich durch unterschiedliche Nutzungsschwerpunkte: Während Named Pipes eher für die Kommunikation in einem lokalen Netzwerk (LAN) ausgelegt ist, bietet sich TCP/IP eher für den Transport über weitere Strecken (z.B. über das Internet) an. In vielen Umgebungen verhindert eine Firewall die Nutzung von Named Pipes, während für TCP/IP lediglich der entsprechende Port (1433) freigeschaltet werden muss. Daher hat sich TCP/IP in der Praxis als Standardprotokoll etabliert, während das unter Umständen schnellere Protokoll Named Pipes seltener Verwendung findet. Wenn Sie die Möglichkeit haben, testen Sie einfach beide Varianten einmal aus und beobachten Sie, ob sich durch die Verwendung von Named Pipes ein Geschwindigkeitszuwachs ergibt. Analog zu den Protokolleinstellungen für den Server gibt es im SQL-Server-Konfigurationsmanager auch ähnliche Einstellungen für den SQL Native Client, nur mit dem Unterschied, dass diese hier auch in eine Reihenfolge gesetzt werden, über die dann versucht wird, eine Verbindung aufzubauen. Schlägt der Verbindungsaufbau mit dem ersten Protokoll der Liste fehl (z.B. weil dieses auf dem Server nicht aktiviert ist), wird das zweite Protokoll probiert. Dies wird so lange fortgesetzt, bis ein Verbindungsaufbau zustande kommt oder alle aktivierten Protokolle erfolglos ausprobiert wurden. Entscheidend ist dabei, dass das gewünschte Netzwerkprotokoll sowohl auf dem Server als auch auf dem Client zur Verfügung steht.
Abbildung 1.8: Die Protokollauswahl für den Native Client
Aus dem oben beschriebenen Vorgehen beim Verbindungsaufbau über den Native Client ergibt sich, dass die schnellsten Protokolle zuerst versucht werden sollten und nur dann auf langsamere ausgewichen wird, wenn die schnelleren vom Server nicht bedient werden. Shared Memory wird aus diesem Grund auch immer vorrangig behandelt, sofern es aktiviert ist. Für die anderen Protokolle kann die Reihenfolge frei gewählt werden.
SQL Server Performance-Ratgeber
39
2030-4.book Page 40 Monday, February 8, 2010 4:26 PM
1 – Vorbereitung der Systemumgebung
Sofern von vornherein bekannt ist, dass ein Protokoll auf dem Server nicht verfügbar ist, sollte man dieses auch für den Native Client deaktivieren, damit dieser nicht vergeblich versucht, darüber eine Verbindung zum Server aufzubauen, bevor er auf ein anderes Protokoll ausweicht. Alternativ kann die Auswahl des clientseitigen Netzwerkprotokolls auch aus der Anwendung selbst heraus erfolgen (z.B. über einen Connection String), doch dazu mehr in Kapitel 8 – Optimierung des Datenzugriffs.
Servereigenschaften Neben dem Netzwerkprotokoll gibt es noch eine Reihe weiterer performancerelevanter Einstellungen, die aber allesamt über das SQL Server Management Studio vorgenommen werden. Im Objekt-Explorer kommt man mit einem rechten Mausklick auf den entsprechenden Server über das Kontextmenü an die Servereigenschaften. Hier gibt es zahlreiche Einstellungen, die sich auf die Performance auswirken können. Die relevanten Einstellungen sind auf verschiedene Registerkarten des Dialogfeldes verteilt:
Arbeitsspeicher 쮿
AWE für die Zuweisung von Arbeitsspeicher verwenden 32-Bit-Systeme können normalerweise lediglich 4 GB RAM adressieren (dabei werden im Normalfall 2 GB für Windows und 2 GB für SQL Server genutzt). Sollten Sie mehr RAM zur Verfügung haben, so kann man AWE (Address Windowing Extensions) aktivieren, durch das höhere Speicherbereiche in den adressierbaren Speicher dynamisch eingeblendet werden. Das ist zwar nicht die effektivste Art der Speichernutzung, aber zumindest kann der Speicher so überhaupt genutzt werden. Einigen Lesern wird dieses Prinzip sicherlich noch vom Expanded Memory System (EMS) bekannt sein, mit dem vor vielen Jahren nach demselben Prinzip die 640-KBGrenze von MS-DOS umgangen wurde. Zusätzlich zur Einstellung in den Eigenschaften des SQL Servers muss noch eine Konfiguration im Betriebssystem über das Windows-Tool für Gruppenrichtlinien (gpedit.msc) vorgenommen werden. Hierüber erhält der Benutzer, über den der SQL Server Dienst läuft, die Berechtigung zum Sperren von Seiten im Speicher (was eine Auslagerung in den virtuellen Arbeitsspeicher – also die Windows-Auslagerungsdatei – verhindert). Rein theoretisch sind über AWE bis zu 64 GB Arbeitsspeicher nutzbar. Erfahrungsgemäß lohnt sich dieses Verfahren allerdings bereits ab etwa 12 GB nicht mehr, da dann die Einbußen durch den Verwaltungsoverhead den Gewinn des zusätzlichen Speichers wieder zunichte machen. Für 64-Bit-Systeme hat diese Einstellung keine Auswirkung, da bei diesen Systemen ohnehin auf direktem Weg wesentlich mehr Speicher adressiert werden kann.
쮿
Minimaler Serverarbeitsspeicher (in MB) Untergrenze des Arbeitsspeichers, der für SQL Server (genau genommen die SQLServer-Pufferspeicher) reserviert ist. Hierüber kann dafür gesorgt werden, dass andere Anwendungen nicht zu viel Speicher für sich beanspruchen. Der Standardwert von 0 MB reserviert den Pufferspeicher erst bei Bedarf.
40
2030-4.book Page 41 Monday, February 8, 2010 4:26 PM
Datenbank-Management-System 쮿
Maximaler Serverarbeitsspeicher (in MB) Obergrenze des Speichers, den SQL Server als Pufferspeicher beanspruchen darf. Hierüber können Sie sicherstellen, dass noch genügend Speicher für andere Anwendungen (z.B. einen Webserver) übrig bleibt. Aus Performancegründen wäre es natürlich sinnvoll, den Server ausschließlich als SQL Server zu nutzen, dennoch sollte man natürlich etwas Speicher für das Betriebssystem und den SQL Server selbst übrig lassen. Der Standardwert 2.147.483.647 MB ist so hoch gewählt, dass der SQL Server bei Bedarf allen verfügbaren Speicher nutzen kann. Sofern Sie ein 64-Bit-System verwenden, das ausschließlich als SQL Server eingesetzt wird, können Sie die Einstellungen für minimalen und maximalen Serverarbeitsspeicher beide auf einen Wert setzen, der etwa 10–20% (aber mindestens 1 GB) unter dem gesamtverfügbaren physikalischen Speicher liegt, damit einerseits noch genügend Platz für das Betriebssystem bleibt, aber andererseits trotzdem der Großteil des Speichers für den SQL Server reserviert ist.
쮿
Arbeitsspeicher für Indexerstellung (in KB, 0 = dynamischer Speicher) Größe des Arbeitsspeichers, der für die Erstellung von Indizes verwendet wird. Der Standardwert 0 sorgt für eine dynamische Zuweisung des benötigten Speichers, ansonsten sind Werte zwischen 704 KB und 2 147 483 647 zulässig.
쮿
Minimaler Arbeitsspeicher pro Abfrage (in KB) Untergrenze des Speichers, der für die Ausführung einer Abfrage verwendet wird. In der Voreinstellung werden hierfür 1.024 KB = 1 MB genutzt, ansonsten sind Werte zwischen 512 KB und 2.147.483.647 möglich.
Prozessoren 쮿
Prozessoraffinität (pro CPU-Kern aktivierbar) Sorgt dafür, dass Threads einzelnen Prozessoren zugewiesen werden und damit der Overhead einer Threadmigration auf eine andere CPU (die ein Neuladen des Prozessorcaches mit sich zieht) vermieden wird.
쮿
E/A-Affinität (pro CPU-Kern aktivierbar) Bewirkt, dass E/A-Operationen einer bestimmten Teilmenge von CPUs zugewiesen werden, was in manchen OLTP-Umgebungen zu einer besseren Performance führen kann.
쮿
Maximale Arbeitsthreadanzahl (Max Worker Threads) Hierüber kann die Anzahl der Arbeitsthreads nach oben begrenzt werden. Der Standardwert 0 bewirkt eine dynamische Festlegung der Arbeitsthreads, was für die meisten Umgebungen auch die optimale Einstellung darstellt.
쮿
SQL-Server-Priorität höher einstufen (Priority Boost) Über diese Eigenschaft können Sie dem SQL-Server-Dienst eine höhere Priorität gegenüber anderen Prozessen geben. Dies ist aber nicht zu empfehlen, da auch einige andere Prozesse essenziell für einen performanten Betrieb von SQL Server sind (z.B. wegen E/A-Operationen).
SQL Server Performance-Ratgeber
41
2030-4.book Page 42 Monday, February 8, 2010 4:26 PM
1 – Vorbereitung der Systemumgebung
Verbindungen 쮿
Maximale Anzahl von gleichzeitigen Verbindungen (0 = unbegrenzt) Mit dieser Einstellung lässt sich die Anzahl gleichzeitiger Verbindungen begrenzen. Dadurch wird gewährleistet, dass die zugelassenen Verbindungen anteilig mehr Ressourcen und damit eine bessere Performance erhalten. Dabei wird die dedizierte Administratorenverbindung nicht mitgezählt. Standardmäßig ist die Anzahl der Verbindungen nicht nach oben begrenzt.
쮿
Abfragekontrolle verwenden, um Abfragen mit langer Ausführungszeit zu verhindern Begrenzt die maximale Ausführungszeit einer Abfrage auf die angegebene Dauer (in Sekunden). Normalerweise ist diese Einstellung inaktiv, wenn sie aktiviert wird, nutzt sie einen Standardwert von 300 Sekunden, also 5 Minuten.
쮿
Standardverbindungsoptionen 왘 implizite Transaktionen Diese Option bewirkt, dass mit jeder Anweisung implizit eine Transaktion gestartet wird. 왘 Schließen des Cursors nach Commit Bewirkt ein automatisches Schließen eines Cursors nach dem Bestätigen einer Transaktion. 왘 Transaktionsabbruch Automatischer Rollback einer Transaktion, wenn eine Anweisung darin einen Fehler erzeugt.
쮿
Timeout für Remoteabfragen (Sekunden, 0 = kein Timeout) Zeit, bis eine Remoteabfrage automatisch abgebrochen wird. Ist der Wert zu hoch eingestellt, dauert es unter Umständen zu lange, bis auf eine zu langsame Remoteabfrageausführung reagiert werden kann. Bei zu niedrigem Wert werden Remoteabfragen eventuell vorzeitig abgebrochen. Normalerweise ist diese Einstellung inaktiv, wenn sie aktiviert wird, nutzt sie einen Standardwert von 600 Sekunden, also 10 Minuten.
쮿
Verteilte Transaktionen für die Kommunikation zwischen Servern verlangen Führt Remote Procedure Calls als verteilte Transaktionen aus. Dadurch wird eine bessere Datenkonsistenz erreicht, es kann aber als negativer Nebeneffekt zu lang laufenden Transaktionen kommen, die Daten für andere Transaktionen sperren.
Datenbankeinstellungen 쮿
Standardfüllfaktor für Indizes Definiert die Standardeinstellung für den Index-Füllfaktor, der angibt, zu welchem Prozentsatz Indexseiten beim Erstellen oder Reorganisieren eines Indizes gefüllt werden. Der Standardwert 0 entspricht dabei dem Wert 100. Werden die Seiten zu einem hohen Grad gefüllt, sind häufige Seitenteilungen notwendig, um weitere Werte einzufügen, was zulasten der Performance geht. Bei einem zu niedrigen Füllfaktor wird zu viel Platz verschwendet. Die Einstellung des Füllfaktors kann für einzelne Indizes beim Erstellen derselben angepasst werden.
42
2030-4.book Page 43 Monday, February 8, 2010 4:26 PM
Datenbank-Management-System 쮿
Sicherung komprimieren Bei SQL Server 2008 Enterprise Edition können Datenbanksicherungen komprimiert werden, was einerseits Speicherplatz spart (und damit die E/A-Last verringert), andererseits den Prozessor stärker belastet. Über diese Einstellung wird der Standardwert für die Komprimierung von Sicherungen definiert, der aber bei jeder einzelnen Sicherung explizit angepasst werden kann.
Erweitert 쮿
Netzwerkpaketgröße Die Netzwerkpaketgröße definiert die Größe der Datenblöcke, die immer gemeinsam übertragen werden. Diese ist normalerweise auf 4096 Bytes = 4 KB voreingestellt. Werden meist größere Datenmengen zwischen Client und Server übertragen, so kann eine größere Netzwerkpaketgröße Vorteile bringen, da dadurch der Verwaltungsoverhead minimiert wird. Wenn aber stattdessen oft Datenmengen übertragen werden, die deutlich kleiner als die Netzwerkpaketgröße sind, so wird Übertragungskapazität verschwendet. Daher bietet der voreingestellte Standardwert meist einen recht guten Kompromiss. Sinnvolle und gültige Alternativen für diesen Wert liegen – je nach Anwendung – zwischen 512 und 32.767 Byte. (Bei verschlüsselter Verbindung reduziert sich der Maximalwert auf die Hälfte.)
쮿
Timeout für Remoteanmeldung Analog zum Timeout für Remoteanfragen definiert dieser Wert die Zeit in Sekunden, nach der ein Remoteverbindungsversuch abgebrochen wird. Der Standardwert liegt bei 20 Sekunden.
쮿
Abfragewartezeit Wartezeit, nach der eine Abfrage (egal ob remote oder lokal) abgebrochen wird. Gültige Werte liegen zwischen 0 und 2.147.483.647 Sekunden. Alternativ kann über den Wert -1 dynamisch das 25-Fache der jeweils geschätzten Abfragezeit als Timeoutwert definiert werden. Dies ist auch die Standardeinstellung.
쮿
Max. Grad an Parallelität (MAXDOP = Maximum Degree of Parallelism) Dieser Wert sollte für OLTP-Server auf 1 stehen, da in den meisten Fällen der Verwaltungsoverhead mehr Performance kostet als die parallele Ausführung von Abfragen bringt. Für einzelne Abfragen kann diese Einstellung durch explizites Setzen des Optimizer Hints maxdop anders definiert werden. Der Standardwert 0 bewirkt, dass alle verfügbaren Prozessoren verwendet werden. Maximal sind bis SQL Server 2008 insgesamt 64 Prozessoren nutzbar, ab SQL Server 2008 R2 (in Verbindung mit Windows 2008 Server R2) sogar bis zu 256.
쮿
Sperren Definiert die maximale Anzahl an verfügbaren Sperren (und begrenzt damit auch den dafür verwendeten Speicher). Der Standardwert 0 entspricht keiner Beschränkung, sodass Sperren in beliebiger Menge dynamisch erzeugt werden können.
SQL Server Performance-Ratgeber
43
2030-4.book Page 44 Monday, February 8, 2010 4:26 PM
1 – Vorbereitung der Systemumgebung 쮿
Cursorschwellwert Definiert die Anzahl der Zeilen, ab der bei einem Keyset-Cursor die Daten asynchron bereitgestellt werden. Liegt die Anzahl über der Gesamtzeilenanzahl des Cursors, wird dieser synchron generiert. Bei einem Wert von -1 werden die Cursors generell synchron generiert, bei einem Wert von 0 generell asynchron (unabhängig von der Zeilenzahl). Die asynchrone Generierung von Keyset-Cursors sorgt dafür, dass die ersten Zeilen bereits weiterverarbeitet werden können, noch bevor der gesamte Cursor komplett abgerufen wurde.
쮿
Für Ad-hoc-Arbeitsauslastungen optimieren Reduziert die Größe des Plancaches, was besonders im Zusammenhang mit vielen einmalig ausgeführten Ad-hoc-Abfragen sinnvoll ist.
쮿
Schwellenwert für blockierte Prozesse Wert in Sekunden, ab dem ein Prozess als blockiert angesehen und ein entsprechender Bericht generiert wird, der unter anderem zur Erkennung von Deadlocks genutzt wird. Gültige Werte liegen zwischen 0 und 86.400. Der Standardwert von 0 bewirkt, dass keine Berichte zu blockierten Prozessen erzeugt werden.
Abbildung 1.9: Die erweiterten Servereigenschaften von SQL Server 2008
44
2030-4.book Page 45 Monday, February 8, 2010 4:26 PM
Zusammenfassung
1.4
Zusammenfassung
Beim Vorbereiten der Systemumgebung für einen SQL Server gibt es viele Dinge zu beachten, die sich allesamt auf die Performance des Systems auswirken können. Dies beginnt schon bei der richtigen Auswahl der richtigen Hardware. Hier sollten genügend leistungsstarke Prozessoren (mit möglichst großem prozessorinternem Cache) und vor allem viel Hauptspeicher vorhanden sein. Dazu kommen möglichst viele getrennte Plattensysteme, um die mit verschiedenen Zugriffscharakteristiken genutzten Dateien entsprechend zu verteilen: 쮿
RAID 1 für das Systemlaufwerk (maximale Sicherheit)
쮿
RAID 10 für Datenbank- und Protokolldateien (maximale Sicherheit & Performance)
쮿
RAID 5 für Datenbank-Backups (Speicherplatz & Sicherheit)
쮿
RAID 0 für Windows-Auslagerungsdatei und TempDB (maximale Schreibperformance ohne Sicherheit)
Auch die Netzwerkadapter sollten eine ausreichende Bandbreite mitbringen, die natürlich auch vom Betriebssystem genutzt werden muss. Bei der Auswahl des Betriebssystems ist zu beachten, dass dies die Nutzung der zur Verfügung stehenden Hardware auch ermöglicht. Dies betrifft einerseits den Hauptspeicher (bei mehr als 4 GB RAM sollte ein 64-Bit-System verwendet werden), aber auch die Anzahl der Prozessorkerne (bei mehr als 64 CPU-Kernen sollte Windows 2008 Server R2 genutzt werden). Im Normalfall reicht aber ein „normaler“ Windows Server der Generation 2003 oder 2008 völlig aus. Auch beim SQL Server selbst bieten die unterschiedlichen Editionen verschiedene Performance-Features. Wer in den Genuss der maximalen Möglichkeiten kommen will, sollte schon die Enterprise Edition verwenden. Die kleineren Editionen sind bezüglich nutzbarer CPUs, Hauptspeicher und maximaler Datenbankgröße begrenzt und bieten auch nicht alle Performance-Tools, die bei der Enterprise Edition mit ausgeliefert werden. Bei der Installation von SQL Server sollten die verschiedenen Dateitypen auf die verfügbaren Plattensysteme verteilt werden. Dazu ist zu beachten, dass nur die benötigten Datenbankdienste automatisch gestartet werden. Schließlich lassen sich noch die Netzwerkprotokolle für Client und Server auswählen sowie im SQL Server Management Studio zahlreiche Konfigurationseinstellungen für den SQL Server festlegen, von denen viele mehr oder weniger direkte Auswirkungen auf die Performance haben können.
SQL Server Performance-Ratgeber
45
2030-4.book Page 46 Monday, February 8, 2010 4:26 PM
2030-4.book Page 47 Monday, February 8, 2010 4:26 PM
2
Einrichten und Warten von Datenbanken
Während von der im letzten Kapitel behandelten Vorbereitung der Systemumgebung sowohl System- als auch Datenbankadministratoren betroffen sind, beziehen sich die Informationen in diesem Kapitel vor allem auf die letztgenannte Gruppe. Dies umfasst einerseits die Konfiguration einer Datenbank, aber auch regelmäßige Wartungsarbeiten, mit denen eine gute Datenbank-Performance auch dauerhaft sichergestellt werden soll.
2.1
Anlegen und Konfigurieren einer Datenbank
Datenbanken können sowohl mit T-SQL als auch über ein Dialogfeld im SQL Server Management Studio angelegt werden. Bei letzterem Weg sind die verschiedenen Einstellmöglichkeiten besser ersichtlich. Ich möchte hier auch bewusst nicht zu sehr auf die Syntax des CREATE TABLE-Statements mit all seinen Parametern eingehen. Bei Interesse können Sie diese aus der Online-Hilfe erfahren oder aber über das Dialogfeld Neue Datenbank nach Konfiguration aller Einstellungen als Skript generieren lassen.
2.1.1
Datenbank- und Protokolldatei
Auf dem ersten Registerblatt des Dialogs zum Erstellen neuer Datenbanken sind neben dem Datenbanknamen und dessen Besitzer vor allem die Informationen zu den verwendeten Dateien zu finden.
Dateien und Dateigruppen Jede Datenbank besteht zumindest aus jeweils einer Datenbank- und einer Protokolldatei. Die Datenbankdateien enthalten die eigentlichen Zeilendaten und sind in Dateigruppen zusammengefasst. Standardmäßig wird eine Dateigruppe mit Namen PRIMARY erstellt, die automatisch verwendet wird, sofern nicht explizit eine andere Dateigruppe angegeben wird. Die Protokolldatei (oder auch Transaktionslog genannt) enthält Änderungen an der Datenbank, die noch nicht in die Datenbankdatei geschrieben wurden. Dadurch werden einerseits Datenbanktransaktionen ermöglicht. Außerdem ermöglicht die Logdatei (bzw. eine Sicherung derselben) ein Wiederaufsetzen einer Datenbank zu einem nahezu beliebigen Zeitpunkt.
SQL Server Performance-Ratgeber
47
2030-4.book Page 48 Monday, February 8, 2010 4:26 PM
2 – Einrichten und Warten von Datenbanken
Die Zuordnung von Datenbankobjekten zu Dateien erfolgt nur indirekt über die Dateiguppen. So können Sie beispielsweise beim Anlegen einer Tabelle angeben, auf welcher Dateigruppe diese gespeichert werden soll. Der Dateigruppe wiederum sind eine oder mehrere Datenbankdateien zugeordnet. Dies lässt sich auf zweierlei Weise nutzen, um die Last auf mehrere Plattensysteme zu verteilen: 쮿
Sie fügen der Dateigruppe weitere Dateien hinzu, die auf verschiedenen Platten liegen. Damit kann die E/A-Last rein theoretisch auf die beteiligten Platten verteilt werden, allerdings haben Sie nur begrenzten Einfluss darauf, welche Daten wo gespeichert werden. Diese Variante ist daher eher empfehlenswert, wenn Sie die verfügbare Kapazität für eine Dateigruppe vergrößern möchten, aber auf der aktuellen Festplatte kein Platz mehr frei ist.
쮿
Sie erzeugen Dateien in neuen Dateigruppen, die jeweils auf separaten Plattensystemen liegen. Damit können Sie die E/A-Last gezielt verteilen, indem Sie die Tabellen und Indizes auf verschiedenen Plattensystemen ablegen und so einen parallelen Zugriff ermöglichen. Diese Variante lässt sich sogar noch verfeinern, indem Sie die Daten besonders großer Tabellen über eine Partitionierungsfunktion auf verschiedene Partitionen (und damit auch Plattensysteme) verteilen. Auf diese Thematik wird in Kapitel 5 – Optimale Verteilung der Daten detaillierter eingegangen.
Wenn Sie stattdessen erreichen möchten, dass eine Tabelle explizit in einer bestimmten Datei gespeichert werden soll, sollte die entsprechende Dateigruppe auch nur diese eine Datei beinhalten. Protokolldateien dagegen sind keinen Dateigruppen zugeordnet, da das Log sequenziell geschrieben wird und damit eine explizite Zuordnung zu einer Protokolldateigruppe keinen Sinn machen würde. Es ist aber möglich, weitere Protokolldateien anzulegen, um beispielsweise weitere Platten nutzen zu können. Das ist dann nötig, wenn die Platte, auf der die ursprüngliche Protokolldatei gespeichert ist, voll ist und damit die Logdatei nicht mehr vergrößert werden kann.
Wiederherstellungsmodell Im Zusammenhang mit der Protokolldatei spielt das für die Datenbank gewählte Wiederherstellungsmodell eine entscheidende Rolle. 쮿
Vollständig – Speichert alle Datenänderungen zusammen mit dem Zeitpunkt der Änderung in chronologischer Reihenfolge im Transaktionslog. Änderungen werden erst bei einer Sicherung des Protokolls in die Datenbank geschrieben. Anschließend wird der Platz im Transaktionslog wieder freigegeben. Bei einer Rücksicherung kann mithilfe einer Komplettsicherung und den folgenden Transaktionslogsicherungen der Zustand der Datenbank zu einem beliebigen Zeitpunkt wiederhergestellt werden. Diese Variante bietet damit die beste Sicherheit, aber sicherlich nicht die beste Performance.
쮿
Einfach – Datenänderungen werden in die Datenbankdatei gespeichert, sobald die dazugehörige Transaktion erfolgreich beendet wurde. Dadurch bleibt das Transaktionslog zwar klein, es existiert aber keine Historie zu Datenänderungen, sodass eine Rücksicherung der Daten nur auf den Zeitpunkt einer Voll- oder Differenzialsicherung der Datenbank erfolgen kann.
48
2030-4.book Page 49 Monday, February 8, 2010 4:26 PM
Anlegen und Konfigurieren einer Datenbank 쮿
Massenprotokolliert – Entspricht im Wesentlichen dem vollständigen Wiederherstellungsmodell, allerdings werden bei Massenoperationen (z.B. BULK INSERT, SELECT INTO etc.) weniger Daten ins Transaktionsprotokoll geschrieben, was diese Operationen zwar beschleunigt, aber für diesen Zeitraum die Möglichkeit einer Wiederherstellung zu einem beliebigen Zeitpunkt verhindert.
Da das Wechseln des Wiederherstellungsmodells auch im laufenden Betrieb möglich ist, ist es gängige Praxis für Datenbanken mit hohen Anforderungen an Sicherung und Wiederherstellung das vollständige Wiederherstellungsmodell zu nutzen und nur für den Zeitraum von größeren Massenoperationen (die auch das Neuanlegen von Indizes beinhalten) kurzzeitig auf Massenprotokolliert umzuschalten. Danach sollte dann möglichst eine Vollsicherung oder Differenzialsicherung durchgeführt werden, weil die Datenbank sonst ab dem Zeitpunkt der Umstellung auf das massenprotokollierte Wiederherstellungsmodell sonst nicht mehr wiederherstellbar ist.
Größe von Datenbank- und Protokolldatei Eine oft vernachlässigte Einstellung ist die Definition der richtigen Größe für Datenbankund Protokolldatei(en). Standardmäßig schlägt SQL Server eine Datenbankgröße von 3 MB und Protokollgröße von 1 MB vor. Da beide Dateitypen bei Bedarf automatisch vergrößert werden, stellt dies kein Problem an die Funktion dar, jedoch benötigt das Vergrößern der Dateien Zeit und das mehrfache Vergrößern in kleinen Schritten hat zur Folge, dass die Dateien eventuell weit verteilt auf der Platte liegen. Auf der anderen Seite sollte man die Dateien nicht zu groß bemessen, denn dann wird unnötig Plattenplatz verschwendet. Daher bietet es sich an, die Größen gleich zu Beginn so festzulegen, dass in absehbarer Zeit keine automatischen Dateivergrößerungen nötig sind. Für die Datendatei lässt sich die Größe – sofern man die Datenstrukturen und Mengen der Datenbank kennt – leicht berechnen. Wenn man anschließend die Zahl nach oben etwas aufrundet, sollte man einen guten Startwert haben. Für die Größe der Protokolldatei wird dies schon etwas schwieriger. Denn diese hängt von verschiedenen Faktoren ab: 쮿
Wiederherstellungsmodell – Bei vollständigem Wiederherstellungsmodell braucht das Transaktionslog viel Platz, beim massenprotokollierten etwas weniger und beim einfachen Wiederherstellungsmodell wird der geringste Platz benötigt.
쮿
Häufigkeit von Logsicherungen – Beim vollständigen Wiederherstellungsmodell wird das Log geleert, sobald eine Transaktionsprotokollsicherung durchgeführt wurde. Daher benötigt das Protokoll weniger Platz, wenn häufiger Logsicherungen stattfinden.
쮿
Umfang und Häufigkeit von Datenänderungen – Wenn Daten primär gelesen werden, belastet dies das Transaktionsprotokoll nicht. Aber je mehr und häufiger Datenänderungen anfallen, desto mehr Platz wird für das Protokoll benötigt.
쮿
Art der Datenänderungen und Umfang von Transaktionen – Da im einfachen Wiederherstellungsmodell nur offene Transaktionen im Protokoll gehalten werden, ist hier die Größe der Transaktionen entscheidend. Die Protokolldatei sollte groß genug sein, um die Datenänderungen der größten verwendeten Transaktion aufnehmen zu können, damit diese auch erfolgreich abgeschlossen werden kann.
SQL Server Performance-Ratgeber
49
2030-4.book Page 50 Monday, February 8, 2010 4:26 PM
2 – Einrichten und Warten von Datenbanken
Bei „normaler“ Nutzung der Datenbank im vollständigen Wiederherstellungsmodell hat sich als Daumenregel für die Größe des Transaktionsprotokolls ca. 20% der Datenbankgröße bewährt. Je nach Einfluss der oben genannten Faktoren kann dieser Wert noch nach oben oder unten angepasst werden. Wie bereits erwähnt, werden sowohl Datenbank- als auch Protokolldatei bei Bedarf automatisch vergrößert. Die Einstellungen, mit denen dies geschieht, sind bei den allgemeinen Datenbankeinstellungen zu finden. Die Voreinstellung für die Protokolldatei ist mit jeweils 10% ohne Obergrenze recht sinnvoll. Bei den Zeilendatendateien ist der absolute Wert von 1 MB jedoch für die meisten Datenbanken recht knapp bemessen. Dies würde dazu führen, dass die Datenbank häufig und in kleinen Schritten vergrößert wird. Ich persönlich verwende hier ebenfalls gerne relative Vergrößerungsschritte von 10%. Gerade bei großen Datenbanken bietet es sich jedoch an, die 10% auszurechnen und als absolute Angabe (in MB) anzugeben, um den „Zinseszins-Effekt“ auszuschließen. So wie sich die Dateien automatisch vergrößern lassen, kann man diese auch automatisch verkleinern. In den Optionen zur Datenbank gibt es eine entsprechende Einstellung. Es wird jedoch dringend geraten, diese Option inaktiv zu lassen, da dies bei häufigen Änderungen in der Datenbank zu einem regelmäßigen Verkleinern und Vergrößern der Dateien führen kann, was einerseits die Plattensysteme stark belastet und andererseits zu einer starken Fragmentierung der Dateien führen kann.
2.1.2
Sonstige Konfigurationseinstellungen
Neben der Einstellung zum automatischen Verkleinern von Datenbanken gibt es noch einige weitere Einstellungen in den Optionen der Datenbank, die sich auf die Performance auswirken können (Abbildung 2.1).
Automatisch schließen Diese Einstellung sorgt dafür, dass die Datenbank automatisch heruntergefahren wird, wenn die letzte Benutzerverbindung geschlossen wurde. Dadurch werden einerseits Ressourcen (insbesondere Hauptspeicher) auf dem Server wieder freigegeben, andererseits erfordert ja auch das Hoch- und Runterfahren einer Datenbank entsprechende Ressourcen (nicht zuletzt Zeit). Daher macht die Aktivierung dieser Einstellung eigentlich nur für Datenbanken Sinn, die sehr selten benötigt werden.
Statistiken automatisch erstellen Ist diese Option aktiv, werden fehlende Tabellen- und Indexstatistiken automatisch erzeugt, sobald sie benötigt werden. Da Statistiken eine wichtige Voraussetzung für die performante Ausführung von Abfragen darstellen, sollte diese Option (wie voreingestellt) aktiviert bleiben.
50
2030-4.book Page 51 Monday, February 8, 2010 4:26 PM
Anlegen und Konfigurieren einer Datenbank
Abbildung 2.1: Die Optionen bei den Datenbankeigenschaften
Statistiken automatisch (asynchron) aktualisieren Ist diese Option aktiv, werden veraltete Tabellen- und Indexstatistiken automatisch aktualisiert, sobald sie benötigt werden. (Als veraltet wird eine Statistik angesehen, wenn sich seit der letzten Aktualisierung mehr als 20% der Daten geändert haben.) Ist zusätzlich die Option zum asynchronen Aktualisieren aktiv, so wird zwar das Aktualisieren von Statistiken bei Bedarf veranlasst, aber nicht darauf gewartet, bis dieser Vorgang abgeschlossen ist. Die gerade ausgeführte Abfrage verwendet also noch die veralteten Statistiken, während diese im Hintergrund neu erzeugt werden. Damit stehen die aktualisierten Statistiken erst für spätere Abfragen zur Verfügung.
SQL Server Performance-Ratgeber
51
2030-4.book Page 52 Monday, February 8, 2010 4:26 PM
2 – Einrichten und Warten von Datenbanken
Da veraltete Statistiken zu ineffizienter Abfrageausführung führen können, sollte auch diese Option (wie voreingestellt) aktiviert bleiben. Bei der Option zum asynchronen Aktualisieren ist es umgekehrt. Diese sollte nur dann aktiviert werden, wenn das automatische Aktualisieren von veralteten Statistiken zu viel Zeit benötigt.
Komprimierung von Datenbanken Ab der Enterprise Edition von SQL Server haben Sie die Möglichkeit, sowohl Datenbanken als auch Datenbanksicherungen komprimiert zu speichern. Der Grundgedanke hierbei liegt darin, die Menge der notwendigen (und meist recht langsamen) Plattenzugriffe zu minimieren. Natürlich bringt die Kompression auch Nachteile mit sich, denn das transparente Komprimieren und Dekomprimieren von Daten und Backups erzeugt natürlich eine zusätzliche Prozessorlast. In den meisten Fällen ist dies jedoch das geringere Problem, da in der Regel die Plattenzugriffe den eigentlichen Flaschenhals bilden. Das ist natürlich auch abhängig von der vorhandenen Hardware, denn bei einem relativ schwachen Prozessor und Unmengen Hauptspeicher kann auch der Prozessor die Gesamtleistung des Datenbankservers stark begrenzen.
2.2
Regelmäßige Wartungsarbeiten
Als Datenbankadministrator sollten Sie sich die Mühe machen, ein paar wichtige Dinge im Auge zu behalten und regelmäßig zu prüfen. Denn eine Anwendung, die momentan performant läuft, tut dies vielleicht schon morgen nicht mehr. Die Gründe dafür liegen einerseits im mehr oder minder konstant wachsenden Datenvolumen, aber auch in den Folgen regelmäßiger Datenänderungen. Wenn Sie natürlich eine Datenbankanwendung administrieren, die auf einem festen Datenbestand basiert, der sich im Laufe der Zeit nicht ändert, können Sie dieses Kapitel getrost überspringen. Das dürfte aber nur für einen verschwindend geringen Anteil der Anwendungen zutreffen. Die meisten der regelmäßigen Wartungsarbeiten werden normalerweise über einen SQL Server Agent-Job gesteuert regelmäßig ausgeführt. Alternativ können Sie auch die mit SQL Server 2005 eingeführten Wartungspläne nutzen. Diese können über das Business Intelligence Studio oder über den Wartungsplanungs-Assistenten im SQL Server Management Studio erzeugt werden und erzeugen ein SSIS-Paket, mit dazu passendem SQL Server Agent-Job. Mit Wartungsplänen lassen sich folgende Wartungsaufgaben automatisieren: 쮿
Datenbankintegrität prüfen
쮿
Datenbanken verkleinern
쮿
Indexwartung ausführen
쮿
Datenbankstatistiken aktualisieren
쮿
Datenbanksicherungen ausführen
52
2030-4.book Page 53 Monday, February 8, 2010 4:26 PM
Regelmäßige Wartungsarbeiten
Dabei sind vor allem das Verkleinern von Datenbanken, die Indexwartung, Aktualisierung von Statistiken sowie Datenbanksicherungen (genau genommen die Transaktionsprotokollsicherungen) relevant für die Datenbank-Performance.
Abbildung 2.2: Auswahl der Wartungstasks beim Wartungsplanungs-Assistenten
i
Bei SQL Server Express haben Sie weder Wartungspläne noch den SQL Server Agent zur Verfügung. Auch hier sollten Sie allerdings nicht auf die regelmäßige Ausführung von Wartungsarbeiten verzichten. Sie können sich hier behelfen, indem Sie SQL-Skripts für die Wartungsarbeiten schreiben und diese über das Kommandozeilentool SQLCMD in den Windows Task Scheduler einbinden und von diesem zu vorgegebenen Zeiten ausführen lassen.
Wichtig ist auch die zeitliche Planung der verschiedenen Wartungsaufgaben. Da viele dieser Aufgaben – während sie laufen – entweder exklusiven Zugriff auf die Datenbank benötigen oder deren Performance negativ beeinflussen, werden in der Regel Wartungsfenster genutzt, also fest definierte Zeiten, in denen die Datenbanken nicht produktiv genutzt werden. In den meisten Szenarien gibt es sowohl tägliche (genau genommen nächtliche) Wartungsfenster, in denen die häufig notwendigen Aufgaben ausgeführt werden, als auch wöchentliche Wartungsfenster (normalerweise am Wochenende), in denen länger andauernde Reorganisationsaufgaben durchgeführt werden. Vorschläge für tägliche Wartungsaufgaben: 쮿
Indizes reorganisieren
쮿
Sicherungen (Differenzial- und Transaktionsprotokoll)
쮿
Statistiken aktualisieren
SQL Server Performance-Ratgeber
53
2030-4.book Page 54 Monday, February 8, 2010 4:26 PM
2 – Einrichten und Warten von Datenbanken
Vorschläge für wöchentliche Wartungsaufgaben: 쮿
Indizes neu aufbauen
쮿
Datenbankvollsicherungen
Je nach Ansprüchen an Datensicherheit und Verfügbarkeit der Daten kann dies natürlich variieren. Doch kommen wir nun zu den Details der einzelnen Wartungsaufgaben.
2.2.1
Datenbank verkleinern
So wie ein automatisches Verkleinern von Datenbanken normalerweise keinen Sinn macht, kann es aber angebracht sein, diese in größeren Abständen explizit zu verkleinern, um unnötigen Ballast loszuwerden, der sich im Laufe der Zeit angesammelt hat. In T-SQL wird zum Verkleinern von Datenbanken das DBCC-Kommando verwendet: DBCC SHRINKDATABASE(datenbankname)
2.2.2 Indizes aktuell halten Auch wenn Indizes eigentlich erst im Teil B Optimierung für Designer & Architekten behandelt werden, so liegt eine wichtige regelmäßige Wartungsaufgabe darin, die bestehenden Indizes zu reorganisieren oder gar neu zu erstellen, da diese aufgrund von Datenänderungen mit der Zeit fragmentiert werden. Wird diese Aufgabe vernachlässigt, führt es früher oder später dazu, dass bestehende Indizes nicht genutzt werden. Zum Aktualisieren von Indizes gibt es zwei unterschiedliche Varianten: 쮿
REORGANIZE – Organisiert die Blattebene eines Indexes neu. Kann online – also während des laufenden Betriebs – ausgeführt werden.
쮿
REBUILD – Komplettes Neuerstellen eines Indexes. Dabei wird der komplette Indexbaum neu aufgebaut und auch der vorgegebene Füllfaktor wiederhergestellt (nähere Informationen zum Thema Füllfaktor folgen im nächsten Kapitel).
Die Notwendigkeit einer Indexwartung lässt sich prüfen, indem man im SQL Server Management Studio einen Index im Objekt-Explorer über die rechte Maustaste und die Indexeigenschaften aufruft und dort zur Registerkarte Fragmentierung wechselt. Hier ist der Wert Fragmentierung gesamt entscheidend. Ab 10% Fragmentierung wird allgemein eine Reorganisation des Indexes empfohlen, ab 40% ein Neuerstellen. Liegt die Fragmentierung unter 10%, so ist kein Eingreifen erforderlich.
54
2030-4.book Page 55 Monday, February 8, 2010 4:26 PM
Regelmäßige Wartungsarbeiten
Abbildung 2.3: Anzeige der Indexfragmentierung bei den Indexeigenschaften
Da es aber nun etwas aufwändig wäre, die Fragmentierung jedes Indexes einzeln anzuzeigen, kann man die Werte zur Indexfragmentierung auch über die Tabellenwertfunktion sys.dm_db_index_physical_stats auslesen und dabei gleich filtern, welche Indizes gewartet werden müssen. Folgende Abfrage zeigt die Indizes an, die zu mindestens 10% fragmentiert sind: SELECT OBJECT_NAME(ips.[object_id]) AS TabellenName, ix.name AS IndexName, ix.type_desc AS IndexType, ips.avg_fragmentation_in_percent FROM sys.indexes AS ix LEFT JOIN sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, NULL) AS ips ON ix.index_id=ips.index_id AND ix.object_id=ips.object_id WHERE OBJECTPROPERTY(ix.object_id, 'IsUserTable') = 1 AND ips.avg_fragmentation_in_percent >= 10 ORDER BY ips.avg_fragmentation_in_percent DESC Listing 2.1: Anzeige der fragmentierten Indizes
SQL Server Performance-Ratgeber
55
2030-4.book Page 56 Monday, February 8, 2010 4:26 PM
2 – Einrichten und Warten von Datenbanken
Dabei wird über einen Aufruf der Funktion OBJECTPROPERTY zusätzlich sichergestellt, dass nur Indizes zu Benutzertabellen geprüft werden. Das Ergebnis der Abfrage sieht dann beispielsweise wie folgt aus:
Abbildung 2.4: Anzeige zu stark fragmentierter Indizes aufgrund einer SQL-Abfrage
Noch sinnvoller ist es aber sicherlich, Indizes in fest definierten Zeitabständen regelmäßig zu pflegen. Um Indizes dauerhaft aktuell zu halten, hat sich in den meisten Umgebungen folgendes Vorgehen bewährt: 쮿
Nächtliches Reorganisieren der Indizes
쮿
Wöchentliches (am Wochenende) Neuerstellen der Indizes
56
2030-4.book Page 57 Monday, February 8, 2010 4:26 PM
Regelmäßige Wartungsarbeiten
In T-SQL lässt sich das Reorganisieren von Indizes mit den folgenden Anweisungen ausführen: -- aktuelle Variante ALTER INDEX indexname ON tabellenname REORGANIZE -- veraltete Variante (bis SQL Server 2008) DBCC INDEXDEFRAG (datenbankname, tabellenname, indexname) Listing 2.2: Reorganisieren von Indizes
Für das Neuerstellen von Indizes werden die folgenden Anweisungen verwendet: -- aktuelle Variante ALTER INDEX indexname ON tabellenname REBUILD WITH (FILLFACTOR = fuellfaktor) -- veraltete Variante (bis SQL Server 2005) DBCC REINDEX (tabellenname, indexname, fuellfaktor) Listing 2.3: Neuerstellen von Indizes
Dabei kann auch der Füllfaktor neu gesetzt werden. (Was es mit dem Füllfaktor auf sich hat, wird in Kapitel 4.2 – Realisierung von Indizes bei SQL Server näher erläutert.) Wenn mit einer Anweisung alle Indizes einer Tabelle reorganisiert oder neu erstellt werden sollen, kann man bei ALTER INDEX als Tabellenname das Schlüsselwort ALL verwenden oder bei DBCC INDEFRAG und DBCC REINDEX einfach den Indexnamen weglassen.
2.2.3 Sicherungen erstellen Das regelmäßige Erstellen von Datenbanksicherungen (engl. Backups) ist eine wichtige Aufgabe, die für alle produktiv eingesetzten Datenbanken Pflichtübung sein sollte. Vollsicherung sowie differenzielle Sicherungen (in denen die seit der letzten Vollsicherung geänderten Datenseiten enthalten sind) werden im Normalfall innerhalb der entsprechenden Wartungsfenster ausgeführt. Für die Datenbank-Performance von Bedeutung sind aber eher die Transaktionsprotokollsicherungen. Denn nur durch diese kann das Transaktionsprotokoll geleert werden, damit die Möglichkeit der Wiederherstellung zu einem beliebigen Zeitpunkt erhalten bleibt. Je öfter Sie Protokollsicherungen durchführen, desto kleiner kann die Protokolldatei sein, wobei immer genügend Platz darin vorhanden sein sollte, um die längste benötigte Transaktion aufnehmen zu können. Bei größerer Sicherungsfrequenz werden außerdem die Protokollsicherungen kleiner und damit auch schneller ausgeführt. Damit dafür nicht unnötig viele Dateien benötigt werden, lassen sich auch mehrere Sicherungen nacheinander in einer Sicherungsdatei ablegen. In T-SQL wird für das Sichern von Transaktionsprotokollen folgende Anweisung verwendet: BACKUP LOG datenbankname TO DISK = sicherungsdateipfad WITH NOFORMAT, NOINIT, NAME = sicherungsname, SKIP, NOREWIND, NOUNLOAD, STATS = 10
SQL Server Performance-Ratgeber
57
2030-4.book Page 58 Monday, February 8, 2010 4:26 PM
2 – Einrichten und Warten von Datenbanken
Lediglich im einfachen Wiederherstellungsmodell sind keine Transaktionsprotokollsicherungen notwendig, da in diesem Modus nur die offenen Transaktionen im Transaktionsprotokoll gehalten werden.
2.2.4 Statistiken aktualisieren Auch wenn die Datenbankstatistiken bei entsprechenden Datenbankeinstellungen automatisch aktualisiert werden, bietet es sich an, diese nächtlich noch einmal explizit zu aktualisieren. Dadurch werden sowohl Notwendigkeit als auch der Aufwand der automatischen Aktualisierungen verringert, da alle Statistiken innerhalb des Wartungsfensters aktualisiert werden – wenn dies den Produktivbetrieb nicht stört. In T-SQL wird die folgende Anweisung verwendet, um die Statistiken für eine Tabelle (oder indizierte Sicht) zu aktualisieren: UPDATE STATISTICS tabellenname
Alternativ können Sie auch die folgende gespeicherte Prozedur nutzen, um alle Statistiken einer Datenbank zu aktualisieren: EXEC sp_updatestats
2.2.5 Prüfung der Systemauslastung Neben den zuvor genannten Wartungsaufgaben, die sich allesamt gut automatisieren lassen, gibt es jedoch einige Aufgaben, die regelmäßig manuell durchgeführt werden sollten. Dabei geht es vor allem darum, anhand von Beobachtungen der Systemauslastung mögliche Probleme frühzeitig zu erkennen, um entsprechend darauf reagieren zu können. Dies kann natürlich nicht innerhalb eines Wartungsfensters geschehen, sondern sollte zu einer Zeit passieren, in der die betreffende Datenbank stark genutzt wird (am besten eignen sich hierfür normalerweise Zeiten, die vormittags innerhalb der Kernzeit liegen). Damit kritische Werte besser erkannt werden können, ist natürlich auch eine gute Kenntnis des Normalzustandes nötig. Dies wird durch eine regelmäßige Überwachung der entsprechenden Werte gleich mit erledigt. Um die allgemeine Systemauslastung zu prüfen, gibt es zwei grundlegende Tools, die beide Bestandteile des Windows-Betriebssystems sind: 쮿
Windows Task-Manager
쮿
Windows Performance Monitor
Mit dem Windows Task-Manager kann man sich schnell einen Überblick verschaffen, wie stark die Prozessoren ausgelastet sind, wie viel Systemspeicher gerade verwendet wird und wie stark die Auslastung der verschiedenen Netzwerkkarten gerade ist. Dies funktioniert allerdings nur für den Rechner, auf dem man gerade angemeldet ist. Für einen Datenbankserver benötigt man demnach einen Remote-Zugriff (beispielsweise über die Windows Terminal Server Dienste).
58
2030-4.book Page 59 Monday, February 8, 2010 4:26 PM
Regelmäßige Wartungsarbeiten
Auf der Registerkarte Systemleistung sind die wichtigsten Werte übersichtlich aufgelistet und teilweise sogar in Form eines Diagramms dargestellt.
Abbildung 2.5: Die Übersicht der Systemleistung im Windows Task-Manager
Dabei kann die CPU-Auslastung auch konstant bei ca. 80% liegen und hin und wieder mal auf 100% gehen. Erst wenn die Auslastung dauerhaft bei nahezu 100% liegt, weist dies darauf hin, dass die Prozessoren zu stark beansprucht werden. Ähnlich verhält es sich mit dem Speicher: Dieser darf auch dauerhaft voll ausgelastet sein, weil SQL Server – je nach Konfiguration – gerne den kompletten verfügbaren Speicher zum Puffern von Daten und Ausführungsplänen nutzt. Wenn aber die Auslastung der Auslagerungsdatei in die Höhe geht, weist dies darauf hin, dass SQL Server wahrscheinlich zu wenig Speicher für andere Anwendungen und Dienste (evtl. sogar das Betriebssystem) übrig gelassen hat. In diesem Fall sollte man die Konfiguration des SQL Servers so anpassen, dass mehr Hauptspeicher für andere Zwecke übrig gelassen wird. Auf der Registerkarte Netzwerk sind alle installierten Netzwerkadapter – drahtlos und kabelgebunden – mit deren Status, Übertragungsrate und Auslastung aufgelistet (wobei die drahtlosen Netzwerkadapter für Datenbankserver natürlich keine Rolle spielen sollten). Dadurch kann man relativ leicht sehen, wenn ein Netzwerkadapter zum Flaschenhals wird oder eventuell mit einer zu niedrigen Übertragungsrate läuft. Schwieriger wird es schon mit der Plattenauslastung. Um diese zu überwachen, benötigt man den Windows Performance Monitor (oder kurz: PerfMon). Dieses Tool hat je nach verwendetem Betriebssystem einen leicht abweichenden Namen, kann aber generell von der Kommandozeile über die Eingabe von PERFMON aufgerufen werden.
SQL Server Performance-Ratgeber
59
2030-4.book Page 60 Monday, February 8, 2010 4:26 PM
2 – Einrichten und Warten von Datenbanken
Mit dem Performance Monitor kann man auch auf andere Rechner im Netzwerk zugreifen, also auch vom eigenen Arbeitsplatzrechner aus andere SQL Server überwachen. Dabei bietet das Tool so genannte Leistungsindikatoren, die sich auf verschiedene Datenobjekte (wie beispielsweise Netzwerkschnittstelle, physikalischer Datenträger, Prozessor oder Speicher) beziehen. Einige Serveranwendungen – zu denen auch der SQL Server gehört – bringen sogar eigene Leistungsindikatoren mit, durch die man deren Dienste besser überwachen kann. Die Liste der möglichen Leistungsindikatoren ist damit fast unüberschaubar lang. Ich möchte daher an dieser Stelle nur ein paar sehr grundlegende aufführen, mit denen die wichtigsten Systemeigenschaften überwacht werden können: Netzwerkschnittstelle: 쮿
Aktuelle Bandbreite
쮿
Ausgabewarteschlangenlänge
쮿
Gesamtanzahl Bytes/s
Physikalischer Datenträger: 쮿
Aktuelle Warteschlangenlänge
쮿
Durchschnittliche Warteschlangenlänge des Datenträgers
쮿
Durchschnittliche Warteschlangenlänge der Datenträger-Lesevorgänge
쮿
Durchschnittliche Warteschlangenlänge der Datenträger-Schreibvorgänge
Prozessor: 쮿
Prozessorzeit (%) (entspricht der Prozessorauslastung)
Speicher: 쮿
Seiten/s
Die oben fett gedruckten Leistungsindikatoren sind bereits vorausgewählt, sodass man nur noch einen oder zwei Indikatoren zur Netzwerkschnittstelle dazunehmen muss, um sich schnell einen Überblick über die wichtigsten Werte zu beschaffen.
Abbildung 2.6: Die Übersicht der wichtigsten Leistungsindikatoren im Performance Monitor
60
2030-4.book Page 61 Monday, February 8, 2010 4:26 PM
Ressourcenkontrolle mit dem Resource Governor
Will man gezielter nach bestimmten Problemen suchen, sollte man natürlich noch weitere Werte einblenden und überwachen. Für eine allgemeine Übersicht, die als Grundlage für eine regelmäßige (möglichst täglich, mindestens aber einmal pro Woche) Beobachtung gilt, sollten diese Leistungsindikatoren eine gute Basis bilden. So deutet beispielsweise ein deutliches Ansteigen der durchschnittlichen Warteschlangenlänge des Datenträgers darauf hin, dass die Festplatten überlastet sind. Nutzen Sie den Performance Monitor, um eine Aufzeichnung der Grundauslastung des SQL Servers (eine so genannte Baseline) als Referenz zu erstellen. Diese können Sie später mit den aktuellen Werten des Performance Monitors vergleichen, um eine außergewöhnliche Belastung leichter zu erkennen. Eine ausführlichere Beschreibung der Tools sowie deren Verwendung ist in Kapitel 9.3 Allgemeine Prüfung des Systems sowie Anhang A – Tools & Features zu finden. Neben den beschriebenen Tools gibt es noch eine Reihe von Zusatztools anderer Hersteller, die ebenfalls im Anhang A des Buches beschrieben sind.
2.3
Ressourcenkontrolle mit dem Resource Governor
SQL Server 2008 bietet mit dem Resource Governor eine Möglichkeit, mit der sich die von bestimmten Benutzern und/oder Anwendungen verwendeten Ressourcen limitieren lassen. Dadurch kann man insbesondere den unterschiedlichen Anforderungen von klassischen OLTP-Anwendungen und OLAP- bzw. Reporting-Lösungen gerecht werden. OLTPAnwendungen zeichnen sich vor allem durch eine große Zahl an vergleichsweise kleinen Transaktionen aus, bei denen eine schnelle Antwortzeit von essenzieller Bedeutung ist. OLAP-Lösungen dagegen verwenden schwerpunktmäßig eher wenige lang laufende Transaktionen zur Massenverarbeitung (in der Regel Auswertung) von Daten. Aus diesem Grund empfiehlt es sich auch, diese beiden Anwendungstypen von verschiedenen SQL Servern zu bedienen. Allerdings wird gerade in kleinen und mittelständischen Unternehmen – alleine aus Kostengründen – oft ein Datenbankserver verwendet, der für beide Aufgaben zuständig ist. Hier muss man dafür sorgen, dass die lang laufenden OLAP-Abfragen nicht den normalen Betrieb der OLTP-Anwendung blockieren. Der Resource Governor ermöglicht dies durch die Definition von verschiedenen Ressourcenpools, für die man Unter- und Obergrenzen für die zur Verfügung stehende Arbeitsspeichernutzung und CPU-Last festlegen kann. Diese Ressourcenpools werden anschließend so genannten Arbeitsauslastungsgruppen zugeordnet. Durch eine selbst zu definierende Klassifizierungsfunktion kann dann festgelegt werden, wann welche Arbeitsauslastungsgruppe zu verwenden ist (und damit indirekt auch, welche Ressourcenbeschränkungen greifen).
SQL Server Performance-Ratgeber
61
2030-4.book Page 62 Monday, February 8, 2010 4:26 PM
2 – Einrichten und Warten von Datenbanken
2.3.1
Ressourcenpools und Arbeitsauslastungsgruppen
Die Definition von Ressourcenpools und Arbeitsauslastungsgruppen (engl. Workload Groups) kann sowohl über T-SQL als auch über das SQL Server Management Studio erfolgen. Im Objekt-Explorer des SQL Server Management Studios finden Sie unterhalb des verwendeten Servers den Zweig Verwaltung/Ressourcenkontrolle/Ressourcenpools. Darunter ist bereits ein Untereintrag Systemressourcenpools zu finden, der die vordefinierten Pools default und internal beinhaltet. Um eigene Ressourcenpools zu definieren, klicken Sie mit der rechten Maustaste auf den oben genannten Eintrag und wählen dann die Option Neuer Ressourcenpool. In dem daraufhin erscheinenden Dialog (siehe Abbildung 2.7) sind eigentlich alle wesentlichen Einstellungen für den Resource Governor zu sehen. Im oberen Bereich stehen die vordefinierten Ressourcenpools mit den Unter- und Obergrenzen für CPU- und Arbeitsspeicherauslastung. Im unteren Bereich sind die Arbeitsauslastungsgruppen für den oben ausgewählten Ressourcenpool mit einigen Einstellungen zu sehen (hier ist insbesondere die Wichtigkeit von entscheidender Bedeutung).
Abbildung 2.7: Zuteilung von Systemressourcen mit dem Resource Governor
Bevor Sie nun eigene Ressourcenpools definieren, müssen Sie die Ressourcenkontrolle erst durch Anklicken der entsprechenden Option aktivieren. Dadurch erscheinen in beiden Listen automatisch leere Einträge, in denen Sie eigene Einstellungen vornehmen können. Fügen Sie nun jeweils einen Ressourcenpool für OLAP und OLTP hinzu und stellen Sie die Werte so ein, dass für OLTP ein gewisser Prozentsatz minimal verfügbar ist, während die Maximalangaben für OLAP deutlich unter 100% liegen sollten. Tragen Sie anschließend zu beiden Ressourcenpools eine gleichnamige Arbeitsauslastungsgruppe ein und setzen Sie die Priorität für OLAP auf Low, für OLTP auf High.
62
2030-4.book Page 63 Monday, February 8, 2010 4:26 PM
Ressourcenkontrolle mit dem Resource Governor
Ressourcenpool
OLAP
OLTP
Minimaler CPU-Prozentsatz
0%
50%
Maximaler CPU-Prozentsatz
40%
100%
Minimaler Arbeitsspeicherprozentsatz
0%
50%
Maximaler Arbeitsspeicherprozentsatz
40%
100%
Arbeitsauslastungsgruppe
OLAP
OLTP
Wichtigkeit
Low
High
Tabelle 2.1: Mögliche Einstellungen für Ressourcenpool und Arbeitsauslastungsgruppe
Wenn Sie die Einstellungen vorgenommen haben, kann man diese durch einen Klick auf OK ausführen oder alternativ über das Skript-Symbol das entsprechende Skript erstellen lassen, das dieselben Einstellungen setzt: CREATE RESOURCE POOL [OLAP] WITH(min_cpu_percent=0, max_cpu_percent=40, min_memory_percent=0, max_memory_percent=40) GO CREATE WORKLOAD GROUP [OLAP] WITH(group_max_requests=0, importance=Low, request_max_cpu_time_sec=0, request_max_memory_grant_percent=25, request_memory_grant_timeout_sec=0, max_dop=0) USING [OLAP] GO CREATE RESOURCE POOL [OLTP] WITH(min_cpu_percent=50, max_cpu_percent=100, min_memory_percent=50, max_memory_percent=100) GO CREATE WORKLOAD GROUP [OLTP] WITH(group_max_requests=0, importance=High, request_max_cpu_time_sec=0, request_max_memory_grant_percent=25, request_memory_grant_timeout_sec=0, max_dop=0) USING [OLTP] GO Listing 2.4: Erstellung von Ressourcenpools und Arbeitsauslastungsgruppen
SQL Server Performance-Ratgeber
63
2030-4.book Page 64 Monday, February 8, 2010 4:26 PM
2 – Einrichten und Warten von Datenbanken
ALTER RESOURCE GOVERNOR RECONFIGURE; GO Listing 2.4: Erstellung von Ressourcenpools und Arbeitsauslastungsgruppen (Forts.)
Was nun noch fehlt, ist die Zuordnung der Arbeitsauslastungsgruppen zu Logins über eine Klassifizierungsfunktion. Diese können Sie zwar in dem vorigen Dialog auswählen, allerdings nur dann, wenn die entsprechende Funktion bereits vorhanden ist.
2.3.2 Klassifizierungsfunktionen erstellen und zuordnen Das Erstellen einer Klassifizierungsfunktion wird zwar nicht durch eine grafische Oberfläche unterstützt, ist jedoch mit grundlegenden SQL-Kenntnissen relativ einfach umzusetzen. Es muss lediglich eine Funktion in der master-Datenbank definiert werden, die – in Abhängigkeit von diversen Systemvariablen – den Namen der zu verwendenden Arbeitsauslastungsgruppe zurückliefert. Darüber lassen sich verschiedene Faktoren wie beispielsweise der Login-Name, der Anwendungsname aber auch die Uhrzeit als Entscheidungsgrundlage für die Wahl der richtigen Arbeitsauslastungsgruppe nutzen. Im folgenden Beispielskript wird aufgrund des Login-Namens entschieden, ob es sich um eine OLAP- oder eine OLTP-Anfrage handelt: USE master GO CREATE FUNCTION dbo.ufnResourceGovernor() RETURNS sysname WITH SCHEMABINDING AS BEGIN DECLARE @Arbeitsauslastungsgruppe sysname IF (SUSER_NAME() LIKE 'Report%') SET @Arbeitsauslastungsgruppe = 'OLAP' ELSE SET @Arbeitsauslastungsgruppe = 'OLTP' RETURN @Arbeitsauslastungsgruppe END Listing 2.5: Erstellung einer Klassifizierungsfunktion
i
64
Wird von der Klassifizierungsfunktion ein Name zurückgeliefert, zu dem es keine entsprechende Arbeitsauslastungsgruppe gibt, so wird stattdessen die Arbeitsauslastungsgruppe default verwendet, deren Werte Sie bei Bedarf ebenfalls anpassen können.
2030-4.book Page 65 Monday, February 8, 2010 4:26 PM
Zusammenfassung
Um die neu erstellte Klassifizierungsfunktion zu aktivieren, öffnen Sie im Objekt-Explorer über das Kontextmenü des Zweiges Verwaltung/Ressourcenkontrolle und anschließender Auswahl der Option Eigenschaften wieder den bereits bekannten Dialog. Hier lässt sich nun als Name der Klassifizierungsfunktion die Funktion dbo.ufnResourceGovernor auswählen und mit OK bestätigen. Alternativ hierzu kann auch das folgende SQL-Skript ausgeführt werden: ALTER RESOURCE GOVERNOR WITH (CLASSIFIER_FUNCTION = [dbo].[ufnResourceGovernor]); GO ALTER RESOURCE GOVERNOR RECONFIGURE; GO Listing 2.6: Aktivierung der Klassifizierungsfunktion
2.4
Zusammenfassung
Beim Einrichten und Konfigurieren einer Datenbank sind zahlreiche Detailaspekte zu berücksichtigen, damit eine zufrieden stellende Performance erreicht werden kann. Das beginnt bei der Wahl des richtigen Wiederherstellungsmodells sowie der Größe und Verteilung von Zeilendaten- und Protokolldatei, die möglichst auf getrennten Plattensystemen unterzubringen sind. Dazu gibt es einige Konfigurationseinstellungen, die für eine automatische „Pflege“ der Datenbank sorgen. Davon ist vor allem das automatische Aktualisieren von Datenbankstatistiken sinnvoll, während die meisten anderen Wartungsarbeiten eher kontrolliert und explizit in Wartungsfenstern ausgeführt werden sollten. Hierzu zählt beispielsweise das Reorganisieren oder gar Neuaufbauen von Indizes, regelmäßige Sicherungen des Transaktionsprotokolls sowie wiederum ein Aktualisieren der Statistiken, um die Notwendigkeit der automatischen Aktualisierung auf ein Minimum zu reduzieren. Neben den typischen Wartungsaufgaben sollte aber auch eine regelmäßige Überwachung der Systemauslastung durchgeführt werden, um auf eventuell auftretende Engpässe möglichst frühzeitig mit entsprechenden Maßnahmen reagieren zu können. Als minimales Set an zu beobachtenden Kriterien gelten hier die Auslastung von CPU, Speicher, Festplatten und Netzwerkadapter. Diese Liste ließe sich noch beliebig um speziellere Werte erweitern, allerdings sollten die Kriterien für eine regelmäßige Überwachung möglichst überschaubar sein. Werden dabei Probleme festgestellt, kann man dann noch weitere Kriterien hinzunehmen, um nach den Ursachen dafür zu forschen, doch dies ist Thema eines späteren Kapitels. Sofern man SQL Server 2008 einsetzt, kann man die Systemauslastung durch verschiedene Anwendungen auch kontrolliert beeinflussen, indem man den Resource Governor nutzt, der aufgrund von Ressourcenpools und Arbeitsauslastungsgruppen die maximal mögliche CPU- und Hauptspeicher-Auslastung begrenzen kann.
SQL Server Performance-Ratgeber
65
2030-4.book Page 66 Monday, February 8, 2010 4:26 PM
2030-4.book Page 67 Monday, February 8, 2010 4:26 PM
Teil B Optimierung für Designer und Architekten In diesem Teil des Buches geht es um die Optimierung aus Sicht des Architekten – vor allem der des Datenbankarchitekten. Während Kapitel 3 – Modellierung einer Datenbank und Kapitel 4 – Indizes sich mit typischen Architekten- oder auch Designeraufgaben beschäftigen, sind die Inhalte von Kapitel 5 – Optimale Verteilung der Daten eventuell auch für Datenbankadministratoren von Bedeutung. Oft werden die Aufgaben des Datenbankarchitekten auch von Datenbankadministratoren oder Datenbankentwicklern übernommen.
SQL Server Performance-Ratgeber
67
2030-4.book Page 68 Monday, February 8, 2010 4:26 PM
2030-4.book Page 69 Monday, February 8, 2010 4:26 PM
3
Modellierung einer Datenbank
Ein entscheidendes Kriterium für die Performance einer Datenbank ist das Datenmodell. Dies ist eigentlich eine typische Aufgabe für Datenbankarchitekten oder Datenbankdesigner (sofern es diese Rolle in dem entsprechenden Umfeld gibt), wird aber oft – mangels dediziert für diese Aufgabe zuständige Mitarbeiter – von Datenbankadministratoren und/oder Datenbankentwicklern wahrgenommen, teilweise sogar von Anwendungsentwicklern. Insbesondere der letztgenannte Fall resultiert oft in „suboptimalen“ Datenmodellen, da Anwendungsentwickler einen ganz anderen Blickwinkel haben und für die Datenmodellierung nicht ausreichend ausgebildet sind. So wird oft ein Datenmodell aufgesetzt, das aus Entwicklersicht erst einmal bestens funktioniert, aber spätestens dann, wenn die Datenbank mit realen Datenmengen gefüllt wird, versagt oder zumindest zu einer schlechten Gesamtperformance der Anwendung beiträgt. Das ist insbesondere dadurch kritisch, dass sich das Datenmodell einer Anwendung später kaum oder nur mit sehr hohem Aufwand wieder ändern lässt.
3.1
Normalisierung & Denormalisierung
Ein zentrales Thema bei der Datenmodellierung ist die Entscheidung, wie die zu speichernden Informationen auf verschiedene Tabellen verteilt werden. Im einfachsten Fall kann man natürlich alle Daten in einer Tabelle ablegen, was erst einmal sehr überschaubar ist, aber spätestens bei größeren Datenmengen sehr ineffizient ist. Wenn man beispielsweise von einer Adresstabelle ausgeht, in der Personen mit ihrer Adresse und Telefonnummer gespeichert sind, könnte diese im einfachsten Fall wie folgt aussehen: Name
Straße
Ort
Telefon
Erwin Mustermann
Hauptstraße 27
60437 Frankfurt
069/4711
Helga Mustermann
Hauptstraße 27
60437 Frankfurt
069/4711
Markus Bauer
Bahnstraße 12
60437 Frankfurt
069/12345
Stefanie Schmidt
Alsterweg 47
20099 Hamburg
040/3210
Tabelle 3.1: Adressdaten in nichtnormalisierter Form
SQL Server Performance-Ratgeber
69
2030-4.book Page 70 Monday, February 8, 2010 4:26 PM
3 – Modellierung einer Datenbank
Aus dieser Art der Speicherung ergeben sich allerdings ein paar Probleme: 쮿
ein Teil der Adresse ist mehrfach gespeichert (weil er für verschiedene Personen gleich ist)
쮿
durch die mehrfache Speicherung wird Speicherplatz verschwendet
쮿
wenn eine Adresse (z.B. von Familie Mustermann) sich durch Umzug ändert, kann dies eine Änderung an mehreren Datenzeilen zur Folge haben, was einerseits aufwändiger im Programmcode ist und andererseits aufwändigere Datenbankoperationen mit sich bringt
Um diesen Problemen zu begegnen, wurden bereits vor vielen Jahren Methoden zur so genannten Normalisierung von Daten entwickelt. Diese beinhalten eine Vielzahl von Regeln – so genannten Normalformen –, von denen ich die wichtigsten drei hier vorstellen möchte, da diese eigentlich jedem, der sich mit der Modellierung von Datenbanken beschäftigt, bekannt sein sollten:
3.1.1
Die ersten drei Normalformen
Die drei im Folgenden beschriebenen Normalformen definieren sich durch bestimmte Bedingungen an die Datenbankstruktur und bauen dabei alle aufeinander auf. Das bedeutet, dass für die zweite Normalform die erste Normalform plus eine zusätzliche Bedingung erfüllt sein muss, für die dritte Normalform müssen die ersten beiden Normalformen zuzüglich einer weiteren Bedingung erfüllt sein.
1. Normalform (1 NF) Jedes Feld der Tabelle muss einen atomaren Wertebereich haben. Damit ist gemeint, dass zur Erfüllung der 1. Normalform keine zusammengesetzten Felder erlaubt sind. Das Beispiel aus Tabelle 3.1 erfüllt diese Normalform nicht, da in allen vier Feldern zusammengesetzte Informationen gespeichert sind. Um die erste Normalform zu erfüllen, müssen diese Informationen auf separate Felder verteilt werden, sodass sich die in Tabelle 3.2 gezeigte Struktur ergibt: Vorname
Nachname
Straße
HausNr
PLZ
Ort
Vorwahl
Durchwahl
Erwin
Mustermann
Hauptstraße
27
60437
Frankfurt
069
4711
Helga
Mustermann
Hauptstraße
27
60437
Frankfurt
069
4711
Markus
Bauer
Bahnstraße
12
60437
Frankfurt
069
12345
Stefanie
Schmidt
Alsterweg
47
20099
Hamburg
040
3210
Tabelle 3.2: Adressdaten in 1. Normalform
Als Primärschlüssel, der die jeweilige Datenzeile eindeutig identifiziert, wird die Kombination aus Vor- und Nachname festgelegt. Diese Felder sind daher auch in der Tabelle optisch hervorgehoben.
70
2030-4.book Page 71 Monday, February 8, 2010 4:26 PM
Normalisierung & Denormalisierung
2. Normalform (2 NF) Jedes Nichtschlüsselfeld ist vom Primärschlüssel (nicht nur von einem Teil davon) voll funktional abhängig. Außerdem muss die Bedingung der 1. Normalform erfüllt sein. Auch diese Normalform ist im Beispiel aus Tabelle 3.2 nicht erfüllt, da die Adressdaten nicht alle direkt vom Primärschlüssel (der Kombination aus Vor- und Nachnamen) abhängig sind, sondern im Fall der Familie Mustermann eher vom Haushalt als von einer einzelnen Person. Um die zweite Normalform zu erreichen, würde man die Adressdaten daher in eine separate Tabelle auslagern. Als Bezug zur Tabelle mit den Personendaten empfiehlt sich aber eher die Telefonnummer, da der Nachname ja auch in anderen Adressen noch einmal vorkommen kann (besonders bei sehr gängigen Nachnamen wie Müller, Meier, Schulz etc.). Es erfolgt also eine Aufteilung in zwei Tabellen (Personen und Haushalte): Vorname
Nachname
Vorwahl
Durchwahl
Erwin
Mustermann
069
4711
Helga
Mustermann
069
4711
Markus
Bauer
069
12345
Stefanie
Schmidt
040
3210
Tabelle 3.3: Die Tabelle Personen in 2. Normalform Vorwahl
Durchwahl
Straße
HausNr
PLZ
Ort
069
4711
Hauptstraße
27
60437
Frankfurt
069
12345
Bahnstraße
12
60437
Frankfurt
040
3210
Alsterweg
47
20099
Hamburg
Tabelle 3.4: Die Tabelle Haushalte in 2. Normalform
In der Tabelle Haushalte reduziert sich die Anzahl der Zeilen von vier auf drei, da die Daten zum Haushalt der Familie Mustermann nun nur noch einmal gespeichert werden müssen. Für die Tabelle Haushalte wird die Kombination aus Vorwahl und Durchwahl als Primärschlüssel verwendet. In der Tabelle Personen wird dieselbe Feldkombination als Fremdschlüssel (der auf die Tabelle Haushalte verweist) genutzt.
3. Normalform (3 NF) Alle Nichtschlüsselfelder müssen direkt (nicht indirekt) vom Primärschlüssel abhängen. Außerdem müssen die Bedingungen der 2. Normalform erfüllt sein. Auch diese Bedingung ist in dem obigen Beispiel natürlich noch nicht erfüllt (es wäre ja sonst auch ein schlechtes Beispiel für dieses Kapitel). Der Grund dafür ist, dass Vorwahl, PLZ und Ort in einer gewissen Form voneinander abhängen, aber nicht von der Kombination aus Vorwahl und Durchwahl. Leider stehen die deutschen Postleitzahlen, Tele-
SQL Server Performance-Ratgeber
71
2030-4.book Page 72 Monday, February 8, 2010 4:26 PM
3 – Modellierung einer Datenbank
fonvorwahlen und Ortsnummern nicht in einem so logischen Zusammenhang, dass sich aus der Postleitzahl eindeutig die beiden anderen Werte ergeben (auch wenn das für die meisten Fälle zutrifft). Um dies sauber abbilden zu können, verwenden wir nun eine Kombination aus Vorwahl und Postleitzahl, aus der sich eindeutig der Ort ergibt, um die dritte Normalform zu erreichen. (Hier ist mir zumindest kein Fall in Deutschland bekannt, bei dem sich mehrere Orte dieselbe Postleitzahl und Vorwahl teilen.) Somit ergeben sich folgende drei Tabellen (Personen, Haushalte und Orte): Vorname
Nachname
Vorwahl
Durchwahl
Erwin
Mustermann
069
4711
Helga
Mustermann
069
4711
Markus
Bauer
069
12345
Stefanie
Schmidt
040
3210
Tabelle 3.5: Die Tabelle Personen in 3. Normalform Vorwahl
Durchwahl
Straße
HausNr
PLZ
069
4711
Hauptstraße
27
60437
069
12345
Bahnstraße
12
60437
040
3210
Alsterweg
47
20099
Tabelle 3.6: Die Tabelle Haushalte in 3. Normalform Vorwahl
PLZ
Ort
069
60437
Frankfurt
040
20099
Hamburg
Tabelle 3.7: Die Tabelle Orte in 3. Normalform
In der Tabelle Orte reduziert sich die Anzahl der Zeilen wiederum um eine Zeile, da der Ort zum Postleitzahlenbereich 60437 mit der Vorwahl 069, der in zwei Haushalten vorkommt, nur noch einmal gespeichert werden muss. Es gibt noch eine ganze Reihe von weiteren Normalformen. In der Praxis reicht die Kenntnis der ersten drei (in Kombination mit etwas gesundem Menschenverstand) völlig aus.
3.1.2
Bewusste Denormalisierung
In manchen Situationen bietet es sich aber auch an, bewusste Denormalisierung zu betreiben, um schnellere Zugriffe auf die Daten zu erhalten. Somit müssen die Daten nicht aus verschiedenen Tabellen – und damit auch Speicherseiten – zusammengesucht werden, sondern stehen so zusammen, wie sie meist benötigt werden. Bei der Aktualisierung von Daten kann dies natürlich bedeuten, dass dieselben Daten an mehreren Stellen geändert werden müssen. In den meisten Fällen kann man also sagen, dass normalisierte
72
2030-4.book Page 73 Monday, February 8, 2010 4:26 PM
Normalisierung & Denormalisierung
Datenstrukturen für Schreibzugriffe effektiver sind, während denormalisierte Strukturen für Lesezugriffe optimiert sind. Dies machen sich auch Data-Warehouse-Lösungen zu Nutze, indem Daten, die meist zusammen abgefragt werden, auch entsprechend (denormalisiert) vorbereitet vorliegen, was natürlich zusätzlichen Speicherplatz erfordert. Die Probleme bei der Datenaktualisierung spielen bei einem Data Warehouse keine Rolle, da es sich ja hierbei um eine zusätzliche Kopie der produktiven Daten handelt, in der keine Datenänderungen stattfinden. Aber auch bei OLTP-Systemen gibt es gute Gründe, die drei Normalformen nicht allzu dogmatisch anzuwenden, sondern stets zu hinterfragen, wo eine Normalisierung sinnvoll ist und wo nicht. Folgende Kriterien können Sie verwenden, um zu entscheiden, wann es Sinn macht, sich bewusst über die Normalformen hinwegzusetzen: 쮿
Wenn Spalten sehr eng logisch zusammengehören und generell zusammen gelesen und geändert werden, kann es effektiver sein, diese Informationen in einem Feld zu speichern. So könnte man im obigen Beispiel Vorwahl und Telefonnummer in einem Feld speichern (allerdings hätte man die Vorwahl dann nicht mehr als Teil des Primärschlüssels für die Tabellen Haushalte und Orte verwenden können). Auch Straße und Hausnummer sind ein Kandidat, den man auch problemlos in einem gemeinsamen Feld speichern kann.
쮿
Spalten, nach deren Inhalten oft gesucht wird, sollten entweder in einer eigenen Spalte stehen oder aber am Anfang einer zusammengesetzten Spalte (damit man zumindest noch mit LIKE 'Suchbegriff%' gut danach suchen kann). Dies ist wichtig, damit bei der Suche auf den Spalten eventuell existierende Indizes effektiv genutzt werden können. Spalten, nach denen nicht gesucht wird, können eventuell an andere Spalten angehängt werden. Auch dies bestätigt die Beispiele oben, denn nach einer Hausnummer wird man selten suchen und nach einer Telefonnummer normalerweise auch nur in Kombination mit der dazugehörenden Vorwahl.
쮿
Wenn die untersuchten Spalten eine geringe Größe haben und noch dazu selten geändert werden, kann man eine redundante Speicherung, die der zweiten und/oder dritten Normalform widerspricht, in Kauf nehmen. So macht die Speicherung des Ortsnamens (sofern zu dem Ort nicht noch zusätzliche Informationen gespeichert werden) in einer separaten Tabelle nicht unbedingt Sinn, da sich dieser dort – zumindest für größere Städte – wegen der verschiedenen Postleitzahlen trotzdem noch recht häufig wiederholen würde. Auch ist der zusammengesetzte Primärschlüssel für die Tabelle Orte etwa genauso groß wie der Ortsname selbst, sodass jeder Ort im Durchschnitt mehr als zweimal verwendet werden muss, damit sich der zusätzliche Speicherbedarf für das doppelte Speichern des Schlüssels (einmal als Fremdschlüssel in der Tabelle Haushalte, einmal als Primärschlüssel in der Tabelle Orte) amortisiert.
Man sollte also eventuell bewusst Redundanzen in Kauf nehmen, falls der für die Normalisierung nötige Verwaltungsaufwand stärker ins Gewicht fällt als der Vorteil, den man durch die Normalisierung erhält.
SQL Server Performance-Ratgeber
73
2030-4.book Page 74 Monday, February 8, 2010 4:26 PM
3 – Modellierung einer Datenbank
Für unser Beispiel von oben empfehle ich daher folgende Tabellenstruktur: Vorname
Nachname
Telefon
Erwin
Mustermann
069/4711
Helga
Mustermann
069/4711
Markus
Bauer
069/12345
Stefanie
Schmidt
040/3210
Tabelle 3.8: Die Tabelle Personen in halbwegs normalisierter Form Telefon
Straße
PLZ
Ort
069/4711
Hauptstraße 27
60437
Frankfurt
069/12345
Bahnstraße 12
60437
Frankfurt
040/3210
Alsterweg 47
20099
Hamburg
Tabelle 3.9: Die Tabelle Haushalte in halbwegs normalisierter Form
Hier wurde also der Ort nicht in einer separaten Tabelle gespeichert und die Felder Straße und Hausnummer sowie Vorwahl und Durchwahl wurden wieder zu einem Feld zusammengefasst. Letzteres erleichtert auch das Handling, wenn die Telefonnummer als Fremd- und Primärschlüssel verwendet wird. Generell sollte man die Grundprinzipien der Normalisierung also kennen, sie aber nicht immer um jeden Preis anwenden, sondern stattdessen nach gesundem Menschenverstand entscheiden, wie die zu speichernden Informationen auf Spalten und Tabellen aufzuteilen sind.
3.2
Datentypen sinnvoll nutzen
Nicht unterschätzen sollte man bei der Datenmodellierung die Wahl der richtigen Datentypen. Ungünstig gewählte Datentypen kosten Speicher und damit indirekt auch Performance. Und auch wenn sich ein paar verschwendete Bytes pro Zeile erst einmal nach nicht viel anhören, so kann dies bei Tabellen mit mehreren Millionen Zeilen schon deutlich mehr ins Gewicht fallen. Gefährlich ist es unter anderem, wenn man sich aus Bequemlichkeit an automatisch vorgeschlagene Datentypen hält, ohne zu hinterfragen, ob diese wirklich passen. Das SQL Server Management Studio schlägt beispielsweise für jedes Feld den Typ nchar(10) vor. Das mag für einige in der Datenbank zu speichernde Informationen sinnvoll sein, für andere weniger und für einige ist dieser Vorschlag sicherlich völlig falsch. Während es in letzterem Fall aber früher oder später auffällt und der Datentyp zwangsweise angepasst werden muss, so sind quasi die Fälle gefährlicher, in denen der Datentyp eigentlich passt, aber alles andere als optimal ist. Denn hier wird der vorgeschlagene Typ gerne beibehalten. So kann man natürlich auch ein Datum in einem Feld vom Typ nchar(10) ablegen, benötigt dann aber einerseits deutlich mehr Speicherplatz als nötig und andererseits wird auch die Sortierung problematisch. 74
2030-4.book Page 75 Monday, February 8, 2010 4:26 PM
Datentypen sinnvoll nutzen
Generell gilt, dass man Datentypen verwenden sollte, die so wenig Speicherplatz belegen wie möglich, aber auch so viel wie nötig. Und dies im Zweifelsfall mit einem kleinen Puffer, durch den erreicht wird, dass nicht regelmäßig das Datenmodell angepasst werden muss, weil eine Information gespeichert werden soll, die lediglich ein oder zwei Zeichen zu groß für die entsprechende Spalte ist.
3.2.1
Unicode oder nicht?
Bevor wir uns mit den Vor- und Nachteilen der verschiedenen alphanumerischen Datentypen befassen, möchte ich eine grundlegende Frage klären, die im Zusammenhang mit alphanumerischen Feldern auftaucht. Dabei geht es um die Frage, ob man einen ASCIIoder Unicode-Zeichensatz verwenden sollte. Textinformationen im ASCII-Format belegen ein Byte Speicher pro Zeichen und können damit 256 verschiedene Zeichen abbilden. Da dies nicht ausreicht, um auch alle möglichen internationalen Sonderzeichen abzubilden, gibt es verschiedene länderspezifische Zeichensätze, die über entsprechende Codepages definiert sind. Die für eine Datenbank verwendete Codepage lässt sich indirekt über die Auswahl der Sortierreihenfolge festlegen (allerdings sind über die Sortierreihenfolge noch zusätzliche Einstellungen definiert, z.B. ob Groß-/Kleinschreibung berücksichtigt werden soll etc.). Auch wenn die zehn Ziffern sowie die 26 Buchstaben des Alphabets bei eigentlich allen Zeichensätzen an derselben Stelle liegen, ist das bei vielen Satz- und Sonderzeichen schon nicht mehr so. Schlimmer wird es noch bei Umlauten, die in den meisten anderen Zeichensätzen gar nicht erst vorhanden sind, weil sie ja sprachspezifisch sind. Daher führt es unweigerlich zu Problemen, wenn man Datenbanken mit unterschiedlichen Codepages bzw. Sortierreihenfolgen gemeinsam verwenden will. Diese Probleme kann man vermeiden, wenn man für die Felddefinitionen UnicodeDatentypen verwendet. Dabei werden dann zwar zwei Byte Speicher pro Zeichen benötigt, dafür lassen sich damit dann aber alle internationalen Zeichen darstellen und Sie brauchen sich keine Sorgen mehr über die Wahl der richtigen Codepage zu machen. Die Unicode-Datentypen sind alle am Präfix n erkennbar und entsprechen sonst ihren ASCII-Äquivalenten. So gibt es beispielsweise für den Datentyp varchar(max) die Unicode-Entsprechung nvarchar(max). Um nun zu entscheiden, ob man ASCII- oder Unicode-Datentypen verwenden sollte, muss man die folgenden Vor- und Nachteile abwägen. Für ASCII spricht: 쮿
weniger Speicherbedarf, dadurch können auch mehr Daten im Puffer gehalten werden, was wiederum gut für die Performance ist
Für Unicode spricht: 쮿
alle internationalen Zeichen darstellbar
쮿
keine Abhängigkeit von der Spracheinstellung des Servers bzw. der Datenbank
Wenn Sie den SQL Server aber vorrangig mit .NET-Anwendungen einsetzen, ist sicherlich das wichtigste Argument für Unicode, dass .NET intern ohnehin ausschließlich mit Unicode-Datentypen arbeitet. Die Verwendung von Nicht-Unicode-Datentypen in der
SQL Server Performance-Ratgeber
75
2030-4.book Page 76 Monday, February 8, 2010 4:26 PM
3 – Modellierung einer Datenbank
Datenbank würde also dazu führen, dass die Werte ständig von ASCII in Unicode (oder zurück) umgewandelt werden müssten, was wiederum einen Performanceverlust zur Folge hat (wobei der Zusatzaufwand hier eher den Prozessor treffen würde). In bestimmten Konstellationen kann dies sogar dazu führen, dass ein existierender Index für eine Tabelle nicht effektiv verwendet werden kann, weil der zu suchende Wert im falschen Format (nämlich Unicode statt ASCII) beim SQL Server ankommt. Diese uneffektive Indexnutzung schließlich bringt gravierende Performanceeinbußen mit sich, die den Performancegewinn durch den geringeren Speicherbedarf deutlich übertreffen. Aus diesem Grund wird inzwischen in den meisten Unternehmen, in denen der SQL Server zusammen mit .NET-Anwendungen eingesetzt wird, komplett mit Unicode gearbeitet. Sofern Sie sich in einem international agierenden Umfeld bewegen, sollten Sie ohnehin Unicode verwenden, damit dies beim Datenaustausch nicht zu Problemen führt. Das soll aber nicht heißen, dass Unicode die einzig mögliche Wahl ist, denn in einem Unternehmen, das ausschließlich innerhalb Deutschlands agiert und anstelle von .NET Programmierframeworks verwendet, die auch intern ASCII-Zeichensätze unterstützen, kann auch der Verzicht auf die Vorzüge von Unicode die richtige Wahl sein und zu einer besseren Gesamtperformance führen. Welche Entscheidung Sie in Ihrem Umfeld auch immer treffen. Wichtig ist vor allem, dass diese Entscheidung getroffen wird und danach entweder ausschließlich UnicodeDatentypen oder generell keine Unicode-Datentypen für alphanumerische Informationen verwendet werden. Denn ein Mischen beider Varianten ist in jedem Fall die ungünstigste Variante.
3.2.2
Alphanumerische Datentypen
SQL Server 2008 bietet verschiedene Varianten von alphanumerischen Datentypen, die für die Speicherung von einzelnen Zeichen bis hin zu längeren Texten ausgelegt sind. Um den richtigen Datentyp zu finden, sind drei Fragen zu beantworten: 1. Welche Maximallänge muss das Feld speichern können? 2. Hat das Feld eine konstante oder variable Länge? 3. Wird für das Feld ein Unicode-Zeichensatz benötigt oder nicht? Die letzte der drei Fragen wurde weiter oben bereits ausgiebig diskutiert und sollte möglichst für die gesamte Datenbank einheitlich entschieden werden. Bleibt nur noch die Frage nach der Maximallänge und wie konstant diese gefüllt ist. Die benötigte Maximallänge ergibt sich direkt aus den zu speichernden Inhalten. Wenn diese bereits in einer SQL-ServerTabelle enthalten sind, lässt sie sich leicht mit einer einfachen Abfrage feststellen: SELECT MAX(LEN(Textfeld)) FROM Tabelle
Da bei großen Tabellen in einigen wenigen Zeilen oft auch fehlerhaft gefüllte Felder vorkommen, sollte man sich sicherheitshalber die Zeilen mit den längsten Feldern der Tabelle einmal ansehen, um zu entscheiden, ob diese auch künftig noch relevant sind:
76
2030-4.book Page 77 Monday, February 8, 2010 4:26 PM
Datentypen sinnvoll nutzen
SELECT TOP 100 LEN(Textfeld) FROM Tabelle ORDER BY LEN(Textfeld) DESC
Je nach Bedeutung der Feldinhalte sollte man diesen Maximalwert noch um ein paar Zeichen aufrunden, um eine sinnvolle Längenvorgabe für das neue Feld zu erhalten. Wenn Sie also beispielsweise Adressdaten importieren, die ein Feld mit Straßennamen beinhalten, das mit maximal 28 Zeichen gefüllt ist, sollte man dieses Feld in der Zieltabelle eher mit 30 Zeichen anlegen, um auch für künftige Werte noch genügend Spielraum zu haben. Schwieriger ist die Frage zu klären, ob das Feld eine variable Länge benötigt oder nicht. Sind Quelldaten vorhanden, lässt sich durch eine Erweiterung der einfachen Abfrage von oben leicht prüfen, wie die Verteilung der Feldlängen ist: SELECT MIN(LEN(Textfeld)), AVG(LEN(Textfeld)), MAX(LEN(Textfeld)) FROM Tabelle
Dadurch wird die kleinste, durchschnittliche und maximale Feldlänge ermittelt. Liegen diese drei Werte nahe beieinander (oder sind sogar gleich), dann ist sicherlich eine konstante Feldlänge zu bevorzugen, wozu die Datentypen char(Feldlänge) bzw. nchar(Feldlänge) zu verwenden sind. Je weiter die Werte auseinanderliegen, desto vielversprechender wird die Nutzung von alphanumerischen Datentypen mit variabler Länge, also varchar(Feldlänge) bzw. nvarchar(Feldlänge). Während die Typen mit konstanter Länge pro Zeichen laut Felddefinition genau ein Byte (bzw. zwei Byte bei Unicode) belegen, sparen die Typen variabler Länge Speicherplatz, indem nur die echten Inhaltsdaten und deren Länge gespeichert werden. Da die Speicherung der Länge ebenfalls zwei Byte belegt, sollte der Feldinhalt im Schnitt schon mehr als zwei Zeichen kürzer als die definierte Maximallänge sein, damit sich ein Datentyp variabler Länge lohnt. Für die alphanumerischen Datentypen variabler Länge gibt es noch Varianten, die keine Obergrenze bezüglich ihrer Länge haben. Wobei keine Obergrenze genau genommen nicht ganz korrekt ist, denn es können maximal 2 GB (also 2.147.483.647 Byte) darin gespeichert werden. Dabei sind die älteren Datentypen text und ntext seit SQL Server 2005 durch varchar(max) und nvarchar(max) abgelöst und damit hinfällig. Sie werden zwar aus Kompatibilitätsgründen selbst von SQL Server 2008 noch unterstützt, sollten aber nicht mehr verwendet werden. Neben der Tatsache, dass text und ntext irgendwann sicherlich nicht mehr unterstützt werden, bieten varchar(max) und nvarchar(max) den Vorteil, dass mit diesen Typen auch Stringfunktionen verwendet werden können und sogar in einer SELECT DISTINCT-Abfrage genutzt werden können. Der Sinn all dieser Überlegungen liegt in der Speicherersparnis, die auch bewirkt, dass mehr Zeilen der Tabelle im Hauptspeicher gehalten werden können.
3.2.3
Numerische Datentypen
Bei numerischen Datentypen sind drei grundsätzlich verschiedene Grundvarianten zu unterscheiden. Am wenigsten Speicher belegen die ganzzahligen Typen bit, tinyint, smallint,
SQL Server Performance-Ratgeber
77
2030-4.book Page 78 Monday, February 8, 2010 4:26 PM
3 – Modellierung einer Datenbank
int und bigint, die damit auch am effektivsten verwaltet werden können. Selbst die größte Variante davon – bigint – belegt lediglich acht Byte Speicher, die kleineren entsprechend weniger. Diese Typen eignen sich hervorragend für die Speicherung von ID-Werten (wofür meist der Typ int, gelegentlich auch bigint genutzt wird). Auch hier sollte natürlich die Größe und damit der abbildbare Wertebereich zum Inhalt des Feldes passen. So reicht zum Speichern eines Alters tinyint mit einem Wertebereich von 0–255 völlig aus. Wie bei den alphanumerischen Feldern könnte man auch hier – bei bereits existierenden Daten – die SQL-Aggregratfunktionen nutzen, um den benötigten Wertebereich (und damit auch Datentyp) zu ermitteln: SELECT MIN(Numfeld), AVG(Numfeld), MAX(Numfeld) FROM Tabelle
Die zweite Grundvariante sind die Kommazahlen mit fester Genauigkeit (z.B. decimal und numeric). Diese belegen zwar vergleichsweise viel Platz, werden aber dann benötigt, wenn exakte Nachkommazahlen erforderlich sind. Auch die Währungsdatentypen money und smallmoney gehören zu dieser Kategorie. Als Letztes gibt es noch die Gleitkommazahlen mit ungefährer Genauigkeit, die sich dadurch auszeichnen, dass sich mit relativ geringem Speicherbedarf extrem hohe Zahlenwerte darstellen lassen. Hier stehen float(n) und real zur Auswahl, wobei real mit einem Zahlenbereich von –3,4E+38 bis + 3,4E+38 für die meisten Fälle ausreichen dürfte.
3.2.4
Binäre Datentypen
Binäre Datentypen dienen zur Speicherung von Binärdaten, die meist ohne weiteres nicht lesbar sind. Da diese oft zum Speichern von Bildern verwendet wurden, hieß einer dieser Datentypen treffenderweise auch image, wurde aber inzwischen (analog zu den Textfeldern) durch den Typ varbinary(max) abgelöst. Dazu gibt es noch Varianten mit vorgegebener Maximallängenangabe sowie mit fester Feldlänge (varbinary(n) sowie binary(n)). Für die Performance interessant ist hier vor allem die neue Variante, die Daten durch Setzen der FileStreamOption außerhalb der eigentlichen Datenbank im Filestream abzulegen. Dadurch bleibt die Datenbank kompakter und auf die Filestream-Daten wird nur dann zugegriffen, wenn die entsprechenden Felder wirklich benötigt werden.
3.2.5
Sonstige Datentypen
Neben den verschiedenen Varianten von alphanumerischen, numerischen und binären Datentypen gibt es noch eine Reihe von spezielleren Datentypen. Ein Teil davon wird zur Speicherung von Datumsangaben verwendet, wobei sich die Varianten hierzu wiederum durch den Wertebereich, aber damit verbunden auch den Speicherbedarf unterscheiden. Seit SQL Server 2008 gibt es endlich auch getrennte Datentypen für Datum und Uhrzeit, sodass man – wenn man nur ein Datum speichern will – auch auf den entsprechenden Platz sparenden Datentyp zugreifen kann. Dazu gibt es noch weitere Datentypen zum Speichern von Global Unique Identifiers (uniqueidentifier), XML-Dokumenten (xml) und geografischen Informationen (geography und geometry).
78
2030-4.book Page 79 Monday, February 8, 2010 4:26 PM
Datentypen sinnvoll nutzen
Gerade bezüglich des Datentyps uniqueidentifier gibt es nahezu religiöse Auseinandersetzungen, ob für einen künstlich erzeugten Primärschlüssel einer Tabelle eher dieser Typ oder doch einfach integer sinnvoller ist. Hier treffen meist zwei verschiedene Blickwinkel aufeinander. Für den Anwendungsentwickler ist uniqueidentifier meist die einfachere Wahl, weil er damit neue Primärschlüsselwerte von der Anwendung aus vergeben und trotzdem sicher sein kann, dass diese eindeutig sind. Primärschlüssel vom Typ integer werden meist automatisch vom SQL Server vergeben (indem man hierfür die Identitätsspezifikation nutzt), sind aber insbesondere dadurch von den meisten „Datenbankern“ bevorzugt, weil sie mit vier Byte (gegenüber den 16 Byte, die ein uniqueidentifier intern zur Speicherung benötigt) viel Speicherplatz sparen. Da es sich hierbei um eine grundlegende Entscheidung handelt, wird dieses Thema weiter unten in Kapitel 3.2.7 Identitätsspezifikation oder Uniqueidentifier? noch einmal ausführlicher diskutiert. Generell gilt, dass der speziellste mögliche Datentyp meist der beste ist, da er normalerweise den geringsten Speicherbedarf hat und außerdem weniger Möglichkeiten bietet, ungültige Werte aufzunehmen. Für viele typische Inhalte aus verschiedenen Anwendungsgebieten gibt es auch bereits allgemein definierte und anerkannte Standards, sodass man sich in diesen Fällen sparen kann, umfangreiche Datenanalysen zu betreiben. So sind E-Mail-Adressen nach RFC 2821 beispielsweise auf maximal 320 Zeichen beschränkt (64 Byte für den Namen, ein Byte für das @-Zeichen und 255 Byte für die Domain). Auch für Web-URLs gibt es eine definierte Maximalgröße, die 2048 Zeichen beträgt und eher daher rührt, dass der Internet Explorer keine größeren URLs verarbeiten kann.
3.2.6
Spalten mit geringer Dichte
Seit SQL Server 2008 gibt es zusätzlich die Möglichkeit, einzelne Spalten als so genannte Sparse Columns zu definieren. Dies ist für Spalten sinnvoll, von denen nur ein geringer Teil der Zeilen Werte enthält, damit diese effektiver verwaltet werden können. Dadurch wird kein unnötiger Speicher von den Zeilen belegt, in denen die Spalte nicht genutzt wird, also den Wert NULL hat. Sparse Columns können im Tabellenentwurfsmodus des SQL Server Management Studios definiert werden, indem Sie die Eigenschaft Ist von geringer Dichte auf Ja setzen. Alternativ kann auch bei der CREATE TABLE-Anweisung hinter dem Datentyp der Zusatz SPARSE verwendet werden, wie im folgenden Beispiellisting zu sehen ist: CREATE TABLE [dbo].[Adresse]( [idAdresse] [int] IDENTITY(1,1) NOT NULL, [Strasse] [nvarchar](50) NULL, [Adresszusatz] [nvarchar](50) SPARSE NULL, [PLZ] [nchar](5) NULL, [Ort] [nvarchar](50) NULL, CONSTRAINT [PK_Adresse] PRIMARY KEY CLUSTERED (idAdresse ASC) ON [PRIMARY] ) ON [PRIMARY]
SQL Server Performance-Ratgeber
79
2030-4.book Page 80 Monday, February 8, 2010 4:26 PM
3 – Modellierung einer Datenbank
Allerdings können nicht alle Spalten als Sparse Columns definiert werden. Die wichtigsten Einschränkungen sind: 쮿
Spalten des Typs text, ntext, image, timestamp, geography und geometry können nicht als Sparse Columns definiert werden
쮿
für Sparse Columns kann keine Identitätsspezifikation definiert werden
쮿
Sparse Columns können nicht in eindeutigen Primärschlüsseln (und damit auch nicht von gruppierten Indizes) verwendet werden
쮿
für Sparse Columns kann kein Standardwert definiert werden, was allerdings auch keinen Sinn machen würde, da ja sonst alle Spalten einen Wert ungleich NULL hätten
3.2.7
Identitätsspezifikation oder Uniqueidentifier?
Eine Frage, die sich im Zusammenhang mit Datentypen oft stellt, ist die Wahl des richtigen Datentyps für einen technischen Primärschlüssel. Damit ist eine Spalte gemeint, die keinen fachlich relevanten Wert enthält, sondern stattdessen einen automatisch generierten Inhalt, der einzig und allein dazu dient, jede Zeile der Tabelle eindeutig zu identifizieren. Hierfür gibt es zwei gängige Lösungen, die beide ihre spezifischen Vor- und Nachteile haben, die auch für die Performance einer Datenbank relevant sind. Die wahrscheinlich gängigere Variante ist die Verwendung eines ganzzahligen Datentyps (meist int, bei großen Tabellen auch bigint) in Kombination mit einer Identitätsspezifikation. Durch die Identitätsspezifikation, die als Eigenschaft im Tabellenentwurfsmodus festgelegt werden kann, werden ein Startwert und eine Schrittweite definiert, mit denen das Feld für jede neu hinzugefügte Zeile automatisch einen entsprechenden Wert erhält. Wird die Tabelle per SQL-Anweisung erstellt, so kann man die Identitätsspezifikation auch durch den Zusatz IDENTITY(1,1) hinter dem Datentyp der Spalte angeben, wobei die erste Zahl der Startwert und die zweite Zahl die Schrittweite der so erzeugten Identitätsspalte ist. Durch die automatische Vergabe der Werte wird sichergestellt, dass diese eindeutig für die jeweilige Tabelle sind. Beim Datentyp uniqueidentifier dagegen handelt es sich um einen Wert, der intern 128 Bit (= 16 Byte) belegt und normalerweise in einem speziellen Format dargestellt wird (hexadezimal mit Bindestrich als Trennzeichen nach dem vierten, sechsten, achten und zehnten Byte). Beispiel: D15FDE70-2829-4A96-B3F4-DDE6FC322469 Um einen neuen Wert dieses Datentyps zu erzeugen, kann man die T-SQL-Systemfunktion newid() nutzen, die auf Basis der Identifizierungsnummer der Netzwerkkarte und eines CPU-internen Taktgeberwerts einen Wert erzeugt, der weltweit eindeutig ist. Daher wird der Datentyp oft auch als Globally Unique Identifier bezeichnet. Setzt man den Standardwert für die entsprechende Spalte auf newid(), so wird auch hier für eine automatische Vergabe der Werte für neue Zeilen gesorgt.
80
2030-4.book Page 81 Monday, February 8, 2010 4:26 PM
Zusammenfassung
Für die Entscheidung, welche von beiden Varianten die bessere für die jeweilige Anwendung ist, sind folgende Kriterien zu berücksichtigen: 쮿
Ein int-Wert (egal ob mit oder ohne Identitätsspezifikation) belegt lediglich vier Byte, ein uniqueidentifier belegt dagegen ganze 16 Byte. Insbesondere dann, wenn viele Fremdschlüssel auf diesen Primärschlüssel verweisen, macht sich der unterschiedliche Speicherbedarf beider Varianten schnell bemerkbar.
쮿
Werte, die durch eine Identitätsspezifikation erzeugt wurden, sind eindeutig für die Tabelle, Uniqueidentifier sind weltweit eindeutig. Daher bieten Letztgenannte die Möglichkeit, Daten gleicher Struktur aus verschiedenen Tabellen oder gar Datenbanken aneinanderzuhängen, ohne die Eindeutigkeit des technischen Primärschlüssels zu verlieren.
쮿
Insbesondere dann, wenn über den technischen Primärschlüssel oft explizit auf einzelne Zeilen einer Tabelle zugegriffen wird, ist das Handling von ganzzahligen Werten deutlich angenehmer als von Uniqueidentifiern.
쮿
Ein Wert aus einer Identitätsspezifikation steht erst dann fest, wenn der Datensatz geschrieben wurde. Ein Uniqueidentifier kann dagegen schon vorher festgelegt werden (sowohl in SQL als auch in .NET gibt es entsprechende Funktionen). Dies kann es beispielsweise bei komplexeren ETL-Workflows erleichtern, Daten in verschiedene Tabellen zu schreiben, wobei der technische Schlüssel in einer Tabelle als Primärschlüssel und in anderen (davon abhängigen) Tabellen als Fremdschlüssel verwendet wird.
Wie sich anhand dieser Kriterien vermuten lässt, kann man keine allgemein gültige Empfehlung für eine von beiden Varianten geben. Lediglich als Tendenz ist festzuhalten, dass – sobald mit verteilten Daten und/oder aufwändigen ETL-Prozessen gearbeitet wird – Uniqueidentifier eine lohnenswerte Alternative sein können, während ganzzahlige Datentypen mit Identitätsspezifikation kompakter und einfacher in der Handhabung sind.
3.3
Zusammenfassung
Ein wesentlicher Punkt der Datenmodellierung ist die Normalisierung. Dabei sind vor allem die ersten drei Normalformen wichtig, wobei auch diese nicht blindlings befolgt werden sollten, sondern man in Einzelfällen auch bewusst von ihnen abweichen kann, um eine bessere Gesamtperformance zu erhalten. Deutlich banaler ist – zumindest auf den ersten Blick – die Wahl der richtigen Datentypen für die verschiedenen Arten von zu speichernden Informationen. Dabei ist unter anderem auch eine Entscheidung zu treffen, ob alphanumerische Daten in einem ASCIIZeichensatz (ein Byte pro Zeichen) oder dem international flexibleren Unicode-Zeichensatz (zwei Byte pro Zeichen) zu speichern sind. Beide Varianten haben ihre Vor- und Nachteile, wesentlich ist aber, dass diese Entscheidung einheitlich für die gesamte Datenbank getroffen wird, um zu vermeiden, dass bei der Abfrage der Daten Typkonvertierungen nötig sind, die beispielsweise die Verwendung eines Indexes verhindern.
SQL Server Performance-Ratgeber
81
2030-4.book Page 82 Monday, February 8, 2010 4:26 PM
3 – Modellierung einer Datenbank
Insbesondere bei alphanumerischen Daten ist die Länge der Spalten von entscheidender Bedeutung, damit alle benötigten Inhalte gespeichert werden können, aber trotzdem möglichst wenig Speicher benötigt wird (und damit mehr Zeilen im Hauptspeicher gehalten werden können). Aber auch für numerische Daten kann die Wahl des richtigen Datentyps entscheidend sein, da diese sich einerseits – insbesondere bei nichtganzzahligen Werten – auf die Genauigkeit der gespeicherten Zahlen auswirkt, aber andererseits auch darauf, wie effektiv die Werte intern verarbeitet werden können. Bei Nutzung von SQL Server 2008 oder neuer kann man zusätzlich noch Gebrauch von Sparse Columns machen, um den Speicherbedarf der Datenzeilen weiter zu reduzieren. Für technische Primärschlüssel ist es außerdem eine Überlegung wert, statt eines ganzzahligen Datentyps mit Identitätsspezifikation eventuell einen Uniqueidentifier zu nutzen. Allerdings haben beide Varianten ihre Vor- und Nachteile, die man gut abwägen sollte.
82
2030-4.book Page 83 Monday, February 8, 2010 4:26 PM
4
Indizes
Indizes sind zusätzliche Datenstrukturen, die den gezielten Zugriff auf bestimmte Datensätze beschleunigen sollen. Ohne Indizes kann keine größere relationale Datenbank performant laufen. Die Definition der richtigen Indizes erfordert jedoch etwas Hintergrundwissen.
4.1
Grundlegende Funktionsweise von Indizes
Im einfachsten Fall kann man sich einen Index wie ein Stichwortverzeichnis in einem Buch vorstellen. Wenn man nun Informationen zu einem bestimmten Stichwort sucht, müsste man ohne das Stichwortverzeichnis das gesamte Buch lesen. Bei Datenbanktabellen entspricht dies einem so genannten Table Scan. Mit einem Stichwortverzeichnis kann man – davon ausgehend, dass das Stichwortverzeichnis alle Begriffe umfasst (was bei einem Index der Fall ist) – dieses nach dem Begriff durchsuchen (man nennt dies dann Index Scan) und dann gezielt auf die passenden Seiten des Buches zugreifen (entspricht einem Row Lookup). Da das Stichwortverzeichnis sortiert ist, muss man nicht einmal das ganze Verzeichnis lesen, sondern kann darin gezielt nach den gewünschten Einträgen suchen, indem man in der Mitte des Stichwortverzeichnisses nachschaut und dann die Hälfte davor bzw. danach weiteruntersucht, in welcher das gesuchte Wort stehen muss. Mit dieser Hälfte verfährt man dann genauso wie im Schritt davor mit dem gesamten Verzeichnis (man nennt dies binäre Suche, da sich die zu durchsuchende Menge mit jedem Schritt halbiert). Somit sind insgesamt nur deutlich weniger Indexeinträge zu lesen, um den gesuchten Begriff zu finden. Bei einem Datenbankindex nennt man diesen Vorgang Index Seek, da der Index nicht komplett gelesen (gescannt), sondern stattdessen gezielt durchsucht wird.
Nachname
Vorname
Ort
Bauer
Schulz
Alfred
Berlin
Meier
Meier
Karlheinz
Köln
Müller
Schmidt
Stefanie
Hamburg
Nagel
Bauer
Markus
Frankfurt
Schmidt
Schultze
Sabine
München
Schmidt
Schmidt
Klaus
Leipzig
Schultze
Nagel
Maria
Wiesbaden
Schulz
Müller
Hans
Bonn
Abbildung 4.1: Vereinfachte Darstellung eines Indexes SQL Server Performance-Ratgeber
83
2030-4.book Page 84 Monday, February 8, 2010 4:26 PM
4 – Indizes
Auch wenn dies in Büchern eher nicht der Fall sein wird, könnte man auch hier mehrere Indizes verwenden, die verschiedene Informationstypen speichern. So kann einer davon das eigentliche Stichwortverzeichnis sein und ein weiterer beispielsweise ein Abbildungsverzeichnis oder ein Namensverzeichnis. Wenn nun nach einer bestimmten Person gesucht wird, kann dafür direkt das Namensverzeichnis angesprochen werden, was sicherlich deutlich kompakter und damit effektiver sein wird als das gesamte Stichwortverzeichnis. Es gibt aber noch eine Sonderform von Indizes, nämlich die gruppierten (oder engl. clustered) Indizes. Bei diesen wird kein zusätzliches Verzeichnis benötigt, sondern die Inhalte selbst sind in der richtigen Reihenfolge angeordnet. Dadurch ergibt sich allerdings auch, dass es pro Tabelle nur einen gruppierten Index geben kann. Die Entsprechung aus dem Verlagswesen ist ein Lexikon, in dem man ja auch mit relativ wenig Blättern die Informationen zu einem Begriff finden kann, ohne das ganze Buch lesen zu müssen (Clustered Index Scan). Stattdessen würde man ganz intuitiv irgendwo in der Mitte des Buches aufschlagen, um nachzuschauen, ob der gesuchte Begriff davor oder dahinter liegt, und dann so fortfahren, wie bereits bei der binären Suche beschrieben.
4.2
Realisierung von Indizes bei SQL Server
Bei SQL Server sind Indizes technisch etwas komplexer realisiert als es in der letzten Abbildung zu sehen war. Um dies korrekt darstellen zu können, muss man allerdings etwas weiter ausholen. Wie bereits in vorangegangenen Kapiteln erwähnt, speichert SQL Server seine Daten (das gilt in diesem Fall sowohl für Zeilendaten als auch für Indexdaten) in Dateien. Diese Dateien sind in 8 KB große Speicherseiten unterteilt, in denen die eigentlichen Daten zu finden sind. Im Falle einer Tabelle ohne gruppierten Index sind die Daten unsortiert in den Speicherseiten zu finden. Man nennt diese Anordnung daher auch Heap (= Haufen). Bei einem nichtgruppierten Index sind die Indexeinträge in Form eines B-Baums gespeichert. Somit kommt man relativ schnell zu dem gesuchten Eintrag. Erst in der Blattebene des Baums ist dann ein Zeiger auf Dateinummer, Seitennummer und Zeilennummer des Heaps gespeichert, sodass hierüber direkt auf die richtige Datenseite im Heap zugegriffen werden kann (Abbildung 4.2). Bei einem gruppierten Index dagegen befinden sich die eigentlichen Daten direkt auf der Blattebene des B-Baums. Nichtgruppierte Indizes auf Tabellen mit gruppiertem Index verweisen dann auch nicht direkt auf die Speicherseite im Heap, sondern auf den gruppierten Index für die entsprechende Datenzeile. Dieses Verfahren bringt sowohl Vor- als auch Nachteile mit sich. Nachteilig ist, dass alle nichtgruppierten Indizes auf eine Tabelle neu angelegt werden müssen, wenn auf dieser Tabelle ein gruppierter Index neu angelegt oder gelöscht wird. Von Vorteil ist dafür, dass ein Reorganisieren des gruppierten Indexes, kein Reorganisieren der nichtgruppierten Indizes nach sich zieht, da diese ja nicht direkt auf die Speicherseiten verweisen.
84
2030-4.book Page 85 Monday, February 8, 2010 4:26 PM
Realisierung von Indizes bei SQL Server
Bauer Schmidt Bauer Müller Bauer (4:834:04) Meier (4:834:02)
Schmidt Schultze Müller (4:835:04) Nagel (4:835:03)
Schmidt (4:835:02) Schmidt (4:834:03)
Schultze (4:835:01) Schulz (4:834:01)
Datei 4 Seite 834
Seite 835
Nachname Vorname 01 Schulz
Alfred
02 Meier
Ort Berlin
Nachname Vorname Ort 01 Schultze
Sabine
München
Karlheinz Köln
02 Schmidt
Klaus
Leipzig
03 Schmidt
Stefanie
Hamburg
03 Nagel
Maria
Wiesbaden
04 Bauer
Markus
Frankfurt
04 Müller
Hans
Bonn
Abbildung 4.2: Korrekte Darstellung eines Indexes (als B-Baum)
Sowohl bei gruppierten als auch bei nichtgruppierten Indizes befinden sich die Indexdaten ebenfalls auf 8 KB großen Speicherseiten. Kommt nun ein neuer Indexeintrag hinzu, so wird dieser auf der Speicherseite eingefügt, sofern der Platz noch ausreicht. Ist nicht mehr genügend Platz vorhanden, so wird die Seite geteilt (Page Split), sodass daraus zwei maximal halb volle Speicherseiten entstehen und der neue Indexeintrag problemlos eingefügt werden kann. Für die zusätzliche Speicherseite muss im Indexbaum auf der Ebene darüber natürlich ebenfalls ein neuer Eintrag erzeugt werden, sodass auch hier die Gefahr besteht, dass ein Page Split durchgeführt werden muss. Das funktioniert zwar so weit recht gut, hat aber drei entscheidende Nachteile: 쮿
Der Page Split selbst kostet Zeit.
쮿
Durch Page Splits wird die B-Baum-Struktur immer unausgeglichener. Der Index wird zunehmend fragmentiert.
쮿
Da die zusätzliche Indexseite an einer freien Stelle eingefügt wird, liegen die Indexseiten nicht mehr physikalisch in der richtigen Reihenfolge vor. Wenn in einer Abfrage ein größerer Bereich des Indexes sortiert gelesen werden muss, kann dies nicht mehr kontinuierlich an einem Stück geschehen, sondern es werden zusätzliche Neu-Positionierungen der Festplattenköpfe nötig, was wiederum Zeit kostet.
Um das Problem der Indexfragmentierung zu lösen, sollte man alle Indizes regelmäßig reorganisieren oder besser noch neu erzeugen (wie in Kapitel 2.2.2 – Indizes aktuell halten beschrieben). Um das Risiko von Page Splits zu minimieren, kann man auf Indexseiten etwas Platz für neue Einträge frei halten, indem man beim Erstellen des Indexes einen so genannten Füllfaktor angibt. Ein Füllfaktor von 80% gibt beispielsweise an, dass beim Erstellen des Indexes die Speicherseiten nur zu 80% gefüllt werden, wodurch 20% für zusätzliche Indexeinträge frei bleiben.
SQL Server Performance-Ratgeber
85
2030-4.book Page 86 Monday, February 8, 2010 4:26 PM
4 – Indizes
Für das Erstellen von Indizes wird die folgende Anweisung verwendet: CREATE INDEX indexname ON tabellenname (feldliste) WITH (FILLFACTOR = fuellfaktor)
Dieser Füllfaktor bezieht sich allerdings erst einmal nur auf die Blattebene des Indexbaums. Sollen auch die anderen Seiten des Indexbaums denselben Füllfaktor erhalten, so ist zusätzlich noch die Option PAD_INDEX = ON anzugeben: CREATE INDEX indexname ON tabellenname (feldliste) WITH (FILLFACTOR = fuellfaktor, PAD_INDEX = ON)
Mit der Zeit werden die Indexseiten natürlich stärker gefüllt, sodass das Risiko einer Notwendigkeit von Page Splits wieder steigt. Allerdings wird beim Neuaufbauen eines Indexes über die Option REBUILD auch der Füllfaktor wiederhergestellt, was ein weiterer Beleg dafür ist, dass man diese Wartungsaktion regelmäßig durchführen sollte.
4.3
Besondere Indexarten
4.3.1
Kombinierte Indizes
Wie die Syntaxdarstellung weiter oben bereits andeutet, kann man Indizes nicht nur für einzelne Felder, sondern auch für Feldkombinationen erstellen. Das zuvor dargestellte Beispiel ließe sich also leicht erweitern, indem man den Index nicht nur für den Nachnamen, sondern für die Kombination aus Nachnamen und Vornamen erstellt. Dadurch kann auch bei vielen Einträgen mit demselben Nachnamen direkt der Datensatz mit der richtigen Kombination aus Nach- und Vornamen gefunden werden. Ein Beispiel: CREATE INDEX IX_Name ON Personen (Nachname, Vorname) WITH (FILLFACTOR = 80)
Hierbei ist auch die korrekte Reihenfolge der Felder im Index von Bedeutung. Denn wenn statt des vollen Namens nur nach einem bestimmten Nachnamen gesucht wird, kann der passende Eintrag trotzdem durch einen Index Seek gefunden werden, da ja auch der kombinierte Index primär nach dem Nachnamen sortiert ist. Wäre der Index jedoch für die Kombination aus Vor- und Nachnamen angelegt, so müsste stattdessen der komplette Index gelesen werden (Index Scan), was deutlich mehr Aufwand bedeutet. Für die Wahl der richtigen Reihenfolge sollten also die Spalten zuerst angegeben werden, nach denen am häufigsten gesucht bzw. gefiltert wird. Werden bei einem kombinierten Index immer alle Spalten bei der Suche angegeben, so sollte die selektivste Spalte (also die mit den meisten unterschiedlichen Werten) vorne stehen, damit das Suchergebnis möglichst schnell möglichst stark eingeschränkt werden kann. Erstellen Sie also einen Index für die Spalten PLZ und Ort einer Adresstabelle, so ist es – wenn von der Anwendung dafür gesorgt ist, dass immer beide Suchkriterien angegeben werden – effektiver, wenn der Index auf die Kombination PLZ, Ort definiert wird, als andersherum, weil es deutlich mehr unterschiedliche Postleitzahlen gibt als Ortsnamen. 86
2030-4.book Page 87 Monday, February 8, 2010 4:26 PM
Besondere Indexarten
4.3.2
Abdeckende Indizes
Unabhängig davon, ob ein Index Seek oder ein Index Scan die passenden Einträge im Index findet, ist normalerweise für jeden Treffer noch ein Row Lookup nötig, um die eigentlichen Zeilendaten zu diesem Index zu lesen. Diese Operation kann eingespart werden, wenn nur Felder selektiert werden, die Bestandteil des Indexes sind. Wenn man also bei einem existierenden Index auf die Kombination aus Nach- und Vornamen lediglich die Vornamen sucht, die zu einem bestimmten Nachnamen gespeichert sind, muss auf die eigentlichen Zeilendaten nicht zugegriffen werden, da die Vornamen ja ebenfalls aus dem Index gelesen werden können. In diesem Fall handelt es sich um einen (für die Abfrage) abdeckenden Index, da in diesem alle Spalten enthalten sind, die in der Abfrage benötigt werden. Man kann die Chance für abdeckende Indizes auch erhöhen, indem man Indizes um so genannte Include-Spalten erweitert. Dabei handelt es sich um Spalten der Tabelle, die zwar ebenfalls im Index mit gespeichert werden, aber nach denen nicht sortiert wird. Ein Beispiel: CREATE INDEX IX_Name ON Personen (Nachname, Vorname) INCLUDE (Telefon)
Wird nun anhand des Nachnamens nach den dazugehörigen Vornamen und Telefonnummern gesucht, ist lediglich ein Zugriff auf den Index nötig, da alle abgefragten Informationen darin enthalten sind.
4.3.3
Gefilterte Indizes
Neu mit SQL Server 2008 hinzugekommen ist die Möglichkeit gefilterte Indizes zu definieren. Dabei handelt es sich um Indizes, die nicht für alle Zeilen einer Tabelle erstellt werden, sondern nur für diejenigen, die eine vorgegebene WHERE-Klausel erfüllen. Das macht beispielsweise dann Sinn, wenn ein Feld in 90% der Fälle denselben Inhalt hat und der Index nur für die anderen 10% gespeichert werden soll. Für die 90% würde er ohnehin nicht genutzt, da SQL Server automatisch erkennt, dass in diesem Fall ein Lesen der kompletten Tabelle (mit anschließendem Herausfiltern der nicht gewünschten 10%) effektiver ist. Für eine Personentabelle, die Mitglieder in einem Boxclub speichert, ließe sich folgender Index verwenden, um schneller gezielt die wenigen Frauen abfragen zu können, die dem Club angehören: CREATE INDEX IX_Frauen ON Personen (Geschlecht, Nachname, Vorname) WHERE Geschlecht = 'F'
Da ein Großteil der Mitglieder Männer ist, benötigt der Index nur wenig Speicher. Wenn man nun gezielt eine Liste der Männer abfragt, wird vermutlich ein Table Scan ausgeführt, also die gesamte Tabelle gelesen. Wird aber eine Liste der Frauen abgefragt, so kann der Index genutzt werden, um schnell die relevanten Datensätze herauszusuchen.
SQL Server Performance-Ratgeber
87
2030-4.book Page 88 Monday, February 8, 2010 4:26 PM
4 – Indizes
4.3.4 Indizierte Sichten Indizes lassen sich nicht nur auf Tabellen erstellen, sondern auch auf Sichten. Das hat zur Folge, dass die in der Sicht enthaltenen Daten redundant gespeichert werden, daher wird dieses Konzept manchmal auch als materialisierte Sicht bezeichnet (bei Oracle beispielsweise wird vorrangig diese Bezeichnung verwendet). Damit Indizes auf eine Sicht erstellt werden können, muss diese mit der Option WITH SCHEMABINDING erstellt sein, da jede Strukturänderung an einer der beteiligten Tabellen auch den Index ungültig machen würde. Außerdem muss ein Index auf eine Sicht immer als eindeutiger gruppierter Index erstellt werden. Als konkretes Beispiel kann man eine Sicht für die AdventureWorks2008-Datenbank erstellen: CREATE VIEW Person.vwPersonPhone WITH SCHEMABINDING AS SELECT pers.FirstName, pers.LastName, phone.PhoneNumber, ptype.Name AS PhoneType FROM Person.Person AS pers INNER JOIN Person.PersonPhone AS phone ON pers.BusinessEntityID=phone.BusinessEntityID INNER JOIN Person.PhoneNumberType AS ptype ON phone.PhoneNumberTypeID=ptype.PhoneNumberTypeID
Wenn Sie anschließend eine Abfrage auf diese Sicht erstellen und sich den Ausführungsplan1 dazu anzeigen lassen, ist eine Kombination aus Index Scans auf verschiedene Indizes (gruppierte und nichtgruppierte) der beteiligten Tabellen zu sehen. Mit der folgenden Anweisung erstellen wir nun einen gruppierten Index auf diese Sicht: CREATE UNIQUE CLUSTERED INDEX ixPersonPhone ON Person.vwPersonPhone (PhoneType, LastName, FirstName, PhoneNumber)
Wenn man den Ausführungsplan der Abfrage betrachtet, ist zu sehen, dass nicht mehr die Tabelle selbst, sondern nur noch der Index angesprochen wird, was zu einer deutlich performanteren Ausführung der Abfrage führt. Interessanter Nebeneffekt dabei ist, dass nun selbst die Abfrage, die zur Erstellung des Indexes genutzt wurde, von der indizierten Sicht profitiert, selbst wenn die Sicht nicht explizit angesprochen wird.
1.
88
Das Arbeiten mit Ausführungsplänen wird im Detail in Kapitel 6.1.2 behandelt.
2030-4.book Page 89 Monday, February 8, 2010 4:26 PM
Besondere Indexarten
Abbildung 4.3: Der Ausführungsplan für die Sicht ohne Index
Abbildung 4.4: Der Ausführungsplan für die indizierte Sicht
SQL Server Performance-Ratgeber
89
2030-4.book Page 90 Monday, February 8, 2010 4:26 PM
4 – Indizes
Dennoch sollte man indizierte Sichten nur gezielt einsetzen, denn einerseits unterwirft man sich durch die Notwendigkeit der Option WITH SCHEMABINDING gewissen Einschränkungen und andererseits erfordert jede Datenänderung an einer der beteiligten Tabellen einen Neuaufbau der indizierten Sicht.
4.4
Index oder nicht?
So hilfreich ein Index auch sein mag, sollte man natürlich nicht für jedes Feld einen oder mehrere Indizes erzeugen. Denn letzten Endes erzeugt auch ein Index redundante Daten (wenn auch nur für einen Teil der Felder), sodass auch hier der Grundsatz gilt: »Redundante Datenhaltung erhöht die Lesegeschwindigkeit auf Kosten der Schreibgeschwindigkeit!« Denn so wie Lesevorgänge durch die Verwendung von Indizes meist erheblich beschleunigt werden können, werden Schreibvorgänge verlangsamt, da neben den eigentlichen Daten ja auch noch die Indizes mit aktualisiert werden müssen. Dies ist insbesondere bei gruppierten Indizes aufwändig, aber auch bei nichtgruppierten Indizes nicht zu unterschätzen. Dazu kommt der Aufwand, der für eine regelmäßige Reorganisation der Indizes anfällt. Unabhängig von den direkten Auswirkungen auf die Performance belegen natürlich auch Indizes Speicherplatz und dies sowohl in der Datenbank selbst als auch bei allen Sicherungen der Datenbank. Es ist also zu entscheiden, für welche Felder und Feldkombinationen man einen Index erstellen sollte und für welche nicht. Hierbei sind folgende Kriterien zu berücksichtigen: 쮿
Felder, nach denen oft gesucht wird, bieten sich für Indizes an
쮿
Felder, die als Fremdschlüsselspalten verwendet werden, sollten indiziert werden, da auf diese Felder bei jedem JOIN gefiltert wird
쮿
Felder und Tabellen, die selten geändert werden, stärker indizieren
쮿
Felder mit geringer Selektivität eignen sich weniger für Indizes
쮿
Wenn es für eine Spalte nur wenige verschiedene Werte gibt (typisches Beispiel wäre hier ein Kennzeichen für das Geschlecht, das nur die Werte m und w zulässt), kann das Suchergebnis durch einen entsprechenden Index nicht maßgeblich verkleinert werden. Ein entsprechender Index würde hier in der Regel gar nicht benutzt, da ein komplettes Lesen der gesamten Tabelle mit anschließendem Filtern effektiver ist. Daher macht es normalerweise auch wenig Sinn, diese Spalte zu indizieren (es sei denn als Bestandteil eines kombinierten Indexes).
쮿
Tabellen, die schnell geschrieben, aber nur selten gelesen werden müssen (z.B. Webserver-Logs), nur minimal indizieren
Neben diesen allgemeinen Erwägungen, die insbesondere dann eine Rolle spielen, wenn eine Datenbank oder auch nur einzelne Tabellen neu erstellt werden, bietet SQL Server auch Unterstützung, wenn es darum geht, im laufenden Betrieb zu entscheiden, welche Indizes eventuell noch fehlen und welche überflüssig sind.
90
2030-4.book Page 91 Monday, February 8, 2010 4:26 PM
Index oder nicht?
Hierfür gibt es derzeit drei Möglichkeiten: 쮿
Sie verwenden den Datenbankoptimierungsratgeber, der aufgrund eines SQL-Skripts oder eines Profiler Trace-Files Vorschläge gibt, welche Indizes sinnvollerweise noch zu erstellen sind
쮿
Sie nutzen die dynamischen Management-Sichten sowie dazugehörende Systemfunktionen, um Vorschläge über fehlende Indizes zu bekommen
쮿
Sie folgen den Hinweisen, die der Abfrageeditor im SQL Server Management Studio gibt, wenn man den Ausführungsplan von Abfragen anzeigen lässt
Beginnen wir zuerst mit der letztgenannten Variante:
4.4.1
Indexhinweise bei Anzeige des Ausführungsplans
Im Abfrageeditor des SQL Server Management Studios lassen sich Abfragen nicht nur ausführen, sondern alternativ auch deren Ausführungspläne anzeigen. Während auf die Ausführungspläne in Kapitel 7 – Abfrageoptimierung näher eingegangen wird, ist an dieser Stelle zumindest von Bedeutung, dass bei Erstellung eines Ausführungsplans geprüft wird, welche Indizes sinnvollerweise verwendet werden können. Fällt auf, dass ein passender Index fehlt, wird sofort eine entsprechende Empfehlung angezeigt. Wenn Sie sich beispielsweise mit der AdventureWorks2008-Datenbank verbinden und anschließend für folgende Abfrage einen Ausführungsplan erstellen lassen … SELECT * FROM Sales.SalesOrderDetail WHERE UnitPrice=39.95
… erhalten Sie neben dem eigentlichen Ausführungsplan den Hinweis, dass ein Index für die Spalte UnitPrice der Tabelle Sales.SalesOrderDetail fehlt, der die Ausführung der Abfrage um ca. 99% beschleunigen würde. Wenn Sie mit der rechten Maustaste auf den Hinweis klicken und im Kontextmenü Fehlende Indexdetails auswählen, wird automatisch die Anweisung zum Erstellen des vermeintlich fehlenden Indexes erstellt und in einem separaten Abfragefenster angezeigt. Allerdings sollten Sie diesen Hinweisen nicht generell folgen, denn diese Empfehlung bezieht sich nur auf diese eine Abfrage. Eventuell profitiert die komplette Datenbankanwendung überhaupt nicht von dem Index, sondern lediglich die Abfrage, die ad hoc ausgeführt werden sollte. Genauso wenig werden auf diesem Weg Indizes empfohlen, die vielleicht viel wichtiger wären, aber von denen eben andere Abfragen profitieren, die Sie nicht explizit geprüft haben. Daher sollte man diese Empfehlung immer kritisch hinterfragen, bevor man das generierte Statement zur Indexerstellung ausführt.
SQL Server Performance-Ratgeber
91
2030-4.book Page 92 Monday, February 8, 2010 4:26 PM
4 – Indizes
4.4.2 Dynamische Management-Sichten zur Erkennung fehlender Indizes Die dynamischen Management-Sichten (DMV = Dynamic Management Views) bieten Zugriff auf SQL-Server-interne Statistiken und Daten. Ein Teil davon ist mittlerweile eigentlich keine Sicht mehr, sondern als Funktion (meist in Form von Tabellenwertfunktionen) realisiert, damit auch Eingabeparameter übergeben werden können. Bereits in Kapitel 2 haben Sie mit sys.dm_db_index_physical_stats eine solche Sicht (in diesem Fall Funktion) kennen gelernt, die beispielsweise Informationen zur Fragmentierung von Indizes preisgibt. Für das Finden von fehlenden Indizes werden drei andere Sichten bemüht: 쮿
sys.dm_db_missing_index_groups – enthält Verweise auf fehlende Indexgruppen und Indizes
쮿
sys.dm_db_missing_index_group_stats – führt in etwa dieselben Statistiken mit wie sys.dm_db_index_physical_stats
쮿
sys.dm_db_missing_index_details – listet die Spalten (sowohl zu indizierende als auch zu inkludierte) der fehlenden Indizes auf
Wenn man diese drei Sichten (in diesem Fall sind es sogar echte Sichten) geschickt kombiniert einsetzt, kann man über eine Abfrage Empfehlungen über noch fehlende Indizes für die aktuelle Datenbank erhalten: SELECT DB_NAME(migd.database_id) as Datenbank, migd.* FROM sys.dm_db_missing_index_groups AS mig INNER JOIN sys.dm_db_missing_index_group_stats AS migs ON mig.index_group_handle = migs.group_handle INNER JOIN sys.dm_db_missing_index_details AS migd ON mig.index_handle = migd.index_handle WHERE migd.database_id = DB_ID() -- aktuelle Datenbank
Hierbei sollte man allerdings berücksichtigen, dass die verwendeten Statistiken nach jedem Server-Neustart geleert werden. Wenn der Rechner frisch gestartet wurde, werden die Empfehlungen also vorerst noch etwas spärlich ausfallen. Außerdem sollte nicht jeder vorgeschlagene Index blindlings erstellt werden, denn oft lassen sich ähnliche Indizes zusammenfassen. Dies ist insbesondere dann der Fall, wenn eine Anwendung aufgrund einer Formularauswahl dynamische Abfragen (in diesem Fall mit unterschiedlichen Suchkriterien) erstellt. Die Indexanalyse mit der oben gezeigten Abfrage schlägt dann unter Umständen jeweils einen Index für jede verwendete Feldkombination vor, auch wenn diese vielleicht nur selten benötigt werden. Wenn man stattdessen diese Indizes derart zusammenfasst, dass vor allem die Felder darin auftauchen, nach denen meistens gesucht wird, wird annähernd derselbe Performancegewinn bei künftigen Lesezugriffen erreicht, dabei aber der Verwaltungs- und AktualisierungsOverhead für viele ähnliche Indizes vermieden.
92
2030-4.book Page 93 Monday, February 8, 2010 4:26 PM
Index oder nicht?
i
In Kombination mit SQL Server 2005 kann man anstelle der dynamischen Management-Sichten auch das SQL Server 2005 Performance Dashboard nutzen. Diese kostenfrei per Download erhältlichen Reports bieten einen komfortablen Überblick über die Daten aus den dynamischen Management-Sichten. Darin enthalten ist unter anderem ein Missing Index Report, der die Empfehlungen für zu erstellende Indizes übersichtlich darstellt. Eine ausführlichere Beschreibung des Performance Dashboards ist in Anhang A.2 – Kostenfreie Messwerkzeuge und Optimierungstools zu finden.
4.4.3 Dynamische Management-Sichten zur Erkennung überflüssiger Indizes Auf einem ähnlichen Weg kann man auch überflüssige Indizes herausfinden, die das System bei Aktualisierungen belasten, bei Lesezugriffen aber keinen deutlichen Performancegewinn bringen, da sie nur selten oder sogar nie verwendet werden. Die folgende Abfrage nutzt die Systemtabelle sys.indexes und die dynamische Management-Sicht sys.dm_db_index_usage_stats, um länger nicht mehr verwendete Indizes zu identifizieren: SELECT ix.name AS IndexName, ix.type_desc AS IndexType, user_seeks, user_scans, user_lookups, last_user_seek, last_user_scan, last_user_lookup FROM sys.indexes AS ix LEFT JOIN sys.dm_db_index_usage_stats AS ius ON ix.index_id=ius.index_id AND ix.object_id=ius.object_id WHERE OBJECTPROPERTY(ix.object_id, 'IsUserTable') = 1 AND (ISNULL(user_lookups,0)=0 OR ISNULL(user_seeks,0)=0 OR ISNULL(user_scans,0)=0)
Anmerkung: Eine explizite Einschränkung auf die aktuelle Datenbank ist hier nicht erforderlich, da die Systemtabelle sys.indexes ohnehin pro Datenbank existiert und damit in der oben gezeigten Abfrage auch nur Indizes für die aktuelle Datenbank berücksichtigt werden. Die Abfrage zeigt alle Indizes, zu denen seit dem letzten Start des SQL-Server-Dienstes entweder kein Index Lookup, Index Seek oder Index Scan stattgefunden hat. Daher zum besseren Verständnis der relevanten Felder hier eine kurze Auflistung der Bedeutungen: 쮿
user_lookup / last_user_lookup – Anzahl bzw. Zeitpunkt des letzten Lookups in einem gruppierten Index (über einen nichtgruppierten Index)
쮿
user_scan / last_user_scan – Anzahl bzw. Zeitpunkt des letzten Index Scans
쮿
user_seek / last_user_seek – Anzahl bzw. Zeitpunkt des letzten Index Seeks
SQL Server Performance-Ratgeber
93
2030-4.book Page 94 Monday, February 8, 2010 4:26 PM
4 – Indizes
Die Abfrage lässt sich auch leicht anpassen, indem man Datum und Zeit des letzten Index Seeks, Scans und Lookups mit berücksichtigt. Die folgende Variante zeigt alle Indizes, die noch nicht in diesem Kalenderjahr verwendet wurden: SELECT * FROM (SELECT ix.name AS IndexName, ix.type_desc AS IndexType, ISNULL(user_seeks,0) AS IndexSeeks, ISNULL(user_scans,0) AS IndexScans, ISNULL(user_lookups,0) AS IndexLookups, ISNULL(last_user_seek, '19000101') AS LastIndexSeek, ISNULL(last_user_scan, '19000101') AS LastIndexScan, ISNULL(last_user_lookup, '19000101') AS LastIndexLookup FROM sys.indexes AS ix LEFT JOIN sys.dm_db_index_usage_stats AS ius ON ix.index_id=ius.index_id AND ix.object_id=ius.object_id WHERE OBJECTPROPERTY(ix.object_id, 'IsUserTable') = 1) AS x WHERE YEAR(LastIndexSeek)
Man kann auch noch einen Schritt weitergehen und das Verhältnis von Indexverwendung zu Indexaktualisierung vergleichen. Das folgende Skript zeigt alle Indizes einer Datenbank zusammen mit der Anzahl der Seek-, Scan- und Lookup-Operationen sowie der Aktualisierungen seit dem letzten Start des SQL-Server-Dienstes an: SELECT sch.name as SchemaName, tab.name as TableName, ix.name as IndexName, ix.type_desc as IndexType, ius.user_seeks, ius.user_scans, ius.user_lookups, ius.user_updates FROM sys.indexes AS ix INNER JOIN sys.tables AS tab ON ix.object_id = tab.object_id INNER JOIN sys.schemas AS sch ON tab.schema_id = sch.schema_id LEFT JOIN sys.dm_db_index_usage_stats AS ius ON ix.index_id=ius.index_id AND ix.object_id=ius.object_id WHERE OBJECTPROPERTY(ix.object_id, 'IsUserTable') = 1 AND ix.type_desc<>'HEAP' ORDER BY user_updates DESC
Sind für einen Index besonders viele Updates erfolgt (daher auch die absteigende Sortierung nach diesem Feld), der Index wurde aber nur wenig oder gar nicht für Leseoperationen (Seek, Scan oder Lookup) genutzt, so weist dies darauf hin, dass es sich eventuell um einen überflüssigen Index handelt. Bevor man nun aber vorschnell zur Tat schreitet und den vermeintlich überflüssigen Index löscht, sollte man sich bewusst sein, dass bei den oben gezeigten Abfragen lediglich die Indexverwendung seit dem letzten Neustart des SQL-Server-Dienstes berück-
94
2030-4.book Page 95 Monday, February 8, 2010 4:26 PM
Index oder nicht?
sichtigt wird. Die Auswertung basiert also nur auf Daten für einen bestimmten Zeitausschnitt. Es kann also gut sein, dass der Index lediglich selten verwendet wird, dann aber von entscheidender Bedeutung für die Performance ist, weil ansonsten per Table Scan auf eine große Tabelle zugegriffen wird. Es empfiehlt sich also, die Indexverwendung über einen längeren Zeitraum zu beobachten und dann vermeintlich überflüssige Indizes nicht gleich zu löschen, sondern erst einmal zu deaktivieren. Damit bleibt die Indexdefinition gespeichert und Sie können den Index durch einen REBUILD jederzeit wieder aktivieren, wenn Sie feststellen, dass er doch noch benötigt wird. Um den Index AK_Product_Name der Tabelle Production.Product zu deaktivieren, verwenden Sie beispielsweise die folgende Anweisung: ALTER INDEX [AK_Product_Name] ON [Production].[Product] DISABLE
Mit der nächsten Anweisung können Sie den Index wiederherstellen. Der REBUILD ist notwendig, da der Index ja seit dem Deaktivieren nicht mehr aktualisiert wurde. ALTER INDEX [AK_Product_Name] ON [Production].[Product] REBUILD
Alternativ hierzu können Sie auch das Eigenschaften-Fenster des Indexes aufrufen und hier auf der Seite Optionen das Häkchen Index verwenden entfernen oder wieder setzen.
4.4.4 Indexempfehlungen des Datenbankoptimierungsratgebers Auf diesen dritten Weg möchte ich an dieser Stelle nur kurz eingehen, da das Thema noch in Kapitel 10 – Schwachstellen identifizieren durch Performancetests detailliert behandelt wird. Zumindest sei hier darauf hingewiesen, dass auch die vom Datenbankoptimierungsratgeber erstellten Indexempfehlungen nur auf dem SQL-Skript oder Profiler-Trace basieren, den man als Input verwendet hat. Ist dieser Input nicht repräsentativ für die gesamte Datenbankanwendung, so können auch falsche Indizes empfohlen oder wichtige Indizes als unnötig dargestellt werden. Unabhängig davon, ob die Empfehlung vom Datenbankoptimierungsratgeber oder von den weiter oben behandelten dynamischen Management-Sichten stammt, sollten Sie sich die Zeit nehmen, zumindest zwei Dinge zu überprüfen, bevor Sie einen Index aufgrund von seltener Verwendung löschen: 1. Ist der Index stark fragmentiert? Stark fragmentierte Indizes werden vom Abfrageoptimierer ignoriert. Weist der fragliche Index eine Fragmentierung von über 40% auf, so kann es gut sein, dass dieser öfter verwendet werden würde, wenn der Fragmentierungsgrad geringer wäre. In diesem Fall sollten Sie gemeinsam mit den Datenbankadministratoren klären, wie eine regelmäßige Reorganisation bzw. Neuaufbau der fragmentierten Indizes eingeführt werden kann. (Dieses Thema wurde bereits in Kapitel 2.2.2 – Indizes aktuell halten näher behandelt.)
SQL Server Performance-Ratgeber
95
2030-4.book Page 96 Monday, February 8, 2010 4:26 PM
4 – Indizes
2. Wird der Index vielleicht nur selten benötigt? Es kann gut sein, dass ein vermeintlich überflüssiger Index zwar selten benötigt wird, dann aber von entscheidender Bedeutung dafür ist, dass eine Abfrage nicht große Teile der Datenbank für längere Zeit blockiert. Beziehen Sie daher – wenn möglich – die Fachseite oder zumindest die Anwendungsentwicklung in die Prüfung, ob ein Index wirklich benötigt wird, mit ein. Insbesondere die Fachseite hat oft eine bessere Kenntnis davon, über welche Felder beispielsweise in einer dynamischen Suchmaske oft gesucht wird. Ähnlich verhält es sich mit Empfehlungen für neu zu erstellende Indizes. Auch hier sollte man nicht bedenkenlos alle vorgeschlagenen Indizes erstellen, sondern erst einmal gründlich überdenken, welche davon wirklich sinnvoll sind. Viele der vorgeschlagenen Indizes enthalten beispielsweise extrem viele INCLUDESpalten, da angestrebt wird, möglichst für jede Abfrage einen abdeckenden Index zur Verfügung zu haben. So kann es vorkommen, dass mehrere fast identische Indizes vorgeschlagen werden, die sich hauptsächlich durch die INCLUDE-Spalten voneinander unterscheiden. Auch wenn dies für die reine Abfrage von Daten sinnvoll sein mag, wird dadurch natürlich unnötiger Speicherplatz verschwendet und auch die Aktualisierungen der beteiligten Tabellen werden unnötig verlangsamt (da ja alle Indizes mit aktualisiert werden müssen). Daher sollte man sich die Frage stellen, ob sich mehrere der vorgeschlagenen Indizes zu einem Index sinnvoll zusammenfassen lassen, der vor allem die Nicht-INCLUDE-Spalten enthält. Damit kann dieser zwar kaum als abdeckender Index dienen, aber für eine Vielzahl von Abfragen als nichtabdeckender Index genutzt werden, was vielleicht nicht den maximal möglichen, aber vielleicht dennoch den entscheidenden Performancegewinn mit sich bringt. Hier gilt es also Kosten (Speicherplatz und Aktualisierungsaufwand) und Nutzen (Zeitersparnis für Abfragen) von verschiedenen Indexvarianten abzuwägen. Besonders bei Tabellen, die als Child-Tabelle in einer Parent-Child-Beziehung stehen und meist nur zusammen mit der übergeordneten Tabelle abgefragt werden, wird oft ein Index vorgeschlagen, der den Fremdschlüssel als Indexspalte enthält und fast alle anderen Spalten als INCLUDE-Spalten. Hier kann es sinnvoll sein, von der Standardvorgabe (Primärschlüssel als gruppierter Index) abzuweichen und stattdessen den Fremdschlüssel als gruppierten Index zu definieren. Die INCLUDE-Spalten kann man sich dann ebenfalls sparen, da deren Inhalte ja dann direkt über den gruppierten Index auffindbar sind.
i
96
Eine solche Indexanalyse zur Prüfung von fehlenden und überflüssigen Indizes sollte regelmäßig (wenn auch in größeren Zeitabständen von beispielsweise drei Monaten) durchgeführt werden, da sich die Erfordernisse – insbesondere durch zusätzliche Auswertungen – im Laufe der Zeit ändern können. Dazu lässt sich eine regelmäßige Überprüfung nutzen, um die Indexanpassungen immer weiter anzupassen und zu verfeinern.
2030-4.book Page 97 Monday, February 8, 2010 4:26 PM
Index oder nicht?
4.4.5 Gruppiert oder nicht? Neben der Entscheidung, für welche Spalten (oder Spaltenkombinationen) überhaupt ein Index zu definieren ist, sollte man sich auch Gedanken machen, welche Spalte(n) für einen gruppierten Index in Frage kommen. In den meisten Fällen entspricht der gruppierte Index dem Primärschlüssel (und wird standardmäßig auch so von SQL Server erstellt). Damit hängt diese Frage eng mit dem in Kapitel 3.2.7 Identitätsspezifikation oder Uniqueidentifier? diskutierten Thema zusammen. Insbesondere dann, wenn die Wahl hier auf den Uniqueidentifier fallen sollte, kann es aber auch sinnvoll sein, einen separaten gruppierten Index zu wählen. Ausschlaggebendes Kriterium ist hier die Tatsache, dass der gruppierte Index die physikalische Speicherung der Datenzeilen vorgibt. Da die automatisch generierten Werte des Typs Uniqueidentifier nicht ständig wachsend sind, hätte dies zahlreiche Page Splits und damit eine starke Fragmentierung des Indexes – also auch der gesamten Tabelle – zur Folge. Ein ähnliches Problem ergibt sich, wenn die Werte von Spalten, die Bestandteil eines gruppierten Indexes sind, sich häufig ändern. Daher gibt es für einen guten gruppierten Index folgende Anforderungen: 쮿
Spalte mit Werten, die kontinuierlich wachsen (z.B. Kundennummern etc.)
쮿
Spalten, deren Werte selten geändert werden
In einigen Fällen eignen sich auch Fremdschlüsselspalten hervorragend als Bestandteil eines gruppierten Indexes. Besonders wenn die Tabellen meist nur zusammen mit der übergeordneten Tabelle abgefragt werden, kann es – wie weiter oben bereits angedeutet – sinnvoll sein, den Fremdschlüssel in Kombination mit weiteren Spalten als gruppierten Index zu definieren. Ein typisches Beispiel hierfür ist eine Tabelle Orders (Bestellungen), an der eine Tabelle OrderItems (Bestellpositionen) hängt. Für beide Tabellen existiert eine ID-Spalte mit automatisch vergebenen numerischen Werten als Primärschlüssel. Statt nun genau diese beiden Spalten auch als gruppierten Index für die jeweilige Tabelle zu definieren, kann man in der Tabelle OrderItems die Fremdschlüsselspalte OrderId als gruppierten Index nutzen, da die Bestellpositionen eigentlich immer in Zusammenhang mit der zugehörigen Bestellung abgefragt werden. Bei Bedarf kann man notfalls noch einen nichtgruppierten Index auf die ID-Spalte der OrderItems-Tabelle erstellen. In der AdventureWorks2008-Datenbank wurde dies ähnlich umgesetzt, indem die Tabelle Sales.SalesOrderHeader den Primärschlüssel (und gruppierten Index) auf der Spalte SalesorderID hat, während die Tabelle Sales.SalesOrderDetail dafür die Kombination aus der Fremdschlüsselspalte SalesOrderID und dem Identitätswert SalesOrderDetailID nutzt. Durch die Einbeziehung der SalesOrderID in den gruppierten Index ist ein schnelles Auffinden aller Bestellpositionen zu einer Bestellung gewährleistet, während die SalesOrderDetailID als zweite Spalte des gruppierten Indexes dessen Eindeutigkeit sicherstellt.
SQL Server Performance-Ratgeber
97
2030-4.book Page 98 Monday, February 8, 2010 4:26 PM
4 – Indizes
4.5
Volltextindizes
Eine absolute Sonderstellung – und daher hier auch getrennt von den „normalen“ Indextypen behandelt – nehmen die Volltextindizes ein. Da dieses Thema einerseits recht komplex, aber andererseits nur in wenigen Anwendungen wirklich sinnvoll einzusetzen ist, wird hier vor allem die grundlegende Funktionsweise geschildert, damit Sie besser entscheiden können, wann es Sinn macht, einen Volltextindex einzusetzen.
4.5.1
Funktionsweise von Volltextindizes
Volltextindizes ermöglichen eine Volltextsuche in verschiedenen Spalten einer Tabelle, wobei auch Felder mit Datentypen integrierbar sind, die sonst nicht indiziert werden können. Grundlage dafür ist ein Volltextkatalog, in dem alle in den betroffenen Spalten verwendeten Begriffe gespeichert werden. Lediglich unnötige Füllwörter (so genannte Noise Words) werden dabei herausgefiltert, um den Volltextkatalog nicht unnötig wachsen zu lassen. Unterschiede zu normalen Indizes: 1. Volltextindizes werden getrennt von konventionellen Indizes verwaltet. Bis SQL Server 2005 geschah dies in Form von separaten Dateien, seit SQL Server 2008 in einem getrennten Bereich der Datenbank. 2. In Volltextindizes sind neben allen alphanumerischen Datentypen (auch lange VARCHAR-Felder) auch Felder der Datentypen IMAGE, VARBINARY und XML nutzbar. Dies funktioniert auch, wenn diese in Filestreams verwaltet werden. 3. Volltextindizes werden nicht automatisch genutzt, sondern müssen über entsprechende Funktionen (z.B. CONTAINS) gezielt abgesprochen werden. 4. Auch die Aktualisierung von Volltextindizes geschieht nicht automatisch und synchron mit jeder Datenänderung, sondern asynchron und muss – je nach Konfiguration des Volltextindexes – eventuell sogar explizit vorgenommen (oder besser noch als regelmäßige Wartungsaufgabe durchgeführt) werden. 5. Über Filter sind auch spezielle Datentypen indizierbar, die in VARBINARY-Feldern abgelegt sind. Dies umfasst momentan vor allem Word-, Excel-, Powerpoint-, HTMLund PDF-Dateien, kann aber durch zusätzliche Filter beliebig erweitert werden. 6. Pro Tabelle ist nur ein Volltextindex möglich (bei SQL Server Express darf dieser auch nur eine Spalte verwenden). 7. Neben der rein textuellen Suche ist auch eine linguistische Suche möglich, die verschiedene Formen desselben Wortes und sogar Synonyme finden kann.
98
2030-4.book Page 99 Monday, February 8, 2010 4:26 PM
Volltextindizes
Damit die Volltextindex-Funktionalität genutzt werden kann, müssen allerdings auch ein paar Voraussetzungen erfüllt sein: 쮿
Die Volltextindex-Funktionalität muss installiert sein. Dies kann bei Bedarf auch nachträglich hinzugefügt werden, indem Sie das Setup erneut aufrufen (bei SQL Server 2008 auch über das SQL-Server-Installationscenter). Anschließend sollten Sie auch eventuell installierte Service Packs neu einspielen, damit auch das Volltextindex-Feature mit aktualisiert wird.
쮿
Der SQL Full-text Filter Daemon Launcher-Dienst muss gestartet sein. Dazu muss der User, unter dem dieser Dienst läuft, Mitglied in der lokalen Benutzergruppe SQLServerFDHostUser sein.
!
4.5.2 Verwendung von Volltextindizes Wie bereits erwähnt ermöglichen Volltextindizes eine effektivere Suche nach Begriffen, die irgendwo in einem Feld vorkommen. Ohne Volltextindizes müsste man vergleichbare Suchen über den LIKE-Operator realisieren: SELECT * FROM Production.ProductReview WHERE Comments LIKE '%bike%'
Selbst wenn für die Spalte Comments ein Index existiert, muss dieser für die Ausführung der Abfrage komplett gelesen werden (Index Scan), da der Begriff bike ja irgendwo im Feld auftauchen kann und der Index damit nicht entsprechend sortiert ist. Existiert stattdessen ein Volltextindex, der die Spalte Comments beinhaltet, wird ein Verzeichnis aller darin enthaltenen Begriffe im Volltext-Katalog abgelegt. Damit die Suche über den Volltextindex stattfindet, muss auch die Abfrage leicht angepasst werden: SELECT * FROM Production.ProductReview WHERE CONTAINS(Comments, '*bike*')
Dies entspricht von der Logik her der Abfrage mit LIKE, nur dass hier eben im Volltextindex gesucht wird statt in der Tabelle selbst. Wenn man die beiden Sterne beim zweiten Parameter der CONTAINS-Funktion weglässt, wird das Wort »bike« als alleinstehender Begriff gesucht, der vor und nach dem Wort mit einem Leer- oder sonstigen Trennzeichen (Satzzeichen wie Punkt, Komma, Semikolon etc.) von den davor und danach stehenden Begriffen getrennt ist. Außerdem kann man als ersten Parameter statt eines Spaltennamens auch einen Stern angeben, damit alle Spalten der Volltextindizes nach dem gesuchten Begriff durchsucht werden. Die Möglichkeiten von Volltextindizes gehen aber noch weiter. So muss das gesuchte Wort nicht zwingend in der korrekten Form und Schreibweise angegeben werden. Durch die Verwendung von zusätzlichen Varianten kann auch nach verwandten Wörtern gesucht werden:
SQL Server Performance-Ratgeber
99
2030-4.book Page 100 Monday, February 8, 2010 4:26 PM
4 – Indizes
SELECT * FROM Production.ProductReview WHERE CONTAINS(Comments, 'FORMSOF(INFLECTIONAL, "bike")')
Dadurch wird beispielsweise auch eine Zeile gefunden, die zwar nicht bike, sondern dafür das Wort biking enthält, das ja vom selben Wortstamm kommt. Selbst die Suche nach komplett anderen Wörtern mit derselben Bedeutung ist durch die Verwendung eines Thesaurus möglich. Diese liegen als vorbereitete XML-Dateien für verschiedene Sprachen im folgenden Ordner bereit und können daher mit einem beliebigen Editor erweitert werden: C:\Programme\Microsoft SQL Server\MSSQL10.SQL2008\MSSQL\FTData Neben der CONTAINS-Funktion gibt es noch die FREETEXT-Funktion, die als Parameter auch ganze Sätze entgegennimmt und daraus die für eine Suche relevanten Begriffe herausfiltert: SELECT * FROM Production.ProductReview WHERE FREETEXT(*, 'show me reviews about socks or bikes')
Sowohl die CONTAINS-Funktion als auch die FREETEXT-Funktion stehen auch in den Varianten CONTAINSTABLE und FREETEXTTABLE als Tabellenwertfunktionen zur Verfügung, die als Ergebnis statt der eigentlichen Datenzeilen den Schlüssel auf die Daten selbst sowie einen Ranking-Wert zurückgeben, der eine Sortierung der Ergebnisse ermöglicht. Durch einen JOIN mit der Tabelle selbst lassen sich auch die entsprechenden Daten dazu anzeigen. SELECT * FROM FREETEXTTABLE(Production.ProductReview, *, 'show me reviews about socks or bikes') AS ft INNER JOIN Production.ProductReview AS pr on ft.[KEY] = pr.ProductReviewID
Im Gegensatz zur Suche über die LIKE-Funktion lassen sich mit Volltextindizes auch binäre und XML-Felder durchsuchen. Ein passendes Beispiel aus der AdventureWorks2008-Datenbank ist in der Tabelle HumanResources.JobCandidate zu finden, in der ein Volltextindex auf das Feld Resume existiert, das vom Typ XML ist. Hier lässt sich die Volltextsuche beispielsweise bestens nutzen, um geeignete Bewerber für ein bestimmtes Stellenprofil herauszufinden: SELECT * FROM HumanResources.JobCandidate WHERE CONTAINS(Resume, 'FORMSOF(INFLECTIONAL, "management")')
In einem Volltextindex können auch mehrere Felder enthalten sein. Bei einer konventionellen Abfrage wären hierfür mehrere OR-verknüpfte LIKE-Bedingungen nötig. Ein passendes Beispiel ist in der AdventureWorks2008-Datenbank die Tabelle Production.Document, in der ein kombinierter Volltextindex für die Spalten DocumentSummary und Document existiert, wobei letztere sogar den Datentyp VARBINARY(MAX) hat, also binäre Daten beinhaltet.
100
2030-4.book Page 101 Monday, February 8, 2010 4:26 PM
Volltextindizes
4.5.3
Anlegen eines Volltextindexes
Das Erstellen eines Volltextindexes ist beispielsweise mit SQL Server 2008 relativ leicht durchzuführen. Klicken Sie dazu mit der rechten Maustaste auf die entsprechende Tabelle und wählen Sie anschließend die Option Volltextindex/Volltextindex definieren, um den Volltextindizierungs-Assistenten aufzurufen. Hier werden Sie zuerst nach einem konventionellen Index gefragt, über den die Zeilen der Tabelle eindeutig identifiziert werden können. Im Normalfall ist hier der Index des Primärschlüssels auszuwählen. Danach folgt die Auswahl der zu indizierenden Felder, wobei alle Spalten mit verwendbaren Datentypen auswählbar sind. Als Nächstes ist der Modus der Änderungsnachverfolgung auszuwählen (Automatisch, Manuell, Änderungen nicht nachverfolgen). Hierüber wird gesteuert, wie der Volltextindex aktualisiert wird. Aus Performancegründen kann es sinnvoll sein, hier von der automatischen Änderungsnachverfolgung abzusehen und dies stattdessen in Form eines nächtlichen Wartungstasks zu erledigen. Anschließend erfolgt die Auswahl bzw. Erstellung (sofern noch keiner für die aktuelle Datenbank vorhanden war) eines Volltextkataloges sowie die Auswahl von Indexdateigruppe und Volltext-Stoppliste. Aus Performancegründen empfiehlt es sich, für Volltextindizes eine separate Dateigruppe zu wählen, zum Ausprobieren können Sie den Volltextindex natürlich auch in der PRIMARY-Dateigruppe anlegen. Die Stoppliste enthält so genannte Noise Words, die bei der Volltextsuche ignoriert werden. Im nächsten Schritt lassen sich noch Auffüllungszeitpläne definieren, zu denen die Indizes für die betroffene Tabelle oder aber den ganzen Volltextkatalog automatisch aufgefüllt werden. Dieser Schritt ist optional und kann auch später noch eingerichtet werden. Schließlich wird – nach einer Zusammenfassung der Einstellungen – der Volltextkatalog und anschließend darin der Volltextindex erstellt und ist somit nutzbar.
i
Das Verfahren für die Erstellung eines Volltextindexes unter SQL Server 2005 ist im Prinzip dasselbe. Auch hier gibt es einen entsprechenden Assistenten, den Sie über das Kontextmenü der Tabelle aufrufen können.
Wundern Sie sich nicht, dass der Volltextindex nicht im Objekt-Explorer bei der Auflistung der Indizes zur Tabelle auftaucht. Wenn Sie stattdessen unterhalb der entsprechenden Datenbank den Zweig Speicher/Volltextkataloge aufklappen, sehen Sie dort den neu angelegten Katalog, der auch die jeweiligen Volltextindizes beinhaltet. Die Verwendung von Volltextindizes erfordert sicherlich etwas Vorarbeit und erzeugt zusätzlichen administrativen Overhead. Dadurch eröffnet sich aber eine Vielzahl an neuen und performanten Suchmöglichkeiten in Feldern verschiedener Datentypen.
SQL Server Performance-Ratgeber
101
2030-4.book Page 102 Monday, February 8, 2010 4:26 PM
4 – Indizes
Wenn regelmäßig eine Volltextsuche benötigt wird, empfiehlt es sich also, Volltextindizes einzusetzen, da die Alternative einer Suche über den LIKE-Operator mit Wildcards ziemlich unperformant ist (weil hierbei kein Index Seek verwendet werden kann). Wird die Volltextsuche aber nur selten benötigt, sollte man eher aufgrund des damit verbundenen Mehraufwandes und Speicherbedarfs auf einen Volltextindex verzichten und sich stattdessen konventioneller Indizes bedienen.
4.6
Zusammenfassung
Eines der wichtigsten, aber auch komplexesten Themen der Performance-Optimierung von Datenbanken ist die Indexproblematik. Indizes müssen für die richtigen Tabellen und Spalten erstellt und gewartet werden. Ein passender Index verlangsamt zwar die Schreibzugriffe etwas, kann aber Lesezugriffe leicht um den Faktor 1.000 oder mehr beschleunigen. Ein großer Vorteil bei Indizes liegt darin, dass sich diese auch leicht nachträglich einfügen, anpassen oder entfernen lassen, ohne dass auch nur eine Zeile Quellcode in den auf die Datenbank zugreifenden Anwendungen geändert werden muss. Um zu erkennen, für welche Spalten man Indizes definieren sollte und für welche nicht, bietet SQL Server verschiedene Hilfsmittel an. Angefangen von den Hinweisen, die das SQL Server Management Studio bei der Anzeige von Ausführungsplänen gibt, über dynamische Management-Sichten bis hin zu den Empfehlungen des Datenbankoptimierungsratgebers. Bei all diesen Hilfestellungen sollte aber immer die Kombination aus eigenen Erfahrungen und Hintergrundwissen über die Datenbankanwendungen die letzte Instanz bilden, wenn es darum geht, zu entscheiden, ob ein neuer Index erstellt werden sollte oder nicht. Insbesondere die Wahl des richtigen gruppierten Indexes sollte wohl überlegt sein, da diese massive Auswirkungen auf die Performance haben kann. Zusätzlich zu „normalen“ Indizes lassen sich auch Volltextindizes nutzen, wenn nach Begriffen gesucht werden soll, die irgendwo in einem oder mehreren Feldern vorkommen können. Der dabei entstehende Mehraufwand (sowohl administrativ als auch vom Speicherbedarf) ist jedoch nicht zu unterschätzen, sodass man diese Möglichkeit nur dann nutzen sollte, wenn sie wirklich benötigt wird.
102
2030-4.book Page 103 Monday, February 8, 2010 4:26 PM
5
Optimale Verteilung der Daten
Es ist einleuchtend, dass auf eine geringe Datenmenge performanter zugegriffen werden kann als auf eine große. Normalerweise wird aber selbst bei sehr großen Datenbanken meist nur mit einem kleinen Teil der Daten gearbeitet, während die breite Masse an Daten nur selten benötigt wird. Was also liegt näher, als diesen Ansatz zu nutzen, indem man die Datenmenge für den ständigen Zugriff auf ein handliches Minimum reduziert?
i 5.1
Auch die im vorigen Kapitel behandelte Normalisierung von Daten kann man ähnlich interpretieren. Während dort die Daten aber vertikal – also anhand von Spalten – aufgeteilt werden, geht es in diesem Kapitel um eine horizontale Aufteilung, also eine Unterteilung der Tabellenzeilen.
Manuelle Aufteilung von Datenzeilen
Die einfachste Variante, Daten derselben Struktur aufzuteilen, liegt darin, mehrere Tabellen derselben Struktur anzulegen und die Datensätze entsprechend auf diese Tabellen zu verteilen. Dabei können die Tabellen in verschiedenen Dateigruppen (und damit sogar auf verschiedenen Laufwerken) liegen. Das Entscheidende hierbei ist, ein geeignetes Kriterium für die Aufteilung der Daten zu finden. Oft bieten sich fachliche Kriterien – wie beispielsweise Mandantennummern – an, anhand derer die Daten auf verschiedene Tabellen gleicher Struktur verteilt werden können. Wichtig ist es, auf jeden Fall ein Kriterium zu wählen, das sich nicht oft ändert, da die Daten sonst von einer in die andere Tabelle verschoben werden müssten, was wiederum zusätzliche Performance kostet. Sollten doch einmal alle Daten zusammen benötigt werden, kann man sich ja mit einer Sicht behelfen, die mithilfe eines UNION SELECT die Daten aus den verschiedenen Tabellen wieder zusammenzieht.
5.1.1
Archivierung von Altdaten
Ein Anwendungsfall, der sich aufdrängt – und in vielen Umgebungen sicherlich auch sinnvoll ist –, liegt darin, das Erstelldatum des Datensatzes als Kriterium für die Aufteilung zu verwenden. So kann man beispielsweise aktuelle Datensätze (z.B. des laufenden Kalenderjahres) in einer Tabelle ablegen, die auf einem schnellen Plattensystem liegt, während die Daten vergangener Jahre in Archivtabellen liegen, die auf langsameren Laufwerken gespeichert sind. Wenn man davon ausgeht, dass sich die Archivdaten nicht mehr ändern, können hierfür sogar zusätzliche Indizes für einen schnelleren Lesezugriff erstellt werden, die in den Tabellen für die aktuellen Daten nicht vorhanden sind, weil sie dort die Schreibzugriffe verlangsamen würden. Letzten Endes können die Archivdaten
SQL Server Performance-Ratgeber
103
2030-4.book Page 104 Monday, February 8, 2010 4:26 PM
5 – Optimale Verteilung der Daten
sogar in separate Datenbanken ausgelagert werden, die nur für lesenden Zugriff zugänglich sind und auch nicht mehr regelmäßig gesichert werden müssen, da sich der Inhalt ja nicht mehr ändert. Mit dem Ende eines Jahres werden also die Daten des Vorjahres quasi eingefroren und auf lesenden Zugriff hin optimiert. In vereinfachter Form sind dafür lediglich zwei SQL-Anweisungen nötig: INSERT INTO ArchivTabelle SELECT * FROM Tabelle WHERE Jahr < year(getdate()) DELETE FROM Tabelle WHERE Jahr < year(getdate()) Listing 5.1: Einfachste Variante der Archivierung
Betrachten wir dies nun am konkreten Beispiel der AdventureWorks2008-Datenbank. Will man beispielsweise die Daten der Tabellen Sales.SpecialOffer und Sales.SpecialOfferProduct archivieren, so sind zuerst einmal entsprechende Archivtabellen zu erstellen. Dazu wird zunächst ein neues Schema angelegt, in dem die Archivtabellen liegen. Diese werden dann mithilfe einer SELECT INTO-Anweisung aus den ursprünglichen Tabellen erzeugt, allerdings ohne Inhalt, indem man eine WHERE-Klausel angibt, die nie zutrifft: CREATE SCHEMA Archiv GO SELECT * INTO Archiv.SpecialOffer FROM Sales.SpecialOffer WHERE 0=1 SELECT * INTO Archiv.SpecialOfferProduct FROM Sales.SpecialOfferProduct WHERE 0=1 Listing 5.2: Vorbereitung der Archivtabellen in einem separaten Datenbankschema
Nun muss man eigentlich nur noch die alten Inhalte in die Archivtabellen kopieren und anschließend aus den ursprünglichen Tabellen löschen, sodass dort lediglich die aktuellen Daten übrig bleiben. Dass dies nicht immer ganz trivial ist, zeigt unser Beispiel an verschiedenen Stellen. Die Tabelle Sales.SpecialOffer verwendet eine Identitätsspezifikation (also einen automatisch generierten ID-Wert). Damit dieser auch explizit in die zugehörige Archivtabelle eingefügt werden kann, muss vorher IDENTITY_INSERT für die Tabelle aktiviert (und nach dem Einfügen wieder deaktiviert) werden. Dazwischen werden die Zeilen in die Archivtabelle eingefügt, deren Enddate aus einem vergangenen Jahr stammt: 104
2030-4.book Page 105 Monday, February 8, 2010 4:26 PM
Manuelle Aufteilung von Datenzeilen
SET IDENTITY_INSERT Archiv.SpecialOffer ON INSERT INTO Archiv.SpecialOffer (SpecialOfferID, [Description], DiscountPct, [Type], Category, StartDate, EndDate, MinQty, MaxQty, rowguid, ModifiedDate) SELECT SpecialOfferID, [Description], DiscountPct, [Type], Category, StartDate, EndDate, MinQty, MaxQty, rowguid, ModifiedDate FROM Sales.SpecialOffer WHERE year(Enddate) < year(getdate()) SET IDENTITY_INSERT Archiv.SpecialOffer OFF Listing 5.3: Einfügen der SpecialOffer-Daten in die erste Archivtabelle
Für die zweite Tabelle (Sales.SpecialOfferProduct) ist dies nicht nötig, sodass hier ein einfaches INSERT INTO ausreicht. Allerdings erfolgt dabei die Auswahl der Zeilen nicht über ein Datumsfeld, sondern über eine Unterabfrage, die genau die Produkte extrahiert, die zu den bereits archivierten SpecialOffer-Zeilen gehören: INSERT INTO Archiv.SpecialOfferProduct SELECT * FROM Sales.SpecialOfferProduct WHERE SpecialOfferID IN (SELECT SpecialOfferID FROM Archiv.SpecialOffer) Listing 5.4: Einfügen der SpecialOfferProduct-Daten in die zweite Archivtabelle
Bevor nun die archivierten Zeilen aus den ursprünglichen Tabellen entfernt werden, muss noch eine Fremdschlüsseleinschränkung gelöscht werden, da die Tabelle Sales.SalesOrderDetail auf die Daten in Sales.SpecialOfferProduct verweist: ALTER TABLE [Sales].[SalesOrderDetail] DROP CONSTRAINT [FK_SalesOrderDetail_SpecialOfferProduct_SpecialOfferIDProductID]
Nun kann das Löschen nach denselben Bedingungen wie zuvor das Auswählen erfolgen. Allerdings muss hier die Reihenfolge umgekehrt sein, da zwischen den Tabellen Sales.SpecialOfferProduct und Sales.SpecialOffer ebenfalls eine Fremdschlüsselbeziehung existiert: DELETE FROM Sales.SpecialOfferProduct WHERE SpecialOfferID IN (SELECT SpecialOfferID FROM Archiv.SpecialOffer) DELETE FROM Sales.SpecialOffer WHERE year(Enddate) < year(getdate()) Listing 5.5: Entfernen der archivierten Daten aus den ursprünglichen Tabellen
SQL Server Performance-Ratgeber
105
2030-4.book Page 106 Monday, February 8, 2010 4:26 PM
5 – Optimale Verteilung der Daten
i
Mehr Sinn macht diese Maßnahme natürlich noch, wenn die Archivtabellen auch auf einer anderen Dateigruppe (und damit in anderen Dateien) gespeichert sind. Darauf wurde bei diesem Beispiel aus Gründen der Einfachheit bewusst verzichtet. Das Zusammenspiel von Datenbanken, Dateigruppen und Dateien wurde bereits in Kapitel 2.1 – Anlegen und Konfigurieren einer Datenbank beschrieben.
Wie zu sehen war, erfordert dieser Ansatz natürlich einigen manuellen Aufwand, der – um beim Beispiel der jährlichen Aufteilung zu bleiben – zu jedem Jahreswechsel erforderlich wird. Genauso offensichtlich ist auch, dass sich dieses Prinzip nicht auf alle Tabellen anwenden lässt, sondern eher für einige wenige Tabellen sinnvoll ist, die aber in der Regel am schnellsten anwachsen, da in ihnen beispielsweise eine Protokollierung irgendeiner Art erfolgt. Schwieriger wird es, wenn die Menge der oft benötigten Daten sich nicht an einem Kalenderjahr orientiert, sondern stattdessen fließend wechselt (beispielsweise alle Daten, die nicht älter als ein Jahr sind). Hier kann man natürlich bei dem vorigen Ansatz bleiben und einfach die Daten des Vorjahres mit dazunehmen. Dadurch hätte man aber kurz vor Jahresende die Daten von zwei ganzen Jahren in der schnell zugreifbaren Tabelle. Wenn man dies genauer steuern will, bliebe nichts anderes übrig, als die Häufigkeit des Archivierens zu erhöhen, sodass im Extremfall jede Nacht alle Daten, die älter als ein Jahr sind, in die Archivtabellen verschoben werden. Dies erfordert zwar ein regelmäßiges Verschieben der Daten, allerdings in kleineren Portionen, sodass hierfür ein kurzes Wartungsfenster ausreicht. Dazu lässt sich diese Aktion mithilfe des SQL Server Agents automatisieren.
5.1.2
Historisierung
In vielen Datenbankanwendungen ist auch – beispielsweise aus Gründen der Revisionssicherheit – eine Historisierung der Daten erforderlich. Das bedeutet, dass bei jeder Änderung eines Datensatzes der alte Stand der Zeile vorher zu sichern ist, damit immer rekonstruierbar bleibt, welche Daten zu welchem Zeitpunkt gespeichert waren. Man kann sich leicht vorstellen, dass sich das sowohl auf die Datenmenge als auch auf die Performance mit der Zeit extrem negativ auswirken kann. Während man gegen die eigentliche Datenmenge recht wenig machen kann, lassen sich die Auswirkungen auf die Performance aber minimieren, indem man die alten Stände der Daten wiederum in eine separate Tabelle wegspeichert, wodurch die Tabelle mit den aktuellen Daten immer relativ kompakt bleibt. Da die Archivierung der Daten bei jeder Änderung durchgeführt werden muss, bietet sich hier die Verwendung von DML-Triggern an. Dadurch wird die Archivierung quasi automatisiert. Um ein möglichst einfaches Beispiel zu verwenden, wird im Folgenden ein Trigger für die Tabelle Production.Culture der AdventureWorks2008-Datenbank erstellt. Dieser Trigger speichert bei jedem Ändern oder Löschen einer Zeile den alten Stand derselben in einer Historientabelle mit dem Namen „Production.Culture_History“:
106
2030-4.book Page 107 Monday, February 8, 2010 4:26 PM
Manuelle Aufteilung von Datenzeilen
CREATE TRIGGER Production.TRG_Production_Culture_Update ON Production.Culture AFTER UPDATE, DELETE AS BEGIN SET NOCOUNT ON; INSERT INTO Production.Culture_History SELECT * FROM deleted SET NOCOUNT OFF; END GO Listing 5.6: Trigger zum Historisieren geänderter und gelöschter Daten
Damit die Historientabelle auch nutzbar ist, muss diese natürlich vorher angelegt werden, wozu wiederum ein SELECT…INTO mit stets falscher WHERE-Klausel verwendet wird: SELECT * INTO Production.Culture_History FROM Production.Culture WHERE 1=2
Um die ursprüngliche Tabelle nicht dauerhaft zu ändern, wird darin erst einmal ein Testdatensatz per SQL-Anweisung angelegt, dieser dann verändert und anschließend gelöscht: INSERT INTO Production.Culture (CultureID, Name) VALUES ('pb','Palumbien') UPDATE Production.Culture SET Name = 'Neu-Palumbien' WHERE CultureID = 'pb' DELETE FROM Production.Culture WHERE CultureID = 'pb' Listing 5.7: Mit diesem Skript kann die Historisierung getestet werden
Bei den letzten beiden Operationen wird durch den Trigger automatisch ein Historieneintrag erzeugt.
SQL Server Performance-Ratgeber
107
2030-4.book Page 108 Monday, February 8, 2010 4:26 PM
5 – Optimale Verteilung der Daten
Bei Bedarf lässt sich mit den folgenden Anweisungen der Ausgangszustand wiederherstellen, indem sowohl Historientabelle als auch Trigger wieder gelöscht werden: DROP TABLE Production.Culture_History DROP TRIGGER Production.TRG_Production_Culture_Update
Auf diesem Weg bleibt die Tabelle mit den aktuellen Daten überschaubar und für Lesezugriffe performant. Es handelt sich hierbei allerdings weniger um eine Methode, mit der die Lesezugriffe beschleunigt werden können, sondern eher um eine Methode, diese trotz Historisierung nicht deutlich langsamer werden zu lassen.
!
Die Schreibzugriffe allerdings werden durch den Trigger deutlich verlangsamt. Dies stellt in den meisten Fällen kein Problem dar, da Datenänderungen in der Regel nur auf einzelnen Datensätzen stattfinden. Falls jedoch ein Massenupdate ausgeführt wird, ist hier mit drastischen Performanceeinbrüchen zu rechnen!
Auch bei der Historisierung kann es zu Problemen kommen, wenn Fremdschlüssel auf die zu archivierenden Datensätze verweisen und sich diese daher nicht ohne Weiteres in eine andere Tabelle verschieben lassen. Stattdessen müsste man – analog zum Archivierungsbeispiel weiter oben – auf die Fremdschlüsseldefinition verzichten und die referentielle Integrität durch die Anwendung selbst sicherstellen.
5.2
Partitionierung
Die oben beschriebenen Ansätze können zwar einiges an Performance bringen, erfordern aber auch einen nicht unerheblichen manuellen Aufwand. Mit der Enterprise Edition von SQL Server – die sich ja vor allem durch Features für bessere Skalierbarkeit und Hochverfügbarkeit von den kleineren Editionen unterscheidet – gibt es auch für das Aufteilen gleichartiger Daten entsprechende Unterstützung. Damit lassen sich sowohl Tabellen als auch Indizes auf (bis zu 1.000) verschiedene Partitionen verteilen, die auf verschiedenen Plattensystemen liegen können. Für die Konfiguration einer Partitionierung werden zwei Datenbankobjekte benötigt: 쮿
Eine Partitionierungsfunktion, in der die Regeln definiert sind, anhand derer die Zeilen der Tabelle (oder des Indexes) auf die verschiedenen Partitionen verteilt werden.
쮿
Ein Partitionierungsschema, das definiert, welche Partitionen in welcher Dateigruppe abgelegt werden.
108
2030-4.book Page 109 Monday, February 8, 2010 4:26 PM
Partitionierung
5.2.1
Erstellen einer Partitionierungsfunktion
Das Erstellen einer Partitionierungsfunktion erfolgt in Abhängigkeit eines Feldes (der so genannten Partitionierungsspalte), mit dem sich die Daten logisch unterteilen lassen. Als Partitionierungsspalte eignet sich jedes Feld, das auch für einen Index verwendet werden kann. Sowohl Mandantennummern als auch Datumsfelder sind hier typische und oft genutzte Beispiele. Die SQL-Anweisung hierzu sieht wie folgt aus: CREATE PARTITION FUNCTION Partitionsfunktionsname (Datentyp) AS RANGE LEFT FOR VALUES (Grenzwerte)
Dabei gibt das Schlüsselwort LEFT bzw. RIGHT hinter dem RANGE an, zu welcher Seite die angegebenen Grenzwerte gezählt werden. Die folgende Partitionierungsfunktion unterteilt die Zeilen anhand des Kalenderjahres einer Datumsangabe: CREATE PARTITION FUNCTION PF_Kalenderjahr (datetime) AS RANGE RIGHT FOR VALUES ('20080101', '20090101', '20100101')
Dabei entstehen vier Partitionen: 쮿
Zeilen mit Datum vor dem 01.01.2008
쮿
Zeilen ab 01.01.2008, aber vor 01.01.2009
쮿
Zeilen ab 01.01.2009, aber vor 01.01.2010
쮿
Zeilen ab 01.01.2010
Um diesen Partitionen nun Dateigruppen zuzuordnen, wird ein Partitionierungsschema benötigt.
5.2.2 Erstellen eines Partitionierungsschemas Die allgemeine Anweisung zum Erstellen eines Partitionierungsschemas sieht wie folgt aus: CREATE PARTITION SCHEMA Partitionsschemaname AS PARTITION Partitionsfunktion TO (Dateigruppenliste)
Um das zum Beispiel oben passende Partitionierungsschema zu erstellen, kann man folgende Anweisung verwenden: CREATE PARTITION SCHEMA PS_Kalenderjahr AS PARTITION PF_Kalenderjahr TO (FGVor2008, FG2008, FG2009, FGAb2010)
Dabei müssen natürlich zuvor die vier verwendeten Dateigruppen erstellt werden, was über das Eigenschaften-Fenster der dazugehörigen Datenbank durchgeführt werden kann.
SQL Server Performance-Ratgeber
109
2030-4.book Page 110 Monday, February 8, 2010 4:26 PM
5 – Optimale Verteilung der Daten
Abbildung 5.1: Anlegen von vier zusätzlichen Dateigruppen
Anschließend können diese Dateigruppen für neue Dateien verwendet werden, die auch auf anderen Laufwerken liegen können, um parallele Zugriffe zu ermöglichen.
Abbildung 5.2: Anlegen von zusätzlichen Dateien, welche die neuen Dateigruppen nutzen
5.2.3
Verwenden von Partitionen
Um nun die erstellten Partitionen für Tabellen und Indizes zu nutzen, kann man dies bereits beim Erstellen der jeweiligen Datenbankobjekte angeben. Dies geschieht alternativ zur Angabe der Dateigruppe: CREATE TABLE ... ON Partitionsschema (Partitionsspalte) CREATE INDEX ... ON Partitionsschema (Partitionsspalte)
110
2030-4.book Page 111 Monday, February 8, 2010 4:26 PM
Partitionierung
Um bereits bestehende Datenbankobjekte von einer fest zugeordneten Dateigruppe zu einem Partitionierungsschema zuzuordnen, gibt es verschiedene Möglichkeiten. Handelt es sich um einen Index, so wird dieser einfach gelöscht und unter Nutzung des Partitionierungsschemas neu angelegt. Bei Tabellen ist dies etwas schwieriger, aber hier kann man sich einen kleinen Trick zu Nutze machen: Um aus einer nichtpartitionierten Tabelle eine partitionierte zu machen, erstellt man einfach einen neuen gruppierten Index auf dem Partitionierungsschema. Da dieser ja auch die eigentlichen Zeilendaten der Tabelle beinhaltet, werden diese ebenfalls mitpartitioniert. Sollte bereits ein gruppierter Index für die entsprechende Tabelle bestanden haben, so muss dieser natürlich vorher gelöscht werden. Umgekehrt geht dies selbstverständlich auch, um eine partitionierte Tabelle in eine nichtpartitionierte Tabelle umzuwandeln. Auch hier wird einfach der existierende gruppierte Index gelöscht und anschließend ein neuer erstellt, der statt eines Partitionierungsschemas eine feste Dateigruppe (z.B. PRIMARY) nutzt. Ist dies geschehen, so können auch Partitionierungsschema und Partitionierungsfunktion wieder gelöscht werden. Alternativ könnte man natürlich auch die Partitionierungsfunktion so anpassen, dass lediglich eine einzige Partition übrig bleibt. Damit bleibt die Tabelle zwar – technisch gesehen – partitioniert, lässt sich dafür aber bei Bedarf auch leichter wieder in mehrere Partitionen aufteilen.
5.2.4 Hinzufügen und Entfernen von Partitionen Falls später für ein existierendes Partitionsschema die Anzahl der Partitionen geändert werden soll, gibt es auch dafür Möglichkeiten. Dem Partitionierungsschema selbst fügt man mit folgender Anweisung eine neue Dateigruppe (und damit auch Partition) hinzu: ALTER PARTITION SCHEME Partitionsschemaname NEXT USED Dateigruppe
Bei der Partitionierungsfunktion geschieht dies, indem man eine der Partitionen durch Angabe eines neuen Grenzwerts in zwei Partitionen aufteilt: ALTER PARTITION FUNCTION Partitionsfunktionsname () SPLIT RANGE (Grenzwert)
Nach demselben Prinzip lassen sich auch Partitionen zusammenfassen, indem man den Grenzwert angibt, der aus der Auflistung der Grenzwerte entfernt werden soll: ALTER PARTITION FUNCTION Partitionsfunktionsname () MERGE RANGE (Grenzwert)
5.2.5
Verschieben von Partitionen
Eine besondere Stärke von Partitionen liegt darin, dass sie relativ leicht verschoben werden können:
SQL Server Performance-Ratgeber
111
2030-4.book Page 112 Monday, February 8, 2010 4:26 PM
5 – Optimale Verteilung der Daten
ALTER TABLE TabellenName SWITCH [PARTITION sourcepartitionnumber] TO ZielTabelle [PARTITION targetpartitionnumber]
Mit dieser SQL-Anweisung lassen sich verschiedene Dinge erreichen: 쮿
Zuweisen einer Tabelle als Partition zu einer bereits vorhandenen partitionierten Tabelle: ALTER TABLE AdressenUnpartitioniert SWITCH TO AdressenPartitioniert PARTITION 1
쮿
Verschieben einer Partition aus einer partitionierten Tabelle in eine andere: ALTER TABLE AdressenPartitioniertA PARTITION 2 SWITCH TO AdressenPartitioniertB PARTITION 1
쮿
Zuweisen einer Partition aus einer partitionierten Tabelle in eine nichtpartitionierte Tabelle: ALTER TABLE AdressenPartitioniert PARTITION 1 SWITCH TO AdressenUnpartitioniert
Somit lässt sich also auch die Umwandlung von nichtpartitionierten in partitionierte Daten (und zurück) auf diesem Weg erledigen. In allen drei Varianten müssen beide Tabellen bereits bestehen und dieselbe Struktur haben.
5.3
Zusammenfassung
Mithilfe von verschiedenen Ansätzen zu Archivierung und Historisierung lassen sich selten benötigte Daten auf andere Tabellen und damit auch Dateigruppen und Dateien verteilen, wodurch die wichtigen Produktivdaten überschaubar und damit schnell im Zugriff bleiben. Ähnliches lässt sich auch mit der Partitionierung von Tabellen und Indizes erreichen, allerdings ist hierfür die Enterprise Edition von SQL Server erforderlich. Sowohl die Aufteilung von Daten durch Archivierung bzw. Historisierung als auch die Partitionierung bringen einen gewissen Verwaltungsaufwand mit sich und sollten daher nur gezielt für einzelne Tabellen durchgeführt werden. In den meisten Datenbanken, die ich in vielen Jahren in verschiedenen Projekten kennen gelernt habe, war es – auch wenn die Datenbank selbst mehrere hundert Tabellen umfasste – immer nur eine Handvoll an Tabellen, die den Großteil des Platzes beanspruchten. Und genau für diese paar Tabellen lohnt es sich, über eine Archivierung oder Partitionierung nachzudenken, wenn man noch etwas mehr Performance gewinnen will. Der große Vorteil der Partitionierung liegt darin, dass diese – wenn sie einmal eingerichtet ist – transparent durchgeführt wird. An der Anwendung selbst sind keinerlei Änderungen nötig, da nach wie vor nur auf eine Tabelle zugegriffen wird. Und auch die referentielle Integrität bleibt somit erhalten. Dazu können einzelne Partitionen mit einer einfachen SQLAnweisung zwischen verschiedenen Tabellen derselben Struktur verschoben werden.
112
2030-4.book Page 113 Monday, February 8, 2010 4:26 PM
Teil C Optimierung für Entwickler In diesem Teil des Buches geht es um die Optimierung aus Sicht des Entwicklers. Dabei werden in Kapitel 6 Performancegrundlagen für Entwickler die notwendigen Hintergrundinformationen vermittelt, auf denen die beiden darauf folgenden Kapitel aufbauen. In Kapitel 7 – Abfrageoptimierung geht es dann um verschiedene Varianten zur Optimierung von SQL-Abfragen. Die ist sowohl für Anwendungs- als auch für Datenbankentwickler wichtig, da letzten Endes alle Datenbankzugriffe vor deren Ausführung in SQL umgewandelt werden. In vielen Unternehmen und Projekten werden diese Aufgaben ohnehin von ein und derselben Person wahrgenommen. Kapitel 8 – Optimierung des Datenzugriffs schließlich beschreibt Aspekte, die vor allem für Anwendungsentwickler relevant sind, nämlich den Datenzugriff aus einer .NET-Anwendung heraus, und welche Optimierungsmöglichkeiten dabei bestehen.
SQL Server Performance-Ratgeber
113
2030-4.book Page 114 Monday, February 8, 2010 4:26 PM
2030-4.book Page 115 Monday, February 8, 2010 4:26 PM
6
Performancegrundlagen für Entwickler
Bevor wir uns also in den nächsten Kapiteln mit der Optimierung von Abfragen und Datenzugriffen beschäftigen, ist es hilfreich, ein wenig mehr über die internen Zusammenhänge dieser Thematiken zu wissen. Wenn Sie sich bereits bestens mit der Funktionsweise von Abfrageoptimierer, Plancache, Sperren und Transaktionen auskennen, können Sie dieses Kapitel auch überspringen und direkt mit der Optimierung von Abfragen bei Kapitel 7 einsteigen.
6.1
Interne Verarbeitung von Abfragen
Um besser einschätzen zu können, an welchen Stellen eine Abfrageoptimierung Erfolg versprechend ist, hilft es zu wissen, wie SQL-Abfragen im Detail intern verarbeitet werden.
6.1.1
Ablauf der Abfrageverarbeitung
Bevor eine Abfrage ausgeführt wird, erstellt SQL Server einen Ausführungsplan, in dem festgelegt wird, wie auf die beteiligten Tabellen zugegriffen wird. Genau genommen geschieht dies in drei Schritten, bei denen verschiedene Komponenten beteiligt sind. 1. Der so genannte Parser prüft die Syntax der SQL-Anweisung und erstellt einen groben Ausführungsbaum. 2. Der Algebrizer erledigt ähnliche Aufgaben wie der Parser, geht hierbei jedoch einen Schritt weiter, da nun auch die vorhandenen Datenstrukturen der von der Abfrage verwendeten Datenbankobjekte berücksichtigt werden. So werden einerseits die Namen von Tabellen und Spalten geprüft, aber auch die Korrektheit der dazugehörigen Datentypen. Als Ergebnis des Algebrizers wird der logische Ausführungsbaum weiter optimiert und als Syntaxbaum im Plancache gespeichert (der Plancache wird weiter hinten in diesem Kapitel ausführlicher behandelt). 3. Der Optimierer (engl.: Optimizer) schließlich erstellt den eigentlichen Ausführungsplan, der die Art und Reihenfolge des Zugriffs auf die beteiligten Tabellen regelt. Auch dieser Ausführungsplan wird im Plancache gespeichert. Dieses Vorgehen greift sowohl bei der Verarbeitung von Ad-hoc-Abfragen, parametrisierten Abfragen und sogar gespeicherten Prozeduren. Im Allgemeinen ist die oben dargestellte Dreiteilung nicht so relevant. Der wesentliche Punkt ist die Tatsache, dass der Optimierer einen Ausführungsplan erstellt, in dem festgelegt wird, wie die Abfrage intern ausgeführt wird.
SQL Server Performance-Ratgeber
115
2030-4.book Page 116 Monday, February 8, 2010 4:26 PM
6 – Performancegrundlagen für Entwickler
Dabei werden die einzelnen Klauseln einer SQL-Abfrage in folgender Reihenfolge abgehandelt: 1. FROM – welche Tabellen werden gelesen und wie werden diese miteinander verknüpft 2. WHERE – filtert die Tabellen zeilenweise aufgrund von Bedingungen 3. GROUP BY – fasst Zeilen des Zwischenergebnisses zusammen 4. HAVING – filtert die Zeilen des Zwischenergebnisses anhand von Bedingungen 5. SELECT – wählt die als Ergebnis zurückzugebenden Spalten aus 6. ORDER BY – sortiert die Ergebniszeilen Auffällig ist dabei, dass dies nicht ganz der Reihenfolge entspricht, in der die einzelnen Klauseln in einer SQL-Abfrage angegeben werden. So werden die einzelnen Spalten, deren Daten gelesen werden, bei der Abfrage zwar gleich am Anfang angegeben, bei der Ausführung aber erst im vorletzten Schritt berücksichtigt.
6.1.2
Ausführungspläne
Im SQL Server Management Studio kann man sich den Ausführungsplan zu einer Abfrage anzeigen lassen. Dabei wird zwischen dem geschätzten und realen Ausführungsplan unterschieden. Der geschätzte Ausführungsplan wird angezeigt, ohne dass die Abfrage wirklich ausgeführt wird. Dazu wählen Sie im Management Studio den Menüpunkt Abfrage/Geschätzten Ausführungsplan anzeigen oder die entsprechende Tastenkombination ((Strg)+(L)) bzw. das dazugehörende Symbol in der Symbolleiste. Alternativ kann der tatsächliche Ausführungsplan angezeigt werden, sobald die Abfrage ausgeführt wird. Auch hierzu gibt es einen entsprechenden Menüpunkt (Abfrage/Tatsächlichen Ausführungsplan einschließen) sowie eine Tastenkombination ((Strg)+(M)) und ein Symbol in der Symbolleiste. Durch jede dieser drei Varianten wird die Option umgeschaltet, also aktiviert bzw. deaktiviert. Wenn Sie die Anzeige des tatsächlichen Ausführungsplans aktiviert haben und anschließend die Abfrage ausführen, erscheint im Ergebnisbereich neben den Registerkarten Ergebnisse und Meldungen noch eine weitere, die mit Ausführungsplan betitelt ist und den tatsächlichen Ausführungsplan beinhaltet. So wird für die folgende Abfrage … SELECT pers.FirstName, pers.LastName, phone.PhoneNumber FROM Person.Person AS pers INNER JOIN Person.PersonPhone AS phone ON pers.BusinessEntityID=phone.BusinessEntityID INNER JOIN Person.PhoneNumberType AS ptype ON phone.PhoneNumberTypeID=ptype.PhoneNumberTypeID WHERE ptype.Name = 'Home' AND pers.LastName = 'Gonzalez'
116
2030-4.book Page 117 Monday, February 8, 2010 4:26 PM
Interne Verarbeitung von Abfragen
… beispielsweise der in Abbildung 6.1 dargestellte Ausführungsplan erstellt.
Abbildung 6.1: Die grafische Darstellung eines Ausführungsplans
Doch wodurch unterscheiden sich der geschätzte und der tatsächliche Ausführungsplan? Der geschätzte Ausführungsplan ist der Plan, der vom Abfrageoptimierer erstellt wird. Während die Abfrage ausgeführt wird, kann es jedoch vorkommen, dass beim Lesen von Tabellen oder Indizes festgestellt wird, dass der geschätzte Ausführungsplan auf veralteten Statistiken beruhte. Dies kann dann zur Folge haben, dass der tatsächliche Ausführungsplan noch während der Ausführung angepasst wird und sich der tatsächliche Ausführungsplan damit vom geschätzten Plan unterscheidet. Verwenden Sie also zur Bewertung einer Abfrage nach Möglichkeit immer den tatsächlichen Ausführungsplan. Bei Abfragen mit extrem langer Laufzeit können Sie notfalls auf den geschätzten Ausführungsplan ausweichen. Unabhängig davon, ob Sie den geschätzten oder tatsächlichen Ausführungsplan anzeigen lassen, ist aus diesem zu erkennen, in welcher Reihenfolge auf welche Tabellen zugegriffen wird, ob und welche Indizes verwendet werden, wie die verschiedenen Zwischenergebnisse miteinander verknüpft werden und vieles mehr. Die im grafischen Ausführungsplan dargestellten Symbole stellen einzelne Operationen dar, von denen es eine relativ große Menge gibt. Eine vollständige Auflistung aller möglichen Operationen mit den dazugehörenden Symbolen finden Sie im Internet unter folgender Adresse: http://msdn.microsoft.com/de-de/library/ms175913.aspx Etwas gewöhnungsbedürftig ist sicherlich die Reihenfolge, in der die grafischen Ausführungspläne zu lesen sind. Da sie in einer Art Baumstruktur dargestellt werden, stellt die Wurzel des Baumes (links oben) das Endergebnis dar, während die ersten Schritte, die ausgeführt werden, ganz rechts in den Blättern der Baumstruktur stehen. Hier sind normalerweise die eigentlichen Tabellen- oder Indexzugriffe zu finden, die auf verschiedene Arten stattfinden können1: 쮿
Table Scan – sequenzielles Lesen der gesamten Tabelle
SQL Server Performance-Ratgeber
117
2030-4.book Page 118 Monday, February 8, 2010 4:26 PM
6 – Performancegrundlagen für Entwickler 쮿
Clustered Index Scan – sequenzielles Lesen des gruppierten Indexes
쮿
Clustered Index Seek – gezielter Zugriff auf einzelne Zeilen des gruppierten Indexes
쮿
Index Scan – sequenzielles Lesen des nichtgruppierten Indexes
쮿
Index Seek – gezielter Zugriff auf einzelne Zeilen des nichtgruppierten Indexes
Verfolgt man die Verbindungslinien nun schrittweise Richtung Wurzel, so lässt sich nachvollziehen, wie die im ersten Schritt gelesenen Daten weiterverarbeitet werden. So folgt beispielsweise nach einem Zugriff auf einen nichtgruppierten Index (per Index Scan oder Index Seek) meist ein Row Lookup, mit dem die eigentlichen Zeilendaten aus der Tabelle gelesen werden. Damit der Abfrageoptimierer entscheiden kann, ob ein Indexzugriff sinnvoll ist, oder es doch effektiver ist, direkt über einen Table Scan die gesamte Tabelle zu lesen, sind zwei Informationen entscheidend. Die erste ist die Anzahl der Zeilen in der Tabelle, die zweite die Selektivität des Indexes. Sowohl bei geringer Zeilenanzahl als auch bei geringer Selektivität des Indexes (also wenn ohnehin fast alle Zeilen der Tabelle gelesen werden müssen) ist es effektiver, einen Table Scan auszuführen als den Umweg über den Index zu gehen. Damit dies möglich ist, wird die Anzahl der Zeilen in den verschiedenen Tabellen in Statistiken mit protokolliert. Diese müssen allerdings von Zeit zu Zeit aktualisiert werden, da sie sonst irreführende Werte beinhalten, die zu nicht optimalen Ausführungsplänen führen können. Das Aktualisieren von Statistiken wurde bereits in Kapitel 2.2.4 – Statistiken aktualisieren behandelt. In dem in Abbildung 6.1 dargestellten Beispiel werden der komplette Index IX_ PersonPhone_PhoneNumber2 der Tabelle PersonPhone gelesen sowie über einen Index Seek auf den Index IX_Person_LastName_FirstName_MiddleName die Namen, deren Nachname Gonzalez entspricht. Anschließend werden beide Teilergebnisse über eine Hash-Match-Operation zusammengefügt, um den INNER JOIN umzusetzen. Zeitgleich erfolgt ein Clustered Index Scan der Tabelle PhoneNumberType. Da diese mit drei Zeilen sehr klein ist, wird einfach die gesamte Tabelle gelesen, dabei aber nur die Zeile herausgefiltert, deren Name-Feld die Zeichenkette Home enthält. Zum Schluss wird auch diese Zeile über einen Hash Match mit dem Zwischenergebnis des unteren Zweiges zusammengeführt. Interessant ist bei der grafischen Darstellung des Ausführungsplans vor allem die prozentuale Angabe des Aufwands bei jedem Teilschritt, durch die sich leicht erkennen lässt, welche Zugriffe den größten Teil der Last verursachen. Sind diese kritischen Stellen
1. 2.
118
Die verschiedenen Zugriffsarten wurden in Kapitel 4.1 – Grundlegende Funktionsweise von Indizes bereits ausführlich erklärt. Da sich die gesamte Beispielabfrage im Person-Schema bewegt, lasse ich in der Beschreibung die Angabe des Schemanamens „Person“ zu Gunsten einer besseren Lesbarkeit weg.
2030-4.book Page 119 Monday, February 8, 2010 4:26 PM
Interne Verarbeitung von Abfragen
eines Ausführungsplans identifiziert, so kann man überlegen, ob sich die Abfrage irgendwie umformulieren lässt, um diese Problemstellen zu umgehen. Bezogen auf das Beispiel kann man schnell sehen, dass der Hauptaufwand durch den Index Scan auf den Index IX_PersonPhone_PhoneNumber (43%) sowie die darauf folgende Hash-Match-Operation (47%) verursacht wird. Hier könnte man nun ansetzen und beispielsweise versuchen, den Index Scan in einen Index Seek zu wandeln. (Näheres dazu folgt im nächsten Kapitel.) Doch der grafische Ausführungsplan hält noch weitere interessante Informationen bereit. Wenn Sie mit der Maus über einem Symbol des Ausführungsplans schweben, wird ein kleines Kästchen eingeblendet, in dem Sie weitere Detailinfos zu diesem Schritt des Plans erhalten. Dies sind unter anderem: 쮿
die Art des Vorgangs
쮿
die Kosten (insbesondere E/A- und CPU-Kosten)
쮿
die geschätzte Anzahl und Größe der betroffenen Zeilen
쮿
welche Felder aus welchen Tabellen gelesen werden
쮿
nach welchen Bedingungen gefiltert wird
Abbildung 6.2: Details eines Clustered Index Scan bei geschätztem und tatsächlichem Ausführungsplan
Wie Abbildung 6.2 zeigt, werden beim tatsächlichen Ausführungsplan auch die tatsächlichen Werte für die Anzahl von Zeilen, erneute Bindungen und Zurückspulvorgänge angezeigt, die sich von den geschätzten Werten auch unterscheiden können (was in dem oben gezeigten Beispiel nicht der Fall ist).
SQL Server Performance-Ratgeber
119
2030-4.book Page 120 Monday, February 8, 2010 4:26 PM
6 – Performancegrundlagen für Entwickler
Wenn Sie mit dem Mauscursor über einem der Verbindungspfeile schweben, werden ebenfalls Zusatzinformationen angezeigt, die darüber Aufschluss geben, wie viele Zeilen diesen Weg voraussichtlich nehmen. Neben der grafischen Anzeige des Ausführungsplans können Sie diesen alternativ auch auf andere Darstellungsweisen anzeigen lassen. Dazu gibt es verschiedene Optionen, die sich mit entsprechenden SET-Anweisungen aktivieren lassen. Über die Anweisung SET SHOWPLAN_TEXT ON wird der Ausführungsplan in einer Tabellenform angezeigt, die eine Baumstruktur wiedergibt. Mit der Variante SET SHOWPLAN_ALL ON wird diese Darstellung noch um einige Spalten ergänzt, die zusätzliche Informationen anzeigen (entspricht weitgehend den Informationen aus den Detailfenstern des grafischen Ausführungsplans wie in Abbildung 6.2 zu sehen).
Abbildung 6.3: Die Darstellung des Ausführungsplans nach SET SHOWPLAN_ALL ON
SET SHOWPLAN_XML ON schließlich gibt den Ausführungsplan im XML-Format aus. Wenn Sie diese XML-Struktur im SQL Server Management Studio anklicken, wird daraus die bereits bekannte grafische Darstellung erzeugt. Da die alternative Darstellung des Ausführungsplans anstelle des eigentlichen Ergebnisses erfolgt, müssen Sie die Option wieder auf OFF stellen, wenn die Abfrageergebnisse wieder ausgegeben werden sollen. Die Anzeige des Ausführungsplans im XML-Format kann auch dafür genutzt werden, um zwei komplexe Ausführungspläne miteinander zu vergleichen. Speichern Sie dazu beide Varianten in einer Textdatei ab und nutzen Sie anschließend eines der gängigen Tools zum Vergleichen von Textdateien (z.B. WinDiff, WinMerge etc.), um die Unterschiede ausfindig zu machen.
6.1.3
Abfragestatistiken
Die Anzeige des tatsächlichen Ausführungsplans bietet vielfältige Informationen zur gerade ausgeführten Abfrage. Wenn es aber lediglich darum geht, zwei verschiedene Varianten einer Abfrage zu vergleichen, reichen ein paar grundlegende Informationen völlig aus. Im Normalfall wird auf der Registerkarte Meldungen lediglich die Anzahl der von einer SQL-Anweisung betroffenen Zeilen angezeigt. Beispiel: (83 Zeile(n) betroffen) In der Statuszeile am rechten unteren Rand des Fensters sind zwar noch einmal die Anzahl der Zeilen sowie die Ausführungszeit zu sehen, beide Werte beziehen sich jedoch
120
2030-4.book Page 121 Monday, February 8, 2010 4:26 PM
Interne Verarbeitung von Abfragen
auf die Gesamtheit aller in einem Batch ausgeführten Abfragen. Wenn Sie also beispielsweise zwei SELECT-Abfragen auf einmal ausführen, erscheint dort die Summe der Zeilen, die von beiden Abfragen zurückgegeben werden. Durch Aktivieren der Option STATISTICS TIME wird im Meldungsbereich neben der Zeilenanzahl auch die Analyse- und Kompilierzeit sowie die Ausführungszeit (jeweils aufgeteilt nach CPU-Zeit und verstrichener Zeit) pro SQL-Anweisung angezeigt:
Abbildung 6.4: Die Ausgabe der Meldungen nach SET STATISTICS TIME ON
Noch aufschlussreicher wird die Ausgabe durch Aktivieren der Option für die I/OStatistiken über die Anweisung SET STATISTICS IO ON. Hier werden für jede beteiligte Tabelle folgende Informationen ausgegeben: 쮿
Scananzahl – Anzahl der Tabellenzugriffe
쮿
logische Lesevorgänge – Anzahl der Datenseiten, die aus dem Cache gelesen wurden
쮿
physische Lesevorgänge – Anzahl der Datenseiten, die von der Festplatte gelesen wurden
쮿
Read-Ahead-Lesevorgänge – Anzahl der Datenseiten, die für den Cache von der Festplatte gelesen wurden
SQL Server Performance-Ratgeber
121
2030-4.book Page 122 Monday, February 8, 2010 4:26 PM
6 – Performancegrundlagen für Entwickler 쮿
logische LOB-Lesevorgänge – Anzahl der LOB-Datenseiten, die aus dem Cache gelesen wurden
쮿
physische LOB-Lesevorgänge – Anzahl der LOB-Datenseiten, die von der Festplatte gelesen wurden
쮿
Read-Ahead-LOB-Lesevorgänge – Anzahl der LOB-Datenseiten, die für den Cache von der Festplatte gelesen wurden Bei LOB-Datenseiten handelt es sich um Speicherseiten, die speziell für die Speicherung von großen Datentypen wie beispielsweise text, image oder xml verwendet werden.
Wenn Sie die Abfrage schon mehrfach ausgeführt haben, wird die Anzahl der physikalischen Lesevorgänge gleich 0 sein, was daran liegt, dass die Daten längst im Hauptspeicher enthalten sind und nur noch über einen logischen Lesevorgang aus diesem gelesen werden müssen. Da man nicht davon ausgehen kann, dass dies immer der Fall ist, empfiehlt es sich, für eine genauere Analyse der Abfrage vorher den Cache mit der folgenden SQL-Anweisung zu leeren: DBCC DROPCLEANBUFFERS
Rufen Sie danach die SELECT-Anweisung noch einmal auf, sollten nun auch physische Lesevorgänge darin enthalten sein. Über den Menüpunkt Abfrage/Clientstatistiken einschließen (oder das entsprechende Symbol in der Symbolleiste) können Sie zusätzlich aktivieren, dass bei jeder Abfrageausführung eine weitere Registerkarte mit verschiedenen Angaben zur Abfrageausführung angezeigt wird. Das besondere daran ist, dass dabei auch die Statistiken von vorherigen Abfragen angezeigt werden. Somit können Sie relativ einfach verschiedene Varianten einer Abfrage miteinander vergleichen. Zur Erzeugung der in Abbildung 6.5 gezeigten Clientstatistiken wurden die drei Anweisungen einzeln ausgeführt (da pro Ausführung eine Statistik erzeugt wird, egal wie viele Anweisungen diese beinhaltet). Somit resultiert Versuch 1 aus der USE-Anweisung, während Versuch 2 und Versuch 3 durch die SELECT-Abfragen entstanden sind.
122
2030-4.book Page 123 Monday, February 8, 2010 4:26 PM
Interne Verarbeitung von Abfragen
Abbildung 6.5: Die Anzeige der Clientstatistiken
6.1.4
Tabellenstatistiken
Während Abfragestatistiken für den Entwickler interessante Informationen zur Abfrageausführung bereitstellen, erfüllen Tabellenstatistiken einen völlig anderen Zweck. In diesen werden Informationen über die Anzahl von Zeilen und Verteilung der verschiedenen Werte von Tabellen gesammelt, die vom Abfrageoptimierer genutzt werden, um einen möglichst guten Ausführungsplan ermitteln zu können. Geht aus einer solchen Statistik beispielsweise hervor, dass eine an einer Abfrage beteiligten Tabelle lediglich über sehr wenige Zeilen verfügt, so wird dies dazu führen, dass für diese Tabelle kein Index verwendet wird, da der direkte Zugriff auf alle Zeilen der Tabelle voraussichtlich schneller ist. Dieses Beispiel zeigt aber auch, dass es fatale Auswirkungen haben kann, wenn die Statistiken veraltet sind. Wurden später sehr viele Zeilen in dieselbe Tabelle eingefügt, ohne dass die Statistik aktualisiert wurde, glaubt der Abfrageoptimierer immer noch, dass ein komplettes Lesen der Tabelle sinnvoll ist, und verwendet daher keinen Index. Daher sollten Statistiken regelmäßig gepflegt und aktualisiert werden wie in Kapitel 2.2.4 – Statistiken aktualisieren beschrieben.
SQL Server Performance-Ratgeber
123
2030-4.book Page 124 Monday, February 8, 2010 4:26 PM
6 – Performancegrundlagen für Entwickler
Wie aber werden diese Statistiken überhaupt erstellt? Sofern die entsprechende Option in den Datenbankeinstellungen nicht deaktiviert wurde, geschieht dies automatisch bei jeder Ausführung einer Abfrage. Da durch die Abfrage ohnehin viele Werte einer Tabelle oder eines Indexes gelesen werden mussten, werden die daraus gewonnenen Erkenntnisse über Anzahl und Verteilung der Werte in Statistiken festgehalten. Diese Statistiken sind im Objekt-Explorer bei der jeweiligen Tabelle unter Statistik zu finden. Beginnt der Name der Statistik mit _WA_Sys_, so weist dies darauf hin, dass die Statistik automatisch durch eine Abfrage generiert wurde. Wenn Sie eine dieser Statistiken mit einem Doppelklick öffnen, sehen Sie in den Statistikeigenschaften, welche Spalten in der Statistik berücksichtigt wurden. Neben den automatisch generierten Tabellenstatistiken gibt es noch Statistiken, die für jeden Index (sofern dieser geeignete Spalten verwendet) bei dessen Erstellung generiert werden und auch denselben Namen wie der dazu300gehörende Index haben. Hier sind bei den Statistikeigenschaften nicht nur die verwendeten Spalten zu sehen, sondern auf der Seite Details auch genauere Informationen über den Inhalt der Statistik.
Abbildung 6.6: Die Details einer aus einem Index resultierenden Statistik Hinweis Beginn
i
Die automatische Erstellung von Statistiken zu einem Index lässt sich im Dialog zum Erstellen eines Indexes auf der Seite Optionen auch deaktivieren, indem Sie das voreingestellte Häkchen bei der Option Statistiken automatisch neu berechnen entfernen.
Dieselben Informationen, die bei den Details der Statistik zu sehen sind, kann man sich auch mit der folgenden SQL-Anweisung anzeigen lassen: DBCC SHOW_STATISTICS('Person.Person', PK_Person_BusinessEntityID)
124
2030-4.book Page 125 Monday, February 8, 2010 4:26 PM
Interne Verarbeitung von Abfragen
Als Parameter werden dabei der Name der Tabelle und der Name der Statistik übergeben. Genau genommen werden dabei drei Ergebnisse zurückgegeben, die auch in Abbildung 6.6 zu sehen sind. Die ersten beiden zeigen allgemeine Informationen zur Statistik. Die wichtigsten davon sind die folgenden: 쮿
Name – Bezeichnung der Statistik
쮿
Updated – Datum der letzten Aktualisierung. Dieses sollte nicht allzu lange zurückliegen. Spätestens dann, wenn das mehr als einen Monat her ist, sollten Sie die Statistik aktualisieren.
쮿
Rows – Anzahl Zeilen der betroffenen Tabelle
쮿
Rows Sampled – Anzahl Zeilen, die gelesen wurden, um die Statistik aufzubauen. Dies können alle Zeilen sein (was der Fall ist, wenn die Statistik beim Erstellen eines Indexes angelegt wurde) oder auch nur ein Teil davon, was einerseits Platz (und Zeit beim Aktualisieren) spart, andererseits aber auch ungenauere Ergebnisse liefern kann.
쮿
Steps – Anzahl der Gruppen von Datensätzen (maximal 200), die gemeinsam analysiert wurden
쮿
Columns – Spalte(n), auf die sich die Statistik bezieht Wenn Sie feststellen, dass zwei Statistiken zur selben Spalte existieren (beispielsweise eine, die aufgrund einer Abfrage, und eine, die aufgrund eines Indexes erstellt wurde), können Sie die mit der geringeren Anzahl für den Wert Rows Sampled löschen. Dadurch wird der Aufwand für das Aktualisieren der Statistiken verringert und die andere Statistik enthält ohnehin dieselben (bzw. eventuell sogar genauere) Informationen. Das Löschen der überflüssigen Statistik geht entweder im Objekt-Explorer über das Kontextmenü oder mit der folgenden SQL-Anweisung: DROP STATISTICS Tabellenname.Statistikname
In der dritten Ergebnisliste schließlich sind die Analysen für jede der Gruppen zu finden, die gemeinsam ein Histogramm für die Verteilung der Werte bilden. Hier sind vor allem folgende Werte interessant: 쮿
RANGE_HI_KEY – höchster Schlüsselwert für diesen Bereich
쮿
RANGE_ROWS – Anzahl der Zeilen, die in den Bereich zwischen diesem und dem vorigen Schlüsselwert fallen (ohne den Schlüsselwert selbst)
쮿
EQ_ROWS – Anzahl der Zeilen, die genau den höchsten Schlüsselwert haben
쮿
DISTINCT_RANGE_ROWS – Anzahl der unterschiedlichen Werte in diesem Bereich, die einen anderen Wert als den Schlüsselwert haben
쮿
AVG_RANGE_ROWS – durchschnittliche Zeilenanzahl pro Wert in diesem Bereich
SQL Server Performance-Ratgeber
125
2030-4.book Page 126 Monday, February 8, 2010 4:26 PM
6 – Performancegrundlagen für Entwickler
Bei Bedarf können Sie sogar Statistiken mit folgender Anweisung explizit erstellen: CREATE STATISTICS Statistikname ON Tabelle (Feldnamen)
In den meisten Fällen sollten jedoch die automatisch erstellten Statistiken völlig ausreichen, sodass man sich eher darauf konzentrieren kann, diese auch aktuell zu halten.
6.1.5
Wiederverwendung von Ausführungsplänen
Der Nutzen von Ausführungsplänen wurde bereits weiter oben in diesem Kapitel behandelt. Die Erstellung eines Ausführungsplans kann jedoch – gerade bei komplexeren Abfragen – sehr prozessorintensiv sein. Damit nicht bei jeder Abfrage zwingend ein neuer Ausführungsplan erstellt werden muss, gibt es den so genannten Plancache, in dem Ausführungspläne zur Wiederverwendung zwischengespeichert werden. Genau genommen gibt es zwei Plancaches sowie zwei weitere Arten von Prozedurcaches, die verschiedene Zwecke erfüllen: 쮿
CACHESTORE_OBJCP: speichert „Object Plans“ für gespeicherte Prozeduren, Funktionen und Trigger
쮿
CACHESTORE_SQLCP: speichert SQL-Pläne für Ad-hoc-Abfragen
쮿
CACHESTORE_PHDR: speichert die von Parser und Algebrizer erzeugten Ausführungsbäume, die für Views, Constraints und Defaults verwendet werden, da für diese keine fertigen Ausführungspläne gepuffert werden
쮿
CACHESTORE_XPROC: wird für Extended Stored Procedures (kurz: Xprocs) verwendet
Für die Wiederverwendung von Ausführungsplänen sind vor allem die ersten beiden Caches interessant. Um schnell und effektiv prüfen zu können, ob für eine Abfrage bereits ein Ausführungsplan existiert, wird zu jedem Plan ein Hash-Wert gespeichert, der auf Basis der Abfrage (inklusive Kommentaren und Leerzeichen) erzeugt wird. Für jede auszuführende Abfrage wird nach demselben Verfahren ein Hash-Wert gebildet und anschließend geprüft, ob bereits ein Ausführungsplan für eine Abfrage mit demselben Hash-Wert existiert. Ist dies der Fall, kann der existierende Ausführungsplan verwendet werden, ansonsten wird ein neuer Plan erstellt. Welche Pläne im Plancache enthalten sind, lässt sich leicht mithilfe einer dynamischen Management-Sicht herausfinden: SELECT * FROM sys.dm_exec_cached_plans
Das Ergebnis der Abfrage ist jedoch recht kryptisch. Wenn man aber den darin enthaltenen plan_handle mit anderen Systemtabellen und -sichten verknüpft, kann man damit an deutlich aussagekräftigere Informationen zu den gespeicherten Ausführungsplänen kommen:
126
2030-4.book Page 127 Monday, February 8, 2010 4:26 PM
Interne Verarbeitung von Abfragen
SELECT st.[text] AS Abfrage, cp.cacheobjtype, cp.objtype, qs.execution_count, qs.query_plan_hash, qp.query_plan FROM sys.dm_exec_query_stats AS qs INNER JOIN sys.dm_exec_cached_plans AS cp ON qs.plan_handle=cp.plan_handle CROSS APPLY sys.dm_exec_sql_text(qs.[sql_handle]) AS st CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) AS qp ORDER BY qs.last_execution_time DESC
Abbildung 6.7: Durch DMVs ermittelte Informationen zu gespeicherten Ausführungsplänen
Dabei enthält die erste Spalte die eigentliche SQL-Abfrage, gefolgt von Cache- und Abfragetyp, der Ausführungshäufigkeit und dem bereits erwähnten Hash-Wert, der verwendet wird, um gleiche Abfragen wiederzuerkennen. Die letzte Spalte schließlich enthält den eigentlichen Ausführungsplan im XML-Format. Durch Anklicken eines Feldes darin wird der Ausführungsplan in grafischer Form angezeigt.
i
Die verwendeten Systemtabellen und -sichten enthalten noch eine ganze Reihe weiterer interessanter Informationen, auf die ich hier aus Platzgründen jedoch nicht detailliert eingehen will. Durch Verwendung von SELECT * können Sie aber schnell prüfen, welche der anderen Felder sonst noch interessant für Sie sein könnten.
In den meisten Fällen wird die Liste der gespeicherten Ausführungspläne relativ lang sein. Daher wurde einerseits eine Sortierung verwendet, mit der die zuletzt verwendeten Ausführungspläne zuerst angezeigt werden. Noch übersichtlicher wird es, wenn man zuvor mit der folgenden Anweisung auf einen Schlag den kompletten Plancache leert: DBCC FREEPROCCACHE
SQL Server Performance-Ratgeber
127
2030-4.book Page 128 Monday, February 8, 2010 4:26 PM
6 – Performancegrundlagen für Entwickler
Manchmal kann es auch sinnvoll sein, trotz vorhandenem Ausführungsplan diesen nicht zu verwenden, sondern explizit einen neuen zu erstellen. Das ist beispielsweise dann der Fall, wenn sich die Mengengerüste der beteiligten Tabellen massiv geändert haben. Das Erstellen eines neuen Ausführungsplans kann durch Verwendung der Abfrageoption RECOMPILE erreicht werden. Der so erstellte neue Plan wird jedoch nur einmal verwendet und nicht gespeichert. Bei der nächsten Ausführung derselben Abfrage wird daher wieder der zuletzt gespeicherte Plan verwendet. Beispiel: SELECT pers.FirstName, pers.LastName FROM Person.Person AS pers WHERE pers.LastName = 'Gonzalez' OPTION (RECOMPILE)
in Plan-Cache vorhanden? keinen Plan gefunden
Abfrage parsen kompilierten Plan gefunden
automatisch parametrisieren Daten binden/Views auflösen
ausführbaren Plan gefunden
Abfrageoptimierung ausführbaren Plan erstellen RAM- und CPU-Zuweisung Abfrage ausführen
WITH RECOMPILE? nein
Plan in Plan-Cache ablegen
ja
Fertig! Abbildung 6.8: Verarbeitung von Abfragen und Ausführungsplänen
128
2030-4.book Page 129 Monday, February 8, 2010 4:26 PM
Interne Verarbeitung von Abfragen
6.1.6
Parametrisierung von Abfragen
Um die Chance für die Wiederverwendung eines Ausführungsplans zu erhöhen, wäre es natürlich am einfachsten, für vergleichbare Abfragen immer dieselbe Schreibweise zu verwenden, damit daraus auch stets derselbe Hash-Wert resultiert. Dies ist jedoch in der Praxis kaum umsetzbar. Insbesondere dadurch, dass eine Abfrage oft mit verschiedenen Werten aufgerufen wird, ist derselbe Hash-Wert nicht mehr gegeben. So werden für die folgenden zwei Abfragen auch zwei separate (wenn auch inhaltlich identische) Ausführungspläne erstellt: SELECT TOP 10 * FROM Person.Person WHERE LastName = 'Gonzalez' SELECT TOP 10 * FROM Person.Person WHERE LastName = 'Miller'
Es ist offensichtlich, dass das Neuerstellen des Ausführungsplans für die zweite Abfrage einen unnötigen Aufwand darstellt, da eigentlich der Ausführungsplan der ersten Abfrage wiederverwendet werden könnte. Um diese unnötige und prozessorintensive Operation einzusparen, gibt es verschiedene Ansätze: 쮿
Auto-Parametrisierung verwenden
쮿
Abfragen explizit parametrisieren
쮿
gespeicherte Prozeduren nutzen
Alle drei Varianten basieren auf der Grundidee, den Wert, der in den Abfragebeispielen oben noch als Konstante angegeben wurde, durch Parameter zu ersetzen. Dadurch kann ein Ausführungsplan erstellt werden, der unabhängig von den jeweils verwendeten Parameterwerten immer wieder genutzt werden kann.
i
Die Parametrisierung von Abfragen ist sowohl für den Zugriff über SQL als auch für den Zugriff über Datenbank-APIs wie beispielsweise ADO.NET ein wichtiges Thema. Daher wird dieses auch in den beiden folgenden Kapiteln noch einmal explizit aufgegriffen.
Automatische Parametrisierung Die einfachste Variante ist die Nutzung der Auto-Parametrisierung. Dies bedeutet, dass für einfache Abfragen, die einen konstanten Wert (z.B. für einen Vergleich in einer WHERE-Klausel) verwenden, nicht ein Ausführungsplan für genau diesen einen Wert erstellt wird, sondern stattdessen der konstante Wert automatisch durch einen Parameter ersetzt wird, damit der Plan auch für andere Werte genutzt werden kann. Leider gibt es einige Voraussetzungen, damit die automatische Parametrisierung einwandfrei funktioniert. So darf beispielsweise weder DISTINCT, TOP, IN noch UNION SELECT verwendet werden. Die größte Einschränkung ist aber, dass die Abfrage nur auf einer Tabelle basieren darf.
SQL Server Performance-Ratgeber
129
2030-4.book Page 130 Monday, February 8, 2010 4:26 PM
6 – Performancegrundlagen für Entwickler
Sind die genannten Voraussetzungen aber erfüllt, nimmt SQL Server die Parametrisierung automatisch vor. Um dies nachzuprüfen, lassen Sie sich zu der folgenden Abfrage den Ausführungsplan anzeigen: SELECT * FROM Person.Person WHERE LastName = 'Gonzalez'
Wenn Sie den voraussichtlichen Ausführungsplan anzeigen lassen, ist nichts zu erkennen. Aktivieren Sie stattdessen die Anzeige des tatsächlichen Ausführungsplans und führen Sie die Abfrage anschließend aus, so ist zu sehen, dass der als konstante Wert übergebene Nachname durch einen Parameter (@1) ersetzt wurde, wodurch der Ausführungsplan auch für weitere Aufrufe mit anderen Nachnamen wiederverwendbar wird.
Abbildung 6.9: Am Ausführungsplan ist zu erkennen, dass die Abfrage parametrisiert wurde
Bei dem Beispiel weiter oben funktioniert dies nicht, da dort die Verwendung des Zusatzes TOP 10 die automatische Parametrisierung verhindert. Es gibt allerdings eine Datenbankeinstellung, mit der sich das Parametrisieren von Abfragen erzwingen lässt. Diese Einstellung heißt Parametrisierung und ist in den Datenbankeigenschaften unter Optionen zu finden. Gültige Werte für diese Einstellung sind Einfach und Erzwungen. In der Standardeinstellung gelten die weiter oben genannten Restriktionen. Stellen Sie die Option jedoch auf Erzwungen um, so versucht der SQL Server zu parametrisieren, was nur irgendwie zu parametrisieren ist. Wenn Sie nun die folgende Abfrage ausführen, werden Sie feststellen, dass nun auch diese parametrisiert wird: SELECT TOP 10 * FROM Person.Person WHERE LastName = 'Gonzalez'
130
2030-4.book Page 131 Monday, February 8, 2010 4:26 PM
Interne Verarbeitung von Abfragen
Diese Möglichkeit sollte Sie jedoch nicht dazu veranlassen, immer und überall nur die erzwungene Parametrisierung zu nutzen (die Standardeinstellung ist nicht umsonst Einfach). Denn gerade bei komplexeren Abfragen kann es oft sinnvoll sein, für andere Parameterwerte auch einen anderen Ausführungsplan zu erstellen. Alternativ zur Änderung über die Datenbankeigenschaften können Sie die Einstellung des Parametrisierungsmodus auch per SQL-Anweisung umstellen: ALTER DATABASE [AdventureWorks2008] SET PARAMETERIZATION SIMPLE|FORCED WITH NO_WAIT
Das ermöglicht es, in einem Skript die erzwungene Parametrisierung für einzelne Anweisungen zu aktivieren und nach deren Ausführung wieder auf die standardmäßige einfache Parametrisierung umzuschalten: -- Umstellen auf erzwungene Parametrisierung ALTER DATABASE [AdventureWorks2008] SET PARAMETERIZATION FORCED WITH NO_WAIT GO -- Abfrage mit erzwungener Parametrisierung ausführen SELECT TOP 10 * FROM Person.Person WHERE LastName = 'Gonzalez' -- Zurückstellen auf einfache Parametrisierung ALTER DATABASE [AdventureWorks2008] SET PARAMETERIZATION SIMPLE WITH NO_WAIT GO Listing 6.1: Ausführung einer Abfrage mit erzwungener Parametrisierung
Explizite Parametrisierung Besser steuern lässt sich die Parametrisierung von Abfragen, wenn Sie diese explizit erzwingen. Dazu wird die Abfrage quasi als dynamische SQL-Anweisung erstellt und anschließend über die EXEC-Anweisung aufgerufen. Dabei können Parameter übergeben werden, die vorher in Variablen vorbereitet wurden: DECLARE @LastName NVARCHAR(50) SET @LastName = 'Gonzalez' EXEC ('SELECT * FROM Person.Person WHERE LastName = ''' + @LastName + '''') Listing 6.2: Explizite Parametrisierung durch Variablennutzung
Interessant ist auch der Unterschied zwischen geplantem und tatsächlichem Ausführungsplan. Bereits der geplante Ausführungsplan lässt erahnen, dass hier eine Parametrisierung stattfindet, da die Execute-Anweisung hier als einzelne Anweisung auftaucht, der neben der konstanten Zeichenkette mit der SELECT-Anweisung auch eine Variable übergeben wird. Aber erst der tatsächliche Ausführungsplan stellt den Plan dar, der für die Ausführung der SELECT-Abfrage verwendet wird.
SQL Server Performance-Ratgeber
131
2030-4.book Page 132 Monday, February 8, 2010 4:26 PM
6 – Performancegrundlagen für Entwickler
Im Gegensatz zur automatischen Parametrisierung lässt sich dabei ganz genau steuern, welche Werte als Parameter und welche als konstante Werte übergeben werden. Dazu eignet sich diese Variante besonders gut für Ad-hoc-Abfragen, da es keine Rolle spielt, ob die Abfrage eventuell mehrfach benötigt wird oder nicht.
Parametrisierung durch gespeicherte Prozeduren Die dritte und sicherlich flexibelste Möglichkeit zur Parametrisierung bietet die Verwendung von gespeicherten Prozeduren. Ähnlich wie bei der expliziten Parametrisierung haben Sie auch hier genauen Einfluss darauf, welche Werte als Konstanten und welche als Parameter im Ausführungsplan verwendet werden. Um beim selben Beispiel zu bleiben, kann man folgende gespeicherte Prozedur erstellen: CREATE PROCEDURE Person.uspShowPersonsByLastName @LastName NVARCHAR(50) AS BEGIN SELECT * FROM Person.Person WHERE LastName = @LastName END GO Listing 6.3: Parametrisierung durch gespeicherte Prozeduren
Hierbei wird noch deutlicher, welche Werte im Plan als Parameter verwendet werden, da dies auch genau den Parametern der gespeicherten Prozedur entspricht. Der Aufruf erfolgt wiederum über die EXEC-Anweisung: exec Person.uspShowPersonsByLastName 'Gonzalez' exec Person.uspShowPersonsByLastName 'Miller'
Wenn Sie die Anzeige des tatsächlichen Ausführungsplans aktiviert haben, ist schnell zu sehen, dass für beide Aufrufe derselbe Plan verwendet wird. Da die gespeicherte Prozedur natürlich dauerhaft erhalten bleibt, macht diese Variante nur Sinn, wenn die in der gespeicherten Prozedur enthaltene Abfrage auch wirklich mehrfach benötigt wird. Der Abfrageplan, der für die gespeicherte Prozedur erstellt wird, wird für die Parameterwerte optimiert, die beim ersten Aufruf übergeben werden. Sollte es sich dabei nicht um repräsentative Werte handeln, kann es sein, dass ein Plan erstellt wird, der für diese Werte zwar sinnvoll, für typische Werte aber nicht optimal ist. Auch Änderungen der Parameterwerte innerhalb der gespeicherten Prozedur werden hierbei leider nicht berücksichtigt. Diesem Problem kann man aber begegnen, indem man die Abfrage mithilfe des Abfragehinweises OPTIMIZE FOR für bestimmte Parameterwerte optimieren lässt. Weitere Informationen dazu finden Sie in Kapitel 7.1.5 Ausführungspläne beeinflussen.
132
2030-4.book Page 133 Monday, February 8, 2010 4:26 PM
Sperren und Transaktionen
6.2
Sperren und Transaktionen
6.2.1
Locking und Blocking
Jeder Zugriff auf ein Datenbankobjekt erzeugt eine Sperre, damit dieses Objekt nicht zeitgleich von einer anderen Datenbankverbindung geändert werden kann und somit Änderungen verloren gehen. Im einfachsten Fall werden die Daten auf Zeilenebene gesperrt, um andere Prozesse möglichst wenig zu behindern. Je nach Art und Umfang der Datenänderung kann es aber auch erforderlich oder zumindest sinnvoll sein, ganze Speicherseiten, Tabellen oder gar Datenbanken zu sperren. Da das Sperren von DB-Objekten einen gewissen Verwaltungs-Overhead pro Objekt mit sich bringt, wird ab einem gewissen Schwellwert automatisch das nächstgrößere Objekt gesperrt. Dadurch wird insbesondere verhindert, dass die Verwaltung der Sperren aufwändiger wird als die eigentliche Datenbankoperation selbst. Wenn also beispielsweise zu viele Zeilen auf einer Speicherseite gesperrt sind, werden die vielen Zeilensperren automatisch durch eine einzelne Seitensperre ersetzt. Sind zu viele Speicherseiten einer Tabelle gesperrt, werden die Seitensperren durch eine Tabellensperre ersetzt. Dieser Vorgang wird auch als Lock Escalation bezeichnet. Will man nun auf ein von einer anderen Verbindung gesperrtes Objekt zugreifen, so muss gewartet werden, bis der bestehende Zugriff beendet ist und das Objekt damit wieder freigegeben wird. Es gibt verschiedene Arten von Sperren, die je nach verwendeter Datenbankoperation gesetzt werden. Die drei wichtigsten möchte ich kurz vorstellen. Beim lesenden Zugriff wird ein so genannter Shared Lock gesetzt. Dies kann für ein und dieselbe Datenzeile mehrfach geschehen, da es kein Problem darstellt, wenn verschiedene Prozesse dieselben Daten nahezu zeitgleich lesen (daher auch die Bezeichnung Shared Lock). Der Shared Lock verhindert lediglich das Durchführen einer Datenänderung, während die Daten gelesen werden. Soll eine Änderung durchgeführt werden, so wird ein Update Lock gesetzt, der lediglich die Absicht dokumentiert, die so gesperrten Datenzeilen zu ändern. Ein Update Lock kann gleichzeitig mit einem oder mehreren Shared Locks vorliegen. Sobald die Shared Locks wieder freigegeben sind, wird der Update Lock zu einem Exclusive Lock gewandelt. Der Prozess hat also die exklusiven Zugriffsrechte auf die betreffenden Daten und kann die Änderung durchführen. Währenddessen ist kein anderer (auch nicht lesender) Zugriff auf dieselben Daten möglich. Andere Prozesse, die auf die Daten zugreifen wollen, werden so lange blockiert, bis die Änderung durchgeführt und die exklusive Sperre wieder freigegeben wurde. Um Sperren und insbesondere Blockierungen aufzuspüren, bietet SQL Server verschiedene Möglichkeiten. Die einfachste Variante ist der Aktivitätsmonitor, der beim SQL Server 2008 durch einen Rechtsklick auf den Server oder über die Symbolleiste aufrufbar ist. Der Aktivitätsmonitor stellt seine Ergebnisse in verschiedenen Bereichen dar, die einzeln auf- und zugeklappt werden können (mehr dazu in Kapitel 9.3 Allgemeine Prüfung des Systems). Die Informationen zu Sperren und Blockierungen finden Sie in dem Bereich Prozesse. Hier sind alle Prozesse aufgelistet, die gerade auf dem SQL Server laufen. Ist
SQL Server Performance-Ratgeber
133
2030-4.book Page 134 Monday, February 8, 2010 4:26 PM
6 – Performancegrundlagen für Entwickler
ein Prozess (hier auch als Sitzung bezeichnet) von einem anderen blockiert, so finden Sie in der Spalte Blockiert von die Sitzungs-ID des blockierenden Prozesses. Mit einem Rechtsklick auf den blockierenden Prozess können Sie die Details des Prozesses anzeigen (insbesondere die letzte von diesem SQL-Prozess ausgeführte SQL-Anweisung) und diesen bei Bedarf sogar direkt abbrechen, damit der blockierte Prozess weiterarbeiten kann. Bei SQL Server 2005 war die entsprechende Ansicht sogar noch etwas detaillierter, zumal es hier separate Ansichten gab, in denen man sich die Sperren entweder nach Objekt oder nach Prozess anzeigen lassen konnte. Am schnellsten – und vor allem unabhängig von der verwendeten SQL-Server-Version – bekommt man die Auskunft über die blockierenden und blockierten Prozesse aber über SQL-Abfragen auf Systemtabellen und dynamische Management-Sichten. SELECT ses.session_id, ses.login_name, ses.[status], ses.[program_name], ses.[host_name], wait.blocking_session_id FROM sys.dm_exec_sessions AS ses LEFT JOIN sys.dm_os_waiting_tasks AS wait ON ses.session_id=wait.session_id WHERE ses.is_user_process=1
Die Ausgabe entspricht ungefähr der Ansicht, die Sie im Aktivitätsmonitor von SQL Server 2008 erhalten. Wenn Sie den LEFT JOIN gegen einen INNER JOIN austauschen, werden nur die Prozesse aufgelistet, die von einem anderen Prozess blockiert sind. Auch die aktiven Sperren lassen sich mithilfe einer Abfrage einfach anzeigen: SELECT lock.resource_type, lock.resource_database_id, db.name AS database_name, lock.request_mode, lock.request_type, lock.request_status, lock.request_session_id FROM sys.dm_tran_locks AS lock INNER JOIN sys.databases AS db ON lock.resource_database_id=db.database_id
Dabei ist in der Spalte request_mode zu erkennen, um was für eine Art von Sperre es sich handelt. S entspricht beispielsweise dem weiter oben erwähnten Shared Lock.
6.2.2 Transaktionen Transaktionen sind Einheiten von Anweisungen, die entweder ganz oder gar nicht ausgeführt werden. Dabei erheben Transaktionen den Anspruch, dem ACID-Prinzip gerecht zu werden: 쮿
A – Atomar: Die Transaktion wird ganz oder gar nicht ausgeführt.
쮿
C – Consistent: Die Datenintegrität wird gewährleistet, die Daten sind also stets konsistent.
134
2030-4.book Page 135 Monday, February 8, 2010 4:26 PM
Sperren und Transaktionen 쮿
I – Isolated: Gleichzeitig laufende Transaktionen dürfen sich nicht gegenseitig beeinflussen.
쮿
D – Durable: Änderungen aus einer erfolgreich abgeschlossenen Transaktion sind dauerhaft.
Implizite und explizite Transaktionen Implizit ist jede SQL-Anweisung, die den Server erreicht, eine Transaktion für sich. Wenn also beispielsweise mit einer UPDATE-Anweisung mehrere Zeilen einer Tabelle geändert werden sollen, aber bereits nach den ersten Zeilen ein Fehler auftritt, wird damit der gesamte UPDATE rückgängig gemacht. Explizit lassen sich aber auch mehrere Anweisungen zu einer Transaktion zusammenfassen, was insbesondere verwendet wird, um die Konsistenz der Daten zu gewährleisten. Dazu wird die Transaktion mit der Anweisung BEGIN TRANSACTION gestartet und später entweder mit COMMIT TRANSACTION bestätigt oder mit ROLLBACK TRANSACTION zurückgerollt, wobei alle Datenänderungen, die innerhalb der Transaktion vorgenommen wurden, wieder rückgängig gemacht werden. Das klassische Beispiel hierzu ist eine Banküberweisung. Ein Geldbetrag wird von einem Konto abgebucht und anschließend auf einem anderen Konto gutgeschrieben. Bei einem Systemausfall direkt nach dem Abbuchen wäre der Geldbetrag verloren, da dieser ja bereits abgebucht, aber noch nirgendwo gutgeschrieben werden konnte. Mit einer Transaktion um den gesamten Vorgang wird der Betrag erst dann endgültig von einem Konto abgebucht, wenn er auf dem Zielkonto gutgeschrieben werden konnte. Ist dies nicht möglich (z.B. durch einen Systemausfall), so ist der Geldbetrag nach wie vor auf dem ursprünglichen Konto vorhanden. Damit dies funktioniert, werden die Daten innerhalb der Transaktion gesperrt und erst dann wieder freigegeben, wenn die Transaktion erfolgreich abgeschlossen (COMMIT) oder komplett rückgängig gemacht wurde (ROLLBACK).
i
Da Tabellenvariablen per Definition keine persistenten Objekte sind, bleiben diese auch nach einem ROLLBACK erhalten.
Geschachtelte Transaktionen Transaktionen lassen sich auch ineinander schachteln. Dabei ist das Verhalten für COMMIT und ROLLBACK aber nicht ganz selbsterklärend: 쮿
Wenn die innere und die äußere Transaktion committed werden, werden die Datenänderungen dauerhaft gespeichert.
쮿
Wenn die innere Transaktion committed und die äußere Transaktion zurückgerollt wird, werden die Datenänderungen aus der inneren Transaktion ebenfalls zurückgerollt.
쮿
Wenn die innere Transaktion zurückgerollt wird, werden die Datenänderungen aus der äußeren Transaktion ebenfalls automatisch rückgängig gemacht.
SQL Server Performance-Ratgeber
135
2030-4.book Page 136 Monday, February 8, 2010 4:26 PM
6 – Performancegrundlagen für Entwickler
Ein ROLLBACK – egal ob in der inneren oder äußeren Transaktion – macht also alle noch offenen Transaktionen rückgängig. Daraus ergibt sich auch, dass keine autonomen Transaktionen von SQL Server unterstützt werden. Es ist also nicht möglich, die Datenänderungen einer inneren Transaktion wirklich persistent zu machen, während eine äußere Transaktion noch geöffnet ist. Mithilfe der Systemvariablen @@TRANCOUNT lässt sich leicht ermitteln, wie viele Transaktionsebenen gerade geöffnet sind. Probieren Sie einmal das folgende SQL-Skript aus, das die Funktionsweise von geschachtelten Transaktionen verdeutlicht: PRINT 'Keine Transaktion geöffnet: ' + CONVERT(CHAR(3), @@TRANCOUNT) BEGIN TRANSACTION PRINT 'Eine Transaktion geöffnet: ' + CONVERT(CHAR(3), @@TRANCOUNT) BEGIN TRANSACTION BEGIN TRANSACTION PRINT 'Zweite und dritte Transaktion geöffnet: ' + CONVERT(CHAR(3), @@TRANCOUNT) COMMIT TRANSACTION PRINT 'Eine Transaktion bestätigt: ' + CONVERT(CHAR(3), @@TRANCOUNT) ROLLBACK TRANSACTION PRINT 'Transaktion zurückgerollt: ' + CONVERT(CHAR(3), @@TRANCOUNT) Listing 6.4: Skript zum Testen von geschachtelten Transaktionen
An den Meldungen, die das Skript ausgibt, ist zu erkennen, dass die ROLLBACKAnweisung tatsächlich beide noch offenen Transaktionen auf einmal rückgängig macht3: Keine Transaktion geöffnet: 0 Eine Transaktion geöffnet: 1 Zweite und dritte Transaktion geöffnet: 3 Eine Transaktion bestätigt: 2 Transaktion zurückgerollt: 0
Verteilte Transaktionen Sofern eine Transaktion sich auf mehr als eine Datenbank erstreckt, handelt es sich um eine verteilte Transaktion. Dabei werden lokale Transaktionen automatisch in verteilte Transaktionen gewandelt, sobald darin eine weitere Datenbank angesprochen wird. Um die Datenintegrität bei einer verteilten Transaktion zu gewährleisten, wird ein so
3.
136
Die CONVERT-Funktion ist hier nur nötig, damit der Inhalt der @@TRANCOUNT-Systemvariablen an die Zeichenkette davor angehängt werden kann.
2030-4.book Page 137 Monday, February 8, 2010 4:26 PM
Sperren und Transaktionen
genannter Two-Phase Commit genutzt. Dabei handelt es sich um ein zweistufiges Verfahren, in dessen ersten Schritt die Datenänderungen auf allen beteiligten Datenbanken vorbereitet und quasi vorläufig ausgeführt werden. Erst dann, wenn dies geschehen ist, werden die Änderungen in der zweiten Phase endgültig bestätigt. Über die dynamische Management-Sicht sys.dm_tran_session_transactions kann man sich alle offenen Transaktionen auflisten und dort anhand der Spalte is_local erkennen, ob es sich um eine lokale oder eine verteilte Transaktion handelt: SELECT * FROM sys.dm_tran_session_transactions
Transaktionen können auf verschiedenen Ebenen definiert werden. Dies kann einerseits in SQL-Skripts (und damit auch gespeicherten Prozeduren etc.) geschehen, aber auch direkt in einer .NET-Anwendung. Daher wird auf diese Thematik in den beiden folgenden Kapiteln noch einmal detailliert eingegangen.
Auswirkungen von Transaktionen auf die Performance Da Transaktionen sowohl Sperren verursachen als auch Speicher belegen, hat die Anzahl und Größe von Transaktionen direkte Auswirkung auf die Performance eines SQL Servers. Je höher der Grad der Isolation, der durch Transaktionen erzeugt wird, desto mehr Sperren werden erzeugt. Dies hat zur Folge, dass weniger Datenbankzugriffe effektiv parallel laufen können, da diese darauf warten, auf von anderen Transaktionen gesperrte Daten zugreifen zu können. Dazu kommt die Tatsache, dass jeder COMMIT in Form von synchronen Schreibzugriffen ausgeführt wird, damit sichergestellt ist, dass die Daten wirklich dauerhaft geschrieben wurden. Dies kann allerdings – je nach Umfang der auszuführenden Schreiboperationen – eine spürbare Pause vor der weiteren Ausführung der Anwendung zur Folge haben. Dieser Effekt lässt sich deutlich verringern, indem man die Transaktionen möglichst kurz hält. Auf der anderen Seite erzeugt jede Transaktion auch einen gewissen Verwaltungs-Overhead, sodass es – wenn man ausschließen kann, dass sich parallel laufende Transaktionen gegenseitig behindern – wiederum effektiver ist, viele kleine Datenänderungsoperationen in einer großen Transaktion zusammenzufassen. Hier gilt es also – wie so oft – den goldenen Mittelweg zu finden und die Transaktionen so zu dimensionieren, dass der Verwaltungs-Overhead minimiert wird, die Transaktionen aber klein genug sind, um sich nicht gegenseitig unnötig durch Sperren zu behindern. Letzteres kann man durch den bewussten Einsatz von Isolationsstufen optimieren.
6.2.3 Isolationsstufen (Isolation Level) Um das Sperren von Datenbankobjekten durch Transaktionen etwas flexibler zu gestalten, kann man verschiedene Isolationsstufen nutzen. Schon in älteren SQL-Server-Versionen gab es vier grundlegende Isolationsstufen, über die gesteuert werden konnte, in welchem Ausmaß Sperren gesetzt werden:
SQL Server Performance-Ratgeber
137
2030-4.book Page 138 Monday, February 8, 2010 4:26 PM
6 – Performancegrundlagen für Entwickler 쮿
READ UNCOMMITTED – Bei dieser Isolationsstufe können Daten auch dann gelesen werden, wenn sie von anderen offenen Transaktionen geändert wurden. Transaktionen, die als READ COMMITTED laufen, werden also nicht von anderen Transaktionen gesperrt und sperren auch keine anderen Transaktionen. Als Folge kann es allerdings zu so genannten Dirty Reads kommen, nämlich zum Lesen von Daten, die aus einer offenen Transaktion stammen, die eventuell wieder zurückgerollt wird.
쮿
READ COMMITTED – Daten, die von anderen offenen Transaktionen geändert wurden, können nicht gelesen werden. Daten, die von der aktuellen Transaktion gelesen wurden, können von anderen Transaktionen geändert werden (allerdings nicht zeitgleich zur Leseoperation), wodurch so genannte nonrepeatable reads entstehen. READ COMMITTED ist die Standard-Isolationsstufe, wenn nicht explizit eine andere gesetzt wird.
쮿
REPEATABLE READ – Daten, die von anderen offenen Transaktionen geändert wurden, können nicht gelesen werden. Daten, die von der aktuellen Transaktion gelesen wurden, werden durch einen Shared Lock für Schreibzugriffe gesperrt, damit ein weiterer Lesevorgang derselben Zeilen auch dieselben Daten zurückliefert (daher die Bezeichnung REPEATABLE READ).
쮿
SERIALIZABLE – Diese Isolationsstufe entspricht weitgehend dem REPEATABLE READ, verhindert aber zusätzlich, dass andere Transaktionen neue Zeilen erzeugen, die sich auf Abfragen der aktuellen Transaktion auswirken würden. Dadurch wird die Entstehung von so genannten Phantomzeilen ausgeschlossen.
Seit SQL Server 2005 kam noch eine weitere Variante dazu, mit denen Sperren komplett vermieden werden können: 쮿
SNAPSHOT – Liest Zeilen immer in dem Zustand, den sie zu Beginn der Transaktion hatten (es sei denn, die Änderungen stammen aus derselben Transaktion). Dadurch können stets konsistente Daten gelesen werden ohne die Notwendigkeit, Sperren zu setzen. Damit diese Isolationsstufe genutzt werden kann, muss die Datenbankoption ALLOW_SNAPSHOT_ISOLATION aktiviert sein.
Zusätzlich gibt es ab SQL Server 2005 eine weitere Datenbankoption (READ_ COMMITTED_SNAPSHOT), mit der das Verhalten der Isolationsstufe READ COMMITTED beeinflusst werden kann. Ist diese Option aktiviert, wird – anstatt die Daten für eine Anweisung mit einem Shared Lock zu belegen – ein Snapshot erzeugt, der die Daten mit dem Stand, den sie zu Beginn der Anweisung hatten, zur Verfügung stellt. Im Gegensatz zur Isolationsstufe SNAPSHOT wird der Snapshot hier nur für die Dauer jeweils einer Anweisung und nicht für die gesamte Transaktion aufrechterhalten. Das Setzen der Isolationsstufen für eine Transaktion erfolgt mit der SQL-Anweisung SET TRANSACTION ISOLATION LEVEL, wie folgendes Beispiel zeigt: SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
Dies kann zu Beginn, aber auch irgendwo innerhalb einer Transaktion geschehen. Dadurch kann die Isolationsstufe auch bei laufender Transaktion gewechselt werden. Lediglich das Setzen der Isolationsstufe SNAPSHOT muss bereits zu Beginn der Transaktion geschehen, da der Snapshot natürlich nicht rückwirkend erstellt werden kann.
138
2030-4.book Page 139 Monday, February 8, 2010 4:26 PM
Sperren und Transaktionen
6.2.4 Tabellenhinweise So wie sich über die Wahl der richtigen Isolationsstufe das Sperrverhalten für ganze Transaktionen steuern lässt, kann man dies durch die Verwendung von Sperrhinweisen für Tabellen (Table Hints) auch für einzelne Abfragen erreichen. Tabellenhinweise lassen sich mit SELECT-, INSERT-, UPDATE-, DELETE- und MERGEAnweisungen verwenden und werden – wie es der Name bereits vermuten lässt – pro Tabelle angegeben. Dies geschieht direkt in der FROM-Klausel bei der Angabe der jeweiligen Tabelle mit dem Zusatz WITH, wobei dahinter in Klammern ein oder mehrere Tabellenhinweise (kommagetrennt) folgen. Beispiel: SELECT * FROM Person.Person WITH (NOLOCK) WHERE Name = 'Müller'
i
Es gibt eine ganze Reihe von Tabellenhinweisen, wovon sich nur ein Teil auf das Sperrverhalten auswirkt. Die anderen performancerelevanten Tabellenhinweise werden zusammen mit weiteren Abfragehinweisen im nächsten Kapitel behandelt.
Mit folgenden Tabellenhinweisen wird der Isolationslevel für eine Abfrage gesetzt: 쮿
READUNCOMMITTED – entspricht der gleichnamigen Isolationsstufe, liest also trotz eventuell vorhandener Sperren und sperrt auch selbst keine Datenzeilen
쮿
NOLOCK – alternative Bezeichnung für den Tabellenhinweis READUNCOMMITTED
쮿
READCOMMITTED – entspricht der gleichnamigen Isolationsstufe, liest also keine Daten, die von anderen offenen Transaktionen geändert werden, und sperrt Daten mit einem Shared Lock, während sie gelesen werden (bei aktiviertem READ_ COMMITTED_SNAPSHOT werden die Daten auf Basis eines Snapshot – also mit dem Stand zu Beginn der Anweisung – gelesen und dadurch auch keine Sperren erzeugt)
쮿
READCOMMITTEDLOCK – entspricht READ COMMITTED, verwendet aber unabhängig von der Datenbankeinstellung READ_COMMITTED_SNAPSHOT immer Shared Locks
쮿
REPEATABLEREAD – entspricht der gleichnamigen Isolationsstufe, liest also keine Daten, die von anderen offenen Transaktionen geändert werden, und sperrt Daten mit einem Shared Lock, während sie gelesen werden (im Gegensatz zu READCOMMITTED aber nicht bis zum Ende der Abfrage, sondern bis zum Ende der Transaktion)
쮿
SERIALIZABLE – entspricht der gleichnamigen Isolationsstufe und ist damit noch restriktiver als ein REPEATABLEREAD, da zusätzlich verhindert wird, dass andere Transaktionen zeitgleich neue Zeilen erzeugen, die sich auf die aktuelle Abfrage auswirken würden
쮿
HOLDLOCK – alternative Bezeichnung für den Tabellenhinweis SERIALIZABLE
SQL Server Performance-Ratgeber
139
2030-4.book Page 140 Monday, February 8, 2010 4:26 PM
6 – Performancegrundlagen für Entwickler
In Kombination mit den Isolationsleveln READCOMMITTED und REPEATABLEREAD kann noch der zusätzliche Hinweis READPAST verwendet werden, der dafür sorgt, dass von anderen Transaktionen gesperrte Zeilen beim Lesen einfach übersprungen werden. Dabei sperrt die Abfrage mit dem READPAST-Hinweis selbst keine Zeilen der betroffenen Tabelle. Dazu gibt es weitere Hinweise, mit denen die Granularität einer Sperre beeinflusst werden kann. Dadurch kann beispielsweise eine während des Abfrageverlaufs eventuell notwendige Lock Escalation vorweggenommen werden: 쮿
NOLOCK – liest die Daten ohne sie zu sperren, was Dirty Reads zur Folge haben kann
쮿
ROWLOCK – sperrt die Daten zeilenweise
쮿
PAGLOCK – sperrt die Daten seitenweise
쮿
TABLOCK – sperrt die Daten für die ganze Tabelle
쮿
TABLOCKX – sperrt die Daten für die ganze Tabelle exklusiv (bis zum Ende der Abfrage)
Zusätzlich zur Granularität der Sperren kann durch entsprechende Hinweise dafür gesorgt werden, dass anstelle eines Shared Locks gleich restriktivere Sperren gesetzt – und bis zum Ende der Transaktion gehalten – werden. 쮿
UPDLOCK – setzt einen Update Lock
쮿
XLOCK – setzt eine exklusive Sperre
Aus den beiden oben aufgelisteten Kategorien (Hinweise für Isolationslevel und Hinweise für die Granularität) kann pro Tabelle nur ein Hinweis verwendet werden.
6.2.5 Deadlocks In manchen Fällen kann es vorkommen, dass zwei Transaktionen sich gegenseitig so blockieren, dass keine der beiden Transaktionen beendet werden kann. Dies ist beispielsweise dann der Fall, wenn Transaktion A einen Kundendatensatz geändert (und damit exklusiv gesperrt) hat und anschließend einen Bestelldatensatz ändern will, während Transaktion B denselben Bestelldatensatz geändert (und damit gesperrt hat) und auf die Freigabe des dazugehör