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örenden Kundendatensatzes wartet. Beide Transaktionen haben damit jeweils ein Objekt exklusiv gesperrt und warten auf die Freigabe des jeweils von der anderen Transaktion gesperrten Objektes. Damit kann eigentlich keine von beiden Transaktionen beendet werden und die Prozesse hängen. Man nennt diese Situation einen Deadlock. Um das Problem zu lösen, erkennt SQL Server solche Deadlocks automatisch und behebt diese, indem einer von beiden Prozessen als Deadlock-Opfer ausgewählt und mit folgender Meldung abgebrochen wird: Meldung 1205, Ebene 13, Status 56, Zeile 1
140
2030-4.book Page 141 Monday, February 8, 2010 4:26 PM
Sperren und Transaktionen
Die Transaktion (Prozess-ID 54) befand sich auf Sperre Ressourcen aufgrund eines anderen Prozesses in einer Deadlocksituation und wurde als Deadlockopfer ausgewählt. Führen Sie die Transaktion erneut aus. Somit kann wenigstens der andere Prozess komplett durchlaufen. Anschließend kann die abgebrochene Transaktion erneut gestartet werden (sofern die entsprechende Anwendung dies unterstützt). Die Auswahl des Deadlock-Opfers erfolgt anhand von zwei Kriterien: 1. Im Normalfall wird der Prozess als Deadlock-Opfer gewählt, der mit weniger Aufwand rückgängig zu machen ist (da für diesen ja ein kompletter Rollback erfolgt). 2. Durch das Setzen von Deadlock-Prioritäten lässt sich explizit steuern, welche Transaktionen eher als Deadlock-Opfer gewählt werden als andere. Haben zwei Prozesse, die sich gegenseitig sperren, unterschiedliche Prioritäten, so wird immer derjenige mit der niedrigeren Priorität als Deadlock-Opfer gewählt und abgebrochen. Das Setzen der Deadlock-Priorität geschieht mit folgender SQL-Anweisung: SET DEADLOCK_PRIORITY { LOW | NORMAL | HIGH |
}
Wenn Sie die Priorität als numerischen Wert angeben, sind ganzzahlige Werte von -10 bis +10 möglich. Für die meisten Fälle reichen aber die drei Standard-Prioritäten LOW (entspricht -5), NORMAL (entspricht 0) sowie HIGH (entspricht +5) aus, wobei NORMAL, wie der Name schon vermuten lässt, die Default-Priorität ist. Durch das explizite Ändern von Deadlock-Prioritäten während einer Transaktion können Sie beispielsweise dafür sorgen, dass eine Transaktion, die fast fertig ist, eher nicht als Deadlock-Opfer gewählt wird. Mit dem SQL Server Profiler lassen sich Deadlocks protokollieren und grafisch anzeigen.4 Erstellen Sie dazu einen Profiler Trace mit der Vorlage TSQL_Locks, die unter anderem die Ereignisse Deadlock Graph, Lock:Deadlock und Lock:Deadlock Chain enthält. Um nun einen Deadlock zu erzeugen, öffnen Sie im SQL Server Management Studio zwei Abfragefenster auf die AdventureWorks2008-Datenbank und starten Sie im ersten Fenster die folgenden Anweisungen: BEGIN TRANSACTION UPDATE Person.PhoneNumberType SET Name='Mobile' WHERE PhoneNumberTypeID=1
4.
Eine detaillierte Erläuterung des SQL Server Profiler ist in Kapitel 10.2 – SQL Server Profiler & Datenbankoptimierungsratgeber zu finden.
SQL Server Performance-Ratgeber
141
2030-4.book Page 142 Monday, February 8, 2010 4:26 PM
6 – Performancegrundlagen für Entwickler
Im zweiten Fenster wird nun auch eine Transaktion gestartet, die zuerst auf eine zweite Tabelle und danach ebenso auf die Tabelle PhoneNumberType schreibend zugreift: BEGIN TRANSACTION UPDATE Person.AddressType SET Name='daheim' WHERE AddressTypeID=2 UPDATE Person.PhoneNumberType SET Name='Handy' WHERE PhoneNumberTypeID=1
Die Transaktion wartet nun, bis die Tabelle PhoneNumberType von der ersten Transaktion wieder freigegeben wird. Wechseln Sie nun wieder zum ersten Abfragefenster und führen Sie dort ein Update auf die Tabelle AddressType aus, die von der zweiten Transaktion bereits gesperrt ist: UPDATE Person.AddressType SET Name='zu Hause' WHERE AddressTypeID=2
Beide Transaktionen haben nun eine Tabelle bereits gesperrt und warten auf die Freigabe der jeweils anderen Tabelle. Nach kurzer Zeit wird eine Transaktion als Deadlock-Opfer gewählt und abgebrochen. Sie können dort nun durch Anzeige der Systemvariablen @@TRANCOUNT prüfen, dass die Transaktion wirklich nicht mehr geöffnet ist. Die andere Transaktion konnte das zweite Update beenden, sodass man diese Transaktion nun mit einem COMMIT TRANSACTION dauerhaft machen könnte. Wenn Sie Ihre Beispieldatenbank aber nicht verändern möchten, können Sie hier explizit ein ROLLBACK TRANSACTION ausführen und alles ist wieder wie zuvor. Im SQL Server Profiler wurde der Deadlock ebenfalls bemerkt. Neben der Protokollierung der abgesendeten SQL-Befehle ist hier auch der Deadlock-Graph zu finden, in dem der Deadlock sowie die Wahl des Deadlock-Opfers anschaulich dargestellt werden.
142
2030-4.book Page 143 Monday, February 8, 2010 4:26 PM
Sperren und Transaktionen
Abbildung 6.10: Der Deadlock-Graph im SQL Server Profiler
Auch wenn die Erkennung von Deadlocks zuverlässig funktioniert, kostet sie einerseits Zeit und erfordert andererseits ein erneutes Starten des abgebrochenen Prozesses. Daher ist natürlich anzustreben, Deadlocks von vornherein zu vermeiden. Zwar lassen sich Deadlocks nicht generell ausschließen (sofern Sie die Datenbank nicht komplett auf Read Only oder Single-User-Betrieb schalten), man kann jedoch mit ein paar einfachen Regeln das Risiko des Auftretens von Deadlocks weitgehend minimieren: 쮿
Transaktionen so klein wie möglich halten Beschränken Sie Transaktionen auf das Wesentliche und halten Sie diese so klein wie möglich. Insbesondere umfangreiche Lesezugriffe können meist bereits vor Beginn der Transaktion erfolgen.
쮿
Keine Benutzerinteraktion während einer Transaktion Wenn Sie Transaktionen innerhalb einer Anwendung verwenden, sollte zwischen Beginn und Ende der Transaktion keine Benutzerinteraktion notwendig sein. Sonst besteht die Gefahr, dass bei einer laufenden Transaktion auf eine Eingabe gewartet wird, der entsprechende Benutzer aber bereits in die Mittagspause (oder schlimmer noch in einen mehrwöchigen Urlaub) verschwunden ist.
쮿
Niedrige Isolationsstufe verwenden Wie weiter oben bereits erwähnt, können Sie durch Setzen einer niedrigen Isolationsstufe die während einer Transaktion erzeugten Sperren minimieren. Nutzen Sie dies, um die gesetzten Sperren so frei wie möglich (aber natürlich trotzdem so restriktiv wie nötig) zu halten.
쮿
Sperrhinweise in Abfragen verwenden Analog zu den Isolationsstufen können Sie auch mit Sperrhinweisen das Sperrverhalten von einzelnen Abfragen beeinflussen.
SQL Server Performance-Ratgeber
143
2030-4.book Page 144 Monday, February 8, 2010 4:26 PM
6 – Performancegrundlagen für Entwickler 쮿
Eindeutige Zugriffsreihenfolge definieren Definieren Sie in Ihrem Entwicklerteam eine eindeutige Zugriffsreihenfolge für Tabellen. Dadurch wird das Risiko minimiert, dass zwei Transaktionen quasi über Kreuz auf Tabellen zugreifen. Bei Tabellen, die miteinander verknüpft sind, bietet es sich beispielsweise an, immer zuerst die übergeordnete Tabelle zu ändern und anschließend auf die abhängigen Tabellen zuzugreifen. Somit wird ein gleichzeitiger Zugriff auf dieselben Datensätze der übergeordneten Tabelle möglichst früh festgestellt.
Dazu sollte natürlich auch berücksichtigt werden, dass durch eine entsprechende Fehlerbehandlung zu prüfen ist, ob eine (als Deadlock-Opfer) abgebrochene Transaktion erneut ausgeführt werden muss.
6.3
Zusammenfassung
In diesem Kapitel wurden verschiedene Grundlagen behandelt, die entweder direkte oder indirekte Auswirkungen auf die Performance von Datenbankanwendungen haben. Zu Beginn haben Sie gelernt, wie SQL-Abfragen vom SQL Server intern verarbeitet werden und was es mit Ausführungsplänen und deren Wiederverwendung auf sich hat. Auch die Bedeutung von Tabellen- und Indexstatistiken, ohne die der Abfrageoptimierer keinen sinnvollen Ausführungsplan ermitteln könnte, wurde eingehend beschrieben. Für die Aufrechterhaltung der Datenkonsistenz bei paralleler Ausführung von Abfragen aus verschiedenen Datenbankverbindungen sind Sperren von Zeilen, Speicherseiten, Tabellen oder gar ganzen Datenbanken ein notwendiges Mittel, das jedoch auch unangenehme Nebeneffekte mit sich bringt. Diese lassen sich durch den bewussten Einsatz von Transaktionen und die Wahl der richtigen Isolationsstufe minimieren. Blockieren sich zwei Transaktionen gegenseitig, so kommt es zu einem Deadlock, der automatisch aufgelöst wird, indem eine Transaktion abgebrochen wird. Daher gilt es, das Risiko des Auftretens von Deadlocks so gut wie möglich zu minimieren, wozu Sie einige hilfreiche Hinweise erhalten haben.
144
2030-4.book Page 145 Monday, February 8, 2010 4:26 PM
7
Abfrageoptimierung
In diesem Kapitel werden verschiedene Ansätze gezeigt, wie Sie einzelne SQL-Abfragen oder auch ganze Skripts optimieren können. Diese Informationen sind wichtig, da letzten Endes ja jeder Datenbankzugriff auf SQL basiert. Um eine Datenbankanwendung zu beschleunigen, können Sie somit SQL-Abfragen in gespeicherten Prozeduren und Funktionen optimieren, aber auch Abfragen, die von der Anwendung direkt an den SQL Server geschickt werden. Obwohl die meisten der weiter unten geschilderten Optimierungsansätze anhand von SELECT-Abfragen diskutiert werden, sind viele davon auch auf die üblichen DML-Anweisungen (UPDATE, INSERT und DELETE) anwendbar.
!
7.1
Die hier beschriebenen Optimierungsansätze sind mit jeder neuen Version von SQL Server erneut zu überprüfen. Denn der interne Abfrageoptimierer von SQL Server wird mit jeder Generation besser, sodass der eine oder andere Optimierungsansatz in der nächsten Version vielleicht schon automatisch vom Optimierer umgesetzt wird. Im Zweifelsfall lohnt also immer ein Blick auf den Ausführungsplan der Abfrage.
Optimierung einzelner Abfragen
Der Einfachheit halber wird zuerst die Optimierung von einzelnen Abfragen behandelt. Hierbei ist es hilfreich, stets die im vorigen Kapitel behandelten Grundlagen (insbesondere die Ausführungsreihenfolge von SQL-Klauseln) zu kennen und stets im Hinterkopf zu behalten.
7.1.1
Voll qualifizierte Bezeichner verwenden
SQL Server kommt mit verschiedenen Varianten von Objektbezeichnungen klar. Für den Zugriff auf eine Tabelle sind beispielsweise folgende Angaben möglich:1 Server\Instanz.Datenbank.Schema.Tabelle
Sind einzelne dieser Angaben nicht explizit in der Abfrage vorgesehen, versucht SQL Server, diese automatisch zu ermitteln. Serverinstanz und Datenbank sind durch die Datenbankverbindung bereits definiert und müssen daher nicht zwingend explizit angegeben werden. Lediglich bei datenbank- oder gar serverübergreifenden Abfragen ist diese Angabe sinnvoll, dann aber auch notwendig. Optional ist dann lediglich die 1.
Damit eine separate Datenbankserverinstanz angegeben werden kann, muss diese als verbundener (engl. linked) Server konfiguriert sein.
SQL Server Performance-Ratgeber
145
2030-4.book Page 146 Monday, February 8, 2010 4:26 PM
7 – Abfrageoptimierung
Angabe der Serverinstanz, sofern es sich um die Standardinstanz des Servers handelt (bei einer benannten Instanz, die nicht gleichzeitig Standardinstanz ist, muss der Instanzname dagegen angegeben werden). Während serverübergreifende Abfragen in der Praxis nicht so häufig benötigt werden, sind Abfragen über Tabellen aus verschiedenen Datenbankschemas schon eher an der Tagesordnung. Aber auch wenn Sie lediglich das dbo-Schema verwenden, sollten Sie den Schemanamen stets mit angeben, da SQL Server sonst ermitteln muss, zu welchem Schema die Tabelle gehört (in diesem Fall wird die Tabelle zuerst im Standardschema des angemeldeten Benutzers und anschließend im dbo-Schema gesucht). Die folgende Variante wäre daher nicht zu empfehlen (zumal diese auch nur dann funktioniert, wenn sich die Tabelle entweder im dbo-Schema oder im Standardschema des angemeldeten Benutzers befindet): SELECT FirstName, LastName FROM Person
Besser (und gleichzeitig weniger fehleranfällig) ist dagegen die folgende Formulierung: SELECT FirstName, LastName FROM Person.Person
Auch bei der Angabe der Spaltennamen in einer Abfrage sind verschiedene Varianten zulässig. Hier kann man den Tabellennamen weglassen, wenn die Spalte trotzdem eindeutig zugeordnet werden kann. (Taucht der Spaltenname in mehreren verwendeten Tabellen auf, kommt es zu einer entsprechenden Fehlermeldung.) Aber auch dann, wenn die Voraussetzung für eine eindeutige Zuordnung gegeben ist, sollte der Tabellenname (oder der entsprechende Alias) verwendet werden, damit SQL Server nicht prüfen muss, zu welcher Tabelle die Spalte gehört. Dazu macht es die Abfrage besser lesbar, da gleich erkennbar ist, aus welcher Tabelle die Daten kommen. Auch hier zuerst die weniger empfehlenswerte Variante: SELECT FirstName, LastName, PhoneNumber FROM Person.Person AS pers INNER JOIN Person.PersonPhone AS phone ON pers.BusinessEntityID=phone.BusinessEntityID
Und hier die bessere Variante, bei der die Aliasnamen der Tabellen auch in der SELECTKlausel verwendet werden: SELECT pers.FirstName, pers.LastName, phone.PhoneNumber FROM Person.Person AS pers INNER JOIN Person.PersonPhone AS phone ON pers.BusinessEntityID=phone.BusinessEntityID
146
2030-4.book Page 147 Monday, February 8, 2010 4:26 PM
Optimierung einzelner Abfragen
Sollte in der Abfrage nur eine Tabelle angesprochen werden, so kann man sich natürlich sowohl den Aliasnamen als auch die Angabe desselben in der SELECT-Klausel sparen.
7.1.2
Datenvolumen so schnell wie möglich reduzieren
Bereits in Kapitel 6.1.1 wurde der interne Ablauf der Verarbeitung von Abfragen beschrieben. Daraus geht unter anderem hervor, dass die einzelnen SQL-Klauseln in einer bestimmten Reihenfolge nacheinander abgearbeitet werden. Mit jedem Schritt wird ein Zwischenergebnis gebildet, das – nach Möglichkeit – im Hauptspeicher gehalten wird. Um sowohl den dafür erforderlichen Speicherbedarf als auch die weiterzuverarbeitende Datenmenge möglichst gering zu halten, sollte das Datenvolumen so früh und weit wie möglich reduziert werden. Dies sollte sowohl vertikal (Auswahl der benötigten Spalten) als auch horizontal (Filter auf die relevanten Zeilen) erfolgen.
Nur benötigte Spalten selektieren Dieser Ansatz ist extrem einfach umzusetzen, wird aber dennoch – vermutlich meist aus Bequemlichkeit – sträflich vernachlässigt. Denn es ist natürlich viel einfacher, die Abfrage mit einem SELECT * zu beginnen, um bei Bedarf alle Spalten aller beteiligten Tabellen zur Verfügung zu haben. Hier sollte man sich aber definitiv die Mühe machen und überlegen, welche Spalten wirklich benötigt werden, um dann genau diese explizit anzugeben. Das Ergebnis ist ein deutlich geringerer Speicherbedarf und damit eine effektivere Ausführung der Abfrage. Teilweise kann dadurch der Zugriff auf einzelne Tabellen sogar komplett wegfallen, da evtl. die Verwendung eines Indexes auf die Tabelle ausreicht (in diesem Fall handelt es sich dann um den in Kapitel 4.3.2 vorgestellten abdeckenden Index). Um dies zu veranschaulichen, können Sie die Ausführungspläne der folgenden beiden Abfragen vergleichen: -- nicht gut SELECT * FROM Person.Person -- besser SELECT FirstName, LastName FROM Person.Person Listing 7.1: Explizite Angabe der benötigten Spalten
Auf den ersten Blick ist kein Unterschied zu erkennen, da für beide Abfragen ein Index Scan ausgeführt wird. Beim näheren Hinsehen stellt man aber fest, dass die erste Abfrage einen Clustered Index Scan ausführt (also eigentlich die gesamte Tabelle liest), während die zweite Abfrage nur den nichtgruppierten Index IX_Person_LastName_FirstName_ MiddleName (sofern vorhanden) liest, der natürlich deutlich kleiner ist. In diesem Fall handelt es sich also wirklich um einen abdeckenden Index. Daher beträgt der Anteil der Abfragekosten für diese Variante auch nur 3% gegenüber den 97% der ersten Variante.
SQL Server Performance-Ratgeber
147
2030-4.book Page 148 Monday, February 8, 2010 4:26 PM
7 – Abfrageoptimierung
Noch deutlicher wird der Unterschied, wenn die Daten sortiert ausgegeben werden sollen: -- nicht gut SELECT * FROM Person.Person ORDER BY LastName, Firstname -- besser SELECT FirstName, LastName FROM Person.Person ORDER BY LastName, Firstname Listing 7.2: Explizite Angabe der benötigten Spalten bei sortierter Ergebnisausgabe
Da die Sortierung zum nichtgruppierten Index passt, ändert sich am Ausführungsplan der zweiten Abfrage nichts, bei der ersten Variante kommt aber noch ein Schritt hinzu, indem die Daten, die aus dem gruppierten Index gelesen wurden, sortiert werden. (Ein alternativer – aber scheinbar noch teurer – Ausführungsplan wäre ein Index Scan des nichtgruppierten Indexes mit einem Row Lookup im gruppierten Index für jede Zeile, um die zusätzlichen Felder zu lesen.)
Abbildung 7.1: Die negativen Auswirkungen von SELECT * sind hier leicht zu erkennen
148
2030-4.book Page 149 Monday, February 8, 2010 4:26 PM
Optimierung einzelner Abfragen
Wie in Abbildung 7.1 zu sehen ist, hat sich dadurch auch die Verteilung der Abfragekosten mit 98% zu 2% weiter verschoben.
Zeilen so früh wie möglich einschränken Genau so, wie man die Spalten einer Abfrage möglichst genau einschränken sollte, kann man dies auch mit den Zeilen tun. In den meisten Fällen wird dies zwar gegeben sein, da die Abfrage sonst schlichtweg ein falsches Ergebnis liefert. Aber dennoch gilt es, diese Zeilenfilter so früh wie möglich anzuwenden, um die Zwischenergebnisse der Abfrage möglichst kompakt zu halten. Hier kommt dann die Ausführungsreihenfolge der einzelnen Abfrageklauseln wieder ins Spiel. Wenn man sich diese ins Gedächtnis zurückruft, liegt die Vermutung nahe, dass eine Bedingung zur Eingrenzung der Ergebniszeilen besser in der WHERE-Klausel aufgehoben ist als in der HAVING-Klausel. (Was natürlich nur möglich ist, wenn die Bedingung sich nicht auf eine Aggregatfunktion der Gruppe bezieht.) In der Praxis ist genau dies ein hervorragendes Beispiel, um nachzuvollziehen, wie der interne Abfrageoptimierer sich von SQL Server 2005 zu SQL Server 2008 verbessert hat. Unter SQL Server 2005 erzeugen die beiden folgenden Abfragen trotz gleichem Ergebnis unterschiedliche Ausführungspläne: -- nicht gut SELECT EmailAddress, COUNT(*) AS Anzahl FROM Person.Contact GROUP BY EmailAddress HAVING EmailAddress LIKE 'john%' -- besser SELECT EmailAddress, COUNT(*) AS Anzahl FROM Person.Contact WHERE EmailAddress LIKE 'john%' GROUP BY EmailAddress Listing 7.3: Einschränkung der Zeilen über die WHERE- oder die HAVING-Klausel
In beiden Varianten wird ein existierender Index auf die Spalte EmailAddress genutzt, allerdings wird dieser in der ersten Variante per Index Scan komplett gelesen, da zuerst die Gruppierung ausgeführt wird und erst danach die Einschränkung aus der HAVINGKlausel greift. In der zweiten Variante dagegen greift zuerst die WHERE-Klausel, sodass die relevanten Zeilen über einen Index Seek gezielt herausgesucht werden können. Die wenigen verbleibenden Zeilen werden dann erst anschließend über die GROUP BY-Klausel zu Gruppen zusammengefasst.
SQL Server Performance-Ratgeber
149
2030-4.book Page 150 Monday, February 8, 2010 4:26 PM
7 – Abfrageoptimierung
Abbildung 7.2: Die verschiedenen Ausführungspläne bei SQL Server 2005
Bei Verwendung von SQL Server 2008 ergibt sich allerdings ein anderes Bild. Hier ist der Abfrageoptimierer so schlau geworden, dass er gleich erkennt, dass die Eingrenzung auf die E-Mail-Adressen, die mit john beginnen, auch vor der Gruppierung erfolgen kann, und für beide Varianten einen Ausführungsplan erzeugt, der einen Index Seek nutzt.
i 7.1.3
Die 2005er- und 2008er-Variante der Beispieldatenbank haben eine leicht abweichende Datenbankstruktur, sodass Sie bei AdventureWorks2008 statt der Tabelle Person.Contact die Tabelle Person.EmailAddress verwenden müssen, um das oben gezeigte Beispiel nachzuvollziehen.
Indexverwendung ermöglichen
Die Möglichkeit (und Art) der Indexverwendung ist von entscheidender Bedeutung für die Ausführungsdauer von Abfragen, da dies die Ausführungsdauer leicht um den Faktor 100 oder mehr beeinflussen kann. Dazu muss natürlich einerseits ein passender Index vorhanden sein. Dieses Thema wurde jedoch ausgiebig in Kapitel 4 – Indizes diskutiert. Aber selbst bei vorhandenen geeigneten Indizes gibt es eine Menge Faktoren (auch in der Abfrage selbst), die ermöglichen bzw. verhindern können, dass ein solcher auch effektiv genutzt werden kann.
i
Über Optimizer Hints lässt sich sogar erzwingen, dass ein bestimmter Index von einer Abfrage verwendet wird. Dieses Thema wird weiter unten in Kapitel 7.1.5 Ausführungspläne beeinflussen behandelt.
Verwendung von Suchargumenten Damit ein Index optimal – also möglichst über einen Index Seek – genutzt werden kann, müssen die Indexspalten in der WHERE-Klausel in einer bestimmten Form genutzt werden.
150
2030-4.book Page 151 Monday, February 8, 2010 4:26 PM
Optimierung einzelner Abfragen
Dazu müssen folgende Regeln befolgt sein: 쮿
Die Indexspalten müssen isoliert auf einer Seite des Vergleichsoperators auftreten. Eine Verwendung der Indexspalte als Funktionsparameter beispielsweise verhindert eine optimale Indexnutzung.
쮿
Die Verwendung von negierten Vergleichsoperatoren (wie beispielsweise NOT IN, NOT EXISTS, NOT LIKE, <>) ist zu vermeiden. Hier wären durch einen Index Seek lediglich die Zeilen zu finden, die nicht im Ergebnis erscheinen sollen. Für die restlichen Zeilen ist dann ein Index Scan möglich. Sofern es sich nicht um einen abdeckenden Index handelt, ist es in den meisten Fällen aufgrund der geringen Selektivität der negierten Vergleichsbedingung sogar effektiver, gleich die gesamte Tabelle über einen Table Scan zu lesen.
쮿
Wird für den Vergleich ein LIKE-Operator verwendet, so muss der Anfang der Vergleichszeichenfolge konstante Zeichen enthalten. Erst weiter hinten dürfen die üblichen Platzhalter (%, _ etc.) verwendet werden.
Sind diese Bedingungen erfüllt, so spricht man von einem gültigen Suchargument (engl.: search argument oder kurz: SARG). Ist eine der Bedingungen nicht erfüllt, ist zwar kein gültiges Suchargument mehr gegeben, was aber nicht bedeutet, dass ein passender Index gar nicht mehr verwendet werden kann. Stattdessen ist meist immer noch ein Index Scan möglich, der zwar immer noch deutlich effektiver ist als ein Table Scan, aber trotzdem weit aufwändiger als ein Index Seek. Die folgende Abfrage enthält beispielsweise kein gültiges Suchargument, da die Spalte LastName als Funktionsparameter verwendet wird: SELECT FirstName, LastName FROM Person.Person WHERE LEFT(LastName, 5)='Shaba'
Stellt man die Abfrage so um, dass statt der LEFT-Funktion der LIKE-Operator verwendet wird, ist dagegen ein Suchargument gegeben, sodass ein Index Seek möglich wird: SELECT FirstName, LastName FROM Person.Person WHERE LastName LIKE 'Shaba%'
Bei der folgenden Verwendung des LIKE-Operators geht dies allerdings nicht, da hier der Platzhalter am Anfang der Suchzeichenkette steht: SELECT FirstName, LastName FROM Person.Person WHERE LastName LIKE '%abalin'
SQL Server Performance-Ratgeber
151
2030-4.book Page 152 Monday, February 8, 2010 4:26 PM
7 – Abfrageoptimierung
Die folgende Abfragevariante verwendet wegen des negierten Operators lediglich einen Index Scan: SELECT FirstName, LastName FROM Person.Person WHERE LastName<>'Shabalin'
Würden anstelle der Spalten FirstName und LastName alle Spalten der Tabelle selektiert, so wäre die Folge sogar ein Clustered Index Scan (also im Prinzip ein Table Scan).
Verwendung einer zusätzlichen WHERE-Bedingung In einigen Fällen kann es Sinn machen, die Abfrage durch eine zusätzliche, eigentlich redundante WHERE-Bedingung zu ergänzen und damit dem Abfrageoptimierer zu ermöglichen, einen eventuell effektiveren Ausführungsplan zu erstellen. Dies kann in zwei Fällen eine Verbesserung der Abfrageperformance bringen: 쮿
Die ursprüngliche Abfrage konnte keinen passenden Index verwenden. Durch die Zusatzbedingung können die weiter zu betrachtenden Daten über einen Index stark eingegrenzt werden, sodass nur noch wenige Zeilen direkt aus der Tabelle gelesen werden müssen.
쮿
Die ursprüngliche Abfrage konnte zwar einen Index nutzen, allerdings nur per Index Scan, da die erste Spalte des Indexes nicht in der WHERE-Klausel enthalten war. Durch eine Zusatzbedingung auf diese erste Spalte wird die effektivere Indexnutzung per Index Seek ermöglicht.
Sehen wir uns für die zweite Variante ein konkretes Beispiel an: SELECT FirstName, LastName, Demographics FROM Person.Person WHERE FirstName='Rostislav'
Die Abfrage verwendet einen Index Scan, da das Feld FirstName zwar in einem Index enthalten ist, dort aber nicht an erster Stelle steht (sondern an zweiter, hinter dem Feld LastName). Wenn man allerdings sicher weiß, dass in der Tabelle nur ein Rostislav enthalten ist und auch dessen Nachnamen kennt, kann man diesen in der WHERE-Klausel ergänzen, damit statt des Index Scan ein Index Seek ausgeführt werden kann: SELECT FirstName, LastName, Demographics FROM Person.Person WHERE FirstName='Rostislav' AND LastName='Shabalin'
Wie das Beispiel zeigt, erfordert dieser Optimierungsansatz Zusatzwissen über die inhaltlichen Zusammenhänge der Daten. Damit sollte sich die Zusatzbedingung transitiv aus den bestehenden Bedingungen ergeben. (Wenn beispielsweise nach einem Adressdatensatz in Berlin gesucht wird, kann man davon ausgehen, dass die Adresse in
152
2030-4.book Page 153 Monday, February 8, 2010 4:26 PM
Optimierung einzelner Abfragen
Deutschland gesucht wird.2) Geht man hier von falschen Zusammenhängen aus, wird vielleicht die Performance besser, aber dafür das Ergebnis verfälscht. Die Spalten der Zusatzbedingung müssen in einem Index enthalten sein, der die Abfrageperformance verbessern kann. Ist dies nicht der Fall, muss man mit einer Verschlechterung der Ausführungsdauer rechnen. Dies gilt insbesondere dann, wenn die Abfrage in der ursprünglichen Form einen abdeckenden Index (per Index Scan) nutzen konnte, durch die Zusatzbedingung aber ein Zugriff auf die Tabelle selbst notwendig wird.
7.1.4
Unterabfragen
In vielen Fällen lassen sich Abfragen mit Unterabfragen schlechter optimieren als solche, in denen die Tabellen über Joins verbunden sind. Auch dies ist ein Bereich, in dem der Abfrageoptimierer von SQL Server 2008 intelligenter geworden ist als der von SQL Server 2005. Generell spielt auch die Art der Unterabfrage eine entscheidende Rolle. Handelt es sich um eine konstante Unterabfrage, die für jeden Datensatz der äußeren Abfrage dasselbe Ergebnis liefert, so ist die Wahrscheinlichkeit relativ hoch, dass diese eigentlich nur einmal physikalisch ausgeführt wird und bei den Folgedurchläufen das Ergebnis direkt aus dem Cache gelesen werden kann. Diese Wahrscheinlichkeit wird umso höher, je kleiner das Ergebnis der Unterabfrage ist. Handelt es sich aber um eine so genannte korrelierte Unterabfrage, die für jeden Datensatz der äußeren Abfrage ein anderes Ergebnis liefert (da die Unterabfrage selbst Felder der äußeren Abfrage verwendet), so kann es sinnvoll sein, einen Weg zu suchen, um diese Unterabfrage durch ein anderes SQL-Sprachkonstrukt zu ersetzen.
Unterabfragen durch JOINs ersetzen Eine mögliche Variante hierzu liegt darin, die Unterabfrage durch einen JOIN zu ersetzen, um damit dem Abfrageoptimierer mehr Spielraum zu geben, wie und in welcher Reihenfolge auf die beteiligten Tabellen zugegriffen wird. Das folgende Beispiel nutzt die 2005er-Version der AdventureWorks-Datenbank: -- Unterabfrage mit EXISTS select pers.FirstName, pers.LastName from Person.Contact pers where exists (select * from HumanResources.Employee emp where emp.VacationHours<20 and emp.ContactID=pers.ContactID) Listing 7.4: Umwandlung von Unterabfragen in JOINs
2.
Wenn man einmal vernachlässigt, dass es die meisten europäischen Städtenamen irgendwo in den USA noch einmal gibt.
SQL Server Performance-Ratgeber
153
2030-4.book Page 154 Monday, February 8, 2010 4:26 PM
7 – Abfrageoptimierung
-- Unterabfrage mit IN select pers.FirstName, pers.LastName from Person.Contact pers where pers.ContactID IN (select emp.ContactID from HumanResources.Employee emp where emp.VacationHours<20) -- JOIN select pers.FirstName, pers.LastName from HumanResources.Employee emp inner join Person.Contact pers on emp.ContactID=pers.ContactID where emp.VacationHours<20 Listing 7.4: Umwandlung von Unterabfragen in JOINs (Forts.)
Vergleicht man nun mit einem 2005er SQL Server die Ausführungspläne der drei Varianten, so ergibt sich tatsächlich ein etwas besserer Ausführungsplan für die Variante mit dem JOIN, da hier die zusätzliche Sort-Operation wegfällt (wie Abbildung 7.3 zeigt).
Abbildung 7.3: Unterschiedliche Ausführungspläne bei Verwendung von SQL Server 2005
154
2030-4.book Page 155 Monday, February 8, 2010 4:26 PM
Optimierung einzelner Abfragen
Um das Beispiel auf einem SQL Server 2008 mit der AdventureWorks2008-Datenbank auszuführen, muss es minimal angepasst werden. Hier ist statt der Tabelle Person.Contact die Tabelle Person.Person zu verwenden und das ID-Feld für die Verknüpfung heißt nicht mehr ContactID, sondern stattdessen BusinessEntityID. Betrachtet man nun auch dort die drei Ausführungspläne, so sind diese absolut identisch, da SQL Server 2008 für die beiden Varianten mit der Unterabfrage keine Sort-Operation mehr benötigt.
Korrelierte Unterabfragen in nichtkorrelierte umwandeln Es gibt allerdings auch zahlreiche Beispiele, in denen die Umwandlung einer Unterabfrage in einen JOIN selbst bei SQL Server 2005 keinen Performancegewinn bringt. Die beiden folgenden Varianten einer Abfrage werden intern in denselben Ausführungsplan umgesetzt: -- korrelierte Unterabfrage SELECT soh.SalesOrderNumber FROM Sales.SalesOrderHeader AS soh WHERE (SELECT COUNT(*) FROM Sales.SalesOrderDetail AS sod WHERE soh.SalesOrderID = sod.SalesOrderID) > 50 -- JOIN SELECT soh.SalesOrderNumber FROM Sales.SalesOrderHeader AS soh INNER JOIN Sales.SalesOrderDetail AS sod ON soh.SalesOrderID = sod.SalesOrderID GROUP BY soh.SalesOrderNumber HAVING COUNT(*) > 50 Listing 7.5: Vergleich einer korrelierten Unterabfrage mit einer JOIN-Variante
Passt man die erste Variante aber so an, dass aus der korrelierten Unterabfrage eine nichtkorrelierte wird, so muss diese nur einmal ausgeführt werden, da das Ergebnis dann im Puffer gehalten wird: -- nichtkorrelierte Unterabfrage SELECT soh.SalesOrderNumber FROM Sales.SalesOrderHeader AS soh WHERE soh.SalesOrderID IN (SELECT sod.SalesOrderID FROM Sales.SalesOrderDetail AS sod GROUP BY sod.SalesOrderID HAVING COUNT(*) > 50) Listing 7.6: Variante mit Verwendung einer nichtkorrelierten Unterabfrage
SQL Server Performance-Ratgeber
155
2030-4.book Page 156 Monday, February 8, 2010 4:26 PM
7 – Abfrageoptimierung
Vergleicht man nun die Ausführungspläne der drei Varianten, so bestätigen diese, dass die ersten beiden Varianten nahezu identisch sind (die kleine Differenz beim Filter links kann man sicherlich vernachlässigen), während die dritte Variante mit der unabhängigen Unterabfrage deutlich schneller ist, da diese nur einmal ausgeführt werden muss, wonach dann über Index-Seek-Operationen die entsprechenden Datensätze aus der übergeordneten Abfrage gesucht werden.
Abbildung 7.4: Die Ausführungspläne für drei Varianten derselben Abfrage
Insgesamt zeigt dies, dass man im Zweifelsfall immer die Ausführungspläne von verschiedenen Abfragevarianten prüfen sollte, bevor man sich für eine davon entscheidet.
7.1.5
Ausführungspläne beeinflussen
Wenn Sie feststellen, dass der interne Abfrageoptimierer nicht automatisch den richtigen Ausführungsplan ermittelt, können Sie den Plan mit entsprechenden Hinweisen bei der Abfrage auch beeinflussen. Es gibt verschiedene Varianten von Hinweisen, mit denen Sie die Ausführung einer Abfrage beeinflussen können. Diese sind in zwei Gruppen unterteilt: Mit Tabellenhinweisen können Sie den Zugriff auf eine Tabelle beeinflussen, Abfragehinweise wirken sich dagegen auf die gesamte Abfrage aus.
156
2030-4.book Page 157 Monday, February 8, 2010 4:26 PM
Optimierung einzelner Abfragen
!
Verwenden Sie diese Optimierungsvariante nur mit äußerster Vorsicht. In den meisten Fällen wird der interne Abfrageoptimierer die richtige Variante wählen und die Gefahr, die Performance durch einen Abfragehinweis zu verschlechtern, ist relativ groß. Dazu kommt, dass der interne Abfrageoptimierer dynamisch arbeitet und durch die Verwendung von Tabellenstatistiken auch immer aktuelle Datenvolumen (sofern die Statistiken aktuell genug sind) berücksichtigt. Dadurch kann es vorkommen, dass nach einer bestimmten Zeit ein anderer Ausführungsplan effektiver ist als zuvor. Wenn Sie den Ausführungsplan nun durch fest vorgegebene Hinweise beeinflussen, kann dies momentan die optimale Lösung sein, aber später eventuell auch verhindern, dass der SQL Server einen dann effektiveren Weg verwenden kann. Letzten Endes benötigen Sie gute Kenntnisse sowohl der technischen Zusammenhänge als auch der Dateninhalte, um Hinweise sinnvoll einsetzen zu können.
Tabellenhinweise Tabellenhinweise (engl. Table Hints) regeln die Art, wie auf einzelne Tabellen, die an einer Abfrage beteiligt sind, zugegriffen wird. Einen Teil der verwendbaren Tabellenhinweise – nämlich die zum Beeinflussen der Sperrart – haben Sie bereits in Kapitel 6.2.4 – Tabellenhinweise kennen gelernt. Diese werden oft auch als Sperrhinweise (engl. Locking Hint) bezeichnet. Um das dort Beschriebene nicht unnötig zu wiederholen, möchte ich an dieser Stelle ein paar andere Tabellenhinweise kurz vorstellen, die ebenfalls Auswirkungen auf die Performance haben. Mit dem Tabellenhinweis INDEX lässt sich steuern, ob ein bzw. wenn ja, welcher Index für die Ausführung der Abfrage verwendet werden soll. So kann man mit folgender Variante die Benutzung des Indexes erzwingen, obwohl dies hier sicherlich nicht sinnvoll ist: SELECT * FROM Person.Person WITH (INDEX=IX_Person_LastName_FirstName_MiddleName)
Über die Variante WITH (INDEX=0) lässt sich dagegen erreichen, dass gar kein Index für die betreffende Tabelle verwendet wird, auch wenn ein passender Index existiert: SELECT LastName, FirstName FROM Person.Person WITH (INDEX=0) WHERE LastName = 'Wood'
Das Gegenstück hierzu ist der Hinweis FORCESEEK, mit dem ein Zugriff über einen Index Seek erzwungen wird: SELECT LastName FROM Person.Person WITH (FORCESEEK) WHERE LastName LIKE 'Wood'
SQL Server Performance-Ratgeber
157
2030-4.book Page 158 Monday, February 8, 2010 4:26 PM
7 – Abfrageoptimierung
Ist die Abfrage für einen Index Seek aber zu abwegig (etwa wenn Sie in dem obigen Beispiel die WHERE-Bedingung weglassen), kann es sein, dass SQL Server die Ausführung der Abfrage komplett verweigert und stattdessen folgende Fehlermeldung bringt: Meldung 8622, Ebene 16, Status 1, Zeile 1 Der Abfrageprozessor konnte wegen der in der Abfrage definierten Hinweise keinen Abfrageplan erzeugen. Wiederholen Sie die Abfrage ohne Angabe von Hinweisen und ohne SET FORCEPLAN. Der Tabellenhinweis NOEXPAND lässt sich nur in Zusammenhang mit indizierten Sichten nutzen und verhindert dann, dass die Sicht aufgelöst und auf die darunterliegenden Tabellen zugegriffen werden kann. Stattdessen wird der Zugriff auf die Indexdaten selbst erzwungen, als würde es sich um eine normale Tabelle handeln. Das Setzen des Hinweises NOWAIT hat zur Folge, dass sofort ein Timeout auftritt, falls die Tabelle im Moment des Zugriffsversuchs gesperrt ist. Wenn Daten besonders schnell in eine Tabelle eingefügt werden sollen, kann man bei Verwendung einer INSERT-Anweisung, die OPENROWSET mit BULK-Option nutzt, die Hinweise IGNORE_CONSTRAINTS und/oder IGNORE_TRIGGERS nutzen. Dadurch werden eventuell vorhandene Constraints nicht geprüft bzw. eventuell existierende Trigger nicht ausgeführt. Im Falle der Constraints werden diese dadurch allerdings als nicht glaubwürdig markiert, sodass eine Überprüfung des Constraints für die gesamte Tabelle notwendig wird, um dieses wieder in einen normalen Zustand zu versetzen. Wenn man maximale Performance beim Ändern von Daten erreichen will, ist es eine Überlegung wert, vom Datenbankdesign her völlig auf Constraints und Trigger zu verzichten. Somit entfällt die Zeit, die das datenbankseitige Prüfen der Constraints sowie die von Triggern ausgeführten Operationen benötigen. Dafür sollte die entsprechende Logik dann clientseitig von der Anwendung her erledigt werden. Es gibt noch eine ganze Reihe von weiteren Tabellenhinweisen, deren Auflistung den Rahmen hier allerdings sprengen würde. Eine vollständige Darstellung finden Sie im SQL Server Developer Center unter folgender URL: http://msdn.microsoft.com/en-us/library/ms187373.aspx
Abfragehinweise Im Gegensatz zu den Tabellenhinweisen beziehen sich Abfragehinweise (engl. Query Hints) nicht auf den Zugriff einer einzelnen Tabelle, sondern auf die gesamte Abfrage. Daher werden diese auch nicht in der FROM-Klausel bei den Tabellen, sondern am Schluss der Abfrage hinter dem Schlüsselwort OPTION angegeben. Eine Variante haben Sie bereits in Kapitel 6.1.5 Wiederverwendung von Ausführungsplänen kennen gelernt: Der Abfragehinweis RECOMPILE bewirkt, dass für die Abfrage ein neuer Ausführungsplan erstellt (und dieser auch nicht für spätere Verwendung im Plancache gespeichert) wird.
158
2030-4.book Page 159 Monday, February 8, 2010 4:26 PM
Optimierung einzelner Abfragen
Mit dem Abfragehinweis OPTIMIZE FOR können Sie bei Skripts oder gespeicherten Prozeduren erreichen, dass diese für einen bestimmten Variablen- oder Parameterwert optimiert werden. Wird der Abfragehinweis nicht verwendet, so geschieht dies bei gespeicherten Prozeduren auf Basis des Wertes, mit dem diese zum ersten Mal aufgerufen werden. Außerhalb von gespeicherten Prozeduren findet eine allgemeine Optimierung auf Basis der Statistiken statt. Letztere kann auch durch Kombination des OPTIMIZE FOR-Hinweises mit dem Schlüsselwort UNKNOWN für eine einzelne Variable oder für die gesamte Abfrage erzwungen werden. Anschaulich wird dies am folgenden Beispiel, in dem eine Abfrage mit einer Variablen aufgerufen wird. Die erste Variante erzwingt eine Optimierung des Ausführungsplans für den Nachnamen Wood, obwohl die Variable einen anderen Wert beinhaltet. Die zweite Variante optimiert für einen unbekannten Wert der Variablen @LastName, während die dritte Variante generell auf unbekannte Variablenwerte hin optimiert: DECLARE @LastName nvarchar(50); SET @LastName = 'Miller'; SELECT * FROM Person.Person WHERE LastName = @LastName OPTION (OPTIMIZE FOR (@LastName = 'Wood')); SELECT * FROM Person.Person WHERE LastName = @LastName OPTION (OPTIMIZE FOR (@LastName UNKNOWN)); SELECT * FROM Person.Person WHERE LastName = @LastName OPTION (OPTIMIZE FOR UNKNOWN); Listing 7.7: Optimierung der Abfrage für bestimmte Variablenwerte
Der Sinn oder Unsinn dieser Varianten macht sich allerdings erst bei wiederholter Verwendung des so erstellten Ausführungsplans bemerkbar. In diesem Zusammenhang sind auch die Abfragehinweise PARAMETERIZATION SIMPLE und PARAMETERIZATION FORCED zu sehen, mit denen man die einfache oder erzwungene Auto-Parametrisierung für eine Abfrage erzwingen kann (unabhängig von der datenbankseitigen Voreinstellung). Mit MAXDOP n (mit ganzzahligem Wert n) lässt sich die Anzahl der Threads begrenzen, die für die Abarbeitung der Abfrage maximal genutzt werden.3 Dies spielt sicherlich nur bei länger laufenden Abfragen eine Rolle, da ansonsten ohnehin keine Aufteilung auf mehrere Threads stattfindet. 3.
Das „DOP“ in MAXDOP steht für Degree of Parallelism, also den Parallelitätsgrad.
SQL Server Performance-Ratgeber
159
2030-4.book Page 160 Monday, February 8, 2010 4:26 PM
7 – Abfrageoptimierung
Die Option FAST n (wobei n wiederum für eine ganze Zahl steht) optimiert den Ausführungsplan darauf hin, dass möglichst schnell n Ergebniszeilen zurückgegeben werden können. Anschließend wird der Rest des Ergebnisses erzeugt. Momentan existiert auch noch ein ähnlicher Tabellenhinweis (FASTFIRSTROW), der in kommenden Versionen von SQL Server aber nicht mehr unterstützt wird und daher möglichst nicht verwendet werden sollte. Auch Tabellenhinweise lassen sich mit der Syntax für Abfragehinweise angeben. Damit würde die Abfrage … SELECT LastName, FirstName FROM Person.Person WITH (INDEX=0) WHERE LastName = 'Wood'
… zum gleichen Ausführungsplan führen wie die folgende Variante: SELECT LastName, FirstName FROM Person.Person WHERE LastName = 'Wood' OPTION (TABLE HINT(Person.Person, INDEX(0)))
Tabellenhinweise, die als Abfragehinweise angegeben werden, haben Vorrang vor Tabellenhinweisen, die in der FROM-Klausel stehen. Wie bereits bei den Tabellenhinweisen wurde auch von den Abfragehinweisen hier aus Platzgründen nur ein kleiner Ausschnitt aufgelistet. Eine vollständige Beschreibung finden Sie im SQL Server Developer Center unter der folgenden URL: http://msdn.microsoft.com/en-us/library/ms181714.aspx
Plan Guides Nicht immer hat man die Möglichkeit, eine Abfragedefinition zu ändern. Insbesondere dann, wenn der Quelltext zu einer fertigen Anwendung nicht verfügbar ist, lassen sich natürlich auch die einzelnen Abfragen nicht direkt anpassen. Seit SQL Server 2005 gibt es jedoch auch in solchen Fällen noch eine Möglichkeit, eine gezielte Optimierung zu betreiben. So lässt sich mithilfe der Systemprozedur sp_create_plan_guide für den zuletzt vorgenommenen Tabellenhinweis auch ein entsprechender Plan Guide erstellen: exec sp_create_plan_guide @name = N'TestPlanGuide', @stmt = N'SELECT LastName, FirstName FROM Person.Person WHERE LastName = ''Wood''', @type = N'SQL', @module_or_batch = NULL, @params = NULL, Listing 7.8: Erstellung eines Plan Guides
160
2030-4.book Page 161 Monday, February 8, 2010 4:26 PM
Optimierung einzelner Abfragen
@hints = N'OPTION (TABLE HINT (Person.Person, INDEX(0)))'; GO Listing 7.8: Erstellung eines Plan Guides (Forts.)
Wird anschließend die Abfrage in derselben Form aufgerufen, so greift der Tabellenhinweis und verhindert die Nutzung eines vorhandenen nichtgruppierten Indexes. Verändert man allerdings die Abfrage nur minimal (sei es durch einen zusätzlichen Zeilenumbruch oder einen anderen Vergleichswert), so greift der Plan Guide nicht mehr. -- Abfragevariante, die den Plan Guide nutzt SELECT LastName, FirstName FROM Person.Person WHERE LastName = 'Wood' -- Abfragevarianten, die den Plan Guide nicht nutzen SELECT LastName, FirstName FROM Person.Person WHERE LastName = 'Wood' SELECT LastName, FirstName FROM Person.Person WHERE LastName = 'Miller' Listing 7.9: Nicht alle Abfragevarianten nutzen den Plan Guide
Um das Problem mit den unterschiedlichen Vergleichswerten zu lösen, lassen sich auch in Plan Guides Parameter nutzen. -- Plan Guide um (nonclustered) Indexnutzung zu vermeiden exec sp_create_plan_guide @name = N'TestPlanGuide2', @stmt = N'SELECT LastName, FirstName FROM Person.Person WHERE LastName = @0', @type = N'SQL', @module_or_batch = NULL, @params = N'@0 nvarchar(50)', @hints = N'OPTION (TABLE HINT (Person.Person, INDEX(0)))'; GO Listing 7.10: Erstellung eines Plan Guides mit einem Parameter
Dazu gibt es ein paar weitere Systemprozeduren, über die sich Plan Guides deaktivieren und wieder aktivieren oder sogar ganz löschen lassen: -- Plan Guide deaktivieren EXEC sp_control_plan_guide N'DISABLE', N'TestPlanGuide'; Listing 7.11: Deaktivieren, Aktivieren und Löschen von Plan Guides
SQL Server Performance-Ratgeber
161
2030-4.book Page 162 Monday, February 8, 2010 4:26 PM
7 – Abfrageoptimierung
-- Plan Guide aktivieren EXEC sp_control_plan_guide N'ENABLE', N'TestPlanGuide'; -- Plan Guide löschen EXEC sp_control_plan_guide N'DROP', N'TestPlanGuide'; Listing 7.11: Deaktivieren, Aktivieren und Löschen von Plan Guides (Forts.)
Plan Guides bilden somit eine separate Schicht für Optimierungs-Hinweise, die quasi zwischen der Anwendung und der Datenbank liegt.
i
7.1.6
Plan Guides können im Prinzip auch zusammen mit O/R-Mappern wie beispielsweise LINQ to SQL oder dem .NET Entity Framework genutzt werden, um die Ausführung der generierten SQL-Anweisungen zu beeinflussen. Allerdings ist hier die Gefahr relativ groß, dass ein Plan Guide nicht greift, weil die generierte Abfrage von der erwarteten Vorlage eventuell minimal abweicht.
Parametrisierung von Abfragen
Die Parametrisierung von Abfragen wurde bereits in Kapitel 6.1.6 – Parametrisierung von Abfragen ausführlich behandelt. Neben der dort beschriebenen Variante zur expliziten Parametrisierung ist auch die Nutzung der gespeicherten Systemprozedur sp_executesql möglich. Letzteres gestattet es, Typ und Inhalt der Parameter zusammen mit der Abfrage in einer Anweisung zu definieren. Damit führen die beiden folgenden Varianten zum selben Ergebnis: -- explizite Parametrisierung durch vorher definierte Variablen DECLARE @LastName NVARCHAR(50) SET @LastName = 'Gonzalez' EXEC ('SELECT * FROM Person.Person WHERE LastName = ''' + @LastName + '''') -- explizite Parametrisierung durch sp_executesql exec sp_executesql N' SELECT * FROM Person.Person WHERE LastName = @LastName', N'@LastName nvarchar(50)',@LastName=N'Gonzalez' Listing 7.12: Explizite Parametrisierung von Abfragen
Sinn und Zweck der Parametrisierung von Abfragen ist es, die Wiederverwendbarkeit von Ausführungsplänen zu ermöglichen. Dazu wird auch der Speicherbedarf des Plancache gering gehalten, wenn nicht für jede neue Variante derselben Abfrage ein neuer Plan erzeugt werden muss.
162
2030-4.book Page 163 Monday, February 8, 2010 4:26 PM
Abfrageübergreifende Optimierung
7.2
Abfrageübergreifende Optimierung
Nachdem wir gerade genauer betrachtet haben, wie sich einzelne Abfragen optimieren lassen, geht es im Folgenden darum, wie sich aus mehreren SQL-Anweisungen bestehende Skripts optimieren lassen. Dabei spielen sowohl Sperren und Transaktionen eine Rolle als auch die Aufteilung der Logik auf einzelne SQL-Anweisungen. Manchmal lässt sich durch Zusammenfassen von Anweisungen ein Performancegewinn erzielen, teilweise aber auch durch bewusstes Aufteilen in mehrere Teilschritte.
7.2.1
Sperren und Transaktionen
Das Thema Sperren und Transaktionen wurde im vorigen Kapitel (genauer in Kapitel 6.2 – Sperren und Transaktionen) schon einmal behandelt, da es sich um ein allgemeines Thema handelt, das für alle Arten des Datenzugriffs (egal ob über SQL, eine DatenbankAPI wie ADO.NET oder aber ein ETL-Tool wie beispielsweise die SQL Server Integration Services) relevant ist. Im Umfeld von reinem SQL – also beispielsweise in gespeicherten Prozeduren – ist zumindest ein Warten auf eine Benutzerinteraktion eher unwahrscheinlich, sodass dieser Teilaspekt hier kaum eine Gefahr darstellt. Trotzdem muss man sich – insbesondere bei Skripts mit längerer Laufzeit – Gedanken machen, ob und in welchem Umfang dafür explizite Transaktionen sinnvoll sind. Für größere Batch-Läufe, die zu Zeiten stattfinden, in denen keine Anwender mit der Datenbank arbeiten, bieten sich große Transaktionen und restriktive Sperren (beispielsweise auf Tabellenebene) an. Bei Skripts, die zur normalen Arbeitszeit auf operativen Datenbanken ausgeführt werden, sollten Transaktionen eher möglichst klein sein und auch die Sperren nur dort gesetzt werden, wo sie nötig sind, um Wartezeiten von anderen Prozessen und auch das Risiko von Deadlocks zu minimieren. Während Transaktionen über die einfachen SQL-Anweisungen BEGIN TRANSACTION, COMMIT TRANSACTION und ROLLBACK TRANSACTION zu steuern sind, lässt sich das Setzen von Sperren auf viele verschiedene Weisen beeinflussen. Für einzelne Abfragen ist dies über Tabellenhinweise möglich, für ganze Transaktionen kann dies über das Setzen der richtigen Transaktions-Isolationsstufe geschehen. Auch für spezielle SQL-Konstrukte, wie beispielsweise die weiter unten in diesem Kapitel behandelten SQL-Cursor, gibt es entsprechende Hinweise, mit denen sich der Sperrmodus der dazugehörenden Abfrage steuern lässt.
7.2.2
Abfragen zusammenfassen
Oft lassen sich mehrere Einzelabfragen zu einer gemeinsamen Abfrage zusammenfassen, wobei entsprechende Synergieeffekte genutzt werden können, da manche Daten dann nicht mehr vielfach abgefragt werden müssen. Dazu eröffnen sich dem Abfrageoptimierer durch das Zusammenfassen von mehreren Einzelabfragen oft auch weitere Optimierungsmöglichkeiten, da der Weg zum Gesamtergebnis noch weniger vorgeschrieben ist.
SQL Server Performance-Ratgeber
163
2030-4.book Page 164 Monday, February 8, 2010 4:26 PM
7 – Abfrageoptimierung
UNION SELECT vermeiden Das UNION SELECT-Konstrukt bietet die Möglichkeit, Abfragen mit derselben Ergebnisstruktur (Anzahl und Typ der Spalten) zu einer zusammenzufassen. Intern werden die Abfragen aber dennoch getrennt ausgeführt und anschließend deren Ergebnisse aneinandergehängt, sodass es sich hierbei im Prinzip nach wie vor um separate Abfragen handelt: SELECT BusinessEntityID, PhoneNumber, 'Mobil' AS Typ FROM Person.PersonPhone WHERE PhoneNumberTypeID=1 UNION SELECT BusinessEntityID, PhoneNumber, 'Privat' FROM Person.PersonPhone WHERE PhoneNumberTypeID=2 UNION SELECT BusinessEntityID, PhoneNumber, 'Arbeit' FROM Person.PersonPhone WHERE PhoneNumberTypeID=1 Listing 7.13: UNION SELECT zur Unterscheidung von drei Varianten
Am Ausführungsplan ist zu erkennen, dass die Tabelle Person.PersonPhone hier insgesamt dreimal durchlaufen werden muss. Durch geschickte Verwendung von CASE WHEN lassen sich die drei Abfragen zu einer zusammenfassen, wobei der IN-Operator in der WHERE-Klausel genutzt wird, um sicherzustellen, dass nur die Zeilen der ursprünglichen drei Abfragen auch im Ergebnis landen: SELECT BusinessEntityID, PhoneNumber, CASE WHEN PhoneNumberTypeID = 1 THEN 'Mobil' WHEN PhoneNumberTypeID = 2 THEN 'Privat' WHEN PhoneNumberTypeID = 3 THEN 'Arbeit' END AS Typ FROM Person.PersonPhone WHERE PhoneNumberTypeID IN (1,2,3) Listing 7.14: CASE WHEN zur Unterscheidung der drei Varianten
Bei Ausführung dieser Variante wird die Tabelle Person.PersonPhone nur noch einmal durchlaufen, wodurch der Aufwand auf etwa ein Drittel der ursprünglichen Variante reduziert wird.
Datenänderungen mit der MERGE-Anweisung zusammenfassen Wenn Daten in einer Zieltabelle mit Daten aus einer Importtabelle aktualisiert werden sollen, sind dafür normalerweise getrennte SQL-Anweisungen für das Einspielen fehlender Daten sowie das Aktualisieren bestehender Datensätze nötig.
164
2030-4.book Page 165 Monday, February 8, 2010 4:26 PM
Abfrageübergreifende Optimierung
INSERT INTO Person.Person (BusinessEntityID, PersonType, FirstName, LastName) SELECT BusinessEntityID, PersonType, FirstName, LastName FROM Person.PersonImport WHERE BusinessEntityID NOT IN (SELECT BusinessEntityID FROM Person.Person) UPDATE pers SET pers.PersonType = import.PersonType, pers.FirstName = import.FirstName, pers.LastName = import.LastName FROM Person.Person AS pers INNER JOIN Person.PersonImport AS import ON pers.BusinessEntityID = import.BusinessEntityID Listing 7.15: Getrennte SQL-Anweisungen zum Abgleichen zweier Tabellen
In der Praxis tritt diese Problematik häufig bei Adressdaten auf (wenn beispielsweise von Agenturen zusätzliche Adressdaten eingekauft wurden, die in den eigenen Adressbestand eingespielt werden sollen). Auch für das Aktualisieren von Data Warehouses wird oft ein ähnliches Verfahren genutzt. Seit SQL Server 2008 gibt es mit dem MERGE-Statement die Möglichkeit, die oben beschriebenen Aktionen zum Einfügen und Aktualisieren in einer einzigen Anweisung auszuführen. MERGE INTO Person.Person AS pers USING Person.PersonImport AS import ON pers.BusinessEntityID=import.BusinessEntityID WHEN MATCHED THEN UPDATE SET pers.PersonType = import.PersonType, pers.FirstName = import.FirstName, pers.LastName = import.LastName WHEN NOT MATCHED THEN INSERT (BusinessEntityID, PersonType, FirstName, LastName) VALUES (import.BusinessEntityID, import.PersonType, import.FirstName, import.LastName); Listing 7.16: INSERT und UPDATE in einer MERGE-Anweisung kombiniert
Aufgrund der Kombination aus UPDATE und INSERT wird die MERGE-Anweisung umgangssprachlich oft auch als UPSERT bezeichnet.
SQL Server Performance-Ratgeber
165
2030-4.book Page 166 Monday, February 8, 2010 4:26 PM
7 – Abfrageoptimierung
Ein Vergleich der Ausführungspläne beider Varianten macht in diesem Fall weniger Sinn, da diese hier sehr unübersichtlich werden. Man kann sich aber vorstellen, dass Ziel- und Importtabelle bei den einzelnen Anweisungen mehrfach durchlaufen werden, während diese beim MERGE nur einmal gejoint werden müssen und dann für jeden Datensatz die entsprechende Änderungsaktion ausgeführt wird. Außerdem ist zu erwarten, dass nicht nur die Anzahl der Lesezugriffe deutlich geringer wird, sondern die Zieltabelle auch effektiver für die Datenänderungen gesperrt werden kann, da die gesamte Aktion durch die Umstellung auf eine SQL-Anweisung auch als eine implizite Transaktion ausgeführt wird. Während der MERGE-Befehl Bestandteil des SQL-Standards ist, wurde dieser in T-SQL noch insofern erweitert, dass selbst das Löschen von im Ziel nicht vorhandenen Zeilen mit derselben Anweisung ausgeführt werden kann. Anstelle der folgenden DELETEAnweisung … DELETE FROM Person.Person WHERE BusinessEntityID NOT IN (SELECT BusinessEntityID FROM Person.PersonImport)
… kann man daher die MERGE-Anweisung wie folgt erweitern: MERGE INTO Person.Person AS pers USING Person.PersonImport AS import ON pers.BusinessEntityID=import.BusinessEntityID WHEN MATCHED THEN UPDATE SET pers.PersonType = import.PersonType, pers.FirstName = import.FirstName, pers.LastName = import.LastName WHEN NOT MATCHED THEN INSERT (BusinessEntityID, PersonType, FirstName, LastName) VALUES (import.BusinessEntityID, import.PersonType, import.FirstName, import.LastName) WHEN NOT MATCHED BY SOURCE THEN DELETE; Listing 7.17: MERGE-Anweisung mit INSERT, UPDATE und DELETE
7.2.3
Zwischenergebnisse speichern
SQL Server versucht nach Möglichkeit, oft benötigte Daten möglichst lange im Hauptspeicher zu halten, damit diese nicht mehrfach gelesen werden müssen. Dasselbe Prinzip kann man aber auch explizit anwenden, indem man temporäre Tabellen und Tabellenvariablen nutzt. Selbst dann, wenn diese nicht im Hauptspeicher gehalten werden können, kann es insbesondere bei komplexeren Abläufen schneller sein, mehrfach benötigte Zwischenergebnisse aus einer temporären Tabelle zu lesen, als sich die Daten mehrfach aus verschiedenen Tabellen wieder zusammensuchen zu müssen.
166
2030-4.book Page 167 Monday, February 8, 2010 4:26 PM
Abfrageübergreifende Optimierung
i
Dieser Abschnitt steht ein wenig im Gegensatz zu Kapitel 7.2.2 – Abfragen zusammenfassen, da durch die Verwendung von Zwischenergebnissen Abfragen in mehrere Abfragen aufgeteilt werden und dadurch auch der Ausführungsweg teilweise vorgegeben wird. Das entscheidende Kriterium für ein Zusammenfassen oder Aufsplitten von Abfragen ist aber, ob die Zwischenergebnisse mehrfach benötigt werden oder nicht.
Insbesondere im Zusammenhang mit Datenänderungsoperationen ist das Aufsplitten in mehrere Abfragen meist sinnvoll, da die Schreibsperren dann nicht so lange bestehen müssen, wenn die vielleicht komplexe und zeitaufwändige Auswahl der zu ändernden Zeilen in einer reinen SELECT-Abfrage vorweggenommen wurde. Sind Änderungen in mehreren Tabellen nötig, so hat dies auch mehrere DML-Anweisungen zur Folge. Durch die Vorauswahl wird vermieden, dass die DML-Anweisungen die Logik zur Auswahl der Datensätze mehrfach ausführen müssen.
Temporäre Tabellen Das folgende – vielleicht etwas konstruierte – Beispiel veranschaulicht dies. Hier sollen alle Personen mit Vornamen „Steven“ und PersonType='IN', die zu einem Kunden gehören, der Bestellungen mit mehr als drei Positionen ausgeführt hat, ausgewählt werden. Dazu sollen die Personendatensätze in eine separate Tabelle kopiert und bei den betreffenden Bestellungen ein Kommentar gespeichert werden. Um die IDs für die DML-Operationen in einer temporären Tabelle #Temp abzulegen, wird folgende Abfrage verwendet: SELECT pers.BusinessEntityID, soh.SalesOrderID INTO #Temp FROM Person.Person AS pers INNER JOIN Sales.Customer AS cust ON pers.BusinessEntityID=cust.PersonID INNER JOIN Sales.SalesOrderHeader AS soh ON cust.CustomerID=soh.CustomerID INNER JOIN Sales.SalesOrderDetail AS sod ON soh.SalesOrderID=sod.SalesOrderID WHERE pers.FirstName='Steven' AND pers.PersonType='IN' GROUP BY pers.BusinessEntityID, soh.SalesOrderID HAVING count(*)>2
Nach Ausführung dieser Abfrage, die sowohl einen Clustered Index Scan als auch einen Non-Clustered Index Scan benötigt, befinden sich neun Datensätze in der temporären Tabelle. Diese kann nun für verhältnismäßig einfach zu schreibende DML-Operationen genutzt werden. So kopiert die folgende Anweisung die entsprechenden Personenzeilen in eine neue Tabelle mit Namen StevensWithMoreThanTwoItemsPerOrder:
SQL Server Performance-Ratgeber
167
2030-4.book Page 168 Monday, February 8, 2010 4:26 PM
7 – Abfrageoptimierung
SELECT * INTO StevensWithMoreThanTwoItemsPerOrder FROM Person.Person WHERE BusinessEntityID IN (SELECT BusinessEntityID FROM #Temp)
Mit dem folgenden Update dagegen wird in den passenden Zeilen der Sales.SalesOrderHeader-Tabelle ein Kommentar hinterlegt: UPDATE Sales.SalesOrderHeader SET Comment = 'Test' WHERE SalesOrderID IN (SELECT SalesOrderID FROM #Temp)
Ohne die temporäre Tabelle müsste die komplexe Abfrage sowohl in die UPDATE- als auch in die SELECT INTO-Anweisung integriert werden: SELECT * INTO StevensWithMoreThanTwoItemsPerOrder FROM Person.Person AS pers2 WHERE EXISTS (SELECT pers.BusinessEntityID, soh.SalesOrderID FROM Person.Person AS pers INNER JOIN Sales.Customer AS cust ON pers.BusinessEntityID=cust.PersonID INNER JOIN Sales.SalesOrderHeader AS soh ON cust.CustomerID=soh.CustomerID INNER JOIN Sales.SalesOrderDetail AS sod ON soh.SalesOrderID=sod.SalesOrderID WHERE pers.FirstName='Steven' AND pers.PersonType='IN' GROUP BY pers.BusinessEntityID, soh.SalesOrderID HAVING count(*)>2 AND pers.BusinessEntityID=pers2.BusinessEntityID) UPDATE Sales.SalesOrderHeader SET Comment = 'Test' FROM Sales.SalesOrderHeader AS soh2 WHERE EXISTS (SELECT pers.BusinessEntityID, soh.SalesOrderID FROM Person.Person AS pers INNER JOIN Sales.Customer AS cust ON pers.BusinessEntityID=cust.PersonID INNER JOIN Sales.SalesOrderHeader AS soh ON cust.CustomerID=soh.CustomerID INNER JOIN Sales.SalesOrderDetail AS sod ON soh.SalesOrderID=sod.SalesOrderID WHERE pers.FirstName='Steven' AND pers.PersonType='IN' GROUP BY pers.BusinessEntityID, soh.SalesOrderID HAVING count(*)>2 AND soh.SalesOrderID=soh2.SalesOrderID) Listing 7.18: Ohne Verwendung der temporären Tabelle sind zwei komplexe Abfragen nötig
168
2030-4.book Page 169 Monday, February 8, 2010 4:26 PM
SQL-Cursor
Vergleicht man beide Varianten anhand der Ausführungspläne, so zeigt sich, dass jede der beiden DML-Operationen ohne Nutzung der temporären Tabelle fast denselben Aufwand verursacht wie das Füllen der temporären Tabelle. Die DML-Operationen auf Basis der temporären Tabelle aber jeweils nur ein Sechstel davon. Damit liegt der Gesamtaufwand bei Nutzung der temporären Tabelle insgesamt bei etwa 2/3 des sonst notwendigen Aufwandes. Hier konnte also ein deutlicher Performancegewinn erreicht werden.
Tabellenvariablen Anstatt der temporären Tabelle hätte man auch eine Tabellenvariable verwenden können, die aber vorher definiert werden muss. Anschließend muss auch anstelle des SELECT INTO ein INSERT INTO verwendet werden, da die Tabellenvariable im Gegensatz zur temporären Tabelle dann ja bereits vorhanden ist. DECLARE @TableVar AS TABLE (BusinessEntityID int, SalesOrderID int) INSERT INTO @TableVar (BusinessEntityID, SalesOrderID) SELECT pers.BusinessEntityID, soh.SalesOrderID FROM Person.Person AS pers INNER JOIN Sales.Customer AS cust ON pers.BusinessEntityID=cust.PersonID INNER JOIN Sales.SalesOrderHeader AS soh ON cust.CustomerID=soh.CustomerID INNER JOIN Sales.SalesOrderDetail AS sod ON soh.SalesOrderID=sod.SalesOrderID WHERE pers.FirstName='Steven' AND pers.PersonType='IN' GROUP BY pers.BusinessEntityID, soh.SalesOrderID HAVING count(*)>2 Listing 7.19: Einfügen von Daten in die Tabellenvariable
Bei den folgenden DML-Operationen muss dann nur noch die temporäre Tabelle (#Temp) durch den Namen der Tabellenvariablen (@TableVar) ersetzt werden. Vorteil der Tabellenvariablen ist, dass diese standardmäßig im Hauptspeicher gehalten und nur dann in der TempDB zwischengespeichert wird, wenn der verfügbare Hauptspeicher nicht ausreicht. Dadurch ist diese Variante im direkten Vergleich unter Umständen noch etwas schneller.
7.3
SQL-Cursor
7.3.1
Funktionsweise eines SQL-Cursors
SQL ist vom Grundansatz her eigentlich eine deklarative Sprache, in der lediglich beschrieben wird, welche Daten benötigt werden, sodass der Abfrageoptimierer sich anschließend den vermeintlich besten Weg suchen kann, auf diese Daten zuzugreifen. Da sich die Ergebnismengen oft auch auf mehrere Datenzeilen erstrecken, spricht man auch von einer mengenorientierten Verarbeitung der Daten.
SQL Server Performance-Ratgeber
169
2030-4.book Page 170 Monday, February 8, 2010 4:26 PM
7 – Abfrageoptimierung
Mit dem Cursor-Sprachkonstrukt bietet die Sprache SQL im Allgemeinen (und Microsofts T-SQL im Speziellen) aber auch eine Möglichkeit, satzweise auf die Daten zuzugreifen, indem der Weg des Zugriffs (oder zumindest ein Teil davon) explizit vorgegeben wird. Dabei hat der Begriff Cursor natürlich nichts mit einem Text- oder Mauscursor zu tun, sondern steht für Current Set of Records und meint die Menge an Datensätzen, die zeilenweise bearbeitet wird. Im Normalfall wird der Cursor durch eine SELECT-Abfrage definiert, die dann in einer Schleife satzweise durchlaufen wird. Dies entspricht im Prinzip dem Vorgehen, das in der Anwendungsentwicklung von ADO Recordsets oder auch ADO.NET DataReadern4 verwendet wird. Hier ein einfaches Beispiel für einen SQL-Cursor: DECLARE curPerson CURSOR FOR SELECT BusinessEntityID, FirstName, LastName FROM Person.Person DECLARE @BusinessEntityID int, @FirstName nvarchar(50), @LastName nvarchar(50) OPEN curPerson FETCH NEXT FROM curPerson INTO @BusinessEntityID, @FirstName, @LastName WHILE (@@fetch_status <> -1) BEGIN IF (@LastName = 'Miller') SELECT @BusinessEntityID, @FirstName, @LastName FETCH NEXT FROM curPerson INTO @BusinessEntityID, @FirstName, @LastName END CLOSE curPerson DEALLOCATE curPerson GO Listing 7.20: Ein einfacher SQL-Cursor
Nachdem der Cursor mithilfe einer SELECT-Abfrage definiert wurde, wird dieser geöffnet und die erste Zeile daraus in die entsprechenden Variablen gelesen. Anschließend wird die Zeile ausgegeben (sofern der Nachname Miller ist) und die nächste Zeile wird gelesen. Dies wird so lange wiederholt, bis die Systemvariable @@fetch_status den Wert 1 annimmt, wodurch das Ende der Abfrage angezeigt wird. Schließlich wird der Cursor geschlossen und der Speicher durch die Anweisung DEALLOCATE wieder freigegeben.
4.
170
ADO.NET DataReader werden ausführlich in Kapitel 8.2.1 – ADO.NET DataReader & Execute behandelt.
2030-4.book Page 171 Monday, February 8, 2010 4:26 PM
SQL-Cursor
7.3.2
Optimierungsmöglichkeiten
Da SQL Server auf mengenbasierte Verarbeitung ausgelegt ist, wird die Verwendung von SQL-Cursor normalerweise nicht empfohlen. Schließlich wird die Möglichkeit der automatischen Abfrageoptimierung bei einem SQL-Cursor dadurch eingeschränkt, dass der Ausführungsweg explizit vorgegeben wird. Dazu kommen noch ein paar weitere Nachteile: 쮿
Cursor sind aufwändig zu implementieren
쮿
Cursor haben ein ungünstiges Sperrverhalten
쮿
Cursor sind in der Ausführung vergleichsweise langsam
Daher gibt es verschiedene Ansätze, wie man SQL-Cursor in „normale“ SQL-Anweisungen umwandeln kann und damit dem Abfrageoptimierer den benötigten Spielraum schaffen kann. Dies ist jedoch nicht immer möglich, aber auch dann gibt es noch ein paar Regeln, die helfen können, die Performanceeinbußen möglichst gering zu halten.
Umwandeln eines SQL-Cursors Vielleicht haben Sie es schon gemerkt, das weiter oben dargestellte Cursor-Beispiel macht eigentlich nichts anderes als die folgende SQL-Abfrage: SELECT BusinessEntityID, FirstName, LastName FROM Person.Person WHERE LastName = 'Miller'
Die hier dargestellte Variante arbeitet natürlich deutlich effektiver. Aber leider ist es nicht immer so trivial, einen SQL-Cursor in mengenbasierte SQL-Operationen umzuwandeln. Im folgenden – etwas komplexeren – Beispiel soll die Personentabelle nach allen Einträgen mit Nachnamen „Whitney“ durchsucht werden und dazu die Anzahl der Einträge sowie eine kommagetrennte Liste der Vornamen ausgeben. Mit einem SQL-Cursor kann dies gelöst werden, indem ein Cursor definiert wird, der über die Tabelle läuft und in zwei separaten Variablen die Anzahl Zeilen mitzählt sowie die Vornamensliste jeweils ergänzt. DECLARE curPers CURSOR FOR SELECT LastName, FirstName FROM Person.Person WHERE LastName='Whitney' DECLARE @LastName nvarchar(50), @FirstName nvarchar(50), @FirstNames nvarchar(max), @Anzahl int Listing 7.21: Cursor zum Erstellen einer kommagetrennten Liste
SQL Server Performance-Ratgeber
171
2030-4.book Page 172 Monday, February 8, 2010 4:26 PM
7 – Abfrageoptimierung
OPEN curPers FETCH NEXT FROM curPers INTO @LastName, @FirstName SET @FirstNames = '' SET @Anzahl = 0 WHILE (@@fetch_status <> -1) BEGIN SET @Anzahl = @Anzahl+1 IF @Anzahl>1 SET @FirstNames = @FirstNames + ', ' SET @FirstNames = @FirstNames + @FirstName FETCH NEXT FROM curPers INTO @LastName, @FirstName END CLOSE curPers DEALLOCATE curPers SELECT @LastName AS Lastname, @FirstNames AS FirstNames, @Anzahl AS Anzahl Listing 7.21: Cursor zum Erstellen einer kommagetrennten Liste (Forts.)
Will man den Cursor vermeiden, so bietet sich beispielsweise eine Variante mit einer rekursiven Common Table Expression an, die alle Kombinationen von Vornamen (in einer bestimmten Reihenfolge) zusammen mit deren Anzahl bestimmt und anschließend die längste davon als Ergebnis auswählt. WITH Pers_CTE AS (SELECT BusinessEntityID, LastName, convert(nvarchar(max), FirstName) AS FirstNames, 1 AS Anzahl FROM Person.Person WHERE LastName='Whitney' UNION ALL SELECT pers.BusinessEntityID, pers.LastName, convert(nvarchar(max), pers.FirstName + ', ' + Pers_CTE.FirstNames) AS FirstNames, Pers_CTE.Anzahl+1 AS Anzahl FROM Person.Person pers INNER JOIN Pers_CTE ON pers.LastName=Pers_CTE.LastName WHERE pers.BusinessEntityID
172
2030-4.book Page 173 Monday, February 8, 2010 4:26 PM
SQL-Cursor
Führt man beide Varianten auf einmal aus und vergleicht dabei die beim tatsächlichen Ausführungsplan angegebenen Abfragekosten, so ist zu sehen, dass der Cursor mit 4 * 17% = 68% deutlich aufwändiger ist als die Variante mit Common Table Expression, die mit lediglich 34% angegeben ist.5 Noch einfacher und effektiver kann man die Aufgabenstellung aber mit geschickter Nutzung von Variablenzuweisungen innerhalb einer einfachen SQL-SELECT-Anweisung lösen. DECLARE @FirstNames nvarchar(max), @Anzahl int SET @FirstNames = '' SET @Anzahl = 0 SELECT @FirstNames = @FirstNames + (CASE WHEN @Anzahl>0 THEN ', ' ELSE '' END) + FirstName, @Anzahl = @Anzahl + 1 FROM Person.Person WHERE LastName='Whitney' SELECT 'Whitney' AS Lastname, @FirstNames AS FirstNames, @Anzahl AS Anzahl Listing 7.23: Verwendung von Variablenzuweisungen im SQL-SELECT
Vergleicht man nun alle drei Varianten anhand der Abfragekosten, so ergeben sich für die Variante mit SQL-Cursor 4 * 16% = 64%, bei Verwendung einer Common Table Expression 33% und bei der Variablenzuweisung in der SELECT-Anweisung lediglich 3%. Diese Variante ist die effektivste, da die Tabelle wie beim Cursor nur einmal durchlaufen wird, dies hier aber nicht in mehreren Einzelzugriffen, sondern mit einem GesamtSELECT erfolgt. Die CTE-Variante ist zwar auch eine einzige Abfrage, die aber eine höhere Komplexität hat und die Tabelle mehrfach durchläuft.
Optimierung eines SQL-Cursors Sofern sich ein SQL-Cursor aufgrund seiner Komplexität nicht vermeiden lässt, kann man zumindest dessen Ausführung durch Befolgung von ein paar einfachen Regeln möglichst performant gestalten. Der sicherlich offensichtlichste Ansatz liegt darin, die SELECT-Anweisung, die für die Cursor-Definition verwendet wird, möglichst gut zu optimieren. Dafür gelten dieselben Regeln wie für jede andere SELECT-Anweisung auch (siehe Kapitel 7.1 – Optimierung einzelner Abfragen). Insbesondere die möglichst genaue Vorselektion der benötigten Daten ist hier noch einmal hervorzuheben. Falls das CursorBeispiel von weiter oben also nicht in mengenbasierte Operationen umgestellt werden
5.
Die insgesamt 102% (68% + 34%) ergeben sich wahrscheinlich aus einem Rundungsfehler. Vermutlich sind es eher 4 * 16,5%, die in der Anzeige zu jeweils 17% aufgerundet werden.
SQL Server Performance-Ratgeber
173
2030-4.book Page 174 Monday, February 8, 2010 4:26 PM
7 – Abfrageoptimierung
kann (z.B. weil innerhalb der Schleife ein Aufruf einer gespeicherten Prozedur zum Versenden einer Mail ergänzt wurde), so kann man diese zumindest so weit optimieren, dass die Cursor-Definition bereits die Einschränkung auf den Nachnamen „Miller“ beinhaltet: DECLARE curPerson CURSOR FOR SELECT BusinessEntityID, FirstName, LastName FROM Person.Person WHERE LastName = 'Miller' DECLARE @BusinessEntityID int, @FirstName nvarchar(50), @LastName nvarchar(50) OPEN curPerson FETCH NEXT FROM curPerson INTO @BusinessEntityID, @FirstName, @LastName WHILE (@@fetch_status <> -1) BEGIN SELECT @BusinessEntityID, @FirstName, @LastName FETCH NEXT FROM curPerson INTO @BusinessEntityID, @FirstName, @LastName END CLOSE curPerson DEALLOCATE curPerson GO Listing 7.24: Optimierung des Cursors durch restriktivere Selektion
Eine weitere Optimierung des Cursors kann erfolgen, indem man durch fest definierte Schlüsselworte bestimmte Attribute des Cursors setzt und dadurch beispielsweise das Sperrverhalten des Cursors beeinflusst. Wenn man die Cursor-Definition wie folgt ändert, werden die Daten der entsprechenden Tabellen nur mit einem Shared Lock gesperrt, wodurch ein gleichzeitiges Lesen durch andere Prozesse möglich bleibt: DECLARE curPerson CURSOR READ_ONLY FOR SELECT BusinessEntityID, FirstName, LastName FROM Person.Person WHERE LastName = 'Miller'
Ohne diesen Hinweis erfolgt eine restriktivere Sperre der Datenzeilen, da mit einem Cursor auch Daten der jeweils aktuellen Zeile bearbeitet werden können, was jedoch selten genutzt wird und normalerweise – aus Performancegründen – auch nicht zu empfehlen ist.
174
2030-4.book Page 175 Monday, February 8, 2010 4:26 PM
SQL-Cursor
Hier eine Auflistung der aus Performancesicht wichtigsten Attribute: 쮿
FAST_FORWARD – nur vorwärts lesbarer Read-Only-Cursor (Kombination aus FORWARD_ONLY und READ_ONLY)
쮿
FORWARD_ONLY – kann nur vorwärts gelesen werden
쮿
READ_ONLY – verhindert Datenänderungen durch den Cursor
쮿
SCROLL_LOCKS – alle vom Cursor gelesenen Zeilen bleiben gesperrt und sind damit für spätere Änderungen innerhalb des Cursors gesichert verfügbar
쮿
OPTIMISTIC – Sperrt keine Zeilen, die gelesen wurden, sondern führt stattdessen vor Aktualisierungen einen Prüfsummenvergleich durch, um festzustellen, ob die Daten inzwischen geändert wurden. Ist dies der Fall, so schlägt die Aktualisierung fehl.
쮿
STATIC – Cursor arbeitet mit temporärer Kopie der Daten (quasi einem Snapshot)
쮿
KEYSET – Cursor arbeitet mit fester Reihenfolge und Zeilenauswahl, von anderen Prozessen geänderte Zeileninhalte sind aber sichtbar
쮿
DYNAMIC – Cursor berücksichtigt alle Datenänderungen aus anderen Prozessen
Dies stellt nur einen Ausschnitt der verwendbaren Attribute dar. Eine vollständige Auflistung ist unter der folgenden URL zu finden: http://msdn.microsoft.com/de-de/library/ms180169.aspx
7.3.3
Sinnvolle Verwendung eines SQL-Cursors
In ein paar Sonderfällen ist die Verwendung eines SQL-Cursors sogar effektiver als die Nutzung von mengenbasierten SQL-Abfragen. Das klassische Beispiel hierzu ist die Erstellung von laufenden Summen (beispielsweise für einen Bericht). Durch den Cursor muss die entsprechende Tabelle nur einmal durchlaufen werden und die laufende Summe wird dabei in einer Variablen mitgeführt. Mit einem SQL-SELECT ließe sich diese Problemstellung am ehesten durch Unterabfragen lösen, die zur Folge haben, dass die Tabelle zusätzlich für jeden Datensatz einmal (zumindest teilweise) durchlaufen werden muss, um die laufende Summe zu berechnen. Schauen wir uns hierzu ein typisches Beispiel an. Die folgende Abfrage liefert zu den ersten 100 Bestellungen jeweils die SalesOrderID, das Versanddatum, die Frachtkosten sowie eine laufende Summe dazu, die über eine Unterabfrage berechnet wird: SELECT TOP 100 SalesOrderID, ShipDate, Freight, (SELECT sum(Freight) FROM Sales.SalesOrderHeader AS soh2 WHERE soh2.SalesOrderID <= soh.SalesOrderID) AS FreightSum FROM Sales.SalesOrderHeader AS soh
SQL Server Performance-Ratgeber
175
2030-4.book Page 176 Monday, February 8, 2010 4:26 PM
7 – Abfrageoptimierung
Wenn man vor der Ausführung die Anzeige der Abfragestatistiken über die Anweisung SET STATISTICS IO ON aktiviert, so zeigen sich zusätzlich zum eigentlichen Abfrageergebnis folgende Werte: 'Worktable'-Tabelle. Scananzahl 100, logische Lesevorgänge 63534, physische Lesevorgänge 0, Read-Ahead-Lesevorgänge 0, logische LOB-Lesevorgänge 0, physische LOB-Lesevorgänge 0, Read-Ahead-LOB-Lesevorgänge 0. 'SalesOrderHeader'-Tabelle. Scananzahl 2, logische Lesevorgänge 691, physische Lesevorgänge 3, Read-Ahead-Lesevorgänge 691, logische LOB-Lesevorgänge 0, physische LOB-Lesevorgänge 0, Read-Ahead-LOB-Lesevorgänge 0. Interessant sind hier vor allem die Werte für die Scananzahl und die logischen Lesevorgänge. Die Worktable-Tabelle steht für eine temporäre Tabelle, die für die Ausführung einiger SQL-Abfragen verwendet wird. Dies ist insbesondere dann der Fall, wenn Unterabfragen oder Cursor verwendet werden. Die cursorbasierte Variante sieht wie folgt aus: DECLARE curSalesOrders CURSOR READ_ONLY FOR SELECT TOP 100 SalesOrderID, ShipDate, Freight FROM Sales.SalesOrderHeader DECLARE @SalesOrderID int, @ShipDate datetime, @Freight money, @FreightSum money SET @FreightSum=0 OPEN curSalesOrders FETCH NEXT FROM curSalesOrders INTO @SalesOrderID, @ShipDate, @Freight WHILE (@@fetch_status <> -1) BEGIN SET @FreightSum = @FreightSum + @Freight SELECT @SalesOrderID, @ShipDate, @Freight, @FreightSum FETCH NEXT FROM curSalesOrders INTO @SalesOrderID, @ShipDate, @Freight END CLOSE curSalesOrders DEALLOCATE curSalesOrders GO Listing 7.25: Cursor zum Berechnen von laufenden Summen
176
2030-4.book Page 177 Monday, February 8, 2010 4:26 PM
Zusammenfassung
Der wesentliche Unterschied ist dabei, dass die laufende Summe für die Frachtkosten hier nicht über eine Unterabfrage, sondern über eine Variable berechnet wird, die mit jedem Schleifendurchlauf erhöht wird. Die Abfragestatistiken sehen nun etwas unübersichtlicher aus, da das Cursor-Konstrukt mehrere Einzelabfragen beinhaltet. Insgesamt ergibt sich aber folgendes Bild. Es entsteht einmalig der folgende Aufwand (um den Cursor aufzubauen): 'Worktable'-Tabelle. Scananzahl 0, logische Lesevorgänge 200, physische Lesevorgänge 0, ReadAhead-Lesevorgänge 0, logische LOB-Lesevorgänge 0, physische LOB-Lesevorgänge 0, ReadAhead-LOB-Lesevorgänge 0. 'SalesOrderHeader'-Tabelle. Scananzahl 1, logische Lesevorgänge 7, physische Lesevorgänge 4, Read-Ahead-Lesevorgänge 682, logische LOB-Lesevorgänge 0, physische LOB-Lesevorgänge 0, Read-Ahead-LOB-Lesevorgänge 0. Dazu kommt 100-mal (für jeden Schleifendurchlauf einmal): 'Worktable'-Tabelle. Scananzahl 0, logische Lesevorgänge 2, physische Lesevorgänge 0, ReadAhead-Lesevorgänge 0, logische LOB-Lesevorgänge 0, physische LOB-Lesevorgänge 0, ReadAhead-LOB-Lesevorgänge 0. Das ergibt insgesamt lediglich sieben logische Lesevorgänge und einen Scan der SalesOrderHeader-Tabelle sowie 400 (200 + 100 * 2) logische Lesevorgänge der WorktableTabelle, die temporär während der Ausführung des Cursors angelegt wird. Damit ist der Gesamtaufwand um ein Vielfaches geringer als bei der vermeintlich einfacheren Variante mit der Unterabfrage.
7.4
Zusammenfassung
T-SQL bietet vielfältige Möglichkeiten der Abfrageoptimierung. Dies beginnt mit vergleichsweise trivialen Themen, wie der Nutzung von voll qualifizierten Bezeichnern und der möglichst frühen Einschränkung der Datenmenge über entsprechende WHEREBedingungen und Spaltenangaben. Eine der wichtigsten Herausforderungen in diesem Umfeld liegt darin, die Abfragen so zu formulieren, dass vorhandene Indizes möglichst effektiv genutzt werden können. Unterabfragen sollten entweder vermieden oder so genutzt werden, dass deren Ergebnisse gepuffert werden können und nicht für jede Zeile der aufrufenden Abfrage neu ermittelt werden müssen. Nur in Notfällen sollte man die explizite Beeinflussung von Ausführungsplänen mit Tabellen- und/oder Abfragehinweisen nutzen, die auch in Form von Plan Guides pauschal für einen bestimmten Abfragetext definiert werden können (falls man auf die Abfrage selbst keinen Zugriff hat). Damit Ausführungspläne von Abfragen möglichst gut wiederverwendbar sind, ist für mehrfach verwendete Abfragen eine Parametrisierung sinnvoll. Diese kann teilweise automatisch erfolgen, ansonsten kann man verschiedene Varianten nutzen, um Abfragen explizit zu parametrisieren.
SQL Server Performance-Ratgeber
177
2030-4.book Page 178 Monday, February 8, 2010 4:26 PM
7 – Abfrageoptimierung
Neben der Optimierung einzelner Abfragen kann aber auch eine abfrageübergreifende Optimierung zusätzliche Performance bringen, indem man beispielsweise ähnliche Abfragen zu einer zusammenfasst oder alternativ eine Abfrage in mehrere aufteilt, um einmal erstellte Zwischenergebnisse mehrfach nutzen zu können. Dazu kommt der richtige Umgang mit Sperren und Transaktionen, der sowohl in T-SQL als auch in dem im nächsten Kapitel behandelten Programmierframework .NET eine wichtige Rolle spielt. Schließlich ging es noch um SQL-Cursor, die man oft sinnvoll durch mengenorientierte SQL-Anweisungen ersetzen kann. Manchmal sind diese aber unvermeidbar oder sogar performanter als die mengenbasierten Alternativen (insbesondere dann, wenn durch Verwendung eines Cursors unnötige Table oder auch Index Scans gespart werden können). Dann gilt es, diese richtig zu verwenden, um keine Performance zu verschenken.
178
2030-4.book Page 179 Monday, February 8, 2010 4:26 PM
8
Optimierung des Datenzugriffs
Ein zentrales Thema für den optimalen Zugriff auf eine Datenbank ist die Art des Datenzugriffs aus einer selbst entwickelten Anwendung heraus. Die verfügbaren Möglichkeiten sind hier mittlerweile sehr vielfältig. Von mehr oder minder direkten Zugriffen über ADO.NET bis hin zur Nutzung von verschiedenen O/R-Mappern, wie beispielsweise dem ADO.NET Entity Framework. Man kann sich leicht vorstellen, dass solche Abstraktionsschichten, die einen höheren Komfort für die Entwickler erzielen, sich nicht gerade positiv auf die Performance auswirken. Aber in welchem Maße geschieht dies und wie kann man diese Performanceeinbußen trotzdem möglichst gering halten?
8.1
Datenzugriffstechnologien
8.1.1
Microsoft Data Access Components
Die Microsoft Data Access Components (kurz: MDAC) wurden mit Windows 98 eingeführt und stellen damit die wohl klassischste der hier vorgestellten Zugriffsmethoden dar. MDAC wird für nichtverwalteten Code eingesetzt, also für klassische ASP-Webseiten oder native (COMbasierte) Anwendungen, die mit Visual Basic 6.0 oder C++ entwickelt werden. Bei den meisten Windows-Varianten wird MDAC bereits mit ausgeliefert, so ist ab Windows XP Service Pack 2 bereits MDAC 2.8 enthalten. Bei Bedarf kann die aktuellste Version der MDAC-Bibliotheken (derzeit MDAC 2.8 SP1) von der folgenden Website heruntergeladen werden: http://msdn.microsoft.com/en-us/data/aa937730.aspx Im Gegensatz zu dem weiter unten vorgestellten SQL Native Client unterstützen die Microsoft Data Access Components Features wie Connection Pooling, Memory Management und clientseitige Cursor. Dabei kann insbesondere durch die Verwendung von Connection Pooling ein Performancegewinn erzielt werden, indem durch Zusammenfassen von Verbindungen der Verwaltungs-Overhead für zahlreiche Connections gespart werden kann. Um MDAC zu verwenden, kann eine von drei verschiedenen Programmierschnittstellen verwendet werden: 쮿
ODBC – Open Database Connectivity
쮿
OLE DB – Object Linking and Embedding (OLE) DB (for database)
쮿
ADO – ActiveX Data Objects
SQL Server Performance-Ratgeber
179
2030-4.book Page 180 Monday, February 8, 2010 4:26 PM
8 – Optimierung des Datenzugriffs
Die letztere Variante wiederum kann wahlweise in Kombination mit OLE DB oder ODBC genutzt werden.
MDAC über ODBC Über die ODBC-Schnittstelle lassen sich zahlreiche Datenquellen ansprechen. Angefangen von verschiedenen Datenbanken bis hin zu Formaten wie Microsoft Excel oder CSVDateien. Als Voraussetzung muss einerseits der ODBC-Treiber für das entsprechende Datenformat installiert sein. Dazu muss über die Systemsteuerung noch eine ODBCDatenquelle angelegt werden, die über einen Datenquellennamen (engl.: Data Source Name oder kurz: DSN) angesprochen wird. In dieser Datenquelle sind die Verbindungsinformationen zur Datenbank (Servername, Datenbankname, Benutzer, Passwort etc.) oder bei dateibasierten Datenquellen (wie z.B. Excel) der komplette Dateipfad (Verzeichnis und Datei) angegeben. Aus der Anwendung wird die Verbindung zur Datenquelle dann über den Datenquellennamen hergestellt. Diese Art des Zugriffs ist zwar sehr flexibel, aber aufgrund des generischen Aufbaus auch recht langsam.
MDAC über OLE DB OLE DB funktioniert vom Prinzip her recht ähnlich, ist jedoch deutlich schneller. Hier sind die verschiedenen Datenprovider in der Registry abgelegt und werden über einen so genannten Class Identifier (kurz: CLSID) identifiziert. Die CLSID ist eigentlich ein 128Bit-Wert, der in hexadezimaler Form in einer Zeichenkette gespeichert wird. Im Gegensatz zu ODBC werden die Datenquellen selbst aber erst in der Anwendung erzeugt, indem unter Verwendung des passenden Class Identifiers Datenquellenobjekte instanziiert werden. Sowohl MDAC über ODBC als auch MDAC über OLE DB sind vor allem für die native Entwicklung mit C++ interessant. Für Visual Basic, klassisches ASP oder JScript muss eine Bibliothek genutzt werden, in der die Zugriffe in handlichere Objekte und Methoden gekapselt werden. Diese zusätzliche Abstraktionsschicht nennt sich ADO.
MDAC über ADO ADO steht für ActiveX Data Objects, wobei diese Technologie nicht allzu viel mit ActiveX zu tun hat. Stattdessen basiert ADO auf OLE DB und kapselt dessen Möglichkeiten in ein hierarchisches Objektmodell. So wird eine Verbindung zu einer Datenbank über ein Connection-Objekt aufgebaut. Mit diesem kann dann – mithilfe einer SQL-Abfrage – ein Recordset-Objekt erzeugt werden, das die Daten einer Tabelle oder Abfrage repräsentiert und satzweise durchlaufen werden kann (Active Record Pattern). Dazu werden entsprechende Methoden bereitgestellt, mit denen in einem Recordset navigiert werden kann: 쮿
MoveFirst – zum ersten Datensatz springen
쮿
MoveLast – zum letzten Datensatz springen
180
2030-4.book Page 181 Monday, February 8, 2010 4:26 PM
Datenzugriffstechnologien 쮿
MovePrevious – zum vorigen Datensatz springen
쮿
MoveNext – zum nächsten Datensatz springen
Das Connection-Objekt stellt die Verbindung zur Datenbank aufgrund einer Verbindungszeichenfolge (oder Connection String) her. Diese kann entweder alle notwendigen Verbindungsdaten beinhalten oder aber einen Verweis auf eine ODBC-DSN enthalten, was den Aufbau der Verbindung wiederum etwas verlangsamt. Um MDAC über ADO verwenden zu können, ist im Projekt ein Verweis auf die entsprechende MDAC-Bibliothek nötig, welche die ADO-Klassen zur Verfügung stellt. Aufgrund der zusätzlichen Schicht ist MDAC über ADO tendenziell etwas langsamer als MDAC über OLE DB, was jedoch nicht allzu sehr ins Gewicht fallen sollte, sofern man nicht zusätzlich noch ODBC nutzt. Die Verwendung von Recordsets ist jedoch verhältnismäßig langsam, da es sich hierbei ja um eine satzweise Bearbeitung von Daten handelt, während der SQL Server – wie jeder SQL-basierte relationale Datenbankserver – für eine mengenorientierte Verarbeitung ausgelegt und optimiert ist.
8.1.2
SQL Native Client
Seit SQL Server 2005 ist auch der SQL Native Client (kurz: SNAC) verfügbar. Wie MDAC kann auch SNAC sowohl über ODBC als auch über OLE DB (jeweils direkt oder über ADO) verwendet werden, hier sind allerdings beide Treiber in einer gemeinsamen DLL enthalten. Da der Native Client dediziert für SQL Server entwickelt wurde, stehen hier auch SQL Server-spezifische Features – wie beispielsweise benutzerdefinierte Datentypen, XMLDatentypen, Multiple Active Result Sets (kurz: MARS) – zur Verfügung, von denen sich einige wiederum positiv auf die Performance auswirken können, wenn sie richtig eingesetzt werden. Daneben werden sowohl Committed Transaction Isolation als auch Snapshot Transaction Isolation unterstützt (MDAC unterstützt lediglich Committed Transaction Isolation). Eine nähere Erläuterung der Themen Transaktionen und Isolationslevel folgt weiter hinten in diesem Kapitel und wurde bereits in Kapitel 6.2 – Sperren und Transaktionen behandelt. Im Gegensatz zu MDAC bietet SNAC eine striktere Fehlerbehandlung sowie detailliertere Fehlerinformationen. Insgesamt bietet der SQL Native Client eine performante Variante, um von COM-basierten Anwendungen auf SQL Server zuzugreifen. Ausführliche Informationen zum SQL (Server) Native Client finden Sie auf folgender Website: http://msdn.microsoft.com/en-us/data/aa937733.aspx
SQL Server Performance-Ratgeber
181
2030-4.book Page 182 Monday, February 8, 2010 4:26 PM
8 – Optimierung des Datenzugriffs
8.1.3
ADO.NET
Wenn man sich in der Welt von .NET bewegt, bietet es sich natürlich an, auch eine Datenzugriffstechnologie zu verwenden, die nicht auf COM, sondern auf verwaltetem .NETCode basiert. Die Bezeichnung ADO.NET ist zwar von ADO abgeleitet, hat damit aber nicht mehr wirklich viel zu tun. ADO.NET stellt im Wesentlichen eine Reihe von Klassen zur Verfügung, über die man auf verschiedene Datenquellen (meist – aber nicht ausschließlich – Datenbanken) zugreifen kann. Dafür verwendet ADO.NET verschiedene Datenprovider, mit denen die datenbankspezifischen Besonderheiten in eine einheitliche Form gegossen werden. Datenprovider werden derzeit unter anderem für die folgenden Datenquellen ausgeliefert: 쮿
SQL Server
쮿
Oracle
쮿
OLE DB
쮿
ODBC
쮿
etc.
Dabei wird schnell klar, dass es mehrere mögliche Wege gibt, um auf einen Microsoft SQL Server zuzugreifen. Entweder man nutzt direkt den SQL Server Provider oder aber man nimmt den Umweg über OLE DB und/oder ODBC, was allerdings sicherlich nicht zu einer guten Performance beiträgt, da hierdurch nur zusätzliche Abstraktionsschichten dazukommen. Auch von Drittanbietern sind zusätzliche Datenprovider erhältlich. Dies ist möglich, indem die allgemeinen ADO.NET-Klassen im Namespace System.Data enthalten sind, während die datenproviderspezifischen Klassen in separaten Unter-Namespaces liegen: 쮿
System.Data.SqlClient für SQL Server
쮿
System.Data.OracleClient für Oracle
쮿
System.Data.OleDb für OLE DB
쮿
System.Data.Odbc für ODBC
Abgesehen von der Notwendigkeit, die passenden Namespaces einzubinden, erfolgt die Auswahl des zu verwendenden Datenproviders über den Connection String. In diesem werden allerdings auch verschiedene andere Einstellungen vorgenommen, die Auswirkungen auf die Performance haben können. Bereits in Kapitel 1.3.3 – Konfiguration wurde auf die verschiedenen Netzwerkprotokolle eingegangen. Neben der dort beschriebenen Auswahl des zu verwendenden Protokolls über den SQL-Server-Konfigurationsmanager kann die Auswahl auch über den Connection String vorgenommen werden. Dies ist insbesondere dann sinnvoll, wenn für den SQL Server Native Client mehrere Netzwerkprotokolle möglich sind.
182
2030-4.book Page 183 Monday, February 8, 2010 4:26 PM
Varianten des Datenzugriffs unter .NET
Beispiel (TCP/IP über Port 1431): Server=tcp:.\SQL2008,1431;Integrated Security=SSPI; Initial Catalog=AdventureWorks2008
Eine weitere Einstellung, die im Connection String gesetzt werden kann, ist die Network Packet Size (Netzwerk-Paketgröße), also die Einheit von Daten, die vom Client an den SQL Server übermittelt werden. Nun könnte man fälschlicherweise annehmen, dass 8 KB hier ein sinnvoller Wert wäre, da SQL Server die Daten in 8-KB-Blöcken ablegt. Dies ist aber weitgehend unabhängig voneinander, da es sich bei den vom und zum Server übertragenen Datenmengen ja nicht um die Datenblöcke handelt, die in den Datenpuffer des Servers geladen werden. Stattdessen handelt es sich um SQL-Anweisungen, die zum Server geschickt, oder Daten, die vom Server gelesen werden und je nach Abfrage völlig unterschiedliche Größen haben. So macht es Sinn, für Abfragen mit sehr kleinen Ergebnismengen auch eine kleine Paketgröße zu wählen (sinnvolle Werte starten ab etwa 512 Bytes) oder, wenn meist größere Datenmengen abgefragt werden, auch eine entsprechende Paketgröße (empfehlenswert sind Werte bis zu 32.767 Bytes) zu verwenden. Da dies aber selten einheitlich zu bestimmen ist und der Wert nur einmal pro Verbindung festgelegt werden kann, ist für die meisten Anwendungen die Voreinstellung, die je nach verwendeter SQL-Server-Version zwischen 4 und 8 KB liegt, durchaus sinnvoll. Beispiel (Paketgröße von 2.048 Bytes): Server=.\SQL2008; Integrated Security=SSPI; Initial Catalog=AdventureWorks2008;Packet Size=2048;
8.2
Varianten des Datenzugriffs unter .NET
Die meisten Entwickler bewegen sich heutzutage im Umfeld von .NET, daher werden die verschiedenen Methoden, mit denen man von .NET aus auf Daten zugreifen kann, im Folgenden etwas detaillierter beleuchtet. Denn selbst wenn man sich auf den Datenzugriff über ADO.NET festgelegt hat, gibt es noch verschiedene Varianten, diese Zugriffstechnologie zu nutzen. Dazu kommen noch diverse O/R-Mapper, die intern ebenfalls auf ADO.NET aufbauen. Der Einfachheit halber wird im weiteren Verlauf davon ausgegangen, dass der SQLServer-Datenprovider verwendet wird, da dieser den direktesten und damit performantesten Zugriff von ADO.NET auf SQL Server bietet. Für die folgenden Beispiele wird ein kleiner Ausschnitt aus der AdventureWorks2008-Datenbank verwendet, der lediglich die in Abbildung 8.1 dargestellten drei Tabellen beinhaltet.
SQL Server Performance-Ratgeber
183
2030-4.book Page 184 Monday, February 8, 2010 4:26 PM
8 – Optimierung des Datenzugriffs
Abbildung 8.1: Die Tabellen Person, PersonPhone und PhoneNumberType
i
Die Beispielquelltexte sind ausschließlich in C# dargestellt, da dies inzwischen die im professionellen Umfeld weiter verbreitete .NET-Sprache ist1. Natürlich sind alle Beispiele mit geringem Aufwand auch auf Visual Basic .NET übertragbar. Sollte die Übertragung von C# zu Visual Basic .NET Probleme bereiten, so kann ich hierbei gerne mit ein paar Tipps unterstützen oder die Visual Basic .NET-Varianten bei Bedarf erstellen und zum Download bereitstellen.
Die Anzeige der Ergebnisse in der Anwendung erfolgt in einer Multiline-TextBox mit Namen txtInfo.1 Um die verschiedenen Varianten zu bewerten, bietet es sich an, die SQL-Befehle, die im Hintergrund ausgeführt werden, mit dem SQL Server Profiler zu protokollieren und anschließend zu vergleichen, welche Variante performanter ist. Sowohl LINQ to SQL als auch das ADO.NET Entity Framework bieten zwar eigene Methoden, um die generierten SQL-Anweisungen auszuwerten, die Nutzung des SQL Server Profilers (mit der Vorlage TSQL) ist hier jedoch zu bevorzugen, da sie eine einheitliche Methode zur Protokollierung der SQL-Anweisungen bietet.2
1.
2.
184
Nutzer und Verfechter von Visual Basic .NET mögen mir diese Aussage verzeihen. Generell ist auch diese Sprache für den professionellen Einsatz inzwischen ebenso gut geeignet wie C#. Allerdings habe ich sowohl bei Projekten als auch bei Projektausschreibungen in den letzten Jahren die Beobachtung gemacht, dass hier meist C# die vom Kunden gewünschte bzw. eingesetzte Sprache ist. Eine detaillierte Erklärung des SQL Server Profilers finden Sie in Kapitel 10.2 – SQL Server Profiler & Datenbankoptimierungsratgeber sowie Anhang A.1 – Performance-Tools von Windows und SQL Server.
2030-4.book Page 185 Monday, February 8, 2010 4:26 PM
Varianten des Datenzugriffs unter .NET
8.2.1
ADO.NET DataReader & Execute
Die Kombination aus ADO.NET DataReader und Execute-Anweisungen ist die einfachste und dabei auch effizienteste Variante des Datenzugriffs über ADO.NET. Aber auch hier kann man eine Menge falsch – oder eben auch besser – machen.
Funktionsweise Bevor man einen DataReader nutzen kann, muss erst einmal der entsprechende Connection String vorbereitet werden: protected string dbConnection = default(string); dbConnection = @"server=localhost\sql2008;" + "Integrated Security=SSPI;" + "Database=AdventureWorks2008"; Listing 8.1: Setzen der Verbindungszeichenfolge
Zur Definition eines DataReaders wird eine SELECT-Abfrage angegeben, deren Ergebnis sequenziell durchlaufen wird. Dieses Vorgehen entspricht einem Forward Only Cursor in SQL. txtInfo.Text = "ADO.NET DataReader: "; using (SqlConnection connection = new SqlConnection(dbConnection)) { private string sqlQuery = default(string); sqlQuery = "SELECT PE.PersonType, PE.FirstName, PE.LastName, PP.* " + "FROM Person.Person AS PE " + " LEFT JOIN Person.PersonPhone AS PP " + " ON PE.BusinessEntityID=PP.BusinessEntityID" + " LEFT JOIN Person.PhoneNumberType AS PNT " + " ON PP.PhoneNumberTypeID=PNT.PhoneNumberTypeID"; SqlCommand command = new SqlCommand(sqlQuery); command.Connection = connection; connection.Open(); SqlDataReader reader; reader = command.ExecuteReader(CommandBehavior.Default); while (reader.Read()) { if (reader.GetString(0)== "SC") txtInfo.Text = txtInfo.Text + reader.GetString(1) + ", "; } } Listing 8.2: Nutzung eines DataReaders zum Durchlaufen einer SQL-Abfrage
SQL Server Performance-Ratgeber
185
2030-4.book Page 186 Monday, February 8, 2010 4:26 PM
8 – Optimierung des Datenzugriffs
Nachdem der DataReader komplett verarbeitet wurde, ist es wichtig, die Verbindung wieder zu schließen3, um sowohl gesperrte Datenbankobjekte als auch Systemressourcen wieder freizugeben. In dem oben gezeigten Beispiel geschieht dies implizit durch Verwendung eines using-Blocks. Für Entwickler, die von ADO auf ADO.NET umsteigen, kann man sich einen DataReader ähnlich wie ein Recordset vorstellen, allerdings mit dem Unterschied, dass der DataReader nur vorwärts durchlaufen werden kann. Dieser Mangel an Flexibilität resultiert allerdings in einer besseren Performance. Datenänderungen sind dagegen nur über separate SQL-Kommandos möglich. Hier werden UPDATE-, INSERT- und DELETE-Anweisungen als SqlCommand-Objekte definiert und direkt auf der Datenbank ausgeführt. Durch die direkte Nähe zu SQL ist eine sehr gute Performance möglich (natürlich nur, sofern die SQL-Anweisungen entsprechend formuliert sind).
Optimierung durch Projektion Da sowohl DataReader als auch die Execute-Anweisung direkt auf SQL-Befehlen basieren, ist eine Optimierung natürlich dadurch gegeben, dass man möglichst performante SQL-Anweisungen verwendet. Die Formulierung von einigermaßen optimalen SQLAnweisungen war jedoch Thema des vorigen Kapitels, sodass dieser Punkt hier bewusst vernachlässigt wird. Dazu gilt es, die Datenmenge – sowohl bezüglich der Zeilen als auch bezüglich der Spalten – recht früh zu reduzieren, damit bereits möglichst wenige Daten vom Server an den Client übergeben werden müssen. Daher wird in der folgenden Variante des Quelltextes die Einschränkung auf PersonType='SC' direkt in der SQL-Abfrage vorgenommen und auch der Stern in der SELECT-Klausel wurde durch eine explizite Spaltenangabe ersetzt. Dazu wird der JOIN auf die Tabelle Person.PhoneNumberType weggelassen, da aus dieser keine Informationen gelesen wurden: txtInfo.Text = "ADO.NET DataReader: "; using (SqlConnection connection = new SqlConnection(dbConnection)) { private string sqlQuery = default(string); sqlQuery = "SELECT PE.FirstName, PE.LastName, PP.PhoneNumber " + "FROM Person.Person AS PE " Listing 8.3: Einschränkung der benötigten Zeilen und Spalten bei der Definition des DataReaders 3.
186
Genau genommen wird die Verbindung nicht physikalisch geschlossen, sondern wandert in einen Connection Pool und kann damit von späteren Anfragen für einen identischen Connection String wiederverwendet werden. Durch dieses Connection Pooling wird ein unnötiges physikalisches Schließen und wieder Öffnen von identischen Verbindungen vermieden, was Ressourcen spart und damit einen Performancegewinn mit sich bringt. Damit das funktionieren kann, muss die Verbindung aber wie beschrieben logisch geschlossen werden, damit sie als momentan nicht mehr verwendet erkannt wird und in den Connection Pool wandern kann.
2030-4.book Page 187 Monday, February 8, 2010 4:26 PM
Varianten des Datenzugriffs unter .NET
+ "LEFT JOIN Person.PersonPhone AS PP " + " ON PE.BusinessEntityID=PP.BusinessEntityID " + "WHERE PE.PersonType='SC'"; SqlCommand command = new SqlCommand(sqlQuery); command.Connection = connection; connection.Open(); SqlDataReader reader; reader = command.ExecuteReader(CommandBehavior.Default); while (reader.Read()) { txtInfo.Text = txtInfo.Text + reader.GetString(0) + ", "; } } Listing 8.3: Einschränkung der benötigten Zeilen und Spalten bei der Definition des DataReaders (Forts.)
Optimierung durch explizite Parametrisierung Ein Nachteil des oben gezeigten Beispiels liegt darin, dass hierbei eine SQL-Anweisung ausgeführt wird, die einen konstanten Wert (in diesem Fall SC) für den Vergleich nutzt. Wird dieselbe Abfrage mit einem anderen Wert aufgerufen, so kann der bereits erstellte Ausführungsplan nicht wiederverwendet werden, da die Abfrage durch die andere Konstante vermeintlich eine andere ist. Eine Optimierung dieser Variante ist daher dadurch zu erreichen, dass man die Abfragen explizit mit Parametern versieht, damit für diese bei wiederholter Ausführung mit neuen Parameterwerten kein neuer Ausführungsplan erzeugt werden muss. Dazu wird die Abfrage wie folgt angepasst: sqlQuery = "SELECT PE.FirstName, PE.LastName, PP.PhoneNumber " + "FROM Person.Person AS PE " + " LEFT JOIN Person.PersonPhone AS PP " + " ON PE.BusinessEntityID=PP.BusinessEntityID " + "WHERE PE.PersonType=@PersonType";
Der Abfrageplan wird so genau für diese Abfrage erstellt und ist damit für variable Werte des Parameters @PersonType verwendbar. Zwischen Erstellung des CommandObjektes und dem Öffnen der Verbindung muss dann noch der Parameterwert zugewiesen werden: command.Parameters.Add("@PersonType", SqlDbType.NChar); command.Parameters[0].Value = "SC";
SQL Server Performance-Ratgeber
187
2030-4.book Page 188 Monday, February 8, 2010 4:26 PM
8 – Optimierung des Datenzugriffs
Mit dem SQL Server Profiler wird man nun feststellen, dass statt der direkten Ausführung einer SQL-Anweisung (Ereignis: SQL:BatchStarting) nun ein Remote Procedure Call zur Ausführung der gespeicherten Prozedur sp_executesql stattfindet (Ereignis: RPC:Starting). Diese gespeicherte Prozedur wird genutzt, um Abfragen mit expliziten Parametern auszuführen: exec sp_executesql N'SELECT PE.FirstName, PE.LastName, PP.PhoneNumber FROM Person.Person AS PE LEFT JOIN Person.PersonPhone AS PP ON PE.BusinessEntityID=PP.BusinessEntityID WHERE PE.PersonType=@PersonType',N'@PersonType nchar(2)',@PersonType=N'SC'
Am Ende des Aufrufs ist der Parameter zu erkennen, der automatisch den Typ nchar(2) bekommen hat. Die Länge von zwei Zeichen resultiert allerdings nicht aus dem Datentyp der entsprechenden Spalte, sondern lediglich aus der Länge des übergebenen Wertes. Wird beim nächsten Mal ein kürzerer oder längerer Wert übergeben, so wird auch ein Datentyp anderer Länge verwendet, was wiederum einen neuen Ausführungsplan als Folge hätte. Lösen kann man dieses Dilemma, indem man die Länge des Parameters direkt im .NETQuelltext mit angibt und dabei eine Maximallänge vorgibt, die für alle Aufrufe ausreicht: command.Parameters[0].Size = 10;
Somit ist sichergestellt, dass der Ausführungsplan auch für Werte anderer Länge wiederverwendet werden kann. Die in SQL Server integrierte Funktion zur Auto-Parametrisierung von Abfragen löst dieses Problem ähnlich, indem alphanumerische Parameter generell die Maximallänge von 8.000 bzw. 4.000 (für Unicode) Zeichen erhalten. In Kombination mit parametrisierten Abfragen bietet es sich auch oft an, diese vor Ausführung durch die Prepare-Methode für mehrfache Ausführung zu optimieren. In dem obigen Beispiel fügt man dazu einfach folgende Zeile direkt vor dem Aufruf der ExecuteReader-Methode ein: command.Prepare();
Wird die SQL-Anweisung allerdings nur einmalig ausgeführt, dann kann die Verwendung der Prepare-Methode sogar zu einer geringfügigen Performanceverschlechterung führen. Von daher sollte man diese nur gezielt einsetzen.
Optimierung durch asynchrone Zugriffe Je nach Anwendung lässt sich die Nutzung von DataReadern durch asynchrone Datenzugriffe noch weiter optimieren. Dabei wird der DataReader nicht mit einer Anweisung komplett ausgeführt, sondern deren Ausführung lediglich mit BeginExecuteReader gestartet, wonach die Anwendung bereits mit weiteren Aufgaben fortfahren kann. Dadurch kann die Zeit, die der DataReader zum Ausführen der Abfrage benötigt, möglichst effektiv genutzt werden. Damit asynchrone Abfragen verwendet werden können, muss allerdings die Eigenschaft Asynchronous Processing=true im Connection String gesetzt sein:
188
2030-4.book Page 189 Monday, February 8, 2010 4:26 PM
Varianten des Datenzugriffs unter .NET
protected string dbConnectionAsync = default(string); dbConnectionAsync = @"server=localhost\sql2008;" + "Integrated Security=SSPI;" + "Database=AdventureWorks2008;" + "Asynchronous Processing=true";
Nun kann man anstelle der im vorigen Beispiel verwendeten Methode ExecuteReader die asynchrone Variante BeginExecuteReader verwenden, mit der die Abfrage gestartet wird. Anschließend kann man weitere Aufgaben erledigen, ohne vorher auf die komplette Fertigstellung des Readers warten zu müssen. Über die Eigenschaft IsCompleted des zurückgegebenen Objekts vom Typ IAsyncResult lässt sich prüfen, wann der asynchrone Reader fertig gelesen ist. Wenn dies der Fall ist, erhält das SqlDataReader-Objekt seine Daten durch die Methode EndExecuteReader. txtInfo.Text = "ADO.NET DataReader (asynchron): "; using (SqlConnection connection = new SqlConnection(dbConnectionAsync)) { private string sqlQuery = default(string); sqlQuery = "SELECT PE.FirstName, PE.LastName, PP.PhoneNumber " + "FROM Person.Person AS PE " + " LEFT JOIN Person.PersonPhone AS PP " + " ON PE.BusinessEntityID=PP.BusinessEntityID " + "WHERE PE.PersonType='SC'"; SqlCommand command = new SqlCommand(sqlQuery); command.Connection = connection; connection.Open(); IAsyncResult result = command.BeginExecuteReader(); while (!result.IsCompleted) { // weitere Operationen ausführen } SqlDataReader reader; reader = command.EndExecuteReader(result); while (reader.Read()) { txtInfo.Text = txtInfo.Text + reader.GetString(0) + ", "; } } Listing 8.4: Asynchroner Zugriff auf einen DataReader
SQL Server Performance-Ratgeber
189
2030-4.book Page 190 Monday, February 8, 2010 4:26 PM
8 – Optimierung des Datenzugriffs
i
Analog zu der gerade beschriebenen asynchronen Variante der Methode ExecuteReader gibt es auch zu den Methoden ExecuteScalar und ExecuteNonQuery entsprechende asynchrone Varianten, die nach demselben Prinzip funktionieren. Hiervon dürfte insbesondere die Variante BeginExecuteNonQuery/ EndExecuteNonQuery interessant sein, da sich hierüber Datenänderungen über DML-Befehle ausführen lassen, ohne dass die aufrufende Anwendung auf das Ergebnis warten muss (das bei DML-Befehlen ohnehin selten ausgelesen wird). Allerdings muss die Anwendung dann auch so aufgebaut sein, dass die nächste Anweisung nicht voraussetzt, dass die Änderungen schon in die Datenbank geschrieben sind.
Verbindungen reduzieren mit Multiple Active Result Sets Mit den Multiple Active Result Sets (kurz: MARS) bietet der SQL Server seit der 2005erGeneration ein Feature, durch das man innerhalb einer Verbindung mehrere SqlCommands gleichzeitig offen halten kann. Dabei handelt es sich zwar weder um asynchrone noch um parallele Ausführungen, sondern lediglich um das Minimieren von offenen Verbindungen. Der geringere Gewinn ist sicherlich darin zu sehen, dass ein wenig Hauptspeicher für ein Verbindungsobjekt gespart wird. Wesentlicher ist dabei, dass weniger Probleme mit Sperren zu erwarten sind, sofern die SQL-Anweisungen aus derselben Verbindung kommen. Um MARS verwenden zu können, ist eine entsprechende Eigenschaft im Connection String zu aktivieren. Der Beispiel-Connection-String aus diesem Kapitel wäre dazu wie folgt zu erweitern4: dbConnection = @"server=localhost\sql2008;" + "Integrated Security=SSPI;" + "Database=AdventureWorks2008;" + "MultipleActiveResultSets=True;";
Sofern MARS nun über die Verbindungszeichenfolge aktiviert ist, kann man beispielsweise mit einer einzigen Verbindung eine SELECT-Abfrage über ein SqlDataReaderObjekt öffnen und satzweise durchlaufen, um dabei für jeden Datensatz eine INSERTAnweisung in eine andere Tabelle auszuführen.
8.2.2 ADO.NET DataSets Funktionsweise DataSets bieten einen größeren Programmierkomfort als DataReader, zumal sich über einen DataAdapter Daten aus einer Datenbank in ein DataSet abrufen lassen und darin 4.
190
Die dargestellte Verbindungszeichenfolge bezieht sich auf den SqlClient .NET Provider. Bei ODBC ist anstelle von MultipleActiveResultSets die Eigenschaft MARS Connection zu verwenden, bei OLE DB heißt diese lediglich MarsConn.
2030-4.book Page 191 Monday, February 8, 2010 4:26 PM
Varianten des Datenzugriffs unter .NET
auch ohne bestehende Verbindung weiterverarbeitet werden können. Auf diesem Weg wird quasi ein Teilbereich der Datenbank (ein oder mehrere Tabellen inklusive Beziehungen etc.) im Hauptspeicher abgebildet. Das Abrufen der Daten in das DataSet erfolgt explizit über die Fill-Methode des DataAdapters. Sind die Daten erst einmal in das DataSet geladen, erfolgt ein Abrufen der Daten von der Anwendung sehr schnell, da hier anstelle eines vergleichsweise langsamen Zugriffs auf den Datenbankserver lediglich auf das sich im Hauptspeicher des Clients befindende DataSet zugegriffen werden muss. Weniger günstig sieht es beim Aktualisieren der Daten aus. Dies geschieht zwar auch erst einmal auf dem im Hauptspeicher abgelegten DataSet. Sollen die Änderungen allerdings persistent gemacht werden, erfolgt ein Aktualisieren der Datenbank wiederum explizit über den DataAdapter (über die Update-Methode). Dabei wird natürlich nicht das gesamte DataSet zurück in die Datenbank geschrieben, sondern lediglich die Änderungen. Damit dies möglich ist, werden die Änderungen am DataSet intern protokolliert, wodurch ein entsprechender Verwaltungs-Overhead entsteht. Damit stellen die DataSets insgesamt eine eher langsame Variante der ADO.NET-Nutzung dar. Das Beispiel von weiter oben sieht in der Variante für ADO.NET DataSets wie folgt aus: txtInfo.Text = "ADO.NET DataSet: "; using (SqlConnection connection = new SqlConnection(dbConnection)) { DataSet dsPerson = new DataSet("Person"); SqlDataAdapter adapter = new SqlDataAdapter(); private string sqlQuery = default(string); sqlQuery = "SELECT PE.FirstName, PE.LastName, PP.PhoneNumber " + "FROM Person.Person AS PE " + " LEFT JOIN Person.PersonPhone AS PP " + " ON PE.BusinessEntityID=PP.BusinessEntityID " + "WHERE PE.PersonType='SC'"; SqlCommand command = new SqlCommand(sqlQuery); command.Connection = connection; adapter.SelectCommand = command; adapter.Fill(dsPerson); DataTable dtPerson = dsPerson.Tables[0]; foreach (DataRow drPerson in dtPerson.Rows) { txtInfo.Text = txtInfo.Text + drPerson.ItemArray[0].ToString() + ", "; } } Listing 8.5: Nutzung eines DataSets zum Durchlaufen einer SQL-Abfrage
SQL Server Performance-Ratgeber
191
2030-4.book Page 192 Monday, February 8, 2010 4:26 PM
8 – Optimierung des Datenzugriffs
Der Vorteil von ADO.NET DataSets liegt darin, dass die Daten von der Quelle entkoppelt sind. Damit eignen sich ADO.NET DataSets hervorragend für den mobilen Einsatz, da nur kurzzeitig eine Verbindung zum Server bestehen muss, wenn die Daten abgerufen oder die Änderungen zurückgesendet werden. Ansonsten sind DataSets vor allem für kleine Datenmengen gut geeignet, wenn es beispielsweise darum geht, Daten abzurufen, um sie auf einem Anwendungsformular in Auswahllisten zu verwenden.
Optimierung durch Anpassung des DataAdapters Wie bereits bei den anderen Zugriffstechnologien gilt natürlich auch für ADO.NET DataSets, die Datenmengen so früh wie möglich zu reduzieren. Das bedeutet, dass die vom SqlDataAdapter verwendete SQL-Abfrage so weit wie möglich eingeschränkt werden sollte, damit nur benötigte Daten im DataSet abgelegt werden. Da dies im Vergleich zum DataReader nichts Neues ist, wurde dieser Ansatz schon im Listing oben berücksichtigt. Die zweite wichtige Optimierungsmöglichkeit in diesem Zusammenhang bezieht sich auf die DML-Anweisungen (UPDATE, INSERT und DELETE), die der DataAdapter zum Aktualisieren der Daten nutzt. Diese werden im Normalfall mithilfe des CommandBuilders auf Basis der SELECT-Anweisung automatisch generiert, sobald ein CommandBuilder-Objekt mit folgender Anweisung erstellt wird: SqlCommandBuilder builder = new SqlCommandBuilder(adapter);
Durch den Aufruf der Methoden GetUpdateCommand, GetInsertCommand und GetDeleteCommand kann man sich die generierten SQL-Anweisungen einmal ansehen und wird dabei feststellen, dass in den meisten Fällen etwas umständliche SQL-Anweisungen erzeugt werden: Console.WriteLine(builder.GetUpdateCommand().CommandText); Console.WriteLine(builder.GetInsertCommand().CommandText); Console.WriteLine(builder.GetDeleteCommand().CommandText); Listing 8.6: Anzeige der generierten DML-Anweisungen
Diesem Problem kann man begegnen, indem man die UPDATE-, INSERT- und DELETEAnweisungen beim Definieren des DataAdapters auch explizit vorgibt, wodurch ein effektiverer Datenbankzugriff erreicht werden kann.5 In diesem Fall benötigt man dann gar kein SqlCommandBuilder-Objekt, sondern definiert die entsprechenden Kommandos als neue SqlCommand-Objekte, die den entsprechenden Eigenschaften des SqlDataAdapter-Objekts zugewiesen werden: adapter.UpdateCommand = new SqlCommand("UPDATE Person.Person " + "SET FirstName=@FirstName, LastName=@LastName " + "WHERE BusinessEntityID=@BusinessEntityID", connection); Listing 8.7: Explizite Definition der DML-Kommandos für einen DataAdapter 5.
192
Spätestens dann, wenn die SELECT-Abfrage des DataAdapters auf mehr als einer Tabelle basiert, müssen die DML-Anweisungen ohnehin explizit definiert werden.
2030-4.book Page 193 Monday, February 8, 2010 4:26 PM
Varianten des Datenzugriffs unter .NET
adapter.InsertCommand = new SqlCommand("INSERT INTO Person.Person " + "(BusinessEntityID, FirstName, LastName) " + "VALUES(@BusinessEntityID, @FirstName, @LastName)", connection); adapter.DeleteCommand = new SqlCommand("DELETE FROM Person.Person " + "WHERE BusinessEntityID=@BusinessEntityID", connection); adapter.InsertCommand.Parameters.Add("@BusinessEntityID", SqlDbType.Int, 4, "BusinessEntityID"); adapter.InsertCommand.Parameters.Add("@FirstName", SqlDbType.NVarChar, 50, "FirstName"); adapter.InsertCommand.Parameters.Add("@LastName", SqlDbType.NVarChar, 50, "LastName"); Listing 8.7: Explizite Definition der DML-Kommandos für einen DataAdapter (Forts.)
Im Gegensatz zu den vom CommandBuilder automatisch generierten SQLCommandObjekten lässt sich hier genau festlegen, für welche Spalten bei einem INSERT oder UPDATE die Werte explizit als Parameter übergeben werden.
8.2.3 LINQ to SQL Funktionsweise Mit dem seit .NET 3.5 verfügbaren LINQ to SQL werden Datenbanktabellen auf Objektklassen abgebildet, auf die man dann typsicher über die Abfragesprache LINQ (Language Integrated Query) zugreifen kann. Intern werden LINQ to SQL-Abfragen in SQL-Abfragen umgewandelt, die – sofern möglich – automatisch parametrisiert werden, um eine Wiederverwendbarkeit der Ausführungspläne zu unterstützen. Das bereits bekannte Beispiel sieht unter Verwendung von LINQ to SQL wie folgt aus: txtInfo.Text = "LINQ to SQL: "; AdventureLINQDataContext db = new AdventureLINQDataContext(); var Persons = from PE in db.Person join PP in db.PersonPhone on PE.BusinessEntityID equals PP.BusinessEntityID join PNT in db.PhoneNumberType on PP.PhoneNumberTypeID equals PNT.PhoneNumberTypeID where PE.PersonType == "SC" select new { Listing 8.8: Durchlaufen einer Abfrage über mehrere Tabellen mit LINQ to SQL
SQL Server Performance-Ratgeber
193
2030-4.book Page 194 Monday, February 8, 2010 4:26 PM
8 – Optimierung des Datenzugriffs
FirstName = PE.FirstName, LastName = PE.LastName, PhoneNumber = PP.PhoneNumber }; if (null != Persons) { foreach (var person in Persons) { txtInfo.Text = txtInfo.Text + person.FirstName + ", "; } } Listing 8.8: Durchlaufen einer Abfrage über mehrere Tabellen mit LINQ to SQL (Forts.)
Dabei wurde wieder die Tabelle PhoneNumberType eingebaut, um zu testen, ob LINQ to SQL schlau genug ist, um festzustellen, dass diese eigentlich nicht benötigt wird. Wenn man die vom SQL Server Profiler protokollierte SQL-Anweisung6 näher betrachtet, wird schnell klar, dass dies leider nicht der Fall ist: exec sp_executesql N'SELECT [t0].[FirstName], [t0].[LastName], [t1].[PhoneNumber] FROM [Person].[Person] AS [t0] INNER JOIN [Person].[PersonPhone] AS [t1] ON [t0].[BusinessEntityID] = [t1].[BusinessEntityID] INNER JOIN [Person].[PhoneNumberType] AS [t2] ON [t1].[PhoneNumberTypeID] = [t2].[PhoneNumberTypeID] WHERE [t0].[PersonType] = @p0',N'@p0 nvarchar(2)',@p0=N'SC'
Dieses Verhalten verwundert allerdings auch nicht weiter, da durch die verwendete LINQ-Syntax quasi explizit vorgegeben wurde, auch diese Tabelle mit in die Abfrage einzubeziehen. Hier bietet LINQ to SQL aber elegantere Möglichkeiten, wie Sie weiter unten sehen werden. Dafür fällt aber auf, dass LINQ to SQL eine automatische Parametrisierung durchgeführt hat, indem der Vergleichswert für die Spalte PersonType automatisch als Parameter des Typs nvarchar(2) übergeben wurde. Dabei tritt also dasselbe Problem auf, das bereits beim ADO.NET DataReader zu beobachten war: Der Parameter wird mit einer Länge definiert, die lediglich aus der Größe des übergebenen Wertes resultiert. Wird beim nächsten Aufruf derselben Abfrage ein Wert mit anderer Länge übergeben, so wird auch ein neuer Ausführungsplan erstellt. Leider gibt es bei LINQ to SQL hierfür keine so einfache Lösung wie beim ADO.NET DataReader. Da die Parametrisierung nicht explizit erfolgt, lässt sich auch keine Länge explizit vorgeben. Allerdings soll dies mit .NET 4.0 besser werden, sodass auch hier alphanumerische Parameter automatisch mit der Maximallänge von 8000 Zeichen (bzw. 4000 Unicode-Zeichen) definiert werden. 6.
194
Die dargestellte Anweisung wurde genau so mit dem SQL Server Profiler protokolliert. Zur besseren Lesbarkeit wurden lediglich ein paar Zeilenumbrüche eingefügt.
2030-4.book Page 195 Monday, February 8, 2010 4:26 PM
Varianten des Datenzugriffs unter .NET
Ansonsten ist das von LINQ to SQL generierte SQL sehr datenbanknah und damit meist recht performant, was sich dadurch ergibt, dass LINQ to SQL ein relativ einfaches 1:1Mapping von SQL-Server-Tabellen auf Objektklassen erstellt.
Optimierung durch Lazy Loading Wenn man die Abfragesyntax so anwendet, dass in der eigentlichen Abfrage nur die Daten aus der Tabelle Person gelesen werden, während die Telefondaten später dazugeladen werden, arbeitet LINQ to SQL unter Umständen noch effektiver .
Die meisten O/R-Mapper bieten zwei verschiedene Ladestrategien an. Beim so genannten Lazy Loading (oft auch als Deferred Loading bezeichnet) werden beim Zugriff auf Objekte nur diejenigen Objekte geladen, die gerade benötigt werden. Das spart unnötig große Datenzugriffe, kann aber mehrere Einzelzugriffe zur Folge haben, wenn zusätzlich benötigte Daten später nachgeladen werden. Die Alternative hierzu nennt sich Eager Loading (oder auch Pre-Fetching) und lädt abhängige Objekte bereits mit. Dadurch ist nur eine große Abfrage nötig, die eventuell nicht benötigte Daten unnötig lädt. Welche Variante die effektivere ist, hängt primär von der benötigten Datenmenge ab. Wenn Sie die abhängigen Objekte – wie hier die Telefonnummern zu den Personen – ohnehin alle benötigen, ist Eager Loading performanter, weil die Gesamtabfrage vom SQL Server effektiver verarbeitet werden kann. Benötigen Sie dagegen nur einen Teil der abhängigen Daten, ist dagegen Lazy Loading vorzuziehen. LINQ to SQL verwendet – wie die meisten O/R-Mapper – per Default Lazy Loading und setzt dieses auch sehr konsequent um. Befinden sich abhängige Objekte bereits im Speicher, wird dies automatisch erkannt, sodass diese nicht noch einmal geladen werden müssen. Man nennt dies auch transparentes (oder implizites) Lazy Loading. Schauen wir uns dazu ein einfaches Beispiel-Listing an: txtInfo.Text = "LINQ to SQL (Lazy Loading): "; AdventureLINQDataContext db = new AdventureLINQDataContext(); var Phones = db.PersonPhone.Where(o => o.BusinessEntityID < 100).ToList(); if (null != Phones) { foreach (var phone in Phones) { txtInfo.Text = txtInfo.Text + phone.PhoneNumber + " (" + phone.PhoneNumberType.Name + "), "; } } Listing 8.9: Abfrage von Daten einer abhängigen Tabelle bzw. Entität
SQL Server Performance-Ratgeber
195
2030-4.book Page 196 Monday, February 8, 2010 4:26 PM
8 – Optimierung des Datenzugriffs
In der Phones-Collection werden alle PersonPhone-Einträge abgelegt, deren BusinessEntityID kleiner als hundert ist. Anschließend wird diese Collection durchlaufen und zu jedem Objekt neben der Telefonnummer auch der Typ der Telefonnummer ausgegeben, der aus der PhoneNumberType-Tabelle nachgeladen werden muss. Dies geschieht automatisch in dem Moment, wo der Wert benötigt wird. Wenn man sich den Profiler Trace ansieht, fällt dabei aber auf, dass jeder Wert nur einmal gelesen wird. Es gibt also nur zwei Zugriffe auf die Tabelle PhoneNumberType, da von der Phones-Collection auf nur zwei verschiedene Einträge in dieser verwiesen wird (mit den PhoneNumberTypeID-Werten 1 und 3). exec sp_executesql N'SELECT [t0].[BusinessEntityID], [t0].[PhoneNumber], [t0].[PhoneNumberTypeID], [t0].[ModifiedDate] FROM [Person].[PersonPhone] AS [t0] WHERE [t0].[BusinessEntityID] < @p0',N'@p0 int',@p0=100
exec sp_executesql N'SELECT [t0].[PhoneNumberTypeID], [t0].[Name], [t0].[ModifiedDate] FROM [Person].[PhoneNumberType] AS [t0] WHERE [t0].[PhoneNumberTypeID] = @p0',N'@p0 int',@p0=1
exec sp_executesql N'SELECT [t0].[PhoneNumberTypeID], [t0].[Name], [t0].[ModifiedDate] FROM [Person].[PhoneNumberType] AS [t0] WHERE [t0].[PhoneNumberTypeID] = @p0',N'@p0 int',@p0=3
Dieses Verfahren ist dann sinnvoll, wenn nur wenige verschiedene Zeilen aus der abhängigen Tabelle gelesen werden müssen. Entweder weil diese nur wenige unterschiedliche Zeilen hat oder aber weil die abhängigen Daten nur für einen kleinen Teil der übergeordneten Zeilen benötigt werden. Letzteres findet vor allem bei Auswahllisten mit Detailansicht für das ausgewählte Element Anwendung, da die abhängigen Daten dann nur für dieses eine Element geladen werden müssen. Werden dagegen zu allen Datensätzen auch abhängige Daten benötigt, ohne dass diese sich ständig wiederholen, so ist es effektiver, diese gleich zusammen mit den anderen Daten zu laden.
196
2030-4.book Page 197 Monday, February 8, 2010 4:26 PM
Varianten des Datenzugriffs unter .NET
Optimierung durch Eager Loading In LINQ to SQL wird das Eager Loading für den gesamten Datenkontext aktiviert, sodass bestimmte abhängige Objekte generell bereits vorab geladen werden. Um das Lazy-Loading-Beispiel auf Eager Loading umzustellen, fügt man direkt nach der Definition des Datenkontextes folgende Zeilen ein: DataLoadOptions options = new DataLoadOptions(); options.LoadWith(p => p.PhoneNumberType); db.LoadOptions = options; Listing 8.10: Laden der abhängigen Tabelle PhoneNumberType per Eager Loading
Mit diesen wird ein DataLoadOptions-Objekt erzeugt, dem mitgeteilt wird, dass mit der Tabelle PersonPhone auch immer die dazugehörenden Daten aus der Tabelle PhoneNumberType zu lesen sind. Mit der dritten Zeile werden diese DataLoadOptions an den Datenkontext gebunden. Schaut man sich nun mit dem SQL Server Profiler das ausgeführte SQL an, so ist dort nur noch eine Anweisung zu finden: exec sp_executesql N'SELECT [t0].[BusinessEntityID], [t0].[PhoneNumber], [t0].[PhoneNumberTypeID], [t0].[ModifiedDate], [t1].[PhoneNumberTypeID] AS [PhoneNumberTypeID2], [t1].[Name], [t1].[ModifiedDate] AS [ModifiedDate2] FROM [Person].[PersonPhone] AS [t0] INNER JOIN [Person].[PhoneNumberType] AS [t1] ON [t1].[PhoneNumberTypeID] = [t0].[PhoneNumberTypeID] WHERE [t0].[BusinessEntityID] < @p0',N'@p0 int',@p0=100
Während beim Lazy und Eager Loading die Daten auf Tabellen- bzw. Objektebene betrachtet wurden, bietet LINQ to SQL aber auch noch eine Variante, um einzelne Felder eines Objekts erst bei Bedarf nachzuladen.
Optimierung durch Verzögertes Laden Im Prinzip handelt es sich auch hierbei um eine Variante des Lazy Loading, allerdings wird diese über die Ansicht des LINQ to SQL-Diagramms festgelegt. Wenn Sie nach dem Öffnen der dbml-Datei eine Spalte einer Tabelle anklicken (hier AdditionalContactInfo in der Tabelle Person), finden Sie auf der rechten Seite eine Eigenschaft Verzögert geladen (oder englisch: Delayed Loading). Wenn Sie diese auf True setzen, wird der Wert der Spalte erst dann geladen, wenn die Spalte explizit von der Anwendung angesprochen wird.
SQL Server Performance-Ratgeber
197
2030-4.book Page 198 Monday, February 8, 2010 4:26 PM
8 – Optimierung des Datenzugriffs
Abbildung 8.2: Das LINQ to SQL-Diagramm
Dieses verzögerte Laden macht vor allem für große Felder (z.B. des Typs varbinary(max), varchar(max) oder xml) Sinn, wenn diese nur für einzelne Zeilen benötigt werden. Das folgende Beispiel veranschaulicht dies, indem die Inhalte des Feldes AdditionalContactInfo nur für eine einzelne Zeile abgefragt werden: txtInfo.Text = "LINQ to SQL (Delay Load): "; AdventureLINQDataContext db = new AdventureLINQDataContext(); var Persons = db.Person.Where(o => o.PersonType == "SC").ToList(); if (null != Persons) { foreach (var person in Persons) { txtInfo.Text += person.FirstName + ", "; if (person.FirstName == "Gustavo" & person.LastName == "Achong") txtInfo.Text += person.AdditionalContactInfo.ToString(); } } Listing 8.11: Routine zum Testen des verzögerten Ladens von einzelnen Feldern
198
2030-4.book Page 199 Monday, February 8, 2010 4:26 PM
Varianten des Datenzugriffs unter .NET
Derselbe Code verhält sich nun unterschiedlich, je nachdem, wie die Eigenschaft Verzögert geladen gesetzt ist. In der Standardeinstellung (False) wird das Feld direkt mitgelesen, wie der folgende SQL Server Profiler Trace zeigt: exec sp_executesql N'SELECT [t0].[BusinessEntityID], [t0].[PersonType], [t0].[NameStyle], [t0].[Title], [t0].[FirstName], [t0].[MiddleName], [t0].[LastName], [t0].[Suffix], [t0].[EmailPromotion], [t0].[AdditionalContactInfo], [t0].[Demographics], [t0].[rowguid], [t0].[ModifiedDate] FROM [Person].[Person] AS [t0] WHERE [t0].[PersonType] = @p0',N'@p0 nvarchar(2)',@p0=N'SC'
Wird die Eigenschaft dagegen auf True gesetzt, so ist das Feld AdditionalContactInfo in der ersten Abfrage nicht enthalten, wird dann aber für die eine Zeile explizit nachgeladen: exec sp_executesql N'SELECT [t0].[BusinessEntityID], [t0].[PersonType], [t0].[NameStyle], [t0].[Title], [t0].[FirstName], [t0].[MiddleName], [t0].[LastName], [t0].[Suffix], [t0].[EmailPromotion], [t0].[Demographics], [t0].[rowguid], [t0].[ModifiedDate] FROM [Person].[Person] AS [t0] WHERE [t0].[PersonType] = @p0',N'@p0 nvarchar(2)',@p0=N'SC' exec sp_executesql N'SELECT [t0].[AdditionalContactInfo] FROM [Person].[Person] AS [t0] WHERE [t0].[BusinessEntityID] = @p0',N'@p0 int',@p0=291
i
Auch das verzögerte Laden von einzelnen Spalten lässt sich über die DataLoadOptions wieder deaktivieren, sodass die Daten wahlweise auch direkt mit dem Rest des Datensatzes geladen werden können, obwohl dies in der dbml-Datei anders vermerkt ist.
8.2.4 ADO.NET Entity Framework Funktionsweise bei Verwendung von LINQ to Entities Auch das Entity Framework – das mit .NET 3.5 Service Pack 1 eingeführt wurde – bietet einen typsicheren Zugriff auf Datenbanken über die Abfragesprache LINQ (genau genommen: LINQ to Entities). Allerdings ist hier die Abbildung des Datenmodells auf das Objektmodell weit flexibler als bei LINQ to SQL, was aufgrund der damit verbundenen höheren Komplexität bereits eine schlechtere Performance befürchten lässt.
SQL Server Performance-Ratgeber
199
2030-4.book Page 200 Monday, February 8, 2010 4:26 PM
8 – Optimierung des Datenzugriffs
Wenn man das Beispiel von oben direkt auf LINQ to Entities umsetzt, ergibt sich folgender Quelltext: txtInfo.Text = "Entity Framework mit LINQ to Entities: "; AdventureWorks2008Entities db = new AdventureWorks2008Entities(GetEntityBuilder().ConnectionString); var Persons = from PE in db.PersonEF join PP in db.PersonPhoneEF on PE.BusinessEntityID equals PP.BusinessEntityID join PNT in db.PhoneNumberTypeEF on PP.PhoneNumberTypeID equals PNT.PhoneNumberTypeID where PE.PersonType == "SC" select new { FirstName = PE.FirstName, LastName = PE.LastName, PhoneNumber = PP.PhoneNumber }; if (null != Persons) { foreach (var person in Persons) { txtInfo.Text = txtInfo.Text + person.FirstName + ", "; } } Listing 8.12: Durchlaufen einer Abfrage über mehrere Tabellen mit LINQ to Entities
Das bei Ausführung generierte SQL ist im Vergleich zu LINQ to SQL erst mal etwas unübersichtlicher (was an den gewöhnungsbedürftigen Tabellen-Aliasnamen und dem umständlichen NULL-Handling liegt). Wirklich ungünstig für die Performance ist aber vor allem die Tatsache, dass hier keine automatische Parametrisierung durchgeführt werden konnte, was bei mehrfacher Ausführung mit anderen Konstanten unnötige Abfragepläne zur Folge hat. SELECT 1 AS [C1], [Extent1].[FirstName] AS [FirstName], [Extent1].[LastName] AS [LastName], [Extent2].[PhoneNumber] AS [PhoneNumber] FROM [Person].[Person] AS [Extent1]
200
2030-4.book Page 201 Monday, February 8, 2010 4:26 PM
Varianten des Datenzugriffs unter .NET
INNER JOIN [Person].[PersonPhone] AS [Extent2] ON ([Extent1].[BusinessEntityID] = [Extent2].[BusinessEntityID]) OR (([Extent1].[BusinessEntityID] IS NULL) AND ([Extent2].[BusinessEntityID] IS NULL)) INNER JOIN [Person].[PhoneNumberType] AS [Extent3] ON ([Extent2].[PhoneNumberTypeID] = [Extent3].[PhoneNumberTypeID]) OR (([Extent2].[PhoneNumberTypeID] IS NULL) AND ([Extent3].[PhoneNumberTypeID] IS NULL)) WHERE N'SC' = [Extent1].[PersonType]
Optimierung durch explizite Parametrisierung Dem Problem der nicht funktionierenden automatischen Parametrisierung7 kann man begegnen, indem man explizit Parameter definiert. Auf diesem Weg kann man sichergehen, dass – und auch genau welche – Parameter beim Ausführungsplan der Abfrage verwendet werden. Die explizite Parametrisierung erreicht man, indem man die gewünschten Parameter zuvor als Variablen definiert und diese dann in der LINQ to Entities-Abfrage verwendet: string PersonType = "SC"; var Persons = from PE in db.PersonEF join PP in db.PersonPhoneEF on PE.BusinessEntityID equals PP.BusinessEntityID join PNT in db.PhoneNumberTypeEF on PP.PhoneNumberTypeID equals PNT.PhoneNumberTypeID where PE.PersonType == PersonType select new { FirstName = PE.FirstName, LastName = PE.LastName, PhoneNumber = PP.PhoneNumber }; Listing 8.13: Explizite Parametrisierung durch Verwendung von Variablen
Im SQL Server Profiler Trace kann man jetzt leicht nachprüfen, dass nun auch für die verwendete T-SQL-Abfrage ein Parameter verwendet wird: exec sp_executesql N'SELECT 1 AS [C1], [Extent1].[FirstName] AS [FirstName], [Extent1].[LastName] AS [LastName], 7.
Auch das Entity Framework unterstützt eigentlich eine automatische Parametrisierung, ist hierbei aber offensichtlich nicht ganz so flexibel wie LINQ to SQL, wie das vorige Beispiel zeigt.
SQL Server Performance-Ratgeber
201
2030-4.book Page 202 Monday, February 8, 2010 4:26 PM
8 – Optimierung des Datenzugriffs
[Extent2].[PhoneNumber] AS [PhoneNumber] FROM [Person].[Person] AS [Extent1] INNER JOIN [Person].[PersonPhone] AS [Extent2] ON ([Extent1].[BusinessEntityID] = [Extent2].[BusinessEntityID]) OR (([Extent1].[BusinessEntityID] IS NULL) AND ([Extent2].[BusinessEntityID] IS NULL)) INNER JOIN [Person].[PhoneNumberType] AS [Extent3] ON ([Extent2].[PhoneNumberTypeID] = [Extent3].[PhoneNumberTypeID]) OR (([Extent2].[PhoneNumberTypeID] IS NULL) AND ([Extent3].[PhoneNumberTypeID] IS NULL)) WHERE [Extent1].[PersonType] = @p__linq__1',N'@p__linq__1 nvarchar(2)', @p__linq__1=N'SC'
i
Dasselbe Verfahren kann auch bei LINQ to SQL verwendet werden, um explizit Parameter zu definieren. Allerdings ist dies dort normalerweise nicht notwendig, da die automatische Parametrisierung bei LINQ to SQL besser greift.
Optimierung durch Lazy Loading Auch bei LINQ to Entities wird standardmäßig Lazy Loading verwendet, sofern die abhängigen Entitäten nicht direkt in der Abfrage mit angegeben werden. Im Gegensatz zu LINQ to SQL muss das Laden von abhängigen Objekten hier allerdings mit der LoadMethode explizit angefordert werden (daher wird diese Variante auch gerne als explizites Lazy Loading bezeichnet): txtInfo.Text = "Entity Framework mit LINQ to Entities (Lazy Loading): "; AdventureWorks2008Entities db = new AdventureWorks2008Entities(GetEntityBuilder().ConnectionString); var Phones = db.PersonPhoneEF.Where(o => o.BusinessEntityID < 100).ToList(); if (null != Phones) { foreach (var phone in Phones) { phone.PhoneNumberTypeReference.Load(); txtInfo.Text = txtInfo.Text + phone.PhoneNumber + " (" + phone.PhoneNumberType.Name + "), "; } } Listing 8.14: Explizites Lazy Loading bei LINQ to Entities
202
2030-4.book Page 203 Monday, February 8, 2010 4:26 PM
Varianten des Datenzugriffs unter .NET
Dies wird dann aber immer durchgeführt, sodass mehrfach benötigte Objekte auch mehrfach geladen werden, was zu großen Mengen an unnötigen Datenzugriffen führen kann. In dem obigen Beispiel würde dies bedeuten, dass bis zu 99 Einzelzugriffe auf die PhoneNumberType-Tabelle erfolgen, auch wenn aus dieser insgesamt nur zwei verschiedene Zeilen gelesen werden. Diesem Problem kann man aber begegnen, indem man vor dem Laden eines abhängigen Objekts prüft, ob sich dieses bereits im Speicher befindet. if (!phone.PhoneNumberTypeReference.IsLoaded) phone.PhoneNumberTypeReference.Load();
Damit wäre quasi dasselbe Verhalten nachgestellt, das LINQ to SQL schon automatisch mitbringt. Interessant ist dabei, dass beim Aufruf der Load-Methode nicht nur auf die Tabelle PhoneNumberType zugegriffen wird, sondern auch der entsprechende Datensatz aus der PhoneNumber-Tabelle noch einmal geladen wird, wie der Profiler Trace unten zeigt. Dafür findet hier zumindest die automatische Parametrisierung statt. SELECT 1 AS [C1], [Extent1].[BusinessEntityID] AS [BusinessEntityID], [Extent1].[PhoneNumber] AS [PhoneNumber], [Extent1].[PhoneNumberTypeID] AS [PhoneNumberTypeID], [Extent1].[ModifiedDate] AS [ModifiedDate] FROM [Person].[PersonPhone] AS [Extent1] WHERE [Extent1].[BusinessEntityID] < 100 exec sp_executesql N'SELECT [Extent2].[PhoneNumberTypeID] AS [PhoneNumberTypeID], [Extent2].[Name] AS [Name], [Extent2].[ModifiedDate] AS [ModifiedDate] FROM [Person].[PersonPhone] AS [Extent1] INNER JOIN [Person].[PhoneNumberType] AS [Extent2] ON [Extent1].[PhoneNumberTypeID] = [Extent2].[PhoneNumberTypeID] WHERE ([Extent1].[BusinessEntityID] = @EntityKeyValue1) AND ([Extent1].[PhoneNumber] = @EntityKeyValue2) AND ([Extent1].[PhoneNumberTypeID] = @EntityKeyValue3)', N'@EntityKeyValue1 int,@EntityKeyValue2 nvarchar(12),@EntityKeyValue3 int', @EntityKeyValue1=1,@EntityKeyValue2=N'697-555-0142',@EntityKeyValue3=1 exec sp_executesql N'SELECT [Extent2].[PhoneNumberTypeID] AS [PhoneNumberTypeID], [Extent2].[Name] AS [Name],
SQL Server Performance-Ratgeber
203
2030-4.book Page 204 Monday, February 8, 2010 4:26 PM
8 – Optimierung des Datenzugriffs
[Extent2].[ModifiedDate] AS [ModifiedDate] FROM [Person].[PersonPhone] AS [Extent1] INNER JOIN [Person].[PhoneNumberType] AS [Extent2] ON [Extent1].[PhoneNumberTypeID] = [Extent2].[PhoneNumberTypeID] WHERE ([Extent1].[BusinessEntityID] = @EntityKeyValue1) AND ([Extent1].[PhoneNumber] = @EntityKeyValue2) AND ([Extent1].[PhoneNumberTypeID] = @EntityKeyValue3)', N'@EntityKeyValue1 int,@EntityKeyValue2 nvarchar(12),@EntityKeyValue3 int', @EntityKeyValue1=2,@EntityKeyValue2=N'819-555-0175',@EntityKeyValue3=3
Optimierung durch Eager Loading Beim Eager Loading zeigt sich das Entity Framework dagegen flexibler als LINQ to SQL, da hier mithilfe der Include-Funktion pro Abfrage definiert werden kann, dass die abhängigen Objekte gleich mitgeladen werden sollen. txtInfo.Text = "Entity Framework mit LINQ to Entities (Eager Loading): "; AdventureWorks2008Entities db = new AdventureWorks2008Entities(GetEntityBuilder().ConnectionString); var Phones = db.PersonPhoneEF.Include("PhoneNumberType").Where(o => o.BusinessEntityID < 100).ToList(); if (null != Phones) { foreach (var phone in Phones) { txtInfo.Text += phone.PhoneNumber + " (" + phone.PhoneNumberType.Name + "), "; } } Listing 8.15: Eager Loading bei LINQ to Entities mithilfe der Include-Methode
Im SQL Server Profiler Trace lässt sich leicht überprüfen, dass nun nur noch eine Abfrage ausgeführt werden muss: SELECT 1 AS [C1], [Extent1].[BusinessEntityID] AS [BusinessEntityID], [Extent1].[PhoneNumber] AS [PhoneNumber], [Extent1].[PhoneNumberTypeID] AS [PhoneNumberTypeID], [Extent1].[ModifiedDate] AS [ModifiedDate],
204
2030-4.book Page 205 Monday, February 8, 2010 4:26 PM
Varianten des Datenzugriffs unter .NET
[Extent2].[PhoneNumberTypeID] AS [PhoneNumberTypeID1], [Extent2].[Name] AS [Name], [Extent2].[ModifiedDate] AS [ModifiedDate1] FROM [Person].[PersonPhone] AS [Extent1] LEFT OUTER JOIN [Person].[PhoneNumberType] AS [Extent2] ON [Extent1].[PhoneNumberTypeID] = [Extent2].[PhoneNumberTypeID] WHERE [Extent1].[BusinessEntityID] < 100
Funktionsweise bei Verwendung von Entity SQL Will man größere Kontrolle über die Abfragen, so lässt sich beim Entity Framework statt LINQ to Entities auch die Abfragesprache Entity SQL (oder kurz: eSQL) einsetzen, deren Anweisungen dann weitgehend dem auf dem Server ausgeführten T-SQL entsprechen.
i
Auch wenn die Syntax von eSQL und T-SQL auf den ersten Blick identisch aussieht, sollte man beachten, dass eSQL lediglich eine Syntax ist, die an SQL angelehnt ist und nicht alle SQL-Features unterstützt. Der Sinn von eSQL liegt vielmehr darin, eine datenquellenübergreifende Abfragesprache zur Verfügung zu haben, mit der alle vom Entity Framework unterstützten Datenquellen genutzt werden können.
Da eSQL-Anweisungen quasi als dynamisches SQL in Strings zusammengesetzt werden, verzichtet man damit auch auf die Typsicherheit von LINQ. Dafür bietet eSQL aber vergleichbare Möglichkeiten wie bei ADO.NET bezüglich expliziter Parametrisierung von Abfragen (inklusive der Möglichkeit, die Größe eines Parametertyps explizit zu definieren). Die eSQL-Variante des Beispiels sieht bei expliziter Parametrisierung wie folgt aus: txtInfo.Text = "Entity Framework mit eSQL: "; AdventureWorks2008Entities db = new AdventureWorks2008Entities(GetEntityBuilder().ConnectionString); using (EntityConnection con = new EntityConnection(db.Connection.ConnectionString)) { con.Open(); sqlQuery = "SELECT PE.FirstName, PE.LastName, PP.PhoneNumber " + "FROM AdventureWorks2008Entities.PersonEF AS PE " + " LEFT JOIN AdventureWorks2008Entities.PersonPhoneEF AS PP " + " ON PE.BusinessEntityID=PP.BusinessEntityID " + " LEFT JOIN AdventureWorks2008Entities.PhoneNumberTypeEF AS PNT " + " ON PP.PhoneNumberTypeID=PNT.PhoneNumberTypeID " Listing 8.16: Explizite Parametrisierung bei Entity SQL
SQL Server Performance-Ratgeber
205
2030-4.book Page 206 Monday, February 8, 2010 4:26 PM
8 – Optimierung des Datenzugriffs
+ "WHERE PE.PersonType=@PersonType"; using (EntityCommand cmd = new EntityCommand(sqlQuery, con)) { EntityParameter param1 = new EntityParameter(); param1.Direction = ParameterDirection.Input; param1.Value = "SC"; param1.Size = 10; param1.ParameterName = "PersonType"; cmd.Parameters.Add(param1); using (DbDataReader dr = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) { while (dr.Read()) { txtInfo.Text = txtInfo.Text + dr[0].ToString() + ", "; } } } } Listing 8.16: Explizite Parametrisierung bei Entity SQL (Forts.)
Auch die hierdurch erzeugte SQL-Anweisung stellt sich wie erwartet dar: exec sp_executesql N'SELECT 1 AS [C1], [Extent1].[FirstName] AS [FirstName], [Extent1].[LastName] AS [LastName], [Extent2].[PhoneNumber] AS [PhoneNumber] FROM [Person].[Person] AS [Extent1] LEFT OUTER JOIN [Person].[PersonPhone] AS [Extent2] ON [Extent1].[BusinessEntityID] = [Extent2].[BusinessEntityID] LEFT OUTER JOIN [Person].[PhoneNumberType] AS [Extent3] ON [Extent2].[PhoneNumberTypeID] = [Extent3].[PhoneNumberTypeID] WHERE [Extent1].[PersonType] = @PersonType', N'@PersonType nvarchar(10)',@PersonType=N'SC'
Wird der Parameter PersonType nicht explizit definiert, findet bei der Abfrage aus diesem Beispiel auch keine automatische Parametrisierung statt.
206
2030-4.book Page 207 Monday, February 8, 2010 4:26 PM
Datenbanklogik auf den Server verlagern
i
Zusammen mit dem .NET Framework 4.0 wird auch die Version 2.0 des Entity Frameworks erscheinen, in der die wesentlichen Schwächen behoben sein sollen, die noch gegenüber LINQ to SQL bestehen. So soll das Entity Framework dann beispielsweise zusätzlich zum Reverse Engineering (Erzeugen von Objektklassen aufgrund einer existierenden Datenbank) auch Forward Engineering unterstützen (also das Erzeugen von Datenbankobjekten aufgrund eines Objektmodells). Für die Performance interessanter dürfte aber sein, dass mit dem Entity Framework 2.0 endlich auch transparentes Lazy Loading möglich sein soll.
8.2.5 Sonstige O/R-Mapper Neben LINQ to SQL und dem ADO.NET Entity Framework gibt es noch eine ganze Reihe an weiteren so genannten O/R-Mappern, also Zwischenschichten für einen objektorientierten Zugriff auf relationale Datenbanken. Diese werden von verschiedenen Drittanbietern zur Verfügung gestellt und sind teilweise auch frei erhältlich. Einige namhafte Vertreter sind beispielsweise nHibernate, SubSonic, .netTiers und Telerik OpenAccess. Eine detaillierte Behandlung einzelner O/R-Mapper würde sicherlich den Rahmen dieses Textes sprengen, von daher habe ich mich bewusst auf die beiden weiter oben beschriebenen, von Microsoft bereitgestellten Technologien beschränkt. Die Arbeitsweise der meisten Drittanbieter-O/R-Mapper ist jedoch vergleichbar, sodass auch hier dieselben Grundregeln gelten: 쮿
ein O/R-Mapper kann nie schneller sein als ein direkter SQL-Zugriff
쮿
sofern keine automatische Parametrisierung stattfindet, sollten konstante Werte nach Möglichkeit explizit als Parameter definiert werden, damit beim wiederholten Aufruf mit anderen Werten kein neuer Ausführungsplan erzeugt werden muss
쮿
der bewusste Einsatz von Lazy und Eager Loading (sofern vom jeweiligen O/R-Mapper unterstützt) kann gravierende Auswirkungen auf die Performance haben
쮿
eventuell muss geprüft werden, dass bereits geladene Objekte nicht nochmal geladen werden
Im Zweifelsfall empfiehlt es sich, mit dem SQL Server Profiler zu beobachten, welche SQL-Anweisungen zu welcher Zeit an den SQL Server geschickt werden, um herauszufinden, wie der jeweilige O/R-Mapper arbeitet.
8.3
Datenbanklogik auf den Server verlagern
Ein weiterer Ansatz, den Datenzugriff zu optimieren, liegt darin, die Datenbanklogik (oder zumindest Teile davon) so weit wie möglich auf den SQL Server zu verlagern und dadurch unter anderem die Menge der Daten, die zwischen Client und Server transportiert werden müssen, auf ein notwendiges Minimum zu reduzieren.
SQL Server Performance-Ratgeber
207
2030-4.book Page 208 Monday, February 8, 2010 4:26 PM
8 – Optimierung des Datenzugriffs
8.3.1
Sichten
Die Nutzung von Sichten verlagert die Logik nicht direkt auf den SQL Server, da dort definierte Sichten immer zusammen mit der aufrufenden Abfrage ausgeführt und daher auch gemeinsam optimiert werden. Zwar wird für Sichten kein fertiger Ausführungsplan im Cache gespeichert, aber zumindest der von Parser und Algebrizer erzeugte Syntaxbaum (siehe Kapitel 6.1 – Interne Verarbeitung von Abfragen), in dem geprüft wurde, dass die Sicht auch zu den aktuellen Datenstrukturen passt. Bei der Kompilierung einer Abfrage, die diese Sicht verwendet, wird also etwas Zeit gespart. Ein wichtigerer Aspekt ist aber dabei, dass in den Sichten schon verschiedene Optimierungsansätze aus den letzten Kapiteln berücksichtigt werden können, ohne dass ein Anwendungsentwickler, der diese Sicht verwendet, diese kennen muss. So kann beispielsweise eine Reduzierung auf die von der Anwendung benötigten Spalten stattfinden oder durch zusätzliche redundante WHERE-Bedingungen sichergestellt werden, dass für die Ausführung der Sicht auch ein Index verwendet werden kann. Insbesondere die Gefahr eines SELECT * FROM GanzGrosseTabelle kann man leicht ausschließen, indem man einen Satz von wohlüberlegten Sichten definiert und in der Anwendung nur über diese Sichten auf die Daten zugreift. Dies lässt sich über eine entsprechende Rechtevergabe leicht sicherstellen. Da hierbei auch noch zusätzliche Sicherheit gewonnen wird, ist es ein gängiger Ansatz – insbesondere bei Webanwendungen –, für die entsprechenden Anwendungsuser nicht den direkten Zugriff auf Tabellen, sondern nur über Sichten (lesend) oder gespeicherte Prozeduren (für Datenänderungen) zuzulassen. Hierüber lässt sich beispielsweise die Gefahr einer so genannten SQL Injection minimieren, wenn nicht sogar ganz ausschließen. Zur Verwendung in .NET lassen sich Sichten überall dort einbinden, wo SQL-Abfragen genutzt werden. Damit ist sowohl die Nutzung mit ADO.NET DataReadern und DataSets als auch mit LINQ to SQL oder dem ADO.NET Entity Framework möglich.
8.3.2 Funktionen und gespeicherte Prozeduren Sowohl für Funktionen als auch für gespeicherte Prozeduren wird ein Ausführungsplan im Plancache gespeichert, der – bei geschickter Parametrisierung – oft wiederverwendet werden kann. Daher lohnt sich bei diesen Varianten auch der Aufwand, die darin enthaltenen Abfragen gut zu optimieren. Die seit SQL 2005 verfügbaren Tabellenwertfunktionen lassen sich ausgezeichnet verwenden, um den Zweck von Sichten zu erfüllen, dabei aber neben den vorkompilierten Ausführungsplänen auch noch die Möglichkeit der Parameterübergabe zu nutzen. Viele der so genannten dynamischen Management-Sichten sind daher inzwischen als Tabellenwertfunktionen realisiert. Während Tabellenwertfunktionen in der Anwendung wie Sichten sind (nur eben mit Parametern – sofern vorhanden), gibt es für gespeicherte Prozeduren sowohl in LINQ to SQL als auch im Entity Framework eine eigene Schnittstelle. Diese werden in Form von Methoden in die Datenzugriffsschicht integriert und lassen sich anschließend komfortabel nutzen. 208
2030-4.book Page 209 Monday, February 8, 2010 4:26 PM
Datenbanklogik auf den Server verlagern
8.3.3
SQL Server .NET CLR
Sollten innerhalb einer gespeicherten Prozedur .NET-Funktionalitäten benötigt werden, so lassen sich diese mithilfe der SQL Server CLR-Integration8 realisieren. Mit dieser bei SQL Server 2005 neu eingeführten Technik ist es möglich, mit .NET Funktionalitäten (Funktionen, gespeicherte Prozeduren, aber auch eigene Datentypen) zu entwickeln, um diese dann direkt vom SQL Server aus nutzen zu können. Sofern man sich aber ohnehin in einer .NET-Anwendung befindet, sollte man diese Funktionalitäten stattdessen eher dort implementieren und nur die Datenzugriffe in gespeicherten Prozeduren und Funktionen unterbringen. Für die Entscheidung, ob es sinnvoller ist, eine Funktion in SQL oder in .NET abzubilden, gibt es eine klare und einfache Regel: Wenn die Funktion hauptsächlich aus Datenzugriffen besteht, sollte diese in SQL implementiert werden. Wenn die Funktion aber primär auf komplexer Logik wie beispielsweise komplizierten Berechnungen basiert, ist .NET die bessere Wahl. Genau genommen handelt es sich hierbei um die Umkehrung der Idee, datenlastige Operationen in gespeicherte Prozeduren auszulagern, damit diese direkt auf dem SQL Server ausgeführt werden. Mit der CLR-Integration wird der .NET-Code zwar ebenfalls auf dem SQL Server ausgeführt, auf dem die entsprechende Funktion registriert ist, allerdings im Kontext der .NET-Laufzeitumgebung, wodurch man die SQL Server Engine von prozessorlastigen Aufgaben befreien kann. Aus Performancesicht richtig interessant wird die Nutzung der CLR-Integration, wenn man mithilfe einer .NET-Funktion so transformieren kann, dass hierdurch die Nutzung eines Indexes ermöglicht oder verbessert wird. Hierzu ein einfaches Beispiel: Die Tabelle Person.Person soll nach einer bestimmten Person durchsucht werden. Leider haben Sie von dieser Person nur eine Variable verfügbar, in der die E-Mail-Adresse gespeichert ist. Der Aufbau der E-Mail-Adresse ist allerdings bekannt: Vorname. [email protected] Nun wäre beispielsweise folgende Abfrage möglich: SELECT * FROM Person.Person WHERE @eMailAddress LIKE FirstName + '.%' AND @eMailAddress LIKE '%.' + LastName + '@%'
Dies funktioniert zwar, kann aber einen auf beide Namensteile vorhandenen Index nur per Index Scan nutzen, da die Datenbankfelder nicht isoliert in der Abfrage verwendet werden.
8.
CLR steht für Common Language Runtime und bezeichnet die Laufzeitumgebung des .NET Frameworks
SQL Server Performance-Ratgeber
209
2030-4.book Page 210 Monday, February 8, 2010 4:26 PM
8 – Optimierung des Datenzugriffs
Hätten Sie stattdessen Funktionen zur Verfügung, mit der die Namensbestandteile aus der E-Mail-Adresse extrahiert werden können, ließe sich die Abfrage in folgende Form umschreiben, die dann auch einen Index Seek nutzen kann: SELECT * FROM Person.Person WHERE LastName = ufnGetLastNameFromEMail(@eMailAddress) AND FirstName = ufnGetFirstNameFromEMail(@eMailAddress)
Ob man die Funktion dann besser in T-SQL oder .NET entwickelt, hängt allerdings wieder davon ab, ob diese eher daten- oder rechenlastige Operationen beinhaltet. In dem Beispiel oben ließe sich das Problem sogar durch geschickte Verwendung der T-SQL-Zeichenfolgenfunktionen direkt in der Abfrage lösen, aber das ist ja nicht immer der Fall. Und wenn man diese Funktionalität öfter benötigen sollte, will man sicherlich eine SQLAbfrage wie die folgende vermeiden: SELECT * FROM Person.Person WHERE LastName = SUBSTRING(@eMailAddress, CHARINDEX('.',@eMailAddress)+1, CHARINDEX('@',@eMailAddress)-CHARINDEX('.',@eMailAddress)-1) AND FirstName = LEFT(@eMailAddress, CHARINDEX('.',@eMailAddress)-1)
8.4
Zusammenfassung
Für eine Optimierung des Datenzugriffs sind viele verschiedene Faktoren zu berücksichtigen. Dies beginnt bereits bei der Wahl der richtigen Zugriffstechnologie, die sich primär dadurch entscheidet, ob die Anwendung auf verwaltetem .NET-Code oder nativem Code basiert. Für nativen Code verwendet man idealerweise SNAC, sofern es sich um einen SQL Server ab der 2005er-Generation handelt, oder MDAC für ältere SQL Server und andere Datenquellen. Im Falle von einer verwalteten ADO.NET-Anwendung erfolgt die Unterscheidung durch die Wahl des passenden Datenproviders. Für SQL Server bietet der native Datenprovider neben der Unterstützung spezieller Funktionen auch die bessere Performance, während die Provider für OLE DB und ODBC über eine weitere Zwischenschicht auch eine größere Vielfalt von Datenquellen ansprechen. Wie bei den Zugriffstechnologien gilt auch für die Programmier-Frameworks, dass der Datenzugriff schneller ist, je direkter er erfolgt. Und je mehr Abstraktionsschichten zwischen Datenbank und Anwendung liegen, umso langsamer wird der Datenzugriff voraussichtlich sein. Das soll natürlich nicht darin ausufern, dass Anwendungsentwickler künftig nur noch direkt per SQL-Anweisung Daten abfragen. Stattdessen gilt es, die performancekritischen Teile einer Anwendung zu identifizieren, um diese gezielt zu optimieren. Sei es durch Verwendung von ADO.NET DataReadern oder im Extremfall sogar durch das gezielte Auslagern in eine gespeicherte Prozedur, die dadurch sehr servernah und damit auch performant ausgeführt werden kann. Aber auch wenn man mit den komfortableren Programmier-Frameworks wie LINQ to SQL oder ADO.NET Entity Framework arbeitet, gibt es einige Punkte, an denen man ansetzen kann, um dennoch
210
2030-4.book Page 211 Monday, February 8, 2010 4:26 PM
Zusammenfassung
eine möglichst gute Performance zu erreichen. Die wesentlichen Aspekte hierbei sind die Themen Parametrisierung sowie der bewusste Umgang mit Lazy und Eager Loading. Im Zweifelsfall gilt es, das im Hintergrund generierte SQL mit dem SQL Server Profiler zu protokollieren und dort zu prüfen, wo eventuell unnötig Performance verschenkt wird.
i
Da mit LINQ to SQL und dem Entity Framework zwei ähnliche Technologien existieren, hat sich Microsoft dazu entschlossen, LINQ to SQL künftig zwar noch weiter zu unterstützen, aber nicht mehr maßgeblich weiterzuentwickeln. Für langfristige Projekte sollte man daher jetzt schon verstärkt auf LINQ to Entities in Verbindung mit dem Entity Framework setzen, wenn man einen O/R-Mapper von Microsoft einsetzen möchte.
Das Geheimnis des optimalen Datenzugriffs liegt vor allem darin, den richtigen Mix aus den vielen zur Verfügung stehenden Technologien zu wählen. Eine sinnvolle Variante kann bei Verwendung des Entity Frameworks beispielsweise die geschickte Kombination von LINQ to Entities mit Entity SQL sein. Somit kann man für performancekritische Teile der Anwendung Entity SQL verwenden, um dabei größtmögliche Kontrolle über das generierte SQL zu haben, während man in den unkritischen Teilen der Anwendung die Typsicherheit und komfortablere Abfragesyntax von LINQ to Entities nutzt. Zeitaufwändige Massenoperationen auf der Datenbank können dann über gespeicherte Prozeduren servernah ausgeführt werden, die vom Entity Framework als Methoden zur Verfügung gestellt werden. In diesem Kapitel konnten natürlich bei weitem nicht alle performancerelevanten Aspekte der vorgestellten Technologien behandelt werden. Alleine zu ADO.NET könnte man ein komplettes Buch schreiben, das sich ausschließlich mit den Performanceaspekten dieser Technologie auseinandersetzt. Dasselbe gilt für LINQ to SQL sowie das ADO.NET Entity Framework. Daher wurden hier einige wesentliche Ansatzpunkte herausgepickt, um generell für die Problematik zu sensibilisieren und ein Verständnis dafür zu vermitteln, wo die wichtigsten Datenbank-Performanceprobleme in einer .NETAnwendung liegen können.
SQL Server Performance-Ratgeber
211
2030-4.book Page 212 Monday, February 8, 2010 4:26 PM
2030-4.book Page 213 Monday, February 8, 2010 4:26 PM
Teil D Optimieren einer bestehenden Datenbank Nachdem in den bisherigen Kapiteln des Buches diskutiert wurde, wie verschiedene Rollen zu einer guten Datenbankperformance beitragen können, geht es nun darum, eine bestehende Datenbank zu optimieren. Dazu müssen alle Rollen mit vereinten Kräften (oder auch ein Einzelkämpfer, der alle Rollen ausübt) das Performanceproblem zuerst lokalisieren, um dieses dann gezielt zu beheben. Kapitel 9 – Strukturierte Performanceanalyse beschreibt einen möglichen Gesamtablauf und erklärt, wie man möglichst schnell ein aktuelles Performanceproblem erkennen kann. In Kapitel 10 – Schwachstellen identifizieren durch Performancetests werden verschiedene Varianten von Performancetests ausführlicher vorgestellt. Kapitel 11 – Durchführung der Optimierung schließlich erläutert, wie die erkannten Performanceprobleme möglichst schmerzfrei behoben werden können.
SQL Server Performance-Ratgeber
213
2030-4.book Page 214 Monday, February 8, 2010 4:26 PM
2030-4.book Page 215 Monday, February 8, 2010 4:26 PM
9
Strukturierte Performanceanalyse
In den bisherigen Kapiteln wurde darauf eingegangen, wie die verschiedenen an einer Datenbankanwendung beteiligten Komponenten (Server, Datenbank, Anwendung) aussehen sollten, damit eine gute Gesamtperformance erreicht wird. Oft steht man jedoch vor dem Problem, eine solche Systemlandschaft nicht auf der grünen Wiese planen zu können, sondern ist stattdessen mit einer existierenden Anwendung (und dahinterliegenden Infrastruktur) konfrontiert, deren Performance zu wünschen übrig lässt.
9.1
Zieldefinition
Bevor man mit der Analyse des aktuellen Zustandes von Server und Anwendung beginnt, sollte man klären, welches Ziel überhaupt mit der Performanceanalyse erreicht werden soll. Dazu können die folgenden Fragestellungen hilfreich sein: 쮿
Wie genau äußern sich die Performanceprobleme? Ist die Ausführung nur langsam oder führt sie sogar zu Fehlern (beispielsweise durch Timeouts oder Deadlocks)?
쮿
War die Performance schon immer schlecht, oder hat sich das Verhalten erst im Laufe der Zeit verschlechtert?
쮿
Wenn der letztgenannte Punkt zutrifft, steht der Zeitpunkt mit einem bestimmten Ereignis in Zusammenhang (z.B. wenn eine neue Version der Anwendung in Betrieb genommen wurde oder eine größere Anzahl von Anwendern hinzugekommen ist)?
쮿
Ist die Performance dauerhaft schlecht oder nur zu bestimmten Zeiten bzw. während bestimmter Aktionen?
쮿
Wenn der letztgenannte Punkt zutrifft, ist das Verhalten reproduzierbar?
Aus diesen Überlegungen lassen sich Ziele ableiten, die zu erreichen sind. Mögliche Ziele können beispielsweise die folgenden sein: 쮿
Leseoperationen sollen generell beschleunigt werden.
쮿
Schreiboperationen sollen generell beschleunigt werden.
쮿
Die Zeit für eine bestimmte Aktion (Batch oder Benutzeraktion) soll unterhalb eines zu definierenden Limits gebracht werden (z.B. Ausführungsdauer der Suche nach einem Nachnamen auf weniger als 10 Sekunden reduzieren).
쮿
Das Auftreten von Deadlocks oder Timeouts soll verhindert werden.
Natürlich sollten diese Ziele auch realistisch und erreichbar sein. Die Zeit für einen Import von hundert Gigabyte Daten auf wenige Sekunden zu reduzieren ist zwar wünschenswert, aber beim aktuellen Stand der Technik sicherlich fernab jeder Realität.
SQL Server Performance-Ratgeber
215
2030-4.book Page 216 Monday, February 8, 2010 4:26 PM
9 – Strukturierte Performanceanalyse
9.2
Überblick des Gesamtablaufs
Um die so zuvor definierten Ziele zu erreichen, wird eine strukturierte Vorgehensweise empfohlen. Dabei gibt es sicherlich nicht eine einzig wahre Methode, sondern viele alternative Vorgehensweisen, von denen im Folgenden eine mögliche Variante vorgestellt wird. Je nach Beobachtungen bzw. Erkenntnissen aus bereits durchgeführten Analysen kann es natürlich auch sinnvoll sein, bewusst von einem vordefinierten Plan abzuweichen. Wesentlich ist jedoch, nicht ziellos verschiedene Optimierungsansätze auszuprobieren und dadurch mit viel Aufwand eventuell nur geringe Performancezuwächse zu erzielen, während die wesentlichen Probleme unerkannt bleiben. Abbildung 9.1 zeigt einen solchen Gesamtablauf, der aus verschiedenen Schritten besteht, die teilweise mehrfach durchlaufen werden.
Abbildung 9.1: Gesamtablauf für eine strukturierte Performanceanalyse
Ein wichtiger Punkt – der dabei oft vernachlässigt wird – liegt darin, den Normalzustand des Gesamtsystems möglichst gut zu kennen. Da dies nicht erst dann erfolgen sollte, wenn ein akutes Problem vorliegt, wurde dieser Punkt bereits in Kapitel 2.2.5 – Prüfung der Systemauslastung behandelt. Aber auch wenn dies nicht erfolgt ist, gibt es manchmal die Möglichkeit den Normalzustand auch nachträglich noch festzustellen, indem man beispielsweise eine Sicherung der Datenbank und/oder Anwendung von einem Zeitpunkt einspielt, an dem die Performanceprobleme noch nicht bestanden. Dazu benötigt man natürlich eine entsprechende Testumgebung, wenn man den laufenden Produktivbetrieb der Anwendung nicht behindern will.
216
2030-4.book Page 217 Monday, February 8, 2010 4:26 PM
Überblick des Gesamtablaufs
Generell ist es sehr hilfreich, eine entsprechende Testumgebung mit vergleichbaren Datenmengen (möglichst aus einer Sicherung der Produktivdaten, die gerne ein paar Tage alt sein darf) zur Verfügung zu haben, um sowohl Performancetests als auch Testläufe der später erarbeiteten Optimierungsmaßnahmen durchführen zu können. Denn bei einer typischen Entwicklungsumgebung, die meist nur wenige Testdaten enthält, werden nur in den seltensten Fällen Performanceprobleme auftreten, da die Datenmenge einfach zu gering ist. Anschließend gilt es, das eigentliche Problem zu rekonstruieren, um dieses mithilfe von Protokollierung genauer einzugrenzen. Spätestens danach sollten die gewünschten Ziele der Performanceoptimierung definiert werden. Ist dies geschehen, kann man anschließend Optimierungsmaßnahmen erarbeiten, diese gewichten und dann der Reihe nach ausführen. Nach jedem umgesetzten Maßnahmenpaket sollten auch die jeweiligen Auswirkungen geprüft werden, um einerseits zu prüfen, ob das Hauptproblem schon beseitigt wurde, und andererseits sicherzustellen, dass eine Maßnahme nicht wider Erwarten eine Verschlechterung der Situation zur Folge hat. Nachdem der in Abbildung 9.1 dargestellte Ablauf noch sehr allgemein gehalten ist, möchte ich im Folgenden eine mögliche Variante vorstellen, wie die in diesem und dem nächsten Kapitel vorgestellten Tools für eine solche Performanceanalyse genutzt werden können. Dazu wird zuerst mit dem Windows Task-Manager und dem SQL Server Activity Monitor der aktuelle Zustand des Systems geprüft, um zu entscheiden, ob ein sofortiges Eingreifen nötig ist. Das kann beispielsweise das Abbrechen eines „störenden“ Prozesses sein oder im schlimmsten Fall sogar das einfache Neustarten des SQL-Server-Dienstes (oder gar des ganzen Servers).
!
Bevor Sie den SQL-Server-Dienst neu starten, sollten Sie sich bewusst sein, dass die Ursache des Fehlers durch einen Neustart der Dienste erheblich erschwert wird. Wenn Sie beispielsweise die in Kapitel 10.1.2 vorgestellten dynamischen Management-Sichten zur Fehleranalyse nutzen möchten, so sollten Sie dies vor dem Neustart durchführen und die Ausgabe beispielsweise in SQL-Tabellen oder einem Excel Sheet zur weiteren Analyse sichern, da die Daten der DMVs beim Start von SQL Server geleert werden.
Nach den so ergriffenen Sofortmaßnahmen (oder auch dann, wenn diese nicht nötig sind) können Sie mit der ausführlichen Ursachen-Analyse beginnen. So kann man mit dem Windows Performance Monitor ein Protokoll der wichtigsten Performance Counter erstellen und in eine Datei speichern. Anschließend wiederholen Sie diesen Vorgang mit dem SQL Server Profiler. Sofern Sie hier ein paar Regeln beachtet haben, können Sie die beiden Protokolle synchronisieren und dadurch feststellen, was auf dem SQL Server zu dem Zeitpunkt geschehen ist (beispielsweise welche SQL-Anweisungen abgesetzt wurden), in dem der Ressourcenverbrauch stark zugenommen hat. Die so erkannten kritischen SQL-Anweisungen kann man dann in Ruhe mit dem SQL Server Management Studio anhand des Ausführungsplans analysieren und optimieren, um die so optimierte Abfragevariante später wieder in die Anwendung oder gespeicherte Prozedur einzubauen.
SQL Server Performance-Ratgeber
217
2030-4.book Page 218 Monday, February 8, 2010 4:26 PM
9 – Strukturierte Performanceanalyse
Aber auch der gespeicherte SQL Server Profiler Trace kann weiterverwendet werden. Dieser kann nämlich vom Datenbankoptimierungsratgeber weiter analysiert werden, der daraufhin neue Indizes oder die Partitionierung von bestimmten Tabellen vorschlägt.
Abbildung 9.2: Mögliche Verwendung der Tools für eine strukturierte Performanceanalyse
Nach einer Prüfung der Vorschläge des Optimierungsratgebers können diese – bzw. der sinnvoll erscheinende Teil davon – umgesetzt werden. Dies skizziert einen möglichen logischen Ablauf, von dem bei Bedarf natürlich auch abgewichen werden kann. Während die detaillierte Beschreibung der Tools zur ausführlichen Analyse im nächsten Kapitel folgt, werden im folgenden Abschnitt die Tools zur schnellen Analyse des aktuellen Systemzustands beschrieben.
9.3
Allgemeine Prüfung des Systems
Oft lässt sich die Ursache des Problems schon anhand der Problemschilderung auf einen bestimmten Bereich eingrenzen, sodass man gleich mit den in Kapitel 10 – Schwachstellen identifizieren durch Performancetests beschriebenen detaillierten Analysen fortfahren kann.
218
2030-4.book Page 219 Monday, February 8, 2010 4:26 PM
Allgemeine Prüfung des Systems
Ist dies nicht der Fall, so ist es sinnvoll, zuerst eine allgemeine Prüfung des Systems durchzuführen, um akute Ressourcenengpässe und ähnliche Probleme möglichst einfach zu erkennen und möglichst schnell Abhilfe schaffen zu können. Auf diesem Weg lassen sich eventuell mit ein paar Sofortmaßnahmen vielleicht schon die gravierendsten Probleme lösen, sodass man sich anschließend in Ruhe mit einer weiteren Optimierung befassen kann. Man kann also sagen, es geht hier um eine Art „Erste Hilfe für Performanceprobleme“, bei der erst einmal die Symptome bekämpft werden, bevor man sich ausführlich mit der Erkennung und Bekämpfung der Problemursache auseinandersetzt. Um eine solche allgemeine Prüfung des Systems durchführen zu können, sollte man möglichst Zugriff auf die Betriebssystemebene des Servers haben (entweder direkt oder per Windows Terminal Server Client). Andernfalls sind zwar auch einige Tests durchführbar, der Handlungsspielraum ist jedoch stark eingeschränkt. Hier ist also in den meisten Fällen der Einsatz eines System- oder Datenbankadministrators gefragt.
9.3.1
Prüfung der wichtigsten Systemressourcen mit dem Windows Task-Manager
Da ein Performanceproblem sich normalerweise direkt in Form von Ressourcenengpässen bemerkbar macht, sollte man sich möglichst schnell einen Überblick über die zentralen System-Ressourcen (CPU-Leistung, Hauptspeicher und Netzwerklast) beschaffen. Die einfachste Möglichkeit hierzu ist der Windows Task-Manager.
Abbildung 9.3: Windows Task-Manager, der eine plötzlich ansteigende CPU-Last zeigt
SQL Server Performance-Ratgeber
219
2030-4.book Page 220 Monday, February 8, 2010 4:26 PM
9 – Strukturierte Performanceanalyse
Die vierte wesentliche Systemressource – nämlich die Festplattenaktivität – kann mit dem Windows Task-Manager leider nicht beobachtet werden. Hierzu muss man dann schon den Windows Performance Monitor bemühen, der bereits deutlich komplexer ist. Ein weiterer Nachteil ist die Tatsache, dass der Task-Manager nur ungefähr die letzten 120 Sekunden darstellt, eine Analyse über einen längeren Zeitraum damit also nicht möglich ist. Doch auch hier bieten die in Kapitel 10 beschriebenen alternativen Tools eine Abhilfe. Um eine dauerhafte Überlastung von CPU, Hauptspeicher oder Netzwerkadapter möglichst schnell festzustellen, reicht der Task-Manager jedoch häufig aus. Für eine weiterführende Detailanalyse kann man dann immer noch zu komplexeren Tools greifen. Analog zum Windows Task-Manager gibt es in die Verwaltungsoberfläche von SQL Server integriert auch ein Tool, mit dem man einen schnellen Überblick über den allgemeinen „Gesundheitszustand“ eines SQL Servers bekommen kann.
9.3.2
Prüfung des SQL-Server-Zustands mit dem Activity Monitor
Den Activity Monitor gibt es bereits seit der Version 6.5 von SQL Server. Bis SQL Server 2000 ist er fest integriert in den SQL Server Enterprise Manager und dort im Bereich Verwaltung aufzurufen. Bei SQL Server 2005 ist der Activity Monitor im SQL Server Management Studio im Objekt-Explorer (und dort ebenfalls unter Verwaltung) zu finden. Allerdings werden die Serveraktivitäten hier in einem separaten Fenster dargestellt. Die Inhalte sind jedoch im Wesentlichen gleich geblieben und werden in drei separaten Listen getrennt dargestellt: 쮿
Prozessinfo – Auflistung der Prozesse mit Prozess-ID, Datenbank, Benutzer, Anwendung, Blockiert von, Blockierung etc.
쮿
Sperren nach Prozess – welche Objekte werden wie von einem bestimmten Prozess gesperrt
쮿
Sperren nach Objekt – welche Sperren von welchen Prozessen existieren für ein bestimmtes Objekt
Abbildung 9.4: Die Prozessinfo des Activity Monitors von SQL Server 2005
220
2030-4.book Page 221 Monday, February 8, 2010 4:26 PM
Allgemeine Prüfung des Systems
Hierüber lässt sich leicht feststellen, welche Prozesse von welchen Benutzern andere Prozesse blockieren. Mit diesen Informationen kann man im Problemfall entweder den Verursacher benachrichtigen oder notfalls sogar den blockierenden Prozess über das Kontextmenü des Prozesses abbrechen, damit die blockierten Prozesse wieder weiterlaufen können. Über das Filter-Symbol kann man die Anzeige der jeweiligen Prozesse oder Sperren eingrenzen sowie die standardmäßig ausgeblendeten Systemprozesse ebenfalls sichtbar machen. Seit SQL Server 2008 wurde der Activity Monitor stark überarbeitet. So ist dieser erst einmal etwas schwerer zu finden, da sich der Menüpunkt nun im Kontextmenü des jeweiligen Servers befindet (alternativ kann aber auch das entsprechende Symbol in der Symbolleiste genutzt werden). Aber auch an der Darstellung hat sich einiges geändert. So ist der Activity Monitor nun in fünf Bereiche aufgeteilt, die sich einzeln ein- und ausblenden lassen: 1. Übersicht – hier sind vier kleine Diagramme zu finden, die einen schnellen Überblick über folgende Bereiche geben: 왘 Anteil der Prozessorzeit (in Prozent) 왘 Anzahl wartende Tasks 왘 Datenbank-E/A (in MB pro Sekunde) 왘 Anzahl Batchanforderungen pro Sekunde 2. Prozesse – entspricht in etwa der Prozessinfo des alten Activity Monitors und stellt alle Prozesse mit Sitzungs-ID, Anmeldename, Datenbank, Anwendung, Blockiert von, Wartezeit (ms), Arbeitsspeicher (KB), Hostname etc. dar. Über das Kontextmenü lässt sich auch hier ein einzelner Prozess abbrechen. 3. Ressourcenwartevorgänge – stellt die durchschnittlichen und kumulierten Wartezeiten sowie Anzahl von Wartevorgängen für bestimmte Wartekategorien dar 4. Datendatei-E/A – zeigt, wie viele MB Daten pro Sekunde von den verschiedenen Datendateien gelesen und geschrieben werden. Hohe Leseraten (nein, nicht Leseratten!) lassen auf Probleme beim Puffern der Daten und/oder häufige Table Scans schließen. Zusätzlich wird auch die durchschnittliche Antwortzeit für Datendateizugriffe angezeigt. 5. Aktuelle wertvolle1 Abfragen – stellt die gerade laufenden und ausgeführten Abfragen mit deren Ausführungsanzahl, CPU-Nutzungsrate, durchschnittlicher Dauer, Planzahl sowie der Anzahl von physischen Lese- und logischen Schreib- und Lesevorgängen dar. Somit kann schnell erkannt werden, wenn eine Abfrage übermäßig viele Ressourcen beansprucht oder unnötig viele Abfragepläne erzeugt. Über das Kontextmenü kann man sogar den Abfragetext in einem separaten Abfragefenster zur Bearbeitung erhalten oder auch den Abfrageplan anzeigen lassen. Generell lassen sich alle Listen über Dropdown-Listen bei den jeweiligen Spaltenüberschriften filtern, sodass man beispielsweise die Anzeige der Prozesse sehr schnell auf die
1.
Das Wort „wertvoll“ ist wieder ein Musterbeispiel für erzwungene Übersetzungen von technischen Begriffen. Gemeint sind mit wertvollen Abfragen sicherlich eher teure – also besonders aufwändige – Abfragen.
SQL Server Performance-Ratgeber
221
2030-4.book Page 222 Monday, February 8, 2010 4:26 PM
9 – Strukturierte Performanceanalyse
Prozesse eines bestimmten Anwenders oder die Anzeige der Abfragen auf die Abfragen auf einer bestimmten Datenbank einschränken kann.
Abbildung 9.5: Der Activity Monitor von SQL Server 2008
Der Hauptnachteil der umfangreichen und optisch deutlich ansprechenderen Darstellung des neuen Activity Monitors liegt darin, dass es mitunter einige Sekunden dauert, bis die ersten Werte angezeigt werden. Über einen rechten Mausklick lässt sich im Kontextmenü aber das Aktualisierungsintervall auf einen von zehn vordefinierten Werten zwischen einer Sekunde und einer Stunde festlegen.
222
2030-4.book Page 223 Monday, February 8, 2010 4:26 PM
Zusammenfassung
9.4
Zusammenfassung
Um akute Datenbank-Performanceprobleme möglichst effektiv zu lösen, sollte man sich einerseits bewusst sein, welches Ziel man mit der Optimierung erreichen will. Zur Umsetzung ist eine zielgerichtete Vorgehensweise sinnvoll, um nicht mit hohem Aufwand wahllos Unmengen an kleinen Optimierungsmaßnahmen umzusetzen, die vielleicht nur einen geringen Performancegewinn bringen, während das hauptsächliche Problem viel schneller zu beheben gewesen wäre. Handelt es sich aber um akute Probleme, so ist – insbesondere bei Produktionsumgebungen – ein schnelles Eingreifen nötig, um kurzfristig die schlimmsten Symptome zu mildern, damit der Produktivbetrieb möglichst wenig gestört wird. Dafür eignen sich einfache Tools wie der Windows Task-Manager und der mit SQL Server ausgelieferte Activity Monitor sehr gut. Sind die akutesten Probleme beseitigt, kann anschließend – mit deutlich geringerem Zeitdruck – eine ausführlichere Performanceanalyse folgen. Die dafür zu empfehlenden Tools werden im nächsten Kapitel ausführlich beschrieben.
SQL Server Performance-Ratgeber
223
2030-4.book Page 224 Monday, February 8, 2010 4:26 PM
2030-4.book Page 225 Monday, February 8, 2010 4:26 PM
10
Schwachstellen identifizieren durch Performancetests
SQL Server stellt eine Vielzahl von Möglichkeiten zur Verfügung, um Performanceprobleme gezielt aufzuspüren. Nachdem im letzten Kapitel ein grober Fahrplan für ein strukturiertes Vorgehen aufgezeigt wurde, werden nun die einzelnen Tools etwas detaillierter vorgestellt.
10.1
Performance Counter und dynamische Management-Sichten
In Kapitel 9.3 – Allgemeine Prüfung des Systems wurde gezeigt, wie man mithilfe des Windows Task-Managers einen schnellen Überblick über die allgemeine Auslastung der wichtigsten Hardware-Ressourcen (insbesondere CPU und Hauptspeicher) bekommen kann. Ein Einblick auf die fast genauso wichtige Plattenauslastung ist schon nicht mehr auf diesem Weg möglich. Und spätestens dann, wenn man detailliertere Informationen benötigt, reicht der Task-Manager generell nicht mehr aus. Dafür bietet das WindowsBetriebssystem mit dem Performance Monitor eine deutlich leistungsfähigere Variante.
10.1.1
Der Windows Performance Monitor
Bereits in Kapitel 2.2.5 – Prüfung der Systemauslastung wurde die allgemeine Funktionsweise des Windows Performance Monitors vorgestellt. Das Tool, das in der WindowsSystemsteuerung über Verwaltung/Leistung (oder alternativ durch Eingabe von perfmon auf der Kommandozeilenebene) aufgerufen wird, bietet zahlreiche Leistungsindikatoren, die gemessen und mithilfe des integrierten Systemmonitors grafisch dargestellt werden können. Alternativ zur Anzeige als Liniendiagramm kann die Darstellung auch als Histogramm oder Bericht erfolgen, wobei diese beiden Varianten keinen Verlauf, sondern nur den aktuellen Wert darstellen und damit nicht ganz so informativ sind wie das Liniendiagramm. Standardmäßig werden in allen drei Darstellungsvarianten die folgenden Indikatoren angezeigt: 쮿
Physikalischer Datenträger (_Total): Durchschnittl. Warteschlangenlänge des Datenträgers
쮿
Prozessor(_Total): Prozessorzeit (%)
쮿
Speicher: Seiten/s
SQL Server Performance-Ratgeber
225
2030-4.book Page 226 Monday, February 8, 2010 4:26 PM
10 – Schwachstellen identifizieren durch Performancetests
Dabei weist das _Total in Klammern darauf hin, dass es sich hier um einen Gesamt- oder Durchschnittswert handelt, aber auch einzelne Werte (beispielsweise für die verschiedenen Festplatten oder Prozessorkerne) verfügbar sind.
!
Für eine ausführliche Analyse sollte man – insbesondere bei der Prozessorzeit – immer auch die einzelnen Instanzen (in diesem Fall Prozessorkerne) betrachten, da ein Performanceengpass auch dadurch entstehen kann, dass ein CPU-Kern überlastet ist, während die anderen sich „langweilen“, was beispielsweise bei einem Datenimport, der nicht auf parallele Ausführung hin optimiert ist, schnell passieren kann. Der Durchschnittswert der CPU-Last ist in einem solchen Fall dann immer noch relativ gut, da dieser nicht die Vollauslastung eines einzelnen Prozessorkerns erkennen lässt.
Abbildung 10.1: Der Windows Performance Monitor mit den Standard-Leistungsindikatoren
Da extrem viele weitere Indikatoren verfügbar sind und einige Serveranwendungen noch eigene zusätzliche Indikatoren mitbringen, sind diese zur besseren Übersicht in so genannte Datenobjekte aufgeteilt. Ein solches Datenobjekt kann beispielsweise ein Dienst oder auch eine Hardware-Ressource (z.B. Prozessor, Speicher oder physikalischer Datenträger) sein. Auch der SQL Server stellt eine ganze Reihe von zusätzlichen Indikatoren zur Verfügung, mit denen sich beispielsweise Transaktionen pro Sekunde, Protokoll-Lesevorgänge, Sperren, Deadlocks, Kompilierungsvorgänge von SQL-Anweisungen und vieles mehr beobachten lassen. Generell kann nicht nur der Rechner, auf dem der Performance Monitor ausgeführt wird, selbst, sondern auch ein anderer Rechner analysiert werden. Dies kann hilfreich sein, wenn man selbst keine Remote-Access-Zugriffsrechte auf dem zu analysierenden SQL Server hat.
226
2030-4.book Page 227 Monday, February 8, 2010 4:26 PM
Performance Counter und dynamische Management-Sichten
Um trotz der hohen Anzahl an möglichen Leistungsindikatoren noch die Übersicht zu behalten, kann man in der Legende unten einen der Indikatoren auswählen und dann über die Tastenkombination (Strg)+(H) oder das entsprechende Glühbirnen-Symbol aus der Symbolleiste diesen Indikator optisch hervorheben lassen. Somit lassen sich plötzliche Ausreißer schnell ausfindig machen und es ist auch schnell zu erkennen, ob es sich um eine kurzzeitig auftretende Spitze handelt (die normalerweise unproblematisch ist) oder um einen dauerhaften Anstieg der beobachteten Werte. In Abbildung 10.1 ist beispielsweise ein sprunghafter Anstieg von Prozessorzeit und Warteschlangenlänge des Datenträgers zu erkennen, der als Folge einer aufwändigen SQLAbfrage entstanden ist. Insbesondere dann, wenn man auf einem PC mehrere Server beobachtet, bietet es sich an, den Performance Monitor gleich mehrfach zu öffnen und dadurch die Anzeige auf mehrere Fenster verteilen zu können. Sinnvolle Varianten wären hier entweder ein Fenster pro Server oder aber ein Fenster pro Datenobjekt – also eines, in dem die CPUs überwacht werden, eines für die Datenträger, eines für den Hauptspeicher und so weiter. Gerade System- und Datenbankadministratoren nutzen diese Variante gerne, um auf einem großen Monitor in verschiedenen Fenstern die wichtigsten Produktivsysteme stets im Überblick zu behalten und somit möglichst schnell auf eventuell auftretende Engpässe reagieren zu können. Für eine längerfristige Beobachtung wesentlich hilfreicher ist allerdings die Möglichkeit, die beobachteten Werte auch dauerhaft speichern zu können, um diese später analysieren oder sogar mit Daten aus anderen Analyse-Tools synchronisieren zu können. Dazu gibt es im Konsolenstamm auf der linken Seite den Punkt Leistungsdatenprotokolle und Warnungen/Leistungsindikatorenprotokolle, unter dem die zu protokollierenden Sätze von Leistungsindikatoren verwaltet werden. Beim Anlegen eines solchen Protokolls können sowohl einzelne Indikatoren als auch ganze Datenobjekte (mit allen dazugehörenden Indikatoren) ausgewählt werden. Zu jedem Indikator und Datenobjekt kann man über die Schaltfläche Erklärung eine Erläuterung der Bedeutung anzeigen lassen, die bei der Vielzahl an teilweise ähnlichen Indikatoren oft sehr hilfreich ist, um die richtige Variante auszuwählen. Die Speicherung erfolgt dann entweder in einer Textdatei (komma- oder tabulatorgetrennt), einer Binärdatei oder sogar einer SQL-Datenbank. Die letzte Variante ist vorzuziehen, wenn Sie selbst ohne weitere Spezialtools Auswertungen und Analysen der aufgezeichneten Werte vornehmen wollen. Für die Auswahl der richtigen Datenbank werden interessanterweise die inzwischen technisch überholten ODBC-SystemDSNs verwendet, die Sie gegebenenfalls vorher über den Punkt Verwaltung/Datenquellen (ODBC) in der Systemsteuerung anlegen müssen. Bei Verwendung von Dateien kann dagegen noch eine numerische Dateinamenserweiterung verwendet werden, die automatisch hochgezählt wird, da die Protokolldateien maximal 1 GB groß werden dürfen. Wird diese Grenze überschritten, wird automatisch eine neue Datei erstellt und darin weiterprotokolliert. Dazu kann – sofern man die Protokollierung nicht manuell starten und beenden will – auch ein Zeitplan festgelegt werden, sodass dies automatisch geschieht.
SQL Server Performance-Ratgeber
227
2030-4.book Page 228 Monday, February 8, 2010 4:26 PM
10 – Schwachstellen identifizieren durch Performancetests
In der Listenansicht der Leistungsdatenprotokolle werden alle gespeicherten Protokolle angezeigt und können dort über die Symbolleiste einfach gestartet und gestoppt werden. Über das Kontextmenü eines Protokolls kann dessen Konfiguration in eine HTML-Datei exportiert werden, um diese später auch von einem anderen Rechner aus ausführen zu können.
Abbildung 10.2: Ansicht der Leistungsindikatorenprotokolle mit einem gestarteten Protokoll
Tipps zur Verwendung des Windows Performance Monitors:
i
쮿
Nutzen Sie (insbesondere wenn produktive Server geprüft werden sollen) einen separaten SQL Server, um den Performance Monitor laufen zu lassen.
쮿
Lassen Sie den Performance Monitor nur für eine begrenzte Zeit laufen.
쮿
Grenzen Sie die zu protokollierenden Leistungsindikatoren möglichst stark ein, um die Datenmenge des Protokolls möglichst gering zu halten. Dazu wird auf diesem Weg auch ermöglicht, dass die relevanten Informationen bei der späteren Auswertung besser gefunden werden können.
쮿
Speichern Sie das Protokoll in eine SQL-Server-Datenbank, wenn Sie dieses später manuell analysieren wollen. Somit können Sie diese mit ein paar einfachen SQL-Abfragen dann leicht auswerten.
10.1.2 Dynamische Management-Sichten Während der Windows Performance Monitor eine sehr systemnahe Variante der Performanceanalyse ermöglicht, bieten die dynamischen Management-Sichten von SQL Server einen komfortablen Zugriff per T-SQL auf viele interne Daten, die teilweise aber nicht ausschließlich für die Performance relevant sind. Nachteil der dynamischen Management-Sichten ist die Tatsache, dass die dahinterliegenden Tabellen bei jedem Start des SQL-Server-Dienstes geleert werden, sodass eine verlässliche längerfristige Beobachtung auf diesem Weg kaum möglich ist. Ansprechbar sind die dynamischen Management-Sichten von jeder Datenbank aus über das sys-Schema. Der Sichtname selbst setzt sich aus dem Präfix dm_ gefolgt vom Typ der Quelle und dem eigentlichen Namen zusammen. Bei SQL Server 2008 werden derzeit unter anderem folgende Typen verwendet, die zu den jeweils angegebenen Bereichen Auskunft geben:
228
2030-4.book Page 229 Monday, February 8, 2010 4:26 PM
Performance Counter und dynamische Management-Sichten 쮿
sys.dm_broker_ : SQL Server Service Broker
쮿
sys.dm_cdc_ : Change Data Capture
쮿
sys.dm_clr_ : Common Language Runtime
쮿
sys.dm_db_ : Datenbank
쮿
sys.dm_exec_ : Abfrageausführung
쮿
sys.dm_filestream_ : Filestreams
쮿
sys.dm_io_ : Ein- und Ausgabe von Daten
쮿
sys.dm_os_ : Betriebssystem
쮿
sys.dm_repl_ : Replikation
쮿
sys.dm_resource_governor_ : Ressource Governor
쮿
sys.dm_tran_ : Transaktionen
쮿
sys.dm_xe_ : Extended Events
Einzelne dynamische Management-Sichten wurden schon an verschiedenen Stellen im Buchtext verwendet (beispielsweise in Kapitel 4.4 – Index oder nicht?). Zur besseren Übersicht folgt an dieser Stelle eine Auflistung der wichtigsten DMVs, von denen einige eigentlich keine Sichten, sondern als Funktionen definiert sind: DMV (im sys-Schema)
Typ
Bedeutung/Inhalte
dm_db_file_space_usage
Sicht
Speicherplatzverwendung einzelner Datenbankfiles
dm_db_index_operational_stats
Funktion
Operative Indexdaten
dm_db_index_physical_stats
Funktion
Physikalische Indexdaten
dm_db_index_usage_stats
Sicht
Verwendung von Indizes
dm_db_missing_index_columns
Funktion
Fehlende Indexspalten
dm_db_missing_index_details
Sicht
Details zu fehlenden Indizes
dm_exec_cached_plans
Sicht
Gepufferte Abfragepläne
dm_exec_connections
Sicht
Offene Verbindungen
dm_exec_query_optimizer_info
Sicht
Informationen zum Optimizer
dm_exec_query_stats
Sicht
Abfragestatistiken
dm_io_pending_io_requests
Sicht
Offene I/O-Anfragen
dm_os_performance_counters
Sicht
Performance Counter, die SQL Server auch dem Windows Performance Monitor zur Verfügung stellt
dm_os_process_memory
Sicht
Speicher des aktuellen Prozesses
dm_os_sys_info
Sicht
Systeminformationen
dm_os_sys_memory
Sicht
Systemspeicher
Tabelle 10.1: Die wichtigsten dynamischen Management-Sichten im Überblick
SQL Server Performance-Ratgeber
229
2030-4.book Page 230 Monday, February 8, 2010 4:26 PM
10 – Schwachstellen identifizieren durch Performancetests
DMV (im sys-Schema)
Typ
Bedeutung/Inhalte
dm_os_tasks
Sicht
Prozesse
dm_os_threads
Sicht
Threads
dm_os_wait_stats
Sicht
Statistiken zu verschiedenen Wartezuständen
dm_os_waiting_tasks
Sicht
Wartende Prozesse
dm_tran_active_transactions
Sicht
Aktive Transaktionen
dm_tran_current_transaction
Sicht
Aktuelle Transaktion
dm_tran_database_transactions
Sicht
Datenbanktransaktionen
dm_tran_locks
Sicht
Transaktionssperren
Tabelle 10.1: Die wichtigsten dynamischen Management-Sichten im Überblick (Forts.)
Dazu gibt es noch viele weitere (derzeit insgesamt 117 Stück), deren Auflistung den Rahmen hier sprengen würde. Mit der folgenden einfachen Abfrage können Sie sich die verfügbaren DMVs auflisten lassen: SELECT * FROM sys.all_views WHERE [name] LIKE 'DM_%' ORDER BY [name]
10.2 SQL Server Profiler & Datenbankoptimierungsratgeber Während der Windows Performance Monitor und die dynamischen Management-Sichten auf jedem Windows-Rechner mit installiertem SQL Server (also auch bei SQL Server Express Edition) verfügbar sind, bieten die kostenpflichtigen Editionen von SQL Server weiterführende Tools zur Performanceanalyse. Beide Tools sind im Startmenü unter Programme/Microsoft SQL Server 2008/Leistungstools (bzw. bei SQL Server 2005 unter dem für diese Version entsprechenden Startmenü-Ordner) zu finden. Sowohl im SQL Server Management Studio als auch im SQL Server Profiler und dem Datenbankoptimierungsratgeber sind in der Symbolleiste und dem Menü Extras Einträge zu finden, mit denen die anderen Tools direkt aufgerufen werden können. Im SQL Server Profiler ist auch ein Eintrag Systemmonitor zu finden, über den der Windows Performance Monitor gestartet werden kann. Somit kann man sich den umständlichen Weg über das Windows-Startmenü meistens sparen.
230
2030-4.book Page 231 Monday, February 8, 2010 4:26 PM
SQL Server Profiler & Datenbankoptimierungsratgeber
10.2.1 SQL Server Profiler Der SQL Server Profiler dient zum Protokollieren von Ereignissen, die auf dem SQL Server laufen. Das Protokoll kann dabei wahlweise als Datei (binär oder Klartext) oder in eine Datenbank geschrieben werden. Dabei werden – je nach protokollierten Ereignissen – innerhalb von kurzer Zeit erhebliche Datenmengen erzeugt. Man sollte also darauf achten, dass auf der entsprechenden Laufwerkspartition noch genügend Speicherplatz frei ist. Da der SQL Server Profiler auch die Performance des Systems zusätzlich belastet, sollte man die Möglichkeit nutzen, den Profiler nicht auf dem zu prüfenden Server selbst, sondern auf einem separaten Rechner (auf dem natürlich ebenfalls SQL Server installiert sein muss) laufen zu lassen. Dies kann beispielsweise auch ein Arbeitsplatzrechner eines Datenbankentwicklers oder Datenbankadministrators sein, auf dem die SQL Server Developer Edition installiert ist. Auf diesem Weg lassen sich auch von einem Rechner aus mehrere SQL Server beobachten, die auf physikalisch getrennten Servern laufen, um beispielsweise das Verhalten eines produktiven Servers und eines Testservers miteinander zu vergleichen.
Erstellen einer Ablaufverfolgung Nach dem Start des SQL Server Profilers ist erst einmal – abgesehen von der Menü- und Symbolleiste – nicht viel zu sehen. Um dies zu ändern, muss zuerst über den entsprechenden Menüpunkt eine neue Ablaufverfolgung gestartet werden. Nach der Angabe der Anmeldeinformationen erscheint das Dialogfeld Ablaufverfolgungseigenschaften, in dem die Ablaufverfolgung konfiguriert wird. Auf der ersten Seite des Dialogfeldes ist neben der Eingabe des Namens für die Ablaufverfolgung auch eine Vorlage auszuwählen, die vorgibt, welche Ereignisse und Eigenschaften dieses Ereignisses protokolliert werden sollen. Hierbei sind insbesondere zwei Vorlagen hilfreich: 쮿
Standard (Standard) – Beinhaltet Informationen zu allen bestehenden Verbindungen (ExistingConnection), An- und Abmeldevorgänge (Audit Login, Audit Logout) sowie die Aufrufe von SQL-Anweisungen (TSQL) und gespeicherten Prozeduren (Stored Procedures). Diese Vorlage eignet sich damit bestens, um einen groben Überblick darüber zu bekommen, was gerade auf dem SQL Server geschieht.
쮿
Tuning – Beinhaltet die wichtigsten Daten zu beendeten Remote-Prozedur-Aufrufen sowie T-SQL-Anweisungen und eignet sich damit als Eingabedatei für den Datenbankoptimierungsratgeber.
Dazu können Sie hier auswählen, ob und, wenn ja, wie die Daten protokolliert werden sollen. Zur Auswahl stehen SQL Server Profiler-Ablaufverfolgungsdateien (*.trc) oder Tabellen in SQL-Server-Datenbanken. Für beide möglichen Ziele lässt sich die Datenmenge anhand weiterer Angaben einschränken. So kann man für Ablaufverfolgungsdateien eine maximale Dateigröße festlegen und für Tabellen eine maximale Zeilenanzahl.
SQL Server Performance-Ratgeber
231
2030-4.book Page 232 Monday, February 8, 2010 4:26 PM
10 – Schwachstellen identifizieren durch Performancetests
Abbildung 10.3: Die Konfiguration der Ablaufverfolgung
Wenn Sie den Profiler aber lediglich nutzen möchten, um zu prüfen, welche SQLAnweisungen gerade ausgeführt werden (was beispielsweise auch zum Debuggen von Anwendungen verwendet werden kann), müssen die Daten nicht zwingend gespeichert werden, sodass es auch möglich ist, keines der beiden Speicherziele anzugeben und damit lediglich eine direkte Anzeige des Protokolls zu erstellen. Die letzte Einstellmöglichkeit auf dieser Seite liegt darin, eine Beendigungszeit für die Ablaufverfolgung zu definieren. Dies ist wichtig, damit die Protokollierung nicht endlos weiterläuft und somit extreme Datenmengen produziert. Auf der zweiten Seite (Ereignisauswahl) ist zu sehen, welche Ereignisse und Ereignisspalten Bestandteil der ausgewählten Vorlage sind. Sie können hier aber auch die einzelnen Ereignisse oder Ereignisspalten durch Entfernen des entsprechenden Häkchens aus der Ablaufverfolgung ausschließen oder – nach Einblenden der übrigen Ereignisse und Spalten – weitere Häkchen setzen, um der Ablaufverfolgung zusätzliche Daten hinzuzufügen. Wenn Sie mit der Maus über einer Eigenschaft schweben, erhalten Sie im unteren Teil des Fensters eine kleine Erläuterung zur Bedeutung von Ereignis und Eigenschaft.
232
2030-4.book Page 233 Monday, February 8, 2010 4:26 PM
SQL Server Profiler & Datenbankoptimierungsratgeber
Abbildung 10.4: Ereignisauswahl für die Standard-Vorlage
Mit der Schaltfläche Spalten organisieren auf der rechten Seite lassen sich die Reihenfolge der Spalten beeinflussen sowie einzelne Spalten für Gruppierungen auswählen. Auf diesem Weg werden die Werte nicht nur nach diesen Gruppen sortiert, sondern die einzelnen Gruppen sind auch einzeln ein- und ausblendbar. Somit lassen sich später leicht Abfragen finden, die von einem bestimmten Benutzer ausgeführt wurden. Noch besser geht dies allerdings, indem Sie über die Schaltfläche Spaltenfilter für einzelne Spalten Filter definieren, wobei die Bezeichnung Spaltenfilter etwas irreführend ist, denn eigentlich geht es eher darum, die Zeilen durch Bedingungen, die auf einzelnen Spalten definiert werden, zu filtern. Dadurch lässt sich die Protokollierung der Daten beispielsweise auf einen bestimmten Benutzer oder eine spezielle Anwendung einschränken. Dieses Vorgehen ist alleine auch deswegen sinnvoll, da sich dadurch auch die Menge der protokollierten Daten drastisch reduziert. Über die Schaltfläche Ausführen wird die Protokollierung sofort gestartet und zeigt die entsprechenden Ergebnisse auch direkt im SQL Server Profiler an. Wenn Sie eine Zeile der Tabellendarstellung anklicken, sind im unteren Bereich des Fensters die Detailinfos (z.B. der komplette SQL-Abfragetext) zu sehen. Über die Option Ereignisdaten extrahieren im Kontextmenü können Sie die entsprechenden SQL-Anweisungen eines Schrittes in eine SQL-Datei schreiben, um diese beispielsweise später mit der Ausführungsplananzeige des SQL Server Management Studios genauer zu analysieren. Die Symbolleiste stellt zahlreiche Icons zur Verfügung, mit denen man die Protokollierung pausieren, beenden oder neu starten kann. Auch eine neue Konfiguration der Parameter ist hierüber möglich.
SQL Server Performance-Ratgeber
233
2030-4.book Page 234 Monday, February 8, 2010 4:26 PM
10 – Schwachstellen identifizieren durch Performancetests
Da der SQL Server Profiler auch die Performance des Systems zusätzlich belastet, sollte man die Möglichkeit nutzen, den Profiler nicht auf dem zu prüfenden Server selbst, sondern auf einem separaten Rechner (auf dem natürlich ebenfalls SQL Server installiert sein muss) laufen zu lassen. Dies kann beispielsweise auch ein Arbeitsplatzrechner eines Datenbankentwicklers oder Datenbankadministrators sein, auf dem die SQL Server Developer Edition installiert ist. Auf diesem Weg lassen sich auch von einem Rechner aus mehrere SQL Server beobachten, die auf physikalisch getrennten Servern laufen, um beispielsweise das Verhalten eines produktiven Servers und eines Testservers miteinander zu vergleichen. Nutzen Sie ab und zu einen SQL Server Profiler Trace, in dem lediglich die Ereignisse TSQL: SQLBatchCompleted und Stored Procedures: RPC:Completed protokolliert werden, um lang laufende Abfragen herauszufinden. Filtern Sie dabei diese Ereignisse über entsprechende Spaltenfilter so, dass nur solche mit einer gewissen Dauer (z.B. >15ms) im Protokoll landen. Wenn die Protokollierung in eine SQL-Server-Tabelle erfolgt, können Sie die kritischen Abfragen mit einer einfachen SQL-Anweisung schnell herausfinden: SELECT * FROM dbo.SQLProfilerTest WHERE EventClass IN (10,12) -- Eingrenzung auf SQL-Abfragen und SP-Aufrufe ORDER BY Duration DESC
Auswerten einer Ablaufverfolgung Um eine beendete und gespeicherte Ablaufverfolgung auszuwerten, gibt es verschiedene Möglichkeiten. Die Trace-Dateien (mit der Endung TRC) sind mit normalen Editoren nicht lesbar, können aber im SQL Server Profiler wieder geöffnet werden, um sie lesbar zu machen. Wenn Sie das Protokoll in eine SQL-Server-Tabelle geschrieben haben, können Sie diese Tabelle mit ein paar einfachen SQL-Anweisungen auswerten. Komfortabler geht es aber mit dem SQL Server Profiler selbst. Hier können Sie gespeicherte Protokolle – egal ob in einer Trace-Datei oder einer SQL-Server-Tabelle – wieder öffnen und in derselben Ansicht betrachten, wie wenn diese gerade erst erstellt worden wären. Interessanter wird dies dadurch, dass man nun eine zeitgleich erzeugte PerformanceMonitor-Protokolldatei öffnen und mit dem Profiler Trace synchronisieren kann. Dies geschieht über den Menüpunkt Datei/Leistungsdaten importieren. Nach Auswahl der richtigen Datei kann noch angegeben werden, welche Leistungsindikatoren in die Ansicht übernommen werden sollen. Als Ergebnis wird zwischen der tabellarischen Darstellung und den Detailinfos des Protokolls die Ansicht des Performance-Monitor-Protokolls eingeblendet. Mit der Maus lassen sich nun einzelne Bereiche – sowohl Zeilen im Profiler Trace als auch Stellen in der grafischen Darstellung des PerfMon-Protokolls – anklicken und die Darstellung wird synchronisiert (soll heißen das andere Protokoll springt auch an denselben Zeitpunkt). Somit lässt sich leicht feststellen, welche SQL-Anweisungen ausgeführt wurden, kurz bevor der Ressourcenverbrauch stark angestiegen ist. In Abbildung 10.5 ist beispielsweise zu erkennen, dass kurz vor dem rapiden Anstieg der Prozessorlast eine ungünstige SQL-Anweisung (nämlich eine mit einem CROSS JOIN) ausge-
234
2030-4.book Page 235 Monday, February 8, 2010 4:26 PM
SQL Server Profiler & Datenbankoptimierungsratgeber
führt wurde. Eine geeignete Optimierungsmaßnahme liegt nun darin, diese Anweisung in der Anwendung (oder der aufrufenden gespeicherten Prozedur, falls es sich um einen RPC-Aufruf handelt) aufzuspüren und in eine günstigere Variante umzuformulieren.
Abbildung 10.5: Gegenüberstellung von Profiler Trace und Performance Monitor Log
Die letzte Variante, einen Profiler Trace auszuwerten, liegt darin, ihn an den Datenbankoptimierungsratgeber zu übergeben, der ihn als Grundlage für Optimierungsratschläge nutzen kann. Tipps zur Verwendung des SQL Server Profilers: 쮿
Nutzen Sie (insbesondere wenn produktive Server geprüft werden sollen) einen separaten SQL Server, um den Profiler laufen zu lassen.
쮿
Lassen Sie den Profiler nur für eine begrenzte Zeit laufen.
쮿
Grenzen Sie sowohl die zu beobachtenden Ereignisse als auch die zu protokollierenden Werte (Ereignisspalten) dieser Ereignisse möglichst stark ein, um die Datenmenge des Protokolls möglichst gering zu halten. Dazu wird auf diesem Weg auch ermöglicht, dass die relevanten Informationen bei der späteren Auswertung besser gefunden werden können.
쮿
Nehmen Sie die Spalten StartTime und EndTime immer in den Profiler Trace mit auf. Diese werden benötigt, wenn Sie das Protokoll später mit einem Performance Monitor Log synchronisieren möchten.
i
SQL Server Performance-Ratgeber
235
2030-4.book Page 236 Monday, February 8, 2010 4:26 PM
10 – Schwachstellen identifizieren durch Performancetests
10.2.2 Der Datenbankoptimierungsratgeber Der Datenbankoptimierungsratgeber (engl.: Database Tuning Advisor) kann einen Profiler Trace nutzen, um die Datenbank daraufhin zu untersuchen und anschließend Optimierungsvorschläge zu unterbreiten. Alternativ zu einem Profiler Trace akzeptiert der Optimierungsratgeber auch SQL-Dateien als Eingabe. Bis zu SQL Server 2000 hieß das Tool noch Indexoptimierungsratgeber, da der Hauptfokus des Tools auf einer Analyse fehlender oder auch überflüssiger Indizes lag. Seit SQL Server 2005 wurde der Optimierungsratgeber erweitert, sodass nun neben den Indizes auch die Partitionierungsstrategie untersucht werden kann.1 Wenn Sie die Datenbankoptimierungsratgeber starten, ist zuerst die Arbeitsauslastung, die für die Analyse verwendet werden soll, auszuwählen. Dabei kann entweder ein SQLSkript oder eine Protokolldatei, die mit dem SQL Server Profiler erstellt wurde, verwendet werden. Dazu lässt sich einschränken, welche Datenbanken oder sogar welche Tabellen untersucht werden.
Abbildung 10.6: Auswahl von Arbeitsauslastung und zu analysierender Datenbanken
Auf einer zweiten Seite mit Einstellungen lässt sich die Optimierungszeit begrenzen. Dies ist generell zu empfehlen, da eine solche Optimierung relativ lange laufen kann. Insbesondere dann, wenn kein vom Datenvolumen her identisches Testsystem verfügbar ist und der Datenbankoptimierungsratgeber stattdessen in einem Wartungsfenster auf einer produktiven Datenbank laufen muss, kann man hierüber sicherstellen, dass die
1.
236
Beachten Sie dabei, dass die Partitionierung erst ab der Enterprise Edition (bzw. zum Entwickeln auch mit der Developer Edition) von SQL Server verwendbar ist.
2030-4.book Page 237 Monday, February 8, 2010 4:26 PM
SQL Server Profiler & Datenbankoptimierungsratgeber
Datenbank zu den normalen Betriebszeiten wieder verfügbar ist. (Bei Bedarf lässt sich aber auch eine laufende Analyse manuell abbrechen.) Der restliche Teil des Dialogfeldes dient dazu festzulegen, welche Typen von Datenbankstrukturen (Indizes, indizierte Sichten, gefilterte Indizes, Partitionierung etc.) erstellt und welche beibehalten werden sollen. Im Zweifelsfall empfiehlt es sich, hier die Option Alle vorhandenen physischen Datenstrukturen beibehalten zu aktivieren, sodass keine Indizes etc. gelöscht werden, die vielleicht später doch noch benötigt werden.
Abbildung 10.7: Einstellung der Optimierungsoptionen
Wenn die Analyse nun über das entsprechende Symbol in der Symbolleiste gestartet wurde, ist auf der Seite Status zu sehen, welche Anweisung gerade analysiert wird. Sobald die komplette Analyse entweder durchgelaufen ist oder aber die vorgegebene Endzeit erreicht wurde, sind zusätzliche Registerkarten verfügbar, in denen das Ergebnis der Analyse angezeigt wird: Auf der Seite Empfehlungen sind die einzelnen Empfehlungen zur Partitionierung und für Indizes zusammen mit der geschätzten Performanceverbesserung zu sehen (Abbildung 10.8). Auf der Seite Berichte können neben einer Optimierungszusammenfassung verschiedene Berichte aufgerufen werden, die weitere Detailinformationen bieten (Abbildung 10.9).
SQL Server Performance-Ratgeber
237
2030-4.book Page 238 Monday, February 8, 2010 4:26 PM
10 – Schwachstellen identifizieren durch Performancetests
Abbildung 10.8: Wird die Beibehaltung bestehender Strukturen nicht explizit vorgegeben, wird insbesondere bei kleinen Auslastungsdateien/-tabellen die Löschung vieler Indizes empfohlen
Abbildung 10.9: Die Optimierungszusammenfassung mit dem Indexverwendungsbericht (aktuell)
238
2030-4.book Page 239 Monday, February 8, 2010 4:26 PM
Management Data Warehouse
Um die entsprechenden Empfehlungen nun weiterzuverarbeiten, kann man alle markierten Empfehlungen über das Aktionen-Menü entweder direkt anwenden oder die notwendigen SQL-Anweisungen in einer Skriptdatei speichern, um diese vor der Ausführung noch einmal genau gegenzuprüfen oder sogar weiter manuell zu optimieren. Dies kann auch genutzt werden, um die meist kryptischen, automatisch generierten Namen für neu zu erstellende Indizes durch aussagekräftigere Bezeichnungen zu ersetzen. Tipps zur Verwendung des Datenbankoptimierungsratgebers: 쮿
Prüfen Sie die Vorschläge des Ratgebers deutlich, bevor Sie diese umsetzen. Insbesondere dann, wenn der Datenbankoptimierungsratgeber als einzigen Input ein SQL-Skript oder ein Profiler Trace über einen relativ kurzen Zeitraum bekommen hat, ist die Gefahr groß, dass Indizes als überflüssig interpretiert werden, die zu anderen Zeiten dringend benötigt werden. Im Zweifelsfall sollten vorhandene Entwurfsstrukturen beibehalten werden.
쮿
Selbst wenn die Tipps geprüft und als sinnvoll erachtet wurden, sollte man die Umsetzung nicht direkt durch den Datenbankoptimierungsratgeber durchführen, sondern diesen lediglich ein SQL-Skript erzeugen lassen, das man vor Ausführung noch leicht nachbearbeiten kann.
i
10.3 Management Data Warehouse Seit der 2008er-Generation beinhaltet SQL Server in den kostenpflichtigen Editionen noch ein weiteres praktisches Tool zur Performanceanalyse: Das Management Data Warehouse (kurz: MDW), das in den Vorab-Versionen von SQL Server 2008 auch als Performance Data Warehouse bezeichnet wurde, da es hauptsächlich performancerelevante Daten beinhaltet.2 Das Management Data Warehouse löst die wichtigsten Probleme, mit denen die alternativen Tools zu kämpfen haben. Mit dynamischen Management-Sichten kann man zwar relativ einfach performancerelevante Daten abfragen, allerdings gehen diese bei jedem Neustart des Serverdienstes verloren. Mit dem SQL Server Profiler dagegen erhält man ausführliche Informationen über Abfragen, Sperren etc., allerdings erzeugen diese eine große Datenmenge und belasten den Server nicht unerheblich. Das Management Data Warehouse bietet dagegen eine Möglichkeit, die wichtigsten Performancedaten bei geringer Systemlast dauerhaft zu protokollieren, indem diese in einer relationalen Datenbank gespeichert werden.
2.
In vielen deutschsprachigen Quellen und selbst in der deutschen Variante des SQL Server Management Studios wird das Management Data Warehouse auch als Verwaltungs-Data Warehouse bezeichnet, was in meinen Augen eine etwas zu erzwungene Übersetzung ist.
SQL Server Performance-Ratgeber
239
2030-4.book Page 240 Monday, February 8, 2010 4:26 PM
10 – Schwachstellen identifizieren durch Performancetests
Auch mit der Express Edition (oder mit älteren Versionen) von SQL Server ist eine vergleichbare Funktionalität realisierbar, indem man mithilfe von SQL Server Agent Jobs die Daten von ausgewählten dynamischen ManagementSichten regelmäßig in eine separate Datenbank kopiert, um sie dort dauerhaft gesichert zu haben. Auf Basis dieser Daten kann man dann entsprechende Auswertungen oder sogar Reports erstellen. Dies ist allerdings mit einem vergleichsweise hohen Aufwand verbunden, der einem bei Verwendung des Management Data Warehouse abgenommen wird. Für Benutzer von SQL Server 2005 gibt es dagegen das SQL Server 2005 Performance Dashboard als Alternative, die als kostenfreier Download erhältlich ist und vorgefertigte Berichte mit performancerelevanten Daten bietet. Dieses Tool wird in Anhang A.2.4 etwas detaillierter beschrieben.
10.3.1 Funktionsweise des Management Data Warehouse Das Management Data Warehouse nutzt auf jedem zu beobachtenden Server einen so genannten Performance Data Collector, der die Daten sammelt und –um die Systemlast möglichst gering zu halten – erst einmal in Cache-Dateien schreibt. Als Quelle für die protokollierbaren Performancedaten werden so genannte Datenprovider genutzt. Bisher stehen hier der Windows Performance Monitor, der SQL Server Profiler sowie T-SQL (insbesondere dynamische Management-Sichten) zur Verfügung. Für kommende Versionen von SQL Server sind weitere Datenprovider (wie beispielsweise Provider für die SQL Server Analysis Services) geplant. Über einen SQL Server Agent Job werden die Daten regelmäßig – sofern der SQL Server nicht voll ausgelastet ist – aus dem Cache-Verzeichnis abgerufen und über eine SQL Server Integration Services-basierte Automatik in das Management Data Warehouse importiert. Dadurch, dass die Performancedaten nicht direkt in das Management Data Warehouse gespeichert werden, kann die zusätzliche Belastung des SQL Servers normalerweise unter 5% gehalten werden. Die zu protokollierenden Daten sind in Form von Auflistsätzen (engl.: Collection Sets) definiert, die einzelne Datenauflistungen (Collections) beinhalten. Basierend auf AnwenderFeedback werden drei Standard-Auflistsätze mit SQL Server ausgeliefert: 쮿
Serveraktivität – Verlauf (engl.: Server Activity)
쮿
Zusammenfassung der Datenträgerverwendung (engl.: Disk Usage)
쮿
Abfragestatistik – Verlauf (engl.: Query Activity)
Für diese Standard-Auflistsätze sind direkt im SQL Server Management Studio vorgefertigte Berichte (auf Basis der SQL Server Reporting Services) abrufbar, mit denen sich die Inhalte des Management Data Warehouse komfortabel betrachten lassen. Alternativ kann man natürlich auch direkt in die Tabellen des Management Data Warehouse schauen oder hierfür eigene Abfragen oder Berichte entwerfen.
240
2030-4.book Page 241 Monday, February 8, 2010 4:26 PM
Management Data Warehouse
10.3.2 Verwendung des Management Data Warehouse Bevor das Management Data Warehouse genutzt werden kann, muss es erst eingerichtet werden. Dies kann man am komfortabelsten über einen Assistenten erledigen, der im Objekt-Explorer über einen Rechtsklick auf den Zweig Verwaltung/Datenauflistung und anschließende Auswahl der Option Verwaltungs-Data Warehouse konfigurieren aufgerufen wird. Der Assistent führt Sie in wenigen Schritten zur Erstellung eines Verwaltungs-Data Warehouses: 1. Im ersten Dialog ist die Option Verwaltungs-Data Warehouse erstellen oder aktualisieren auszuwählen.
Abbildung 10.10: Der Assistent für die Konfiguration des Management Data Warehouse 쮿
Darauf ist eine Datenbank auszuwählen – oder neu einzurichten –, in der das Verwaltungs-Data Warehouse abgelegt wird.
쮿
Bei der Zuordnung der Anmeldenamen und Benutzer kann man einzelnen Benutzern Rechte auf die Datenbank erteilen und diese den folgenden Gruppen zuweisen: 왘 mdw_admin 왘 mdw_reader 왘 mdw_writer
쮿
Nach einer abschließenden Zusammenfassung wird das Verwaltungs-Data Warehouse angelegt.
Im Objekt-Explorer finden Sie nun die entsprechenden Tabellen und Sichten in der neuen (oder ausgewählten, bereits vorher existierenden) Datenbank vor, wie Abbildung 10.11 zeigt.
SQL Server Performance-Ratgeber
241
2030-4.book Page 242 Monday, February 8, 2010 4:26 PM
10 – Schwachstellen identifizieren durch Performancetests
Abbildung 10.11: Die Tabellen und Sichten des Management Data Warehouse
Dabei sind die Tabellen und Sichten im Data Warehouse auf drei wesentliche Datenbankschemas verteilt: 쮿
core – enthält die Metadaten des Management Data Warehouse
쮿
snapshots – enthält die Daten, die mit den Standard-Datenauflistungen aufgezeichnet wurden
쮿
custom_snapshots – enthält die Daten, die durch selbst definierte Datenauflistungen entstanden sind
Für das letztgenannte Schema sind allerdings noch keine Objekte vorhanden, da ja noch keine selbst definierten Datenauflistungen erstellt wurden. Mit demselben Assistenten lassen sich auch die vordefinierten Datenauflistungen einrichten:
242
2030-4.book Page 243 Monday, February 8, 2010 4:26 PM
Management Data Warehouse
1. Im ersten Dialog ist die Option Datenauflistung einrichten auszuwählen. 2. Anschließend ist wieder die für das Management Data Warehouse zu verwendende Datenbank auszuwählen sowie zusätzlich das Cache-Verzeichnis anzugeben, in dem die Daten vorerst protokolliert werden. 3. Nach einer kurzen Zusammenfassung startet der Assistent die Systemauflistsätze und aktiviert die Auflistung. 4. Wenn Sie nun im Objekt-Explorer den Zweig Verwaltung/Datenauflistung aufklappen, finden Sie dort die in Abbildung 10.12 dargestellten Systemdaten-Auflistsätze.
Abbildung 10.12: Anzeige der Systemdaten-Auflistsätze im Objekt-Explorer
Die Daten der so eingerichteten Systemdaten-Auflistsätze werden jeweils nach einer bestimmten Zeit in das Cache-Verzeichnis geschrieben. Von dort werden sie wiederum in vorgegebenen Intervallen in das Management Data Warehouse geladen und dort eine gewisse Zeit aufbewahrt. Dabei sind die Standardzeiten je Systemdaten-Auflistsatz unterschiedlich: Standard-Auflistsatz
Auflist-Intervall
Upload-Intervall
Aufbewahrungszeit
Abfragestatistik
10 Sekunden
15 Minuten
14 Tage
Datenträgerverwendung
60 Sekunden
6 Stunden
2 Jahre (730 Tage)
Serveraktivität
60 Sekunden
15 Minuten
14 Tage
Tabelle 10.2: Standard-Zeitintervalle für Auflistung, Upload und Aufbewahrungsdauer
i
Die Zeitintervalle für Auflistung, Upload und Aufbewahrungsdauer können über die Eigenschaften des entsprechenden Auflistsatzes angepasst werden. Zum Entfernen der Daten nach der Ablaufzeit wurde vom Assistenten automatisch ein SQL Server Agent Task eingerichtet, den Sie bei Bedarf ebenfalls selbst anpassen (z.B. deaktivieren oder den Zeitplan ändern) können.
SQL Server Performance-Ratgeber
243
2030-4.book Page 244 Monday, February 8, 2010 4:26 PM
10 – Schwachstellen identifizieren durch Performancetests
Um die aufgezeichneten Daten nun über die vorgefertigten Berichte anzuzeigen, klicken Sie im Objekt-Explorer den Zweig Verwaltung/Datenauflistung mit der rechten Maustaste an und wählen Sie dann im Kontextmenü unter Berichte/Verwaltungs-Data Warehouse den gewünschten Bericht aus. (Hier gibt es zu jedem der drei Systemdaten-Auflistsätze einen entsprechenden Bericht.)
Abbildung 10.13: Der Standardbericht für die Serveraktivität
244
2030-4.book Page 245 Monday, February 8, 2010 4:26 PM
Management Data Warehouse
Die Berichte sind dabei nicht starr, sondern bieten verschiedene Möglichkeiten zur Navigation: 쮿
Durch die kleinen Lupen und Pfeilsymbole unterhalb des Zeitstrahls kann man den angezeigten Zeitraum anpassen.
쮿
Viele Elemente (sowohl grafische als auch Textanzeigen) lassen sich anklicken, um auf einem weiteren Bericht Detailinformationen zu erhalten.
쮿
Mit dem kleinen Pfeil nach links in der Symbolleiste oben kommt man zur jeweils vorigen Anzeige zurück.
10.3.3 Einrichten von eigenen Datenauflistungen Sogar das Erstellen von eigenen Auflistsätzen ist möglich. Allerdings wird dies bisher nicht über ein komfortables Front-End unterstützt, sondern geschieht auf Programmierebene über T-SQL. Da die entsprechende SQL-Syntax recht umfangreich ist, verzichte ich an dieser Stelle auf eine detaillierte Beschreibung, möchte aber einen Tipp geben, wie man mit vergleichsweise geringem Aufwand eine eigene Datenauflistung einrichten kann: 1. Klicken Sie im Objekt-Explorer mit der rechten Maustaste auf einen der drei vorhandenen Systemdaten-Auflistsätze. 2. Wählen Sie im darauf erscheinenden Kontextmenü die Option Skript für Datenauflistung als/CREATE in/Neues Abfrage-Editor-Fenster. 3. Passen Sie das generierte Skript an, um damit eine eigene Datenauflistung zu erstellen. Begin Transaction Begin Try Declare @collection_set_id_21 int Declare @collection_set_uid_22 uniqueidentifier EXEC [msdb].[dbo].[sp_syscollector_create_collection_set] @name=N'Abfragestatistik', @collection_mode=0, @description=N'Listet Abfragestatistiken, T-SQL-Text und Abfragepläne für die meisten Anweisungen mit Auswirkungen auf die Leistung auf. Ermöglicht die Analyse von Abfragen mit schlechter Leistung im Verhältnis zur Gesamtaktivität des SQL ServerDatenbankmoduls.', @logging_level=0, @days_until_expiration=14, @schedule_ name=N'CollectorSchedule_Every_15min', @collection_set_id=@collection_set_id_21 OUTPUT, @collection_set_uid=@collection_set_uid_22 OUTPUT Select @collection_set_id_21, @collection_set_uid_22 Declare @collector_type_uid_23 uniqueidentifier Select @collector_type_uid_23 = collector_type_uid From [msdb].[dbo]. [syscollector_collector_types] Where name = N'Query Activity Collector Type'; Declare @collection_item_id_24 int Listing 10.1: SQL-Skript zum Erstellen des Abfragestatistik-Systemdaten-Auflistsatzes
SQL Server Performance-Ratgeber
245
2030-4.book Page 246 Monday, February 8, 2010 4:26 PM
10 – Schwachstellen identifizieren durch Performancetests
EXEC [msdb].[dbo].[sp_syscollector_create_collection_item] @name=N'Abfragestatistik - Abfrageaktivität', @parameters=N'', @collection_item_id=@collection_item_id_24 OUTPUT, @frequency=10, @collection_set_id=@collection_set_id_21, @collector_type_ uid=@collector_type_uid_23 Select @collection_item_id_24 Commit Transaction; End Try Begin Catch Rollback Transaction; DECLARE @ErrorMessage NVARCHAR(4000); DECLARE @ErrorSeverity INT; DECLARE @ErrorState INT; DECLARE @ErrorNumber INT; DECLARE @ErrorLine INT; DECLARE @ErrorProcedure NVARCHAR(200); SELECT @ErrorLine = ERROR_LINE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(), @ErrorNumber = ERROR_NUMBER(), @ErrorMessage = ERROR_MESSAGE(), @ErrorProcedure = ISNULL(ERROR_PROCEDURE(), '-'); RAISERROR (14684, @ErrorSeverity, 1 , @ErrorNumber, @ErrorSeverity, @ErrorState, @ErrorProcedure, @ErrorLine, @ErrorMessage); End Catch; GO Listing 10.1: SQL-Skript zum Erstellen des Abfragestatistik-Systemdaten-Auflistsatzes (Forts.)
i
Während hier der Query Activity Collector Type als Auflistungstyp verwendet wurde, wird man in der Praxis für benutzerdefinierte Auflistungen den Typ Generic T-SQL Query Collector Type nutzen, der die Auflistung von Daten aus beliebigen Abfragen erlaubt. Somit lassen sich dann beispielsweise die dynamischen Management-Sichten als Quelle für eine Datenauflistung verwenden.
Der Inhalt der Skripts ist vom Aufbau her für alle drei Systemdaten-Auflistsätze gleich: Zuerst wird ein Auflistdatensatz (Collection Set) erzeugt, dann wird eine Auflistung (Collection) dazu angelegt. Wenn Sie dies einmal ausprobieren möchten, empfehle ich die Nutzung des Systemdaten-Auflistsatzes für die Abfragestatistik, da dieser am einfachsten zu lesen ist. Wenn Sie hier lediglich den Namen von Auflistsatz und Auflistung anpassen und das Skript anschließend ausführen, wird eine benutzerdefinierte Datenauflistung erzeugt, die sofort im Objekt-Explorer zu sehen ist.
246
2030-4.book Page 247 Monday, February 8, 2010 4:26 PM
Zusammenfassung
Tipps zur Verwendung des Management Data Warehouse:
i
쮿
Richten Sie das Management Data Warehouse auf einem separaten Server ein (oder nutzen Sie alternativ einen bereits existierenden Data Warehouse Server dafür). Somit werden einerseits die zu beobachtenden produktiven Server weniger belastet. Andererseits haben Sie damit eine zentrale Stelle, an der die Performancedaten von verschiedenen SQL Servern gesammelt und ausgewertet werden können.
쮿
Nutzen Sie die Option Auflisten und jetzt hochladen im Kontextmenü des gewünschten Auflistsatzes, um die entsprechenden Tabellen im Management Data Warehouse unabhängig von den definierten Zeitplänen sofort zu aktualisieren.
쮿
Definieren Sie bei Bedarf eigene Datenauflistungen und nutzen Sie dabei die dynamischen Management-Sichten in Kombination mit dem Generic T-SQL Query Collector Type.
쮿
Beenden Sie nicht mehr benötigte Datenauflistsätze, da auch diese Ressourcen verbrauchen.
10.4 Zusammenfassung Selbst wenn man auf die zahlreichen von Drittanbietern erhältlichen Tools verzichtet, bieten das Windows-Betriebssystem und SQL Server von Haus aus zahlreiche Tools, mit denen man die Performanceschwachstellen lokalisieren kann. Das beginnt beim Windows Performance Monitor, der zahlreiche Counter sowohl für das Betriebssystem und die Serverhardware als auch für SQL Server und andere Dienste bietet. Dazu kommen die dynamischen Management-Sichten, mit denen man per T-SQL viele statistische Informationen über den Zustand des SQL Servers erhalten kann. Nur für die größeren SQL-Server-Editionen verfügbar sind leistungsfähigere Performance Tools wie der SQL Server Profiler, mit dem sich alle möglichen Ereignisse (insbesondere SQL-Anweisungen und Prozeduraufrufe) für die spätere Auswertung protokollieren lassen. Dasselbe gilt für den Datenbankoptimierungsratgeber, der Datenbanken auf Basis von Profiler-Protokollen oder SQL-Skripts analysiert und daraufhin entsprechende Vorschläge für neu zu erstellende Indizes oder die Partitionierung von Tabellen unterbreitet. Mit SQL Server 2008 dazu gekommen ist das Management Data Warehouse, durch das sich – bei vergleichsweise geringer Serverlast – zahlreiche Performancedaten regelmäßig und dauerhaft in einer relationalen Datenbank sammeln lassen, die man anschließend mit passenden interaktiven Berichten auswerten kann.
SQL Server Performance-Ratgeber
247
2030-4.book Page 248 Monday, February 8, 2010 4:26 PM
2030-4.book Page 249 Monday, February 8, 2010 4:26 PM
11
Durchführung der Optimierung
Nachdem im vorigen Kapitel aufgezeigt wurde, wie man die Ursachen für Performanceprobleme herausfinden kann, gilt es nun geeignete Maßnahmen zu ergreifen, um diese dauerhaft zu beheben.
11.1
Maßnahmen definieren
Beim Definieren der möglichen Maßnahmen sind insbesondere zwei Fehler zu vermeiden, die nur allzu oft gemacht werden. 1. Ressourcenengpässe nicht durch Aufstockung der Ressourcen bekämpfen Groß ist die Versuchung, nach Feststellung eines Ressourcenengpasses – wie beispielsweise ausgelastetem Hauptspeicher – diese Ressource einfach aufzurüsten. Sofern die entsprechende Ressource aber nicht deutlich unterdimensioniert ist, werden dadurch in den meisten Fällen lediglich die Symptome des Problems gemildert oder der Engpass zu einer anderen Ressource (z.B. der CPU-Leistung) hin verschoben. So wird bei der nächsten Performanceoptimierung dann diese Ressource aufgerüstet und man befindet sich sehr schnell in einem Wettrüsten wie zuletzt in Zeiten des Kalten Krieges. Versuchen Sie stattdessen, die eigentliche Ursache des Problems herauszufinden und diese zu beheben. Oft sind es nur kleine Maßnahmen (wie beispielsweise das Erstellen eines Indexes), mit denen sich die Performance maßgeblich verbessern lässt, ohne in teure Hardware investieren zu müssen. 2. Durch Tools vorgeschlagene Optimierungsmaßnahmen nie bedenkenlos umsetzen Tools wie der in Kapitel 10 vorgestellte Datenbankoptimierungsratgeber, aber auch das SQL Server Management Studio (bei der Ausführung von Abfragen) geben sehr genaue Anweisungen, welche Optimierungsmaßnahmen durchzuführen sind (beispielsweise das Erstellen oder Entfernen von Indizes). Viele dieser Maßnahmen sind sicherlich sinnvoll, andere könnten aber auch unnötig oder gar kontraproduktiv sein, da die Tools, von denen die Maßnahmen vorgeschlagen werden, natürlich nur einen Teil des Gesamtsystems sehen und auch auf keine fachlichen Hintergrundinformationen über die Verwendung der Datenbank zugreifen können. So nutzt der Datenbankoptimierungsratgeber beispielsweise lediglich den Input, den er von Ihnen bekommt (also ein SQL-Skript oder einen SQL Server Profiler Trace), und entscheidet aufgrund dieser Grundlage, welche Maßnahmen zu ergreifen sind. Da dieser Input aber lediglich einen kleinen Ausschnitt der möglichen Aktionen widerspiegelt, wird die Datenbank durch die empfohlenen Maßnahmen auch nur für diesen Ausschnitt optimiert. Andere Aktionen profitieren eventuell nicht davon oder leiden sogar unter den geänderten Randbedingungen.
SQL Server Performance-Ratgeber
249
2030-4.book Page 250 Monday, February 8, 2010 4:26 PM
11 – Durchführung der Optimierung
Daher sollten Sie vor der Umsetzung einer Optimierungsmaßnahme gründlich prüfen, ob diese auch allgemein sinnvoll (oder zumindest nicht schädlich) ist. Insbesondere vor der Löschung von vermeintlich überflüssigen Indizes macht es Sinn, Kollegen mit einzubeziehen, die regelmäßig mit der Anwendung bzw. Datenbank arbeiten, da diese vielleicht noch Situationen kennen, in denen der Index sinnvoll oder gar notwendig ist. Mit diesen beiden Themen im Hinterkopf kann man eine Auflistung der möglichen Optimierungsmaßnahmen erstellen, von denen später einige direkt umgesetzt werden. Die Maßnahmen, die nicht sofort umgesetzt werden, sind auf diesem Weg zumindest dokumentiert und können für spätere weitere Optimierungen (im Sinne eines Feintunings) wieder in Betracht gezogen werden.
11.2 Gewichtung der Optimierungsansätze Sofern die möglichen Optimierungsmaßnahmen erkannt sind, sollten diese noch in eine sinnvolle Reihenfolge gebracht werden. Der Sinn dieser Gewichtung liegt darin, die Maßnahmen, die bei möglichst geringem Aufwand den maximalen Performancegewinn versprechen, zuerst umzusetzen. Als kleine Hilfestellung folgt eine Übersicht der wichtigsten Maßnahmen mit Erfahrungswerten für den Aufwand der Anpassung und dem normalerweise zu erwartenden Performancegewinn1: Maßnahme
Änderungsaufwand
Auswirkungen
Hardware aufrüsten
hoch
mittel
Windows-Konfiguration optimieren
gering
klein
SQL-Server-Konfiguration optimieren
gering
klein
Datenmodellierung (normalisieren bzw. denormalisieren)
sehr hoch
mittel bis groß
Datenmodellierung: Datentypen
hoch
klein bis mittel
Zusätzliche Indizes erstellen
gering
groß
Überflüssige Indizes entfernen
gering
klein bis mittel
Archivierung von Altdaten
mittel
mittel bis groß
Partitionierung von großen Tabellen
mittel
mittel bis groß
Optimierung von Sperren und Isolationsstufen
gering bis mittel
klein bis mittel
SQL-Zugriffe optimieren (gespeicherte Prozeduren)
variabel
mittel bis groß
(ADO).NET-Zugriffe optimieren
variabel
mittel bis groß
Tabelle 11.1: Typen von Optimierungsmaßnahmen in einer Kosten/Nutzen-Betrachtung
1.
250
Die Angaben für Aufwand und Auswirkungen sind reine Erfahrungswerte, die je nach Anwendungsumfeld auch deutlich abweichen können. Daher sind diese Werte hier nur als grobe Richtwerte zu interpretieren.
2030-4.book Page 251 Monday, February 8, 2010 4:26 PM
Gezielt optimieren und Erfolge messen
Bei der Optimierung der Anwendung sind die Optimierungen von einzelnen Anweisungen mit vergleichsweise geringem Aufwand möglich. Der hohe Aufwand entsteht hier eher durch die Vielzahl an Anweisungen, die zu optimieren sind. Besonders effektiv kann aber die Optimierung von einzelnen Abfragen sein, die an zentraler Stelle in einer gespeicherten Prozedur liegen und damit besonders häufig aufgerufen werden. Beachten Sie dabei, dass die Angaben zu Änderungsaufwand und Auswirkungen in Tabelle 11.1 individuelle Erfahrungswerte sind, die lediglich als Orientierungshilfe angegeben sind. Je nach Umgebung und Datenbankanwendung können diese völlig unterschiedlich ausfallen. Insbesondere dann, wenn ein Punkt in extremem Maße vernachlässigt wurde, sind hier generell große Auswirkungen zu erreichen. Wenn also ein Datenbankserver deutlich zu wenig Speicher hat und damit die Windows-Auslagerungsdatei intensiv beansprucht, so kann mit einer Speicheraufrüstung ein massiver Performancegewinn erzielt werden. Bei der Aufrüstung der Serverhardware ist allerdings ferner zu beachten, dass der Änderungsaufwand mit zusätzlichen Kosten für die Hardware selbst (Hauptspeicher, Prozessoren etc.) verbunden ist und außerdem stark dadurch beeinflusst wird, ob der Server selbst dies noch zulässt. Wenn beispielsweise ein Server noch freie Steckplätze für RAMModule frei hat, lässt sich der Hauptspeicher mit wenig Aufwand und relativ geringen Kosten erweitern. Sind die Speichermodulsteckplätze alle belegt, so wäre für einen Ausbau der RAM-Kapazität ein Austausch der gesamten Hauptplatine notwendig, was sowohl von den Kosten als auch vom Arbeitsaufwand her einen erheblichen Aufwand zur Folge hätte, zumal dann in der Regel auch die komplette Software neu installiert und konfiguriert werden muss. Hier gilt es also, die Summe aus Anschaffungskosten und Arbeitsaufwand gut gegen den voraussichtlichen Nutzen – also Performancegewinn – abzuwägen. Behalten Sie dabei auch stets die Ziele im Auge, die Sie zu Beginn der Performanceanalyse selbst definiert haben.
11.3 Gezielt optimieren und Erfolge messen Wenn es daran geht, die Optimierungsmaßnahmen umzusetzen, liegt bei produktiven Datenbanken die größte Herausforderung meist darin, den laufenden Betrieb möglichst wenig zu beeinflussen. Bei den meisten Unternehmensanwendungen sind entsprechende Wartungsfenster vorhanden, also Zeiträume, in denen die Anwendung nicht produktiv genutzt wird. In diesen werden normalerweise die regelmäßigen Wartungsarbeiten wie beispielsweise Datenbanksicherungen, Wartung von Indizes etc. durchgeführt. Aber auch unregelmäßige Aktionen wie eben Maßnahmen zur Datenbankoptimierung sollten in diesen Zeiträumen stattfinden, damit dadurch nicht der produktive Betrieb der Datenbank (bzw. der dazugehörenden Datenbankanwendungen) negativ beeinflusst wird. In den meisten Fällen werden diese Wartungsfenster nachts liegen (oder bereits abends nach Ende der üblichen Geschäftszeiten beginnen). Für größere Aktionen bleiben dann immer noch Feiertage und Wochenenden übrig. Dabei ist natürlich nicht zwingend eine Anwesenheit des durchführenden Datenbankadministrators notwendig. Stattdessen kann
SQL Server Performance-Ratgeber
251
2030-4.book Page 252 Monday, February 8, 2010 4:26 PM
11 – Durchführung der Optimierung
man die Änderungen als SQL-Skript vorbereiten und dann über SQL Server Agent zeitgesteuert ausführen lassen. Schwieriger wird es da schon bei Anwendungen, die beispielsweise aufgrund von Verwendung in unterschiedlichen Zeitzonen oder aufgrund der Notwendigkeit zur Nutzung auch außerhalb der üblichen Geschäftszeiten (was insbesondere bei Internetanwendungen der Fall ist) im 24x7-Betrieb – also 24 Stunden 7 Tage die Woche – laufen. Hier gibt es zwei sinnvolle Varianten, die Änderungsmaßnahmen durchzuführen: 1. Offline (schnell bei hoher Ressourcennutzung) In dieser Variante wird ein außerordentliches Wartungsfenster definiert und die Anwendung in dieser Zeit gezielt offline genommen. Bei einer Webanwendung kann beispielsweise ein entsprechender Hinweis erfolgen, ab wann die Anwendung wieder nutzbar ist. Um die Offline-Zeit möglichst gering zu halten, sollten die Änderungen bei dieser Variante natürlich möglichst schnell durchgeführt werden. Dabei dürfen aber alle benötigten Ressourcen beansprucht werden, weil zu dieser Zeit ja niemand mit der Anwendung arbeitet, man sich die verfügbaren Ressourcen also nicht mit den Endanwendern teilen muss. Hier kann man also getrost die Datenbank in den Single-User-Modus schalten und bei Änderungen an den Daten gleich die ganze Tabelle oder gar Datenbank sperren, um den Overhead für viele kleine Sperren zu sparen. 2. Online (langsam bei niedriger Ressourcennutzung) Viele Aktionen lassen sich auch durchführen, während die Anwendung in Betrieb ist. Hier ist dann weniger die Laufzeit der Änderung entscheidend, sondern eher die momentane Zusatzbelastung, die durch Anbringen der Änderung erzeugt wird. Beispielsweise bietet SQL Server seit der 2005er-Generation bei der Enterprise Edition die Möglichkeit der Online-Indizierung. Damit können Indizes neu aufgebaut werden, während Daten der betroffenen Tabellen gelesen oder sogar geändert werden. Der Neuaufbau eines Indexes dauert auf diesem Weg natürlich deutlich länger, aber da währenddessen normal mit den Daten gearbeitet werden kann, spielt die Dauer der Wartung in dieser Variante nur noch eine untergeordnete Rolle. Unabhängig davon, welche Variante Sie verwenden, ist nach erfolgter Umsetzung der gewünschten Optimierungsmaßnahmen zu prüfen, inwiefern diese erfolgreich waren. Anschließend kann entweder die nächste Optimierungsmaßnahme umgesetzt oder sogar ein weiterer Durchlauf der Performanceanalyse gestartet werden. Dadurch kann eine schrittweise Verfeinerung der Optimierung durchgeführt werden. Aufgrund der Tatsache, dass sich die Rahmenbedingungen, wie beispielsweise die Anzahl der Anwender, das von der Datenbank zu verwaltende Datenvolumen etc., im Laufe der Zeit ändern, empfiehlt es sich, dies regelmäßig zu wiederholen. Auch bei unveränderten Bedingungen lässt sich somit die Optimierung der Datenbankperformance nach und nach weiter verfeinern. Um einen Überblick darüber zu haben, inwiefern sich die Datenbankperformance über einen längeren Zeitraum verändert (also langsamer wird durch stetiges Anwachsen der Datenmenge und Benutzerzahl sowie schneller wird durch Optimierungsmaßnahmen), kann man für eine zu definierende Menge an immer wiederkehrenden Aktionen eine
252
2030-4.book Page 253 Monday, February 8, 2010 4:26 PM
Zusammenfassung
Protokollierung über einen längeren Zeitraum durchführen. Die gemessenen Zeiten für diese Aktionen können Sie entweder ganz banal manuell in einem Excel-Sheet protokollieren oder Sie können vielleicht sogar ein kleines Testprogramm schreiben, das diese Aktionen (ähnlich einer Gruppe von Unit-Tests) nacheinander ausführt und die Zeiten in einer SQL-Server-Tabelle festhält. Somit erhalten Sie einen Langzeitvergleich, aus dem sich eventuell sogar frühzeitig erkennen lässt, wann die Datenbankperformance wieder in kritische Bereiche kommt.
11.4 Zusammenfassung In diesem Kapitel wurde ein strukturiertes Vorgehen für die zielgerichtete Umsetzung von Optimierungsmaßnahmen beschrieben. Dies beginnt mit einer Auflistung der möglichen Maßnahmen, die anschließend nach Aufwand und voraussichtlichem Nutzen gewichtet werden. Schließlich werden die vielversprechendsten Optimierungsmaßnahmen ausgewählt und so umgesetzt, dass der produktive Betrieb der Anwendung möglichst wenig beeinflusst wird. Durch eine Langzeitbetrachtung der Laufzeiten für immer wiederkehrende Aktionen kann eine schlechter werdende Performance frühzeitig erkannt werden, sodass man eine erneute Performanceoptimierung starten kann, bevor dies kritische Ausmaße annimmt. Damit sind Sie – abgesehen vom folgenden Anhang – am Ende des SQL-Server-Performanceratgebers angekommen. Ich hoffe, Sie haben einige hilfreiche Anregungen erhalten, worauf beim Einrichten und Administrieren eines Datenbankservers sowie beim Entwerfen und Entwickeln einer Datenbankanwendung zu achten ist bzw. wie die Performance Ihrer Datenbankanwendung optimiert werden kann. Wenn Sie möchten, schreiben Sie mir gerne Ihre Erfahrungen, die Sie mit den hier vorgestellten (oder auch anderen) Optimierungsansätzen gemacht haben.
SQL Server Performance-Ratgeber
253
2030-4.book Page 254 Monday, February 8, 2010 4:26 PM
2030-4.book Page 255 Monday, February 8, 2010 4:26 PM
Teil E Anhang Um das Buch abzurunden, sind in diesem Anhang einige zusätzliche Informationen zusammengestellt, die zum späteren Nachschlagen hilfreich sein können. So ist in Anhang A – Tools & Features eine Auflistung verschiedener nützlicher Tools zur SQL Server Performance zu finden. Anhang B – Informationsquellen im Internet bietet Verweise auf verschiedene Websites mit weiterführenden Informationen. In Anhang C – Checklisten sind die wichtigsten Regeln zu jedem Abschnitt des Buches zusammengestellt, die man sich immer wieder ins Gedächtnis rufen sollte. Anhang D schließlich bietet ein Glossar, in dem die wichtigsten Abkürzungen und Begriffe kurz erläutert sind.
SQL Server Performance-Ratgeber
255
2030-4.book Page 256 Monday, February 8, 2010 4:26 PM
2030-4.book Page 257 Monday, February 8, 2010 4:26 PM
A
Tools & Features
Es gibt eine ganze Reihe von nützlichen Zusatztools zu SQL Server, die für das Performance Tuning sehr hilfreich sind. Einige davon werden mit SQL Server direkt ausgeliefert, andere sind bei Microsoft selbst oder Drittanbietern zum Download verfügbar.
A.1
Performance-Tools von Windows und SQL Server
Sowohl das Windows-Betriebssystem als auch die verschiedenen Varianten von SQL Server bieten einige nützliche Tools, um die Performance zu analysieren. Da diese schon im eigentlichen Buchtext – insbesondere in Kapitel 10 – Schwachstellen identifizieren durch Performancetests – behandelt wurden, werden sie im Folgenden nur der Übersicht wegen noch einmal kurz vorgestellt.
A.1.1
Windows Task-Manager
Der Windows Task-Manager ist Bestandteil jeder Windows-Installation und über das Menü anwählbar, das erscheint, wenn man die Tastenkombination (Strg)+(Alt)+(Del) drückt. Über die Registerkarte Systemleistung ist relativ einfach die Auslastung der CPU und der Auslagerungsdatei (über einen einfachen Graphen dargestellt) sowie des Speichers zu sehen. Dabei ist der Speicher weiter unterteilt in Physikalischer Speicher, Zugesicherter Speicher (durch die Auslagerungsdatei) sowie Kernel-Speicher. Über eine weitere Registerkarte (Netzwerk) sind auch die verfügbaren Netzwerkadapter – sowohl drahtlos als auch kabelgebunden – und deren Auslastung dargestellt.
A.1.2
Windows Performance Monitor
Auch der Windows Performance Monitor ist Bestandteil jeder Windows-Installation. Im Gegensatz zum Windows Task-Manager lassen sich mit diesem Tool die Werte aber nicht nur anzeigen, sondern auch dauerhaft in Dateien oder auch SQL-Server-Tabellen protokollieren. Die Auswahl der darzustellenden bzw. zu protokollierenden Leistungsindikatoren ist auch erheblich größer, zumal hier nicht nur Angaben zu Hardware und Betriebssystem, sondern auch Werte von verschiedenen Serverdiensten – inklusive des Microsoft SQL Servers – verfügbar sind.
SQL Server Performance-Ratgeber
257
2030-4.book Page 258 Monday, February 8, 2010 4:26 PM
A – Tools & Features
A.1.3
SQL Server Profiler
Der SQL Server Profiler wird mit der Enterprise Edition von SQL Server 2005 bzw. 2008 ausgeliefert und zeigt an bzw. protokolliert eine Vielzahl von Ereignissen und Daten zu diesen Ereignissen, die gerade auf dem SQL Server eintreten. Auf diesem Weg lassen sich beispielsweise Anmeldungen, ausgeführte SQL-Anweisungen, Prozeduraufrufe und vieles mehr protokollieren. Als Ziel des Protokolls kann eine Trace-Datei oder eine SQLServer-Tabelle verwendet werden. Besonders interessant ist die Möglichkeit, ein mit dem Windows Performance Monitor erstelltes Protokoll zu importieren und mit dem SQL Server Profiler Log zu synchronisieren. Dadurch lässt sich leicht erkennen, welche SQL-Anweisungen beispielsweise zu dem Zeitpunkt ausgeführt wurden, zu dem der Ressourcenbedarf stark anstieg.
A.1.4
Datenbankoptimierungsratgeber
Auch der Datenbankoptimierungsratgeber ist nur mit den Enterprise Editionen von SQL Server 2005 bzw. 2008 verfügbar. Dieses Tool analysiert eine Arbeitsauslastungsdatei, die entweder als SQL-Skript oder in Form eines SQL Server Profiler-Protokolls vorliegt, und erstellt daraufhin Optimierungsvorschläge, die sich auf die Erstellung, Entfernung oder Anpassung von Indizes sowie die Partitionierung von Tabellen beziehen. Die Optimierungsvorschläge lassen sich anschließend direkt anwenden oder als SQL-Skript zur späteren Ausführung speichern.
A.1.5
SQL Server Management Studio
Das SQL Server Management Studio selbst ist zwar eher eine allgemeine Verwaltungsoberfläche für den SQL Server, beinhaltet aber auch einige performancerelevante Tools. Dies sind vor allem die Anzeige des geschätzten und/oder tatsächlichen Ausführungsplans mithilfe des Abfrageeditors oder auch der Activity Monitor, der verschiedene Informationen zum aktuellen Zustand des SQL Servers übersichtlich darstellt.
A.1.6
Management Data Warehouse
Das Management Data Warehouse ist erst seit SQL Server 2008 verfügbar (für alle kostenpflichtigen Editionen) und ermöglicht eine längerfristige Aufzeichnung von Performancedaten, die beispielsweise aus dem Windows Performance Monitor oder auch von dynamischen Management-Sichten stammen. Die Daten werden in einem zentralen Data Warehouse abgelegt, das mit komfortablen Berichten auszuwerten ist. Neben den vordefinierten Datenauflistungen und Berichten lassen sich mit einigen gespeicherten Prozeduren auch eigene erstellen.
258
2030-4.book Page 259 Monday, February 8, 2010 4:26 PM
Kostenfreie Messwerkzeuge und Optimierungstools
A.2 Kostenfreie Messwerkzeuge und Optimierungstools A.2.1
SQLIO
SQLIO ist ein Tool, um Lasttests von Festplattensystemen durchzuführen. Dabei ist der Name eigentlich irreführend, denn es wird kein SQL Server benötigt. Der Name trägt eher der Empfehlung Rechnung, vor Installation eines SQL Servers die entsprechenden Plattensysteme damit auf Funktionalität und Geschwindigkeit zu testen. Dazu kann man folgende Angaben machen: 쮿
zu testende Laufwerke (auch kombiniert)
쮿
Read Only/Write
쮿
Testdauer
쮿
sequenzieller oder zufälliger Zugriff
쮿
Anzahl Threads
쮿
Größe der Testdatei (in MB)
쮿
Größe der IOs (in KB)
쮿
Länge der Warteschlange (outstanding I/O requests)
Als Ausgabe erhält man Angaben über Durchsatzraten (I/Os pro Sekunde und MB pro Sekunde) und Latenzzeiten (minimal, maximal und durchschnittlich, jeweils in ms). SQLIO ist über das Microsoft Download Center (http://www.microsoft.com/downloads) verfügbar und wird – wie die meisten kostenfreien Microsoft-Tools – leider nicht supported.
A.2.2 SQLIOSim Etwas weiter als SQLIO geht das – ebenfalls von Microsoft kostenfrei erhältliche (und damit nicht supportete) – Tool SQLIOSim. Auch hier wird kein installierter SQL Server benötigt, es werden aber – unabhängig von der SQL Server Engine – SQL Server-typische Zugriffsmuster simuliert. SQLIOSim wird scheinbar auch mit einigen Editionen von SQL Server (z.B. der Developer Edition von SQL Server 2008) direkt ausgeliefert und ist dann im Installationsverzeichnis (z.B. C:\Programme\Microsoft SQL Server\MSSQL10.SQL2008\MSSQL\Binn) zu finden. Alternativ kann man es auch von folgender Website herunterladen: http://support.microsoft.com/kb/231619/de Das herunterladbare Tool bezieht sich vorrangig auf die SQL-Server-Versionen 7.0 bis 9.0 (= SQL Server 2005), während die bei SQL Server 2008 mitgelieferte Variante natürlich auch auf diese Version ausgelegt ist.
SQL Server Performance-Ratgeber
259
2030-4.book Page 260 Monday, February 8, 2010 4:26 PM
A – Tools & Features
Als Vorgänger von SQLIOSim gab es bereits ein ähnliches Tool mit Namen SQLIOStress, das auch ältere Versionen von SQL Server adressiert.
A.2.3 SQL Stress Nicht mit SQLIOStress zu verwechseln ist das Tool SQL Stress, das der Autor David Kubelka ebenfalls kostenfrei zum Download anbietet. Dieses unterscheidet sich insofern gravierend von den zuvor genannten, da es einen installierten SQL Server voraussetzt und nutzt, um auf diesem eine vorher zu definierende Client-Workload zu simulieren. Diese Workloads können sich sowohl auf SQL als auch auf SMTP-Aktivitäten beziehen. Bei SQL Workloads muss die zu nutzende Datenbank jedoch vorher manuell angelegt werden. Dazu gibt es noch ein paar weitere Einschränkungen, die – zumindest in der aktuellen Version – zu berücksichtigen sind. Auch wenn SQL Stress viele Benutzer simuliert, findet kein Locking und damit auch kein Blocking von Objekten statt. Dies ist einerseits begrüßenswert, weicht aber natürlich deutlich von realen Szenarien ab. Ebenso scheint das Tool Probleme mit benannten SQL-Instanzen zu haben. Diese lassen sich zwar im Tool eintragen, werden aber anschließend nicht gefunden, da das Tool versucht, sich mit der Standardinstanz des angegebenen Servers zu verbinden, und daher die Datenbank nicht findet. Weitere Informationen zu diesem Tool finden Sie auf der folgenden Website: http://www.sqlstress.com
A.2.4 SQL Server 2005 Performance Dashboard Das SQL Server 2005 Performance Dashboard stellt eine Reihe von Berichten zur Verfügung, mit denen man die Daten aus den dynamischen Management-Sichten übersichtlich und komfortabel analysieren kann. Die 1,4 MB große Installationsdatei ist unter folgender URL kostenfrei per Download erhältlich: http://www.microsoft.com/downloads/details.aspx?FamilyId=1d3a4a0d-7e0c-4730-8204e419218c1efc&displaylang=en Eigentlich funktionieren die Reports nur mit SQL Server 2005, mit ein paar kleinen Änderungen kann man diese aber auch mit SQL Server 2008 nutzen. Eine Beschreibung hierfür sowie die notwendigen Modifikationen sind auf folgender Website zu finden: http://blogs.technet.com/rob/archive/2009/02/18/performance-dashboard-reports-for-sql-server2008.aspx
260
2030-4.book Page 261 Monday, February 8, 2010 4:26 PM
Kostenfreie Messwerkzeuge und Optimierungstools
A.2.5 PAL – Performance Analysis of Logs PAL ist ein Tool zur Analyse von Protokolldateien, die mit dem Windows Performance Monitor erstellt wurden. Auf Basis von mitgelieferten Schwellwertdateien (derzeit für Active Directory, BizTalk, Exchange, IIS, MOSS, MS Project Server, SQL Server, System Overview und Windows 2008 Hyper-V) werden die Protokolldateien analysiert und als Ergebnis ein HTML-Bericht mit anschaulichen Diagrammen erzeugt. Durch ein paar Fragen an den Benutzer des Tools werden die Schwellwerte dynamisch an die jeweilige Hardware des Servers angepasst. Allerdings lassen sich auch die Schwellwertdateien selbst anpassen oder erweitern.1
Abbildung A.1: Auswahl der Schwellwertdatei und Fragen zur Systemumgebung
Der große Vorteil des Tools liegt darin, dass man – ohne die einzelnen Schwellwerte selbst kennen zu müssen – auf deutliche Abweichungen hingewiesen wird. Dabei spricht für das Tool auch die Tatsache, dass es selbst vom Microsoft Premium Support genutzt wird, um Performanceprobleme zu analysieren.
1.
Für SQL Server ist derzeit zwar nur eine SQL Server 2005-Schwellwertdatei vorhanden. Diese ist aber auch für SQL Server 2008 noch verwendbar.
SQL Server Performance-Ratgeber
261
2030-4.book Page 262 Monday, February 8, 2010 4:26 PM
A – Tools & Features
Das Tool ist (derzeit in der Version 1.35) als kostenfreier Download bei CodePlex erhältlich: http://www.codeplex.com/PAL Dort kann man ebenfalls den Quelltext (VB.NET und VBScript) herunterladen, da es sich um ein Open-Source-Tool handelt. Zur Installation werden lediglich die folgenden drei Komponenten – die ebenfalls kostenfrei erhältlich sind – vorausgesetzt: 쮿
Microsoft Log Parser v2.2
쮿
Microsoft Office Web Components 11
쮿
.NET Framework v2.0 (für UI)
A.3 Kommerzielle Tools von Drittanbietern Neben den bereits erwähnten kostenfreien Tools gibt es noch eine ganze Reihe von kostenpflichtigen performancerelevanten Tools von diversen Drittanbietern. Zu den meisten dieser Tools gibt es auch entsprechende Testversionen, sodass man diese erst einmal ausprobieren kann, bevor man in eine eventuell nicht ganz billige Lizenz investiert. Hier gibt es vor allem zwei gut etablierte Anbieter. Um keinen von beiden zu bevorzugen oder zu benachteiligen, möchte ich an dieser Stelle gar nicht detailliert auf die Tools eingehen, sondern lediglich eine Auflistung der relevanten Tools in diesem Umfeld präsentieren. Detaillierte Informationen zu den Tools erhalten Sie auf der Website der jeweiligen Hersteller.
A.3.1
Quest Software
Website: http://questsoftware.de
Tools 쮿
Benchmark Factory for SQL Server – Skalierbarkeits- und Belastungstests
쮿
Foglight Performance Analysis for SQL Server – Tool zur Performanceanalyse
쮿
Foglight for SQL Server – Tool zur Langzeitüberwachung (inkl. automatischer Optimierung)
쮿
LiteSpeed for SQL Server – Backup Management (verkürzt Backup-/Restore-Zeiten)
쮿
Quest Capacity Manage for SQL Server – zentrale Verwaltung von Kapazitäten, Indizes etc.
쮿
Spotlight on SQL Server – Monitoring der SQL Server-Auslastung
쮿
Toad for SQL Server – SQL-Editor
262
2030-4.book Page 263 Monday, February 8, 2010 4:26 PM
Kommerzielle Tools von Drittanbietern
A.3.2 Redgate Website: http://www.red-gate.com
Tools 쮿
ANTS Performance Profiler – Überwachung des Verhaltens einer .NET-Anwendung
쮿
ANTS Memory Profiler – Überwachung der Speichernutzung einer .NET-Anwendung
쮿
SQL Backup Pro – Backup Management (verkürzt Backup-/Restore-Zeiten)
쮿
SQL Response – Überwachung der SQL-Server-Auslastung
A.3.3 Idera Website: http://www.idera.com
Tools 쮿
SQL check – einfaches aber dafür kostenfreies Performance Monitoring Tool
쮿
SQL defrag manager – Index-Fragmentierung überwachen und beheben
쮿
SQL diagnostic manager – Diagnose-Tool
쮿
SQL mobile manager – mobile Variante des Diagnose-Tools
쮿
SQL safe backup / SQL safe lite – Backup Management (verkürzt Backup-/Restore-Zeiten)
SQL Server Performance-Ratgeber
263
2030-4.book Page 264 Monday, February 8, 2010 4:26 PM
2030-4.book Page 265 Monday, February 8, 2010 4:26 PM
B B.1
Informationsquellen im Internet
Websites zum Buch
Bedeutung
Hyperlink
Website des Autors
http://www.panthercomputing.de
Website des Verlags
http://entwickler-press.de
Tabelle B.1: Websites zum Buch
B.2
Websites zur SQL Server Performance
Bedeutung
Hyperlink
SQL Server Performance (englisch)
http://www.sql-server-performance.com
SQL Server Pedia (Performance Tuning WIKI)
http://sqlserverpedia.com/wiki/Performance_Tuning
Tabelle B.2: Websites zur SQL Server Performance
B.3
Allgemeine Websites zum SQL Server
Bedeutung
Hyperlink
SQL Server Developer Center (dt.)
http://msdn.microsoft.com/de-de/sqlserver/default.aspx
SQL Server Developer Center (eng.)
http://msdn.microsoft.com/en-us/sqlserver/default.aspx
MSDN-Library
http://msdn.microsoft.com/de-de/library/bb418432(SQL.10).aspx
Microsoft TechNet
http://technet.microsoft.com/de-de/sqlserver/default.aspx
SQL Server Best Practices
http://technet.microsoft.com/en-us/sqlserver/bb671430.aspx
Tabelle B.3: Microsoft Websites zum SQL Server
SQL Server Performance-Ratgeber
265
2030-4.book Page 266 Monday, February 8, 2010 4:26 PM
B – Informationsquellen im Internet
Bedeutung
Hyperlink
Inside-SQL (dt.)
http://www.insidesql.org
SQL Server Customer Advisory Team
http://sqlcat.com
SQL Server Central (eng.)
http://www.sqlservercentral.com
SQL Server Pedia (eng.)
http://www.sqlserverpedia.com
SQL Share (fka JumpStartTV.com)
http://www.sqlshare.com
CodePlex (SQL Server Area)
http://www.codeplex.com/site/search?TagName=SQL%20Server
Tabelle B.4: Sonstige Websites zum SQL Server Bedeutung
Hyperlink
SQL Server Query Optimization Team
http://blogs.msdn.com/queryoptteam
SQLCAT Blog
http://blogs.msdn.com/sqlcat
Tabelle B.5: Interessante Blogs zum SQL Server
B.4
Foren & Newsgroups zum SQL Server
Bedeutung
Hyperlink
SQL Server MSDN-Forum (dt.)
http://social.msdn.microsoft.com/Forums/de-DE/category/sqlserver
SQL Server MSDN-Forum (eng.)
http://social.msdn.microsoft.com/forums/en-US/category/sqlserver
SQL Server Newsgroup (dt.)
http://www.microsoft.com/communities/newsgroups/en-us/ default.aspx?dg=microsoft.public.de.sqlserver
SQL Server Newsgroup (eng.)
http://www.microsoft.com/communities/newsgroups/en-us/ default.aspx?dg=microsoft.public.sqlserver.server
Tabelle B.6: Foren & Newsgroups zum SQL Server
266
2030-4.book Page 267 Monday, February 8, 2010 4:26 PM
C
Checklisten
In diesem Anhang finden Sie Checklisten, die Ihnen die wichtigsten Regeln wieder in Erinnerung rufen sollen, die Sie in Ihrer jeweiligen Rolle als Administrator, Designer, Architekt oder Entwickler stets beachten sollten. Dabei sind die Listen bewusst so kurz gehalten, dass sie pro Zielgruppe auf eine Seite passen. Es wird hier also nicht der Anspruch erhoben, alle relevanten Themen noch einmal vollständig und detailliert aufzulisten. Stattdessen sollen die Regeln übersichtlich herausgestellt werden, die man immer im Hinterkopf haben sollte. Wenn Sie nähere Erläuterungen zu den hier aufgelisteten Regeln benötigen, können Sie diese im entsprechenden Abschnitt des Buches nachschlagen.
C.1
Checkliste für Administratoren
Dinge, die jeder (System- oder Datenbank-) Administrator beachten sollte: 쮿
Vermeiden Sie Performanceprobleme alleine durch Hardwareaufrüstung zu lösen Oft wird versucht, ein Performanceproblem durch massive Aufrüstung der Hardware zu lösen. Dies kann sehr hilfreich sein, wenn die Hardware – insbesondere der Hauptspeicher – stark unterdimensioniert ist. In den meisten Fällen liegt die Ursache des Problems jedoch an anderer Stelle, sodass die Hardwareaufrüstung lediglich das Symptom bekämpft, ohne die Ursache zu lösen.
쮿
Nutzen Sie separate Plattensysteme zur Lastverteilung Verteilen Sie Betriebssystem, Windows-Auslagerungsdatei, Datenbankdateien, Protokolldateien, TempDB und Backupdateien auf physikalisch getrennte Plattensysteme, um einen parallelen Zugriff zu ermöglichen. Durch die Wahl des passenden RAID-Typs für die verschiedenen Systeme können die jeweiligen Zugriffsmuster optimal unterstützt werden.
쮿
Wählen Sie eine zu Betriebssystem und Hardware passende SQL Server Edition Um einen Server mit mehreren Prozessorkernen und großem Hauptspeicher wirklich ausreizen zu können, benötigen Sie die passende Edition von Windows-Betriebssystem und SQL Server. Für die Nutzung von mehr als 4 GB RAM sind beispielsweise die 64-Bit-Varianten von Betriebssystem und SQL Server notwendig.
SQL Server Performance-Ratgeber
267
2030-4.book Page 268 Monday, February 8, 2010 4:26 PM
C – Checklisten 쮿
Verwenden Sie für jede Datenbank das passende Wiederherstellungsmodell Beim vollständigen Wiederherstellungsmodell werden mehr Protokolldaten gespeichert, um die Datenbank zu einem beliebigen Zeitpunkt wiederherstellen zu können. Das einfache Wiederherstellungsmodell bietet dies nicht, verwaltet dafür die Daten effektiver.
쮿
Dimensionieren Sie Datenbank- und Protokolldateien ausreichend groß Die automatische Vergrößerung der Dateien kostet unnötig Zeit und findet unter Umständen zu einem ungünstigen Zeitpunkt statt. Wählen Sie daher gleich zu Beginn eine passende Größe für beide Dateien.
쮿
Pflegen Sie die Datenbank regelmäßig! Insbesondere Indizes müssen hin und wieder reorganisiert oder am besten neu aufgebaut werden. Ansonsten werden diese nicht effektiv genutzt oder erzeugen zur falschen Zeit unnötige Page Splits. Auch Statistiken müssen regelmäßig aktualisiert werden, damit keine ungünstigen Ausführungspläne erstellt werden. Im Extremfall kann es vorkommen, dass die Statistiken völlig ignoriert werden (und stattdessen mit geschätzten Werten gearbeitet wird), da sie als veraltet betrachtet werden.
쮿
Verlassen Sie sich nicht auf automatische „Pflegefunktionen“! Wenn Sie sich auf automatische Funktionen verlassen, kann es passieren, dass diese zu einem ungünstigen Zeitpunkt ausgeführt werden und den SQL Server zu kritischen Zeiten belasten. Arbeiten Sie mit Wartungsplänen und SQL Server Agent Tasks, um Wartungsaufgaben gezielt nachts oder am Wochenende durchzuführen.
쮿
Nutzen Sie den Resource Governor, um die Ressourcenverteilung zu kontrollieren Wenn Sie SQL Server 2008 verwenden, können Sie Obergrenzen für die Ressourcennutzung der verschiedenen Anwendungen oder auch Anwender definieren.
268
2030-4.book Page 269 Monday, February 8, 2010 4:26 PM
Checkliste für Designer und Architekten
C.2
Checkliste für Designer und Architekten
Dinge, die jeder Datenbankdesigner oder -architekt beachten sollte: 쮿
Nehmen Sie sich viel Zeit für das Datenmodell und normalisieren Sie mit gesundem Menschenverstand. Zu wenig Normalisierung verlangsamt Schreibvorgänge. Zu viel Normalisierung erhöht die Komplexität und kann Lesevorgänge verlangsamen.
쮿
Wählen Sie die richtigen Datentypen und Feldgrößen mit Bedacht. Zu große Felder belegen nicht nur unnötig Speicher, sondern kosten auch Performance, da dann weniger Daten gepuffert werden können.
쮿
Verwenden Sie für technische Primärschlüssel ganzzahlige Datentypen mit Identitätsspezifikation, sofern Sie keine Uniqueidentifier für die lose Kopplung von Daten benötigen.
쮿
Nutzen Sie gruppierte Indizes für Felder, die sich nicht oder nur selten ändern, da dies sonst aufwändige Tabellenreorganisationen zur Folge hätte.
쮿
Definieren Sie nichtgruppierte Indizes für alle Felder, nach denen oft gesucht wird.
쮿
Achten Sie bei kombinierten Indizes auf die richtige Reihenfolge der Spalten, sodass die Spalten, nach denen primär gesucht wird, vorne stehen.
쮿
Versuchen Sie nicht, für jede Abfrage einen abdeckenden Index zu definieren, sondern nur für die Kombinationen, die sehr häufig benötigt werden.
쮿
Setzen Sie gefilterte Indizes und indizierte Sichten gezielt dort ein, wo sie benötigt werden.
쮿
Prüfen Sie die Datenbank in gewissen Zeitabständen mithilfe von dynamischen Management-Sichten oder dem Datenbankoptimierungsratgeber auf fehlende oder überflüssige Indizes.
쮿
Wenn eine Stichwortsuche (insbesondere in größeren Feldern) benötigt wird, sollten Sie statt der Verwendung des LIKE-Operators bei den Abfragen eine Volltextsuche einsetzen.
쮿
Ziehen Sie bei besonders großen Tabellen (ab etwa 50–100 GB) die Verteilung der Daten auf mehrere Tabellen oder – wenn Sie die Enterprise Edition von SQL Server nutzen – die Partitionierung der Daten in Betracht.
SQL Server Performance-Ratgeber
269
2030-4.book Page 270 Monday, February 8, 2010 4:26 PM
C – Checklisten
C.3
Checkliste für Entwickler
Dinge, die jeder (Anwendungs- oder Datenbank-) Entwickler beachten sollte: 쮿
Geben Sie in SQL-Abfragen bei Tabellen stets das zugehörige Schema mit an.
쮿
Geben Sie in SQL-Abfragen bei Spalten stets die zugehörige Tabelle mit an.
쮿
Fragen Sie nur die Daten (Zeilen und Spalten) aus der Datenbank ab, die Sie wirklich benötigen.
쮿
Verwenden Sie Spalten, für die Indizes existieren, in der WHERE-Klausel möglichst isoliert (und somit als gültiges Suchargument), um eine effektive Indexnutzung zu ermöglichen.
쮿
Prüfen Sie im Zweifelsfall den Ausführungsplan der Abfrage, um nach Möglichkeit insbesondere Table Scans und Clustered Index Scans zu vermeiden.
쮿
Vermeiden Sie unnötige Unterabfragen, um dem Optimizer mehr Spielraum für den optimalen Ausführungsplan zu geben. Lässt sich eine Unterabfrage nicht ohne weiteres durch ein anderes Sprachkonstrukt ersetzen, so versuchen Sie zumindest, eine nichtkorrelierte Unterabfrage zu nutzen.
쮿
Verwenden Sie Abfragehinweise und Plan Guides nur in Ausnahmefällen.
쮿
Versuchen Sie ähnliche Anweisungen durch Verwendung von UNION SELECT zusammenzufassen. Datenänderungsaktionen, die vorhandene Daten aktualisieren und nicht vorhandene einfügen sollen, können Sie mithilfe der seit SQL Server 2008 verfügbaren MERGE-Anweisung zusammenfassen.
쮿
Erzeugen Sie aufwändig zu erstellende Zwischenergebnisse in Tabellenvariablen oder als temporäre Tabellen, wenn diese mehrfach benötigt werden.
쮿
Verwenden Sie SQL Cursor nur dann, wenn sie wirklich notwendig sind oder aber sinnvoll sind, um unnötige Leseoperationen zu vermeiden.
쮿
Nutzen Sie explizite Parametrisierung, um für ähnliche Abfragen dieselben Ausführungspläne nutzen zu können.
쮿
Setzen Sie bei länger dauernden Abfragen und Transaktionen eine geeignete Isolationsstufe, um das Risiko von unnötigen Blockierungen und Deadlocks zu minimieren.
쮿
Halten Sie Transaktionen möglichst klein (insbesondere ohne Benutzerinteraktion dazwischen), um das Risiko von unnötigen Blockierungen und Deadlocks zu minimieren.
쮿
Für Datenzugriffe von .NET-Anwendungen bietet die Kombination aus ADO.NET DataReadern und Execute-Anweisungen meist die schnellste Variante. Nutzen Sie ADO.NET DataSets nur dann, wenn die Performance – beispielsweise aufgrund der Datenmenge – zweitrangig ist oder die Verbindung zur Datenbank nicht dauerhaft besteht.
쮿
O/R-Mapper wie LINQ to SQL oder das ADO.NET Entity Framework bieten mehr Komfort beim Zugriff, sind aber meist mit einer schlechteren Performance verbunden. Setzen Sie hierbei Lazy Loading und Eager Loading bewusst ein, um die Performanceeinbußen möglichst gering zu halten.
쮿
Lagern Sie insbesondere aufwändige Abfragen, die nur kleine Ergebnismengen zurückliefern, in gespeicherte Prozeduren aus, um das zwischen Client und Server zu übertragende Datenvolumen möglichst gering zu halten.
270
2030-4.book Page 271 Monday, February 8, 2010 4:26 PM
Checkliste zum Optimieren einer bestehenden Anwendung
C.4
Checkliste zum Optimieren einer bestehenden Anwendung
Dinge, die Sie beim Optimieren einer bestehenden Anwendung beachten sollten: 쮿
Bekämpfen Sie die Krankheit, nicht die Symptome! Meist ist es nicht sinnvoll, ein Performanceproblem durch Aufrüsten von Hardware zu erschlagen. Versuchen Sie stattdessen die Ursache zu finden. Oft liegt diese im Datenmodell oder in der Anwendung begründet, sodass eine Hardwareaufrüstung nur kurzzeitige Abhilfe schaffen würde.
쮿
Ziele setzen! Definieren Sie vor einer Performanceanalyse klare Ziele, die erreicht werden sollen.
쮿
„Sofortmaßnahmen am Unfallort“ Bei gravierenden akuten Problemen können ein paar einfache Sofortmaßnahmen (z.B. das Beenden eines Prozesses oder ein Neustart des SQL Servers) Abhilfe schaffen. Danach sollte trotzdem immer eine ausführlichere Performanceanalyse folgen, da die Problemsymptome schnell wieder auftreten können, solange nicht die eigentliche Ursache beseitigt wurde.
쮿
Analysieren Sie die Probleme ausführlich, bevor Sie mit der Umsetzung von Optimierungsmaßnahmen beginnen.
쮿
Agieren Sie gezielt! Versuchen Sie nicht, alle möglichen Performanceoptimierungen umzusetzen, sondern stattdessen die Stelle zu finden, an der mit dem geringsten Änderungsaufwand der größte Performancegewinn erreicht werden kann.
쮿
Setzen Sie nicht zu viele Optimierungsmaßnahmen gleichzeitig um, sondern teilen Sie dies in mehrere Schritte auf, um dazwischen prüfen zu können, welche Maßnahme den erhofften Erfolg brachte.
쮿
Nutzen Sie den Windows Performance Monitor in Kombination mit dem SQL Server Profiler, um kritische SQL-Abfragen ausfindig zu machen.
쮿
Nutzen Sie den SQL Server Profiler zusammen mit dem Datenbankoptimierungsratgeber (oder alternativ entsprechende dynamische Management-Sichten), um fehlende oder überflüssige Indizes aufzuspüren. Insbesondere die Löschung von scheinbar überflüssigen Indizes sollte aber nie ungeprüft erfolgen!
쮿
Bei den kostenpflichtigen Editionen von SQL Server 2008 kann das Management Data Warehouse genutzt werden, um bei geringer Ressourcenbelastung dauerhaft Informationen zur Systemauslastung und zu besonders aufwändigen SQL-Abfragen zu erhalten. Bei SQL Server 2005 können Sie alternativ das kostenfrei erhältliche SQL Server 2005 Performance Dashboard verwenden.
쮿
Beobachten Sie die Ressourcenauslastung des Servers regelmäßig (auch dann, wenn keine akuten Probleme bestehen), um einerseits ein Gefühl für das Normalverhalten zu bekommen und andererseits um eintretende Änderungen schnell erkennen und darauf reagieren zu können.
SQL Server Performance-Ratgeber
271
2030-4.book Page 272 Monday, February 8, 2010 4:26 PM
2030-4.book Page 273 Monday, February 8, 2010 4:26 PM
D
Glossar
.NET CLR Laufzeitumgebung von .NET, die den vorkompilierten Intermediate Language-Code unmittelbar vor dessen Ausführung in ausführbaren Code übersetzt
Abfragehinweis (engl.: Query Hint) Optimierungshinweis, mit dem die Erzeugung des Ausführungsplans für eine Abfrage beeinflusst werden kann
Ablaufverfolgung Protokoll, das durch den Windows Performance Monitor erstellt wird; kann entweder in Form einer Datei oder einer SQL Server Tabelle existieren
Activity Monitor (siehe SQL Server Activity Monitor)
ADO ActiveX Data Objects Programmierschnittstelle zum Zugriff auf verschiedene Datenbanken unterschiedlicher Anbieter
ADO.NET ADO for .NET Framework .NET-Variante von ADO, die eigentlich eher eine Neuentwicklung als eine neue Version von ADO darstellt. ADO.NET stellt Klassen zum Zugriff auf relationale Datenbanken, aber auch XML-Strukturen zur Verfügung.
ADO.NET Entity Framework auf ADO.NET aufbauender O/R-Mapper von Microsoft, der seit .NET Framework Service Pack 1 in das Framework integriert ist
SQL Server Performance-Ratgeber
273
2030-4.book Page 274 Monday, February 8, 2010 4:26 PM
D – Glossar
Algebrizer Komponente der SQL Server Engine, die auf Basis eines groben Ausführungsbaumes und unter Berücksichtigung vorhandener Datenstrukturen einen Syntaxbaum erstellt
Ausführungsbaum Vorstufe des Ausführungsplans, die vom Parser erstellt und anschließend vom Algebrizer weiterverarbeitet wird
Ausführungsplan Plan, der festlegt, wie für die Ausführung einer SQL-Abfrage auf die einzelnen Tabellen, Indizes etc. zugegriffen wird
Auslagerungsdatei versteckte Datei, die vom Windows-Betriebssystem genutzt wird, um bei Bedarf virtuellen Arbeitsspeicher bereitzustellen
AWE Address Windowing Extensions Windows-Erweiterung zur Unterstützung von mehr als 4 GB Hauptspeicher auf 32-BitSystemen
B-Baum vollständig balancierte Baumstruktur, die zur Speicherung von Indizes verwendet wird, um eine effektive Suche zu ermöglichen
BLOB/LOB (Binary) Large Object Bezeichnung für große (binäre) Objekte, bei SQL Server werden hierfür die Datentypen varbinary(max) oder image verwendet
CLR Common Language Runtime (siehe .NET CLR)
Clustergröße kleinste Speichereinheit, die von einem Datenträger am Stück gelesen wird; bei den aktuellen Dateisystemen ist diese bei der Formatierung der Partition einstellbar
Connection Pooling Variante des Datenzugriffs, bei der der Verwaltungs-Overhead vieler einzelner Verbindungen durch Zusammenfassen derselben eingespart wird
274
2030-4.book Page 275 Monday, February 8, 2010 4:26 PM
CTE Common Table Expression mit SQL Server 2005 eingeführtes SQL-Konstrukt, das es ermöglicht, Abfragen zu definieren, die in einer weiteren Abfrage weiterverwendet werden; kann in Kombination mit UNION SELECT genutzt werden, um rekursive Abfragen zu definieren
Cursor SQL-Sprachkonstrukt, das eine zeilenweise Verarbeitung der Daten aus einer SELECTAbfrage ermöglicht
Datenbankoptimierungsratgeber (engl.: Database Tuning Advisor) mit SQL Server (Enterprise Edition) ausgeliefertes Tool, das SQL-Skripts oder SQL Server Profiler Logs analysiert und anschließend Empfehlungen für zu erstellende oder löschende Indizes sowie für die Partitionierung von großen Tabellen gibt
DBCC Database Console Commands (früher auch als Database Consistency Checker bekannt) stellt eine Reihe von T-SQL-Befehlen zur Administration von SQL Server zur Verfügung
Deadlock Situation, in denen zwei Prozesse sich gegenseitig sperren und daher endlos warten würden; SQL Server erkennt solche Situationen und bricht einen der beiden Prozesse ab, damit zumindest der andere Prozess weiterlaufen kann
DMV Dynamic Management View (siehe dynamische Management-Sichten)
Dynamische Management-Sichten vordefinierter Satz von Systemsichten (und -prozeduren), mit denen SQL Server-interne Daten abgefragt werden können
Eager Loading von vielen O/R-Mappern unterstützter Ansatz, bei dem später benötigte Daten bereits im Voraus geladen werden
Entity Framework (siehe ADO.NET Entity Framework)
SQL Server Performance-Ratgeber
275
2030-4.book Page 276 Monday, February 8, 2010 4:26 PM
D – Glossar
eSQL Entity SQL auf der SQL-Syntax basierende Abfragesprache, die in Verbindung mit dem ADO.NET Entity Framework verwendet werden kann
Extent 64 KB große Speichereinheit (bestehend aus jeweils acht Speicherseiten), die in einem Stück vom physikalischen Datenträger gelesen wird
FileStream mit SQL Server 2008 eingeführte Technologie, die es ermöglicht, große Dateien außerhalb der Datenbankfiles zu speichern, diese aber trotzdem komplett durch die Datenbank zu verwalten
Fragmentierung bezeichnet eine ungeordnete und lückenhafte Anordnung von Daten; wird sowohl in Bezug auf Festplatten, aber auch bei Indizes verwendet, wenn diese durch viele Löschund Einfügeaktionen ihre geordnete Struktur nach und nach verlieren
Füllfaktor prozentuale Angabe des Platzes, der in einer Speicherseite normalerweise belegt wird, der Rest wird für neu hinzugefügte Einträge genutzt, bis der ursprüngliche Füllfaktor durch eine Reorganisation des Indexes wiederhergestellt wird
GUID Globally Unique Identifier global eindeutige Zahl, die aus 16 Bytes (128 Bit) besteht, die in hexadezimaler Form dargestellt werden
Indexoptimierungsratgeber (siehe Datenbankoptimierungsratgeber)
Indexstatistiken Statistiken, die Auskunft über die Zeilenanzahl und Verteilung der Werte von indizierten Feldern geben
Isolationsstufe (engl.: Isolation Level) steuert das Sperrverhalten innerhalb einer einzelnen SQL-Anweisung oder Transaktion; kann durch entsprechende Anweisungen oder Sperrhinweise explizit gesetzt werden
276
2030-4.book Page 277 Monday, February 8, 2010 4:26 PM
Lazy Loading von vielen O/R-Mappern unterstützter Ansatz, bei dem die Daten erst dann geladen werden, wenn sie wirklich benötigt werden
Leistungsindikator Messwerte, die mit dem Windows Performance Monitor in einer Ablaufverfolgung aufgezeichnet werden können
LINQ Language Integrated Query Abfragesprache, die den typsicheren Zugriff auf Objekte ermöglicht; existiert in verschiedenen Ausprägungen für unterschiedliche Objekttypen
LINQ to Entities LINQ-Implementierung, die in Verbindung mit dem Entity Framework verwendet wird
LINQ to SQL LINQ-Implementierung zum Zugriff auf SQL-Datenbanken, beinhaltet auch einen einfachen O/R-Mapper, der die Daten in Form von Objekten zur Verfügung stellt
Management Data Warehouse mit SQL Server 2008 eingeführtes Konzept, bei dem Daten aus verschiedenen Quellen (dynamischen Management-Sichten, PerfMon-Leistungsindikatoren etc.) in einer relationalen Datenbank abgelegt werden, um auf dieser beispielsweise Abfragen zur Performanceanalyse erstellen zu können; dazu existieren vordefinierte Standard-Berichte, mit denen die Daten des Management Data Warehouse übersichtlich dargestellt werden können
MARS Multiple Active Result Sets mit SQL Server 2005 eingeführte Option, mit der innerhalb einer Verbindung mehrere SqlCommands gleichzeitig offen gehalten werden können
MAXDOP Maximum Degree of Parallelism Wert, der steuert, wie viele Prozessorkerne maximal gleichzeitig verwendet werden können
MDAC Microsoft Data Access Components
SQL Server Performance-Ratgeber
277
2030-4.book Page 278 Monday, February 8, 2010 4:26 PM
D – Glossar
Bibliothek zum Datenzugriff, die für nichtverwalteten Code in Kombination mit ADO, ODBC oder OLE DB verwendet werden kann
Named Pipes Netzwerkprotokoll, das benannte FIFO-Datenströme zur bidirektionalen Kommunikation zwischen verschiedenen Rechnern nutzt
Normalisierung Aufteilung von Daten auf mehrere Tabellen, um redundante Datenhaltung zu vermeiden; dazu sind mehrere Normalformen definiert, die dies genau beschreiben; in der Praxis kann manchmal auch eine gezielte Denormalisierung sinnvoll sein, wenn Daten stets gemeinsam abgefragt und nur selten geändert werden
ODBC Open Database Connectivity universelle Programmierschnittstelle, mit der sich verschiedenste Datenquellen (relationale Datenbanken, CSV-Dateien, Excel-Sheets etc.) ansprechen lassen; mittlerweile weitgehend durch OLE DB abgelöst
OLAP Online Analytical Processing Art der Datenbanknutzung, bei der große Mengen von Daten in möglichst kurzer Zeit abgefragt werden und dazu oft in denormalisierter Form in so genannten Data Warehouses vorliegen
OLE DB Object Linking and Embedding (OLE) DB (for database) universelle Programmierschnittstelle, mit der sich verschiedenste Datenquellen (relationale Datenbanken, CSV-Dateien, Excel-Sheets etc.) ansprechen lassen; ersetzt das mittlerweile veraltete ODBC
OLTP Online Transaction Processing Art der Datenbanknutzung, bei der eher kleine Mengen von Daten in Transaktionen gezielt abgefragt oder bearbeitet werden
Optimierer (engl.: Optimizer) wird von SQL Server verwendet, um für SQL-Abfragen den vermeintlich bestmöglichen Ausführungsplan zu ermitteln
278
2030-4.book Page 279 Monday, February 8, 2010 4:26 PM
O/R-Mapper Entwicklertool, das eine Zugriffsschicht zwischen objektorientierten Klassen und relationalen Datenbanken generiert
Page Split Effekt, der auftritt, wenn Daten in eine Speicherseite eingefügt werden sollen, die nicht mehr über genügend freien Platz verfügt; die Seite wird dann in zwei Speicherseiten aufgeteilt, sodass wieder genügend Platz vorhanden ist
Pagefile (siehe Auslagerungsdatei)
PAL Performance Analysis of Logs Tool zur Analyse von Windows Performance Monitor-Protokolldateien, das diese anhand von vordefinierten (aber anpassbaren) Schwellwerten auf kritische Werte hin untersucht und darauf einen Bericht erzeugt
Parser Komponente der SQL Server Engine, die nach einer Syntaxprüfung einer SQL-Abfrage einen logischen Ausführungsbaum dazu erstellt, der dann vom Algebrizer weiterverarbeitet wird
Partitionierung Feature der Enterprise Edition von SQL Server, das eine Aufteilung der Daten einer Tabelle (oder eines Indexes) auf mehrere Dateigruppen erlaubt und damit die Verteilung der Daten auf mehrere physikalische Platten ermöglicht; zur Entscheidung, welche Datensätze in welcher Partition abgelegt werden, wird eine Partitionierungsfunktion mit dazugehörendem Partitionierungsschema genutzt
PASS Professional Association for SQL Server weltweite SQL Server Community mit nationalen und regionalen Untergruppen (die Mitgliedschaft ist kostenlos und jedem zu empfehlen, der sich intensiv mit Microsoft SQL Server auseinandersetzt)
PerfMon (siehe Windows Performance Monitor)
SQL Server Performance-Ratgeber
279
2030-4.book Page 280 Monday, February 8, 2010 4:26 PM
D – Glossar
Performance Data Warehouse (siehe Management Data Warehouse)
Plancache Bereich im Hauptspeicher, in dem fertige Ausführungspläne zur Wiederverwendung gespeichert werden
Plan Guide mit SQL Server 2005 eingeführtes Konstrukt, mit dem Abfragehinweise für Abfragen mit einem bestimmten Abfragetext definiert werden können; somit lassen sich auch für Abfragen, auf die man keinen direkten Zugriff hat, Hinweise festlegen
RAID Redundant Array of Independent DisksTechnologie, die durch den Einsatz von mehreren einfachen Festplatten eine erhöhte Datensicherheit und/oder Performance erreicht. RAID ist in verschiedenen Varianten definiert. Angefangen vom einfachen Spiegeln (Mirroring) oder Aneinanderhängen (Striping) von Platten bis hin zu komplexeren Systemen mit mehreren Platten, die sowohl eine erhöhte Ausfallsicherheit als auch bessere Performance garantieren.
Resource Governor mit SQL Server 2008 eingeführte Funktionalität, die es ermöglicht, die Ressourcen für einzelne Benutzer oder Anwendungen zu begrenzen
RPC Remote Procedure Call Technik zum Aufruf einer Funktion von einem anderen Prozess; im Zusammenhang mit SQL Server meist zum Aufruf von gespeicherten Prozeduren verwendet
SAN Storage Area Network Netzwerk zur Anbindung von Festplattensystemen an Server; ermöglicht eine transparente Zuordnung von (evtl. verteilt liegenden) Plattenkapazitäten an einzelne Server
Schema logische Gruppierung von Datenbankobjekten mithilfe eines Schemanamens
Shared Memory Kommunikationsprotokoll, bei dem zwei Prozesse einen gemeinsamen Speicherbereich nutzen, um miteinander zu kommunizieren
280
2030-4.book Page 281 Monday, February 8, 2010 4:26 PM
SNAC SQL Native Client seit SQL Server 2005 verfügbare Bibliothek zum nativen Zugriff auf SQL Server über ADO, ODBC oder OLE DB
Speicherseite 8 KB große Speichereinheit, aus der Daten und Indizes aufgebaut sind; jeweils acht dieser Speicherseiten werden zu einem Extent zusammengefasst; das Lesen von Daten aus dem Cache erfolgt seitenweise, von der Festplatte dagegen in Extents
Sperrhinweis (engl.: Locking Hint) Optimierungshinweis, mit dem das Sperrverhalten einer Abfrage gesteuert werden kann
SQL Cursor (siehe Cursor)
SQL Server 2005 Performance Dashboard kostenfrei erhältliches AddOn für SQL Server 2005, das komfortable Berichte zur Auswertung der dynamischen Management-Sichten zur Verfügung stellt
SQL Server Activity Monitor in das SQL Server Management Studio integriertes Tool zur Analyse der aktuellen Aktivität des SQL Servers
SQL Server Agent Komponente von SQL Server zur zeitgesteuerten Ausführung von Jobs, die beispielsweise SQL-Skripts beinhalten
SQL Server Enterprise Manager grafische Benutzeroberfläche zur Verwaltung von SQL Servern, die bis SQL Server 2000 genutzt wurde
SQL-Server-Konfigurationsmanager ab SQL Server 2005 ausgeliefertes Tool zur Konfiguration der SQL-Server-Dienste und Netzwerkprotokolle
SQL Server Management Studio grafische Benutzeroberfläche zur Verwaltung von SQL Servern, Nachfolger des SQL Server Enterprise Managers, beinhaltet aber zusätzlich einen Abfrageeditor
SQL Server Performance-Ratgeber
281
2030-4.book Page 282 Monday, February 8, 2010 4:26 PM
D – Glossar
SQL Server Profiler ab SQL Server 2005 ausgeliefertes Tool zur Anzeige und Protokollierung von SQLServer-Ereignissen (z.B. Ausführung von SQL-Anweisungen)
SQL Stress kostenfrei erhältliches Test-Tool, das viele Benutzer simuliert, um Belastungstests zu ermöglichen
SQLCMD Kommandozeilenprogramm zum Ausführen von SQL-Anweisungen oder ganzen SQLSkripts
SQLIO kostenfrei von Microsoft erhältliches Tool zur Durchführung von Lasttests für Plattensysteme (benötigt keinen installierten SQL Server)
SQLIOSim kostenfrei von Microsoft erhältliches Tool, das für die Durchführung von Lasttests für Plattensysteme SQL-Server-typische Zugriffsmuster simuliert (ohne dass ein SQL Server installiert sein muss)
SSAS SQL Server Analysis Services mit SQL Server (ab Version 2005) ausgelieferter Serverdienst, mit dem Daten mehrdimensional modelliert und verwaltet werden können
SSD Solid State Disk auf Flash-Speichern basierte „Festplatte“, die aufgrund nicht erforderlicher Mechanik eine höhere Geschwindigkeit als herkömmliche Festplatten erzielt
SSIS SQL Server Integration Services mit SQL Server (ab Version 2005) ausgelieferter Serverdienst, mit dem ETL-Prozesse modelliert und ausgeführt werden können
282
2030-4.book Page 283 Monday, February 8, 2010 4:26 PM
SSRS SQL Server Reporting Services mit SQL Server (ab Version 2005) ausgelieferter Serverdienst, mit dem auf diversen Datenquellen basierende Berichte erstellt und bereitgestellt werden können
Syntaxbaum Ergebnis des Algebrizers, das vom Optimierer weiterverarbeitet wird, um einen Ausführungsplan zu erstellen
Tabellenhinweis (engl.: Table Hint) Optimierungshinweis, mit dem die Art des Zugriffs auf eine Tabelle innerhalb einer Abfrage gesteuert werden kann
Tabellenstatistiken Statistiken, die Auskunft über die Zeilenanzahl und Verteilung der Werte von einzelnen Spalten geben
TCP/IP Transmission Control Protocol/Internet Protocol betriebssystemübergreifende Familie von Netzwerkprotokollen, auf denen das Internet basiert; hat sich aber auch als Standard für viele lokale Netzwerke etabliert
Transaktion Einheit von Anweisungen, die entweder komplett oder gar nicht ausgeführt werden
Trigger Variante einer gespeicherten Prozedur, die automatisch beim Auftreten eines bestimmten Ereignisses ausgeführt wird
T-SQL Transact-SQL Microsofts Variante der Datenbanksprache SQL
Unique Identifier (siehe GUID)
SQL Server Performance-Ratgeber
283
2030-4.book Page 284 Monday, February 8, 2010 4:26 PM
D – Glossar
VIA Netzwerkprotokoll (Virtual Interface Adapter), das eine spezielle Hardware voraussetzt und auf geringe Prozessorlast sowie optimale Netzwerkauslastung ausgelegt ist
Volltextindex spezielle Indexform, die unter Nutzung eines Volltextkataloges effektivere Suchen in Textfeldern ermöglicht
Wiederherstellungsmodell Modus, der steuert, wie viele Daten im Transaktionsprotokoll aufbewahrt werden, um entweder Speicher zu sparen oder aber eine Wiederherstellung zu einem beliebigen Zeitpunkt zu ermöglichen; zur Auswahl stehen das einfache, das massenprotokollierte und das vollständige Wiederherstellungsmodell
Windows Performance Monitor Tool, das mit dem Windows-Betriebssystem ausgeliefert wird und die Anzeige und Protokollierung von zahlreichen Leistungsindikatoren ermöglicht
Windows Task-Manager Bestandteil des Windows-Betriebssystems, das unter anderem eine schnelle Übersicht über die Auslastung der wichtigsten Systemressourcen bietet
284
2030-4.book Page 285 Monday, February 8, 2010 4:26 PM
Stichwortverzeichnis Symbole .NET 75 .NET CLR 209, 273 .NET Framework 209 .netTiers 207
A abdeckender Index 87, 96, 269 Abfrage Parametrisierung 129, 162 Abfrageeditor 91 Abfragehinweis 158, 270, 273 FAST 160 FASTFIRSTROW 160 MAXDOP 159 OPTIMIZE FOR 159 PARAMETERIZATION FORCED 159 PARAMETERIZATION SIMPLE 159 Abfragekontrolle 42 Abfrageoptimierung 145 Abfragestatistik 120, 243 Abfragewartezeit 43 Ablaufverfolgung 231, 273 Eigenschaften 231 Ereignisse 231 Spaltenfilter 233 ActiveX Data Objects 179, 273 Activity Monitor 220, 258, 273 Aktuelle wertvolle Abfragen 221 Datendatei-E/A 221 Prozesse 221 Prozessinfo 220 Ressourcenwartevorgänge 221 Sperren nach Objekt 220 Sperren nach Prozess 220 Übersicht 221 Address Windowing Extensions 274 Address Windowing Extensions siehe AWE ADO 273 ADO for .NET Framework 273 ADO siehe ActiveX Data Objects
SQL Server Performance-Ratgeber
ADO.NET 182, 185, 250, 270, 273 BeginExecuteReader 189 CommandBuilder 192 DataAdapter 190 DataReader 185, 270 DataSet 190, 270 EndExecuteReader 189 Execute 185 ExecuteNonQuery 190 ExecuteReader 189 ExecuteScalar 190 SqlCommand 186 ADO.NET Entity Framework 199, 270, 273, 275 Aktuelle wertvolle Abfragen 221 Algebrizer 115, 274 Alphanumerische Datentypen 76 ANTS Memory Profiler 263 ANTS Performance Profiler 263 Arbeitsauslastungsgruppe 62 Arbeitsspeicher 40 Archivierung 103, 250 ASCII 75 asynchrone Zugriffe 188 Aufbewahrungszeit 243 Auffüllungszeitplan 101 Auflist-Intervall 243 Auflistsatz 240 Abfragestatistik 243 Datenträgerverwendung 243 Serveraktivität 243 Auflistungstyp 246 Ausführungsbaum 115, 274 Ausführungsplan 91, 116, 156, 270, 274 geschätzt 116 tatsächlich 116 Wiederverwendung 126 Auslagerungsdatei 25, 257, 274, 279 Auslastung CPU 59 Netzwerk 59 AWE 40, 274
285
2030-4.book Page 286 Monday, February 8, 2010 4:26 PM
Stichwortverzeichnis
B Backup siehe Sicherung Backupdatei 25 B-Baum 84, 274 BEGIN TRANSACTION 163 BeginExecuteReader 189 Benchmark Factory for SQL Server 262 Betriebssystem 25, 33 Auswahl 33 Dienste 34 Konfiguration 34 Windows 2003 Server 33 Windows 2008 Server 33 Windows 7 33 Windows Essential Business Server 2008 33 Windows Server 2008 R2 34 Windows Small Business Server 2008 33 Windows XP 33 bigint 78 Binäre Datentypen 78 binary 78 Binary Large Object 274 bit 77 BLOB 274 Blockgröße 31 Blockierung 270 Blocking 133
C Cache CPU-Cache 24 Plattencache 31 Cache-Verzeichnis 243 Caching 25 CASE WHEN 164 char 77 clientseitige Cursor 179 CLR 274 CLR siehe Common Language Runtime Clustered Index Scan 84, 118 Clustered Index Seek 118 Clustergröße 31, 274 Codepage 75 CodePlex 266 Collection 240, 246 Collection Set 240, 246 CommandBuilder 192
286
Commit 42 COMMIT TRANSACTION 163 Common Language Runtime 209, 274 Common Table Expression 172, 275 Connection Pooling 179, 274 CONTAINS 98, 100 CONTAINSTABLE 100 CPU 23, 249 Auslastung 59 CPU-Cache 24 Mehrprozessorsystem 24 Taktfrequenz 23 CPU-Auslastung 59 CPU-Leistung 219 CTE 275 Cursor 42, 169, 270, 275, 281 DYNAMIC 175 FAST_FORWARD 175 FORWARD_ONLY 175 KEYSET 175 Optimierung 173 OPTIMISTIC 175 READ_ONLY 175 SCROLL_LOCKS 175 STATIC 175 Cursorschwellwert 44
D Data Source Name 180 DataAdapter 190 Database Consistency Checker 275 Database Console Commands 275 Database Tuning Advisor 236, 275 DataReader 185, 270 DataSet 190, 270 Datei 47 Dateigruppe 47, 103 PRIMARY 47 Datenauflistung 240, 242 benutzerdefiniert 245 Datenbank anlegen 47 konfigurieren 47 Datenbank verkleinern 52, 54 Datenbankdatei 25, 47–48 Datenbankeinstellungen 42 Datenbankintegrität 52 Datenbank-Management-System 35
2030-4.book Page 287 Monday, February 8, 2010 4:26 PM
Stichwortverzeichnis
Datenbankoptimierungsratgeber 36, 91, 95, 218, 230, 235–236, 249, 258, 269, 271, 275– 276 Datenbankoptionen automatisch schließen 50 Statistiken automatisch aktualisieren 51 Statistiken automatisch asynchron aktualisieren 51 Statistiken automatisch erstellen 50 Datenbankschema 146 Datenbanksicherung 52 Datenbankstatistiken 52 Datendatei-E/A 221 Datenmodellierung 69, 250 Datenobjekt 226 Datenprovider 240 Datenquellenname 180 Datenseite 84 Datenträgerverwendung 243 Datentyp 74, 250, 269 alphanumerisch 76 bigint 78 binär 78 binary 78 bit 77 char 77 decimal 78 float 78 geography 78 geometry 78 image 78 int 78 money 78 nchar 74, 77 ntext 77 numeric 78 numerisch 77 nvarchar 77 nvarchar(max) 75, 77 real 78 smallint 77 smallmoney 78 text 77 tinyint 77 uniqueidentifier 78 varbinary 78 varbinary(max) 78 varchar 77 varchar(max) 75, 77
SQL Server Performance-Ratgeber
variable Länge 77 xml 78 Datenzugriffstechnologien 179 DBCC 54, 275 DROPCLEANBUFFERS 122 FREEPROCCACHE 127 INDEXDEFRAG 57 REINDEX 57 SHOW_STATISTICS 124 SHRINKDATABASE 54 Deadlock 140, 270, 275 Deadlock Graph 141 Deadlock-Opfer 142 decimal 78 Denormalisierung 69, 72 dm_db_index_physical_stats 55 DML-Trigger 106 DMV 92, 275 dm_db_index_physical_stats 55 sys.dm_exec_cached_plans 126 sys.dm_exec_query_plan 127 sys.dm_exec_query_stats 127 sys.dm_exec_sql_text 127 sys.dm_tran_session_transactions 137 DROPCLEANBUFFERS 122 DSN 180 Dynamic AWE 36 Dynamic Management View 92, 275 dynamische Management-Sichten 91–92, 217, 225, 228, 239, 258, 260, 269, 271, 275
E E/A-Affinität 41 Eager Loading 197, 204, 270, 275 EndExecuteReader 189 Enterprise Manager 220 Entity Framework 199, 270, 275 Entity SQL 205, 276 entwickler.press 265 eSQL 276 eSQL siehe Entity SQL Execute 185 ExecuteNonQuery 190 ExecuteReader 189 ExecuteScalar 190 explizite Parametrisierung 187, 201, 270 Extent 276
287
2030-4.book Page 288 Monday, February 8, 2010 4:26 PM
Stichwortverzeichnis
F
I
Festplatte 25 Blockgröße 31 Cache 31 Clustergröße 31 Fragmentierung 32 IDE 31 SATA 31 SCSI 31 SSD 31 Festplattenaktivität 220 FileStream 78, 276 float 78 Foglight for SQL Server 262 Foglight Performance Analysis for SQL Server 262 Foren 266 Fragmentierung 32, 85, 97, 276 FREEPROCCACHE 127 FREETEXT 100 FREETEXTTABLE 100 Fremdschlüssel 96 Fremdschlüsselspalte 90 Füllfaktor 57, 85, 276 Funktionen 208
Identitätsspezifikation 79–80, 104, 269 IDENTITY 80 IDENTITY_INSERT 104 image 78 implizite Transaktionen 42 INCLUDE 96 Index 54, 250 abdeckend 87, 96, 269 Fragmentierung 85 Füllfaktor 57 gefiltert 87, 269 gruppiert 84, 97, 269 Include-Spalten 87 indizierte Sicht 88 kombiniert 86, 269 nichtgruppiert 84, 269 Online 252 REBUILD 54, 86, 95 REORGANIZE 54 Volltextindex 98 Index Scan 83, 86–87, 118 Index Seek 83, 86–87, 118 Indexdateigruppe 101 Indexfragmentierung 85 Indexhinweise 91 Indexoptimierungsratgeber 236, 276 Indexstatistiken 50, 276 Indexverwendung 150 Indexwartung 52 Indizes 83 Indizierte Sicht 88 Informationsquellen 265 Installation 37 int 78 Internet Information Service 34 Isolation Level 137, 276 Isolationsstufe 137, 270, 276 READ COMMITTED 138 READ UNCOMMITTED 138 REPEATABLE READ 138 SERIALIZABLE 138 SNAPSHOT 138 Isolationsstufen 250
G Gefilterter Index 87, 269 geography 78 geometry 78 geschätzter Ausführungsplan 116 gespeicherte Prozedur 250, 270 gespeicherte Prozeduren 208 Globally Unique Identifier 276 gruppierter Index 84, 97, 269 GUID 276, 283
H Hardware 250–251 Hardwareaufrüstung 267, 271 Hardwareumgebung 23 Hauptspeicher 219, 249, 251, 267 Hauptspeicher siehe RAM Heap 84 Historisierung 106 HOLDLOCK 139
288
J JOIN 153
2030-4.book Page 289 Monday, February 8, 2010 4:26 PM
Stichwortverzeichnis
K Klassifizierungsfunktion 64 Kombinierter Index 86, 269 Komprimierung 52 Konfiguration 38
L Language Integrated Query 193, 277 Large Object 274 last_user_lookup 93 last_user_scan 93 last_user_seek 93 Lastverteilung 267 Lazy Loading 195, 202, 270, 277 Leistungsindikator 225, 277 Leistungsindikatorenprotokoll 227 Lesevorgänge logisch 121 physisch 121 Read-Ahead 121 LIKE 73, 99 LINQ 277 LINQ to Entities 199, 277 Eager Loading 204 Lazy Loading 202 LINQ to SQL 193, 270, 277 Eager Loading 197 Lazy Loading 195 Verzögertes Laden 197 LiteSpeed for SQL Server 262 LOB 274 LOB-Datenseiten 122 Locking 133 Locking Hint 157, 281 logische Lesevorgänge 121 logische LOB-Lesevorgänge 122
M Management Data Warehouse 239, 258, 271, 277, 280 Datenbankschema 242 MARS 277 MARS siehe Multiple Active Result Sets Max Worker Threads 41 MAXDOP 43, 277 Maximale Arbeitsthreadanzahl 41 Maximum Degree of Parallelism 277
SQL Server Performance-Ratgeber
Maximum Degree of Parallelism siehe MAXDOP MDAC 277 MDAC über ADO 180 MDAC über ODBC 180 MDAC über OLE DB 180 MDAC siehe Microsoft Data Access Components Memory Management 179 MERGE 164, 270 Microsoft Data Access Components 179, 277 Missing Index Report 93 money 78 Multiple Active Result Sets 190, 277
N Named Pipes 278 nchar 74, 77 Network Packet Size 183 Netzwerk 32 Auslastung 59 Duplex-Modus 32 Verbindungsgeschwindigkeit 32 Netzwerkadapter 257 Netzwerkauslastung 59 Netzwerklast 219 Netzwerkpaketgröße 43, 183 Netzwerkprotokoll 38 Named Pipes 38 Shared Memory 38 TCP/IP 38 newid 80 Newsgroups 266 nHibernate 207 nichtgruppierter Index 269 Noise Words 98, 101 NOLOCK 139–140 Normalformen 70 1. Normalform 70 2. Normalform 71 3. Normalform 71 Normalisierung 69, 269, 278 ntext 77 NULL 79 numeric 78 Numerische Datentypen 77 nvarchar 77 nvarchar(max) 75, 77
289
2030-4.book Page 290 Monday, February 8, 2010 4:26 PM
Stichwortverzeichnis
O O/R-Mapper 207, 270, 279 .netTiers 207 nHibernate 207 SubSonic 207 Telerik OpenAccess 207 Object Linking and Embedding DB 179, 278 OBJECTPROPERTY 56 Objekt-Explorer 241 ODBC 278 ODBC siehe Open Database Connectivity ODBC-Datenquelle 180 OLAP 278 OLE DB 278 OLE DB siehe Object Linking and Embedding DB OLTP 278 Online Analytical Processing 278 Online Transaction Processing 278 Online-Indizierung 36, 252 Open Database Connectivity 179, 278 Optimierer 115, 278 Optimierung abfrageübergreifend 163 Optimizer 115, 270, 278 OPTION 158
Partitionierungsstrategie 236 PASS 279 PERFMON 59 PerfMon 279 Performance Analysis of Logs 261, 279 Performance Counter 225 Performance Data Collector 240 Performance Data Warehouse 239, 280 Performanceanalyse 215 Performancetest 225 physische Lesevorgänge 121 physische LOB-Lesevorgänge 122 Plan Guide 160, 270, 280 Plancache 115, 280 Plattensysteme 267 Primärschlüssel 96, 269 Priority Boost 41 Professional Association for SQL Server 279 Profiler Trace 91 Protokolldatei 25, 47 Prozesse 221 Prozessinfo 220 Prozessor 251, 267 Prozessor siehe CPU Prozessoraffinität 41
Q P Page Split 85, 97, 268, 279 PageDefrag 32 Pagefile 34, 279 PAGLOCK 140 Paketgröße 183 PAL 261, 279 Parallele Indexoperationen 36 Parallelität 43 Parametrisierung 129, 162, 187, 201 automatisch 129 durch gespeicherte Prozeduren 132 explizit 131, 187, 201, 270 Parser 115, 279 Partitionen 48, 110 entfernen 111 hinzufügen 111 verschieben 111 verwenden 110 Partitionierung 36, 108, 236, 250, 269, 279 Partitionierungsfunktion 48, 108–109 Partitionierungsschema 108–109
290
Query Hint 158, 273 Quest Capacity Manage for SQL Server 262 Quest Software 262
R RAID 26, 267, 280 Hardware-RAID 26 Mirroring 27 Software-RAID 26 Striping 26 RAID 0 26 RAID 01 29 RAID 1 27 RAID 10 29 RAID 5 28 RAM 24 Random Access Memory siehe RAM READ COMMITTED 138 READ UNCOMMITTED 138 Read-Ahead-Lesevorgänge 121 Read-Ahead-LOB-Lesevorgänge 122 READCOMMITTED 139
2030-4.book Page 291 Monday, February 8, 2010 4:26 PM
Stichwortverzeichnis
READCOMMITTEDLOCK 139 READUNCOMMITTED 139 real 78 REBUILD 54, 86, 95 Redgate 263 Redundant Array of Independent Disks 280 Redundant Array of Independent Disks siehe RAID Redundant Array of Inexpensive Disks 280 Redundant Array of Inexpensive Disks siehe RAID Remote Procedure Call 280 Remote Procedure Calls 42 Remoteabfragen Timeout 42 Remoteanmeldung 43 REORGANIZE 54 REPEATABLE READ 138 REPEATABLEREAD 139 Resource Governor 36, 61, 268, 280 Ressourcen 249 Ressourcenkontrolle 61 Ressourcenpool 62 Systemressourcenpools 62 Ressourcenwartevorgänge 221 ROLLBACK TRANSACTION 163 Row Lookup 83, 87, 118 ROWLOCK 140 RPC 280
S SAN 30, 280 SARG 151 Scananzahl 121 Schema 280 SERIALIZABLE 138–139 Serveraktivität 243 Serverarbeitsspeicher 40 Servereigenschaften 40 Serverinstanz 146 SET DEADLOCK_PRIORITY 141 SET STATISTICS IO ON 121 SET STATISTICS TIME ON 121 SET TRANSACTION ISOLATION LEVEL 138 Shared Memory 280 SHOW_STATISTICS 124 Sicherung 57 differenzielle Sicherung 57
SQL Server Performance-Ratgeber
komprimieren 43 Transaktionsprotokollsicherung 57 Vollsicherung 57 Sichten 208 smallint 77 smallmoney 78 SNAC 281 SNAC siehe SQL Native Client SNAPSHOT 138 Solid State Disk 282 Solid State Disk siehe SSD Sortierreihenfolge 75 sp_create_plan_guide 160 Sparse Columns 79 Speicherseite 84, 281 Sperre 163 Sperren 43, 133, 250 Sperren nach Objekt 220 Sperren nach Prozess 220 Sperrhinweis 157, 281 Spotlight on SQL Server 262 SQL 250 SQL Backup Pro 263 SQL Cursor 270, 281 SQL Full-text Filter Daemon Launcher 99 SQL Merge 36 SQL Native Client 39, 181, 281 SQL Response 263 SQL Server Konfiguration 38 SQL Server 2005 Performance Dashboard 93, 260, 271, 281 SQL Server Activity Monitor 217, 273, 281 SQL Server Agent 52, 240, 281 SQL Server Analysis Services 37, 282 SQL Server Edition 33, 267 Auswahl 35 Compact Edition 36 Developer Edition 36 Enterprise Edition 36 Express Edition 36 Standard Edition 36 Web Edition 36 Workgroup Edition 36 SQL Server Enterprise Manager 220, 281 SQL Server Express Edition 230 SQL Server Integration Services 37, 240, 282 SQL Server Management Studio 91, 217, 220, 258, 281
291
2030-4.book Page 292 Monday, February 8, 2010 4:26 PM
Stichwortverzeichnis
SQL Server Performance 265 SQL Server Profiler 36, 143, 217, 230–231, 239, 249, 258, 271, 282 SQL Server Reporting Services 34, 37, 240, 283 SQL Stress 260, 282 SQLCMD 53, 282 SqlCommand 186 SQL-Cursor 169 SQLIO 259, 282 SQLIOSim 259, 282 SQLIOStress 260 SQLServerFDHostUser 99 SQL-Server-Konfiguration 250 SQL-Server-Konfigurationsmanager 37, 281 SQL-Server-Priorität 41 SSAS 282 SSD 31, 282 SSIS 282 SSRS 283 Standardauflistsatz 243 Standardfüllfaktor 42 Standardverbindungsoptionen 42 Statistiken 50, 58, 92, 268 Indexstatistiken 50 Tabellenstatistiken 50 Storage Area Network 280 Storage Area Network siehe SAN SubSonic 207 Suchargument 151, 270 Syntaxbaum 115, 283 sys.dm_exec_cached_plans 126 sys.dm_exec_query_plan 127 sys.dm_exec_query_stats 127 sys.dm_exec_sql_text 127 sys.dm_tran_session_transactions 137 Sysinternals 32 Systemauslastung 58 Systemleistung 257 Systemmonitor 225 Systemressourcen 219 Systemumgebung 23
T Tabellenhinweis 139, 157, 283 FORCESEEK 157 HOLDLOCK 139 IGNORE_CONSTRAINTS 158 IGNORE_TRIGGERS 158
292
INDEX 157 NOEXPAND 158 NOLOCK 139–140 NOWAIT 158 PAGLOCK 140 READCOMMITTED 139 READCOMMITTEDLOCK 139 READUNCOMMITTED 139 REPEATABLEREAD 139 ROWLOCK 140 SERIALIZABLE 139 TABLOCK 140 TABLOCKX 140 UPDLOCK 140 XLOCK 140 Tabellenstatistiken 50, 123, 283 Tabellenvariable 169, 270 Table Hint 157, 283 Table Scan 83, 87, 117 TABLOCK 140 TABLOCKX 140 tatsächlicher Ausführungsplan 116 TCP/IP 283 Telerik OpenAccess 207 TempDB 25 temporäre Tabelle 167, 270 Testumgebung 216 text 77 Timeout für Remoteabfragen 42 tinyint 77 Toad for SQL Server 262 Transact-SQL 283 Transaktion 133–134, 163, 270, 283 explizit 135 geschachtelt 135 implizit 42, 135 verteilt 42, 136 Transaktionsabbruch 42 Transaktionslog siehe Protokolldatei Transmission Control Protocol/Internet Protocol 283 Trigger 106, 283 T-SQL 283 BEGIN TRANSACTION 163 CASE WHEN 164 COMMIT TRANSACTION 163 CONTAINS 98, 100 CONTAINSTABLE 100 FREETEXT 100
2030-4.book Page 293 Monday, February 8, 2010 4:26 PM
Stichwortverzeichnis
FREETEXTTABLE 100 IDENTITY_INSERT 104 JOIN 153 LIKE 73, 99 MERGE 164, 270 newid 80 OPTION 158 ROLLBACK TRANSACTION 163 SET DEADLOCK_PRIORITY 141 SET STATISTICS IO ON 121 SET STATISTICS TIME ON 121 SET TRANSACTION ISOLATION LEVEL 138 UNION SELECT 164, 270 WITH 157
U Unicode 75 UNION SELECT 164, 270 Unique Identifier 78, 80, 269, 283 Unterabfrage 153, 270 korreliert 155 nichtkorreliert 155 UPDLOCK 140 Upload-Intervall 243 UPSERT 165 Upsert siehe SQL Merge user_lookup 93 user_scan 93 user_seek 93
V varbinary 78 VARBINARY(MAX) 100 varbinary(max) 78 varchar 77 varchar(max) 75, 77 Verbindungen 42 Verteilte Transaktionen 42 Verzögertes Laden 197 VIA 284 Voll qualifizierte Bezeichner 145 Volltextindex 98, 284 anlegen 101
SQL Server Performance-Ratgeber
Funktionsweise 98 Verwendung 99 Volltextkatalog 98, 101 Volltext-Stoppliste 101 Volltextsuche 269
W Wartungsarbeiten 52 Wartungsfenster 251 Wartungsplan 52 Websites 265 Wiederherstellungsmodell 48, 268, 284 einfach 48 massenprotokolliert 49 vollständig 48 Windows 2003 Server 33 Windows 2008 Server 33 Windows 7 33 Windows Essential Business Server 2008 33 Windows Performance Monitor 58, 217, 220, 225, 257, 271, 279, 284 Windows Server 2008 R2 34 Windows Small Business Server 2008 33 Windows Task-Manager 58, 217, 219, 257, 284 Windows Vista 33 Windows XP 33 Windows-Auslagerungsdatei 25, 34 Windows-Konfiguration 250 WITH 157 Workload Group siehe Arbeitsauslastungsgruppe Worktable 176
X XLOCK 140 xml 78
Z Zeichensatz ASCII 75 Unicode 75 Zieldefinition 215
293
2030-4.book Page 294 Monday, February 8, 2010 4:26 PM