MS SQL Server 2005 T-SQL Programmierung und Abfragen

Werbung
MS SQL Server 2005
T-SQL Programmierung und Abfragen
Marco Skulschus
Marcus Wiederstein
1
2
MS SQL Server 2005
T-SQL Programmierung und Abfragen
Marco Skulschus
Marcus Wiederstein
Webseite zum Buch: www.comelio-medien.com/dedi3_357.php
© 2006 Comelio Medien
3
Alle Rechte vorbehalten. Das Werk einschließlich aller seiner Teile ist urheberrechtlich
geschützt. Jeder Verwertung außerhalb der engen Grenzen des Urheberrechtsgesetzes ist
ohne Zustimmung des Verlages unzulässig und strafbar. Das gilt insbesondere für die
Vervielfältigung, Übersetzung, Mikroverfilmung und die Einspeicherung und Verbreitung in elektronischen Systemen.
© Comelio GmbH
Rellinghauser Straße 10, D-45128 Essen
Tel (0201) 437517-0, Fax (0201) 437517-10
www.comelio.com, [email protected]
Umschlaggestaltung: Daniel Winter
Umschlag- & Autorenfoto: Daniel Winter
Satz: Daniel Winter
Druck und Bindung: Bookstation GmbH
Hilzingerstr. 14 in 78244 Gottmadingen, www.bookstation.de
Printed in Germany
ISBN 3-939701-02-5 | 978-3-939701-02-6
4
Inhaltsverzeichnis
Vorwort ............................................................................................................. 11
Zu dieser Reihe.............................................................................................. 13
Autoren .......................................................................................................... 14
Aufbau des Buchs.......................................................................................... 15
Beispieldateien............................................................................................... 18
Kontakt zu Autoren und Verlag...................................................................... 18
1
2
Grundlagen............................................................................................... 19
1.1
Installation ........................................................................................ 21
1.2
Erste Schritte .................................................................................... 28
1.2.1
Management Studio ..................................................................... 29
1.2.2
Abfragen direkt ausführen............................................................ 39
1.2.3
Abfragen im Editor ausführen ...................................................... 43
1.2.4
Vorlagen-Editor ............................................................................ 55
1.2.5
Dokumentation ............................................................................. 57
1.3
Programmierbarkeit.......................................................................... 59
1.4
Beispieldatenbank AdventureWorks ................................................ 66
1.4.1
Allgemeine Design-Prinzipien ...................................................... 67
1.4.2
Darstellung einzelner Tabellenbereiche....................................... 71
Einfache Abfragen ................................................................................... 79
2.1
2.1.1
Grundstruktur von SELECT.............................................................. 81
Spaltenauswahl............................................................................ 83
5
2.1.2
Aliasnamen ...................................................................................84
2.1.3
Qualifizierte Spaltennamen ..........................................................86
2.2
2.2.1
Einfache Bedingungen und Operatoren .......................................87
2.2.2
Boolesche Operatoren..................................................................91
2.2.3
Mathematische Operatoren: .........................................................97
2.2.4
Mengen-Operatoren .................................................................. 102
2.3
Duplikate ein-/ausblenden ......................................................... 109
2.3.2
Ergebnisse sortieren.................................................................. 112
2.3.3
Standard-Aggregate .................................................................. 114
2.3.4
Gruppieren................................................................................. 117
2.3.5
Zufällige Datenauswahl ............................................................. 122
Eingebaute Funktionen .................................................................. 124
2.4.1
Datums- und Zeitfunktionen ...................................................... 125
2.4.2
Mathematische Funktionen ....................................................... 131
2.4.3
Zeichenkettenfunktionen ........................................................... 140
2.4.4
Systemfunktionen ...................................................................... 147
Komplexe Abfragen .............................................................................. 151
3.1
6
Ergebnisse aufbereiten .................................................................. 109
2.3.1
2.4
3
Bedingungen .....................................................................................87
Verknüpfungen............................................................................... 153
3.1.1
Manuelle Verknüpfungen........................................................... 153
3.1.2
ANSI-SQL-Verknüpfungen ........................................................ 161
3.2
3.2.1
Einfache Unterabfragen ............................................................. 175
3.2.2
Spaltenunterabfragen................................................................. 180
3.2.3
Abgeleitete Tabellen .................................................................. 182
3.2.4
Korrelierte Unterabfragen .......................................................... 189
3.2.5
Operatoren für Unterabfragen.................................................... 193
3.3
CASE mit Selektor ..................................................................... 198
3.3.2
Selektorlose CASE-Anweisung.................................................. 201
Zusätzliche Aggregate.................................................................... 203
3.4.1
Rangfolgen................................................................................. 204
3.4.2
Untersummen und Würfel .......................................................... 207
Datenmanipulation................................................................................. 219
4.1
Datenstrukturen anlegen ................................................................ 222
4.1.1
Tabellen ..................................................................................... 222
4.1.2
Sichten ....................................................................................... 255
4.2
5
Verzweigungen............................................................................... 197
3.3.1
3.4
4
Unterabfragen................................................................................. 175
Daten bearbeiten ............................................................................ 262
4.2.1
Vorbereitung............................................................................... 262
4.2.2
Einfügen ..................................................................................... 268
4.2.3
Aktualisieren............................................................................... 278
4.2.4
Löschen...................................................................................... 294
Grundlagen T-SQL ................................................................................. 303
5.1
T-SQL Blöcke ................................................................................. 305
7
5.1.1
Variablen und Anweisungen...................................................... 306
5.1.2
Datentypen ................................................................................ 308
5.2
5.2.1
Fallunterscheidungen ................................................................ 316
5.2.2
Schleifen .................................................................................... 318
5.3
Einsatz von EXEC ..................................................................... 321
5.3.2
Einsatz von sp_executesql ........................................................ 324
Fehlerbehandlung .......................................................................... 327
5.4.1
Ausnahmen................................................................................ 327
5.4.2
Traditionelle Fehlerbehandlung ................................................. 337
5.5
Cursor ............................................................................................ 340
5.5.1
Cursor-Varianten ....................................................................... 340
5.5.2
Verwendung............................................................................... 344
5.5.3
Beispiele .................................................................................... 348
5.6
Transaktionen ................................................................................ 360
5.6.1
Einfache Transaktionen............................................................. 361
5.6.2
Sicherungspunkte ...................................................................... 370
5.6.3
Erweiterte Transaktionssteuerung............................................. 372
Analysen................................................................................................. 377
6.1
6.1.1
8
Dynamische Anweisungen............................................................. 320
5.3.1
5.4
6
Kontrollanweisungen...................................................................... 316
Tabellenausdrücke......................................................................... 379
Grundprinzip .............................................................................. 380
6.1.2
Erweiterte Tabellenausdrücke ................................................... 384
6.1.3
Datenmanipulation und CTEs .................................................... 393
6.2
6.2.1
Aggregate mit OVER.................................................................. 396
6.2.2
Akkumulationen und Durchschnitte ........................................... 405
6.2.3
Hitparaden.................................................................................. 412
6.2.4
Bereiche/Quantile....................................................................... 426
6.3
7
Aggregate und Rangfolgen ............................................................ 396
Pivot................................................................................................ 430
6.3.1
Klassisches Pivotieren ............................................................... 431
6.3.2
Einsatz von (UN)PIVOT ............................................................. 435
Programmierbarkeit............................................................................... 449
7.1
Prozeduren ..................................................................................... 451
7.1.1
Einführung.................................................................................. 452
7.1.2
Prozedurarten ............................................................................ 460
7.1.3
Parameter und Aufruf................................................................. 465
7.1.4
Sonderfälle ................................................................................. 474
7.2
Funktionen...................................................................................... 478
7.2.1
Skalare Funktionen .................................................................... 481
7.2.2
Tabellenwertfunktion .................................................................. 486
7.2.3
Optionen..................................................................................... 494
7.2.4
APPLY-Operator ........................................................................ 501
7.3
7.3.1
Verwaltungsarbeiten....................................................................... 504
Katalogsichten für Objekte......................................................... 504
9
7.3.2
Funktionen ................................................................................. 508
7.3.3
Sicherheit................................................................................... 510
Index ............................................................................................................... 517
10
Vorwort
Vorwort
11
Vorwort
12
Vorwort
Vorwort
Herzlich Willkommen zu einem Fachbuch von Comelio Medien, ein Bereich der Comelio GmbH. Wir hoffen sehr, dass Sie mit der Darstellung und Aufbereitung zu den verschiedenen Themengebieten im Bereich MS SQL Server zufrieden sind und die für
Ihren Berufsalltag wesentlichen und hilfreichen Lösungen finden.
Zu dieser Reihe
Diese Buchreihe konzentriert sich auf verschiedene Aspekte der Verwendung des
Microsoft SQL Servers 2005. Dies soll alle Themen umfassen, welche bei der Verwendung als Programmierer und Administrator auftreten. Zusammen genommen sollen alle
geplanten Bücher der Reihe die Themenvielfalt und Einsatzmöglichkeiten des SQL
Servers abdecken. Es ist dabei nicht unbedingt notwendig, alle Bücher zu lesen, weil die
Themen so auf die einzelnen Werke verteilt werden sollen, dass jedes für sich als einzelnes Werk gelten kann; es ist jedoch ratsam.
Die Reihe enthält folgende Themen:
ƒ
Programmierung einer SQL Server Datenbank und Einstieg in den SQL Server mit
der Abfragesprache T-SQL (dieses Buch).
ƒ
Administration der SQL Server-Datenbank unter MS Windows Server 2003.
ƒ
Erzeugen von XML aus relationalen Daten und speichern, verarbeiten von XML
mit Hilfe von T-SQL.
ƒ
Programmierung des MS SQL Servers mit .NET.
ƒ
Nutzung der Bereiche Reporting und Analysis Services sowie Gestaltung von
OLAP- und Data Warehouse-Anwendungen mit SQL Server.
13
Vorwort
Autoren
Die beiden Autoren Marco Skulschus und Marcus Wiederstein blicken bereits auf zahlreiche Bücher zu Themen rund um Programmierung und auch Datenbanken zurück. Bei
der Comelio GmbH sind sie im Bereich Softwareentwicklung als Projektleiter tätig und
stehen mit Hilfe der Veröffentlichungen ihre Erfahrungen im Bereich der Softwareentwicklung einem breiteren Publikum zur Verfügung. Dies geschieht auch im Bereich
Weiterbildung, der von der Comelio GmbH im gesamten deutschsprachigen Raum
angeboten wird.
Marco Skulschus (Jahrgang 1978) studierte Ökonomie in Wuppertal und Paris. Er ist
nach der Beschäftigung mit PHP und Java im Jahre 2004 auf .NET umgestiegen und
setzt nun C# und den MS SQL Server für Kundenprojekte ein. Nichtsdestoweniger ist er
auch in anderen Themenbereichen aktiv, wobei insbesondere der Themenbereich Datenbanken, Datenmodellierung und XML an erster Stelle steht. Seine Spezialthemen
sind Ontologien auf Basis von XML-Standards wie XTM oder OWL. Neben den gemeinsamen Veröffentlichungen mit Marcus Wiederstein ist er Autor von verschiedenen
Büchern zum Einsatz von PHP im Zusammenhang mit Oracle und XML.
Marcus Wiederstein (Jahrgang 1971) studierte Elektrotechnik in Bochum und Dortmund. Er ist schon sehr früh, im Jahre 2002, von Java auf .NET umgestiegen und beschäftigt sich nicht nur mit Softwareentwicklung auf Basis von MicrosoftTechnologien, sondern auch mit der Administration von Microsoft-Servern. Seine speziellen Interessensgebiete sind die Sicherheit und die Planung von sicheren Anwendungsstrukturen.
Marco Skulschus und Marcus Wiederstein haben zusammen bereits vier Bücher zu
XML (XML Schema, XSLT und XSL-FO) sowie zu Datenbanken (ein Programmierhandbuch zu Oracle und ein Werk zu Standard-SQL) herausgebracht. Sie haben unterschiedliche Zertifizierungen von Microsoft und Oracle erworben.
14
Vorwort
Aufbau des Buchs
Als Leser haben wir uns die Teilnehmer, welche die verschiedenen T-SQL-Seminare im
Bereich MS SQL Server besuchen, vorgestellt. Hierbei handelt es sich um Programmierer, die für eine oder in einer MS SQL Server-Datenbank Software entwickeln. Ihre
Aufgabe ist es normalerweise nicht, diese Datenbank einzurichten, von den Datenstrukturen her zu planen oder von Grund auf neu aufzubauen. Die Tabellen und die allgemeinen Strukturen sind bereits vorgegeben, sodass ihre Aufgabe daraus besteht, umfangreiche Abfragen und Analysen auszuführen, die Datenbank mit Hilfe von Funktionen und Prozeduren in T-SQL oder .NET zu erweitern und möglicherweise auch mit
XML umzugehen. Weitere Bücher in dieser Reihe behandeln die an anderer Stelle erwähnten weiteren Themen.
1. Das erste Kapitel stellt verschiedene Grundlagen zum (neuen) MS SQL
Server vor. Es beschreibt im Wesentlichen die Installation der Datenbank,
den Umgang mit dem grafischen Werkzeug Management Studio sowie die
Beispieldatenbank AdventureWorks. Dieses Kapitel stellt an verschiedenen
Beispielen dar, wie man fast ohne SQL, was einen Hauptteil des Buchs
bestimmt, Tabellen anlegt, mit Daten füllt bzw. Abfragen durchführt. Ob
man später diese grafischen Hilfsmittel weiterhin benutzen wird, ist dem
persönlichen Belieben überlassen, doch im Normalfall sollten insbesondere
die vielen Beispiele zur Verwendung von SQL und T-SQL dafür Sorge
tragen, dass man verstärkt doch eher den entsprechenden Quelltext schreibt
anstatt ihn über grafische Werkzeuge zu erzeugen.
2. Das zweite Kapitel stellt Standard-SQL am Beispiel vom MS SQL Server
und vor allen Dingen der AdventureWorks-Datenbank vor. Dabei beginnt
es ausdrücklich mit der einfachen Standardabfrage SELECT * FROM
tabelle und arbeitet sich dann durch die typischen Bereiche wie filtern,
sortieren und gruppieren, die nicht nur mit dem MS SQL Server, sondern
mit jeder Datenbank möglich sind. Es stellt anschließend die verschiedenen
Standard-SQL-Funktionen
für
Aggregate
und
damit
für
Datengruppierungen vor, ehe es schließlich sehr ausführlich die vielen
15
Vorwort
Funktionen vom MS SQL Server vorstellt, welche für viele Fragestellungen
eine Lösung bieten, ohne die Daten in einer äußeren Anwendung zu
verarbeiten, und die sich für jede Datenbank anders darstellen. Hier sind
von System zu System große Unterschiede hinsichtlich de
Funktionsumfangs und der Fähigkeiten der Funktionen zu beobachten.
3. Das dritte Kapitel konzentriert sich auf die Darstellung von so genannten
komplexen Abfragen. Dies bedeutet zunächst, dass man die Daten nicht nur
aus einer einzigen Tabelle abruft, sondern mehrere Tabellen miteinander
über ihre Primärschlüssel-Fremdschlüssel-Verknüpfung verbinden muss.
Hier stellt das Kapitel die traditionelle Variante den neuen, so genannten
ANSI-SQL-Verknüpfungen gegenüber. Eine zweite Stufe hinsichtlich der
Verwendung von komplexen Abfragen ist dann der Einsatz von
Unterabfragen. Hier folgt eine Darstellung von einfachen Unterabfragen,
Spaltenunterabfragen, abgeleiteten Tabellen und korrelierten Unterabfragen.
Die verschiedenen Techniken sind in vielen Datenbanken gleich oder
wenigstens ähnlich nutzbar, sind auch in der Praxis sehr sinnvoll und
werden häufig eingesetzt - jedoch gibt es hier eine große Menge an
Programmierern, die einen viel zu großen Bogen um diese Techniken
machen und es vorziehen, in einer äußeren Anwendung die gleichen
Operationen nachzuvollziehen. Dieses Kapitel möchte allerdings gerade
Lust auf diese Techniken machen, da eine viel kürzere Syntax zu gleichen
Ergebnissen führt. Schließlich folgt noch die Darstellung, wie man
Fallunterscheidungen über die CASE-Anweisung in SQL realisiert und wie
zusätzliche Aggregate errechnet werden können. Darunter sind Rangfolgen,
Untersummen und Würfel zu verstehen, wobei hier auf der einen Seite
verschiedene spezielle MS SQL Server-Techniken als auch StandardTechniken, die allerdings selten auswendig niedergeschrieben, sondern
vielmehr auf Basis eines fertigen Beispiels angewandt, zum Einsatz
kommen.
16
Vorwort
4. Das vierte Kapitel arbeitet den Bereich der Datenmanipulation durch. Dies
erfordert in verschiedenen Beispielen bereits einige einfache Techniken aus
T-SQL. In einem ersten Teil erstellt man über SQL die Datenstrukturen für
Tabellen und Sichten. Hierbei geht es weniger um den
Administrationsaspekt als um die Grundlagen, welche für den
Programmierer wesentlich sind. In einem zweiten, umfangreicheren Teil
werden dann für verschiedene vereinfachten Tabelle der Beispieldatebank
die typischen Bearbeitungsszenarien von Datenerfassung-, -bearbeitung, aktualisierung und -löschung vorgestellt. Dabei werden neben den
Standard-Möglichkeiten, wie sie in den meisten Datenbanksystemen
möglich sind, gerade auch die seltenen und weniger geläufigen
Möglichkeiten der verschiedenen Anweisungen vorgestellt.
5. Das fünfte Kapitel bietet schließlich eine ausführliche Einführung in die
SQL-Erweiterung von MS SQL Server mit dem Namen Transact SQL (TSQL). Zwar gibt es in einigen vorherigen Kapiteln bereits verschiedene
Beispiele, die mit einfachen Mitteln von T-SQL operieren, doch die
Erstellung von Variablen, die Verwendung und die Auswahl von geeigneten
Datentypen, die Erstellung und Nutzung von Cursorn sowie schließlich
auch die Erstellung von Prozeduren und Funktionen ist den einzelnen
Abschnitten dieses fünften Kapitels vorbehalten.
6. Das sechste Kapitel greift noch einmal den Bereich der Abfragen auf, wobei
hier die sehr fortgeschrittenen und teilweise auch neuen Techniken für mehr
in den Bereich der Analysen reichende Anweisungen dargestellt werden.
Hier wird die neue Technik der Common Table Expressions eingefügt, die
in vielen Beispielen dieses Kapitels genutzt wird. Als Beispiele für
Analysen finden sich dann fortgeschrittene Aggregate wie Akkumulationen
und Durchschnitte und auch die Erstellung von Rangfolgen bzw.
Hitparaden in diesem Kapitel wieder. Auch das Thema der Pivot-Abfragen
wird behandelt. Neben klassischen Lösungen stellt dieses Kapitel auch
insbesondere die neuen Technologien der Version 2005 vor.
17
Vorwort
Beispieldateien
Als Beispiel-Datenbank dient die sehr umfangreiche Datenbank AdventureWorks, welche im kommenden Kapitel kurz eingeführt und vorgestellt wird. Sie ersetzt die seit
Jahren bekannte Datenbank Nordwind. Sie ist wesentlich umfangreicher als die bekannte Nordwind-Datenbank und ermöglicht es nun auch mit einer speziellen DataWarehouse-Variante sämtliche Themengebiete des SQL Servers 2005 hervorragend darzustellen.
Die Datenbank selbst kann entweder direkt bei der Installation des SQL Servers zusätzlich installiert oder auch direkt von der Microsoft-Webseite herunter geladen werden.
Die verschiedenen Abfragen und Programmdateien, welche in diesem Buch erstellt und
diskutiert werden, liegen ebenfalls im Internet zum Download bereit. Die einzelnen
Quelltexte sind vollständig dokumentiert und enthalten neben dem eigentlichen Quelltext auch in einem Kommentarbereich die Ergebnisse. Dies ermöglicht es, die Dateien
auch ohne Testen vollständig zu verwenden.
Es werden nur für sehr wenige Beispiele eigene Tabellen erstellt, da der Leser, für den
dieses Buch geschrieben ist, im Normalfall eine bereits bestehende Datenbank bearbeiten, erweitern und vor allen Dingen nutzen soll. Das Administrationsbuch geht verstärkt
auf die Techniken der Erstellung ein.
Kontakt zu Autoren
Sie erreichen die Autoren unter [email protected] und [email protected]. Die Verlagswebseite finden Sie unter der Adresse
http://www.comelio-medien.com.
ƒ
MS SQL Server-Dienstleistungen: http://www.comelio.com/dedi2_318.php
ƒ
MS SQL Server-Seminare: http://www.comelio.com/deti3_PHP.php
18
Grundlagen
1 Grundlagen
19
Grundlagen
20
Grundlagen
1 Grundlagen
Dieses Buch geht davon aus, dass allgemeine Grundkenntnisse zu Datenbanken vorhanden sind, dass allerdings die Beschäftigung mit dem Datenbanksystem von Microsoft
erst ab Version 2005 zu den Arbeitsinhalten gehört. Dieses Buch als erstes in der MS
SQL Server-Serie soll daher auch einige einleitende Worte zur Installation, zur Architektur und zu den ersten Schritten mit dem Management Studio als Programm zur Nutzung der Datenbank verlieren. Für Leser, die bereits eine fertige Installation besitzen
und sich schon mit der Version 2000 auskennen, sind daher die Abschnitte zur Installation und den ersten Schritten nicht relevant, sondern möglicherweise nur der Abschnitt
zur Architektur.
1.1
Installation
Für gewöhnlich stellt die Installation von Software heute kein größeres Problem mehr
da, sodass wir dieses Thema auch auf die notwendigen Informationen beschränken
möchten. Grundsätzlich ist die Installation des SQL Servers 2005 ebenfalls sehr einfach,
solange nur Testzwecke oder reine Anwendungsentwicklung betroffen ist und nicht
etwa der Aufbau eines realistischen Systems. Dies soll dann auch im Rahmen des Buchs
zur Administration thematisiert werden. Wichtig ist vielmehr, dass für die Funktionstüchtigkeit der Beispiele deutlich ist, welche Version zum Einsatz kommt und welche
Voraussetzungen im Rahmen der Installation getroffen werden.
Der SQL Server 2005 ist in unterschiedlichen Varianten erhältlich. Eine sehr umfassende Übersicht der möglichen Download-Dateien befindet sich unter www.microsoft.com/
germany/sql/downloads/default.mspx. Im Normalfall interessiert man sich für die Testversion (www.microsoft.com/germany/sql/downloads/testsoftware.mspx), die 180 Tage
gültig ist oder die Developer Edition, da beide Versionen den vollen Umfang der Datenbank bieten. Zusätzlich ist auch noch eine SQL Server 2005 Express Edition verfügbar, welche die Datenbankgröße auf 4 Gigabyte und maximal eine CPU sowie bis zu 1
GB Arbeitsspeicher unterstützt. Sie ist für die kostenlose Weiterverteilung und Einbet-
21
Grundlagen
tung in Anwendungen gedacht. Eine Übersicht über die verschiedenen Editionen und
ihre einzelnen Fähigkeiten findet man unter http://www.microsoft.com/ germany/sql/editionen/default.mspx. Wie man sich leicht denken kann, unterscheiden sich die
Versionen insbesondere in leichten Bedienbarkeit und den sehr fortgeschrittenen Anwendungen. Für gewöhnlich verhält es sich so, dass Anwendungen, die überhaupt mit
einer Großdatenbank wie dem MS SQL Server oder Oracle erstellt werden, auch umfangreiche Technologien erfordern, sodass die Enterprise-Ausgabe notwendig ist.
Sie bietet als einziges System folgende Möglichkeiten an, die für fortgeschrittene Anwendungsszenarien notwendig sein können. Allerdings ist insbesondere der Bereich
Business Intelligence besonders an der neuen Version hervorzuheben, da hier auf der
einen Seite deutliche Erweiterungen und Verbesserungen im Gegensatz zur Vorgängerversion zu begrüßen sind, und auf der anderen Seite die totale Integration mit anderen
Microsoft-Produkten, wodurch vielfältige Anforderungen mit geringem oder wenigstens
akzeptablem Aufwand umgesetzt werden können.
Die nachfolgende kurze Aufstellung stellt für die einzelnen Bereiche dar, welche Fähigkeiten die Enterprise-Version bietet, welche die anderen nicht bieten.
ƒ
ƒ
ƒ
22
Bereich Skalierbarkeit und Leistung:
–
Partitionierung für umfangreiche Datenbanken
–
Parallelindexoperationen für die Parallelverarbeitung von Indexoperationen
–
Indizierte Ansichten mit Vergleichen
Bereich Hochverfügbarkeit:
–
Onlineindizierung
–
Onlinewiederherstellung
–
Schnelle Wiederherstellung
Bereich Integration und Interoperabilität:
Grundlagen
ƒ
–
Integration Services mit erweiterten Transformationen mit Data Mining, Text
Mining und Datenbereinigung
–
Oracle-Replikation mit Transaktionsreplikation mit Oracle als Publisher
Bereich Business Intelligence:
–
Berichtsserver mit Lastverteilung
–
Datengesteuerte Abonnements
–
Uneingeschränktes Durchklicken
–
Erweiterte Geschäftsanalyse bietet Kontointelligenz, Metadatenübersetzung,
perspektivische und semiadditive Measures
–
Proaktives Caching bietet automatische Zwischenspeicherung für verbesserte
Skalierbarkeit und Leistung
–
Erweiterte Datenverwaltung
–
Erweiterte Leistungsoptimierung
–
Datenflussintegration in SQL Server Integration Services
–
Text Mining
Für die Beispiele verwenden wir die Developer-Version, wobei Sie bei einem Test zuhause oder in der Firma auf die angesprochene 180-Tage-Testversion zurückgreifen.
Wir haben uns ganz bewusst gegen die Express-Ausgabe entschieden (so wie wir dies
auch im Rahmen eines Oracle-Buchs machen würden), da die Software, die wir im
Rahmen von Projekten betreuen, typischerweise damit bei Weitem nicht lauffähig wäre
und wir hier daher nur wenig sinnvollen Einsatz sehen würden. Nichtsdestoweniger gibt
es eine Reihe Spezialliteratur zu diesem Thema, die diese leichtgewichtige Alternative
im Einsatz zeigt. Die meisten Beispiele dieses Buch funktionieren auch mit den unterschiedlichen Versionen, da in diesem Band die Fähigkeiten von der Enterprise-Ausgabe
noch nicht genutzt werden.
Folgende Schritte sind für eine Standardinstallation notwendig:
23
Grundlagen
1. Nach dem Starten der Installation öffnet sich das Dialogfenster
Installationsvoraussetzungen, das darüber informiert, dass das .NET
Framework 2.0 (immer eine gute Wahl), das entsprechende Sprachpaket
sowie der Microsoft SQL Native Client (eine unverzichtbare
Voraussetzung) installiert werden müssen. Es bleibt nicht mehr zu tun, als
die Schaltfläche INSTALLIEREN zu wählen.
2. Nachdem die genannten Voraussetzungen geschaffen wurden, erscheint ein
ähnliches Dialogfenster mit Bestätigungen der installierten Teile. Hier wählt
man WEITER.
3. Es folgt die so genannte Systemkonfigurationsüberprüfung, welche im
Normalfall mit einer Reihen an Erfolgen beendet wird. Bei einem
gewöhnlichen Windows XP Professional-Rechner sollten keine
Schwierigkeiten und damit Misserfolge auftreten. Ansonsten müssen die
entsprechenden Korrekturen vorgenommen werden, auf die allerdings in
deutlichen Fehlermeldungen hingewiesen wird. Dieses Dialogfenster
verlassen Sie mit WEITER.
4. Schließlich gelangen Sie in das Dialogfenster Zu installierende
Komponenten. Je nach ausgewählter Datenbankversion bieten sich hier auch
unterschiedliche Komponentenlisten, da ja nicht alle Komponenten in jeder
Version verfügbar sind. Für dieses Buch und auch für andere Bücher aus
dieser Reihe werden insbesondere auch die Reporting- und AnalysisServices ausgewählt.
5. Im Dialofenster Instanzname wählen Sie die Option Standardinstanz aus
und bestätigen mit WEITER.
6. Im Dialogfenster Dienstkonto können Sie die Anmeldekonten angeben. Hier
verzichten wir darauf, für jedes Dienstkonto eigene Anmeldeinformationen
anzugeben. Stattdessen genügt die einfachste Lösung, die aus dem
integrierten
Systemkonto
besteht.
Da
die
Beispieldatenbank
AdventureWorks verwendet wird und wenigstens im Rahmen des Buchs
24
Grundlagen
keine geheimen Informationen in die Datenbank gelangen, ist dies völlig
ausreichend. Zusätzlich wählen Sie im unteren Bereich des Dialogfensters
aus, welche Dienste am Ende der Installation gestartet werden sollen. Für
dieses Buch sind zwar die Reporting- und Analysis-Services noch nicht
wichtig, doch sind sie derart interessant, dass es sich lohnt, sie für die Zeit
nach Transact SQL zu starten. Das Dialogfenster verlassen Sie mit WEITER.
7. Auch eine Anmeldung über die Windows-Informationen ist völlig
ausreichend, sodass Sie im Dialogfenster Authentifizierungsmodus die
Option Windows-Authentifizierungsmodus auswählen. Klicken Sie auf
WEITER.
8. Schließlich müssen Sie noch die Sortierreihenfolge sowie einige
Sprachmerkmale angeben. Wir haben uns für eine Unterscheidung von
Groß-/Kleinschreibung entschieden, da man durch Funktionen wie UPPER
und LOWER leicht die Schreibung ignorieren kann und sie ansonsten sehr
wohl auf exakt Gleichheit prüfen kann. Auch Akzente unterscheiden einen
Buchstaben erheblich, sodass diese Unterscheidungsoption ebenfalls
gewählt wird. Klicken Sie auf WEITER.
9. Im Dialogfenster Berichtsserver-Installationsoptionen belassen Sie die
Voreinstellungen, um später eine Konfiguration durchzuführen.
10. Im Dialogfenster Einstellungen für Fehler- und Verwendungsberichte
entscheiden Sie, ob Sie Microsoft Hilfeinformationen zur Verbesserung
senden wollen.
11. Zum Schluss erscheinen noch zwei letzte Bestätigungsfenster.
25
Grundlagen
1
2
3
4
Abbildung 1.1: Installation (1)
26
Grundlagen
1
2
3
4
Abbildung 1.2: Installation (2)
27
Grundlagen
1
2
3
4
Abbildung 1.3: Installation (3)
1.2
28
Erste Schritte
Grundlagen
Wie alle Microsoft-Produkte ist auch der MS SQL Server 2005 auf eine einfache Bedienung ausgelegt, damit bereits von Anfang an einfache Aufgaben erledigt werden
können. In diesem Zusammenhang ist dies für den Programmier oder für den DBAnfänger die Verwendung von SQL in der Datenbank und die Ausführung von Abfragen und Anweisungen. In diesem Kapitel soll die für die Datenbank wesentliche Desktop-Anwendung SQL Server Management Studio vorgestellt werden. Im Vergleich zur
Vorgängerversion haben sich allerlei Änderungen ergeben, doch der Grundaufbau ähnelt natürlich der Version 2000 und auch anderen Anwendungen, die einen GUIorientierten Zugriff auf Datenbanken erlauben.
1.2.1
Management Studio
Abbildung 1.4: Management Studio Starten
Das Management Studio ist die zentrale Anlaufstelle für die Arbeit mit dem neuen MS
SQL Server 2005. Starten Sie das Programm, indem Sie unter Start / Programme /
Microsoft SQL Server 2005 / SQL Server Management Studio auswählen. Zusätzlich
zeigt die nachfolgende Abbildung auch noch, wie Sie die überaus wichtige Hilfe zur
29
Grundlagen
Datenbank öffnen. Dazu öffnen Sie innerhalb des Eintrags Microsoft SQL Server 2005
den Eintrag Dokumentation und Lernprogramme / SQL Server-Onlinedokumentation.
Auf der linken Seite des sich öffnenden Fensters befindet sich der so genannte ObjektExplorer, der - wie sein Name schon verspricht - die Objekten des Servers anzeigt. Dies
sind zunächst die Datenbanken selbst, danach gefolgt von weiteren Bereichen wie Sicherheit, Verwaltung, unterschiedliche Dienste und Datensicherung.
Abbildung 1.5: Objekt-Explorer einer Datenbank
1. Wählen Sie den Eintrag Datenbanken / AdventureWorks aus, um die
AdventureWorks-Datenbank zu öffnen. Wie Sie sehen, sind bereits zwei
verschiedene Beispiel-Datenbanken installiert, nämlich zum einen die
gewöhnliche AdventureWorks-DB und zum anderen die für die Vorführung
30
Grundlagen
der Data Warehouse-Funktionalitäten des SQL Servers angepasste
AdventureWorksDW-Datenbank. Sie wird in einem anderen Buch
verwendet.
1
3
2
4
Abbildung 1.6: Abrufen von Tabellen- und Spalteninformationen
12. Navigieren Sie zu einer einzelnen Tabelle, indem Sie Datenbanken /
AdventureWorks / Tabellen / Production.Product auswählen. Es öffnet sich
31
Grundlagen
eine Übersicht über die in dieser Tabelle vorhandenen Spalten, Schlüssel,
Einschränkungen und Programmierobjekte mit ihren jeweiligen
Eigenschaften.
So gibt die Übersicht der Spalte ProductID der Tabelle Production.Product in
einer kleinen Übersicht neben dem Spaltennamen bereits an, dass es sich hierbei um den
Primärschlüssel (PS) handelt, der vom Datentyp einer Ganzzahl (int) und nicht leer
(Nicht NULL) sein darf. Da der Primärschlüssel eine Spalte eindeutig referenziert und
daher für die Tabelle von entscheidender Bedeutung für die Identifikation einer Datenzeile ist, ist zusätzlich als Erkennungsmerkmal noch ein Schlüsselsymbol neben der
Spalte angebracht. Dieser Schlüssel hat eine gelbe Färbung, während ein grauer Schlüssel neben den so genannten Fremdschlüsselspalten (FS) angebracht ist. Bspw. ist die
Spalte SizeUnitMeasureCodes ein solcher Fremdschlüssel vom Datentyp nchar(3).
Diese Spalte darf allerdings auch leer sein, d.h. den Standardwert NULL enthalten. Der
Wert in diesen Spalten ist in einer anderen Tabelle ein Primärschlüsselwert, sodass man
diesen Wert für die Verknüpfung von beiden Tabellen verwenden kann. Diese Verknüpfung ermöglicht es, zusätzliche Informationen zu diesem Fremdschlüsselwert abzurufen.
Die Zahlen neben den einzelnen Datentypen wie bspw. bei nchar(3) gibt an, dass
insgesamt drei Zeichen (Buchstaben oder Zahlen) in dieser Spalte gespeichert werden
dürfen. Sie informiert also über die Länge des zulässigen Spaltenwerts.
13. Die Schlüssel befinden sich zusätzlich auch noch im Ordner Schlüssel,
wobei auch hier wiederum die gelb gefärbten Schlüssel die Primärschlüssel
und die grau gefärbten die Fremdschlüssel enthalten.
Die meisten Tabellen haben nur eine Primärschlüsselspalte, wobei grundsätzlich auch
mehrere zulässig sind. Der vollständige Primärschlüsselwert setzt sich dann aus den
einzelnen Werten der Primärschlüsselspalten zusammen. Es ist allerdings keine Seltenheit, dass eine Tabelle mehrere Fremdschüsselspalten enthält. Diese sind normalerweise
einzeln zu betrachten und führen im Regelfall auch zu verschiedenen Tabellen. Lediglich in Sonderfällen und bei Tabellen mit einem zusammengesetztem Primärschlüssel
32
Grundlagen
müssen natürlich die einzelnen Schlüssel auch wieder als Fremdschlüssel in einer anderen Tabelle erscheinen.
14. Weitere Informationen zu einem einzigen Schlüsselwert erhalten Sie über
das Detailmenü, das mit Hilfe eines Doppelklicks zu öffnen ist.
1
2
Abbildung 1.7: Schlüssel und Fremdschlüssel
Dieses Detailmenü hat den Titel Spalteneigenschaften und entspricht der Anzeige wie
sie auch für gewöhnliche Spalten existiert. Insbesondere für Schlüsselfelder ist diese
Übersicht allerdings besonders interessant. Unter der Überschrift IDENTITÄT befindet
33
Grundlagen
sich der Name für dieses Schlüsselfeld sowie eine allgemeine Beschreibung in Form
eines Kommentars. Der Name soll genau diese Schlüsselangabe identifizieren, da ja
grundsätzlich der Spaltenname auch in einer anderen Tabelle als Fremdschlüssel oder in
ganz anderer Bedeutung erscheinen kann. Über den eindeutigen Namen lassen sich dann
auch Fehlermeldungen viel besser interpretieren, da man aus ihnen ablesen kann, welche
Schlüsselangabe
oder
-beziehung verletzt wurde. Unter der Überschrift TABELLEN-DESIGNER sind verschiedene zusätzliche Aktionen angegeben, die bspw. automatisch geschehen sollen, sobald
eine Datenänderung (eintragen, löschen oder aktualisieren) erfolgt.
1
2
Abbildung 1.8: Check-Bedingungen
15. Wählen Sie den Order EINSCHRÄNKUNGEN aus, um die für die Tabelle
angegebenen
Überprüfungsregeln
für
Wertänderungen
und
Werterfassungen zu kontrollieren.
34
Grundlagen
Unter einer Einschränkung oder einer CHECK-Bedingung, welche ihren Namen von der
sie erstellenden SQL-Syntax herleitet, versteht man eine Überprüfung des Wertebereichs. Diese Überprüfung hat nur indirekt etwas mit der reinen Überprüfung zu tun, ob
die einzutragenden Daten zu dem für die Spalte angegebenen Datentyp passen. Sie
enthalten darüber hinaus vielmehr weitere Regeln und Ausdrücke, die zur genauen
Überprüfung von zulässigen Werten herangezogen werden. Dies können Bereiche,
Grenzen und Intervalle genauso sein wie der Vergleich mit anderen Spalten oder sonstigen Vergleichen und Wertbeschreibungen von zulässigen Einträgen in dieser Spalte.
Auch diese Einschränkungen besitzen einen eindeutigen Namen, damit man im Fehlerfall exakt nachvollziehen kann, gegen welche Einschränkung eine Operation verstoßen
hat.
16. Öffnen Sie den Ordner TRIGGER, um die für die Tabelle zugewiesenen
automatischen Operationen zu lesen.
2
1
ALTER TRIGGER [uProduct] ON [Production].[Product]
AFTER UPDATE NOT FOR REPLICATION AS
BEGIN
SET NOCOUNT ON;
UPDATE [Production].[Product]
SET [Production].[Product].[ModifiedDate] = GETDATE()
FROM inserted
WHERE inserted.[ProductID] =
[Production].[Product].[ProductID];
END;
Abbildung 1.9: Trigger
Bei einem Trigger handelt es sich um ein programmierbares Objekt in der Syntax von
Transact SQL, der Spracherweiterung von SQL für den MS SQL Server. Bisweilen
bezeichnet man auch die gesamte SQL-Sprache für den SQL Server als Transact SQL.
Ein Trigger wird automatisch durch eine in ihm angegebene DB-Aktion ausgelöst, in-
35
Grundlagen
dem bspw. Daten in eine Tabelle neu eingetragen oder einer Tabelle gelöscht oder aktualisiert werden sollen. Einen Trigger kann man nur über eine solche Aktion auslösen,
d.h. ein direkter Aufruf wie bei einer Funktion oder Prozedur ist ausgeschlossen. Die
Anweisungen eines Triggers dienen der erweiterten Überprüfung von Benutzeraktionen
und sollen die Datenintegrität noch besser schützen als bspw. ein einfacher Wertebereich oder eine Einschränkung. Erlauben die Einschränkungen nur einfache Überprüfungsregeln, so kann man in einem Trigger nahezu beliebige Programmanweisungen
vorgeben.
17. Für die Optimierung von Abfragen, d.h. für ihre beschleunigte Ausführung,
kann man Indizes (Singular: Index) für eine Tabelle angeben. Öffnen Sie
mit einem Klick den Ordner Indizes und wählen Sie einen der
verschiedenen schon vorhandenen Indizes aus. Sie gelangen in die
Detailansicht des gewählten Index, indem neben seinem Typ auch solche
Eigenschaften wie die indizierte Spalte, Sortierreihenfolge, Datentyp und
Größe angegeben werden. An dieser Stelle befinden sich auch in einer
Ordner-Hierarchie verschiedene weitere Dialogfenster für die IndexBearbeitung bzw. ihre Speicherung und sonstige Eigenschaften.
Mit Hilfe des in einem späteren Kapitel erläuterten Schlüsselworts SELECT ist es möglich, die Daten einer Tabelle auszugeben bzw. zu bestimmen, welche Spalten in die
Ergebnismenge übernommen werden sollen und welchen Bedingungen die Daten genügen sollen. Dabei kann die Suche nach passenden Datensätzen entweder sequenziell
oder indiziert vorgenommen werden. Die Möglichkeit eines sequenziellen Vorgehens
bedeutet, dass die Bedingung für jede einzelne Zeile der Tabelle überprüft wird. Man
kann sich hier vorstellen, dass man mit dem Finger jeden Datensatz einzeln berührt und
ihn untersucht. Dies geschieht in der gespeicherten Reihenfolge der Daten auf der Festplatte. Da dies bei sehr großen Tabellen lange dauern kann, gibt es die Möglichkeit, die
Spalten, in denen häufig gesucht wird bzw. in denen die Bedingungen enthalten sind,
nach denen die Daten gefiltert werden sollen, zu indizieren. Dabei handelt es sich hierbei um eine gezielte Suche wie in einem Lexikon oder Wörterbuch.
36
Grundlagen
Mit einem so genannten Clustered Index ordnet man die Reihen der Tabelle nach der
angegebenen Sortierung physikalisch, d.h. auf der Festplatte, bereits in der benötigten
Reihenfolge. Da die Daten nur einmal physikalisch geordnet werden können, ist auch
nur ein solcher Index pro Tabelle zulässig. Sofern die Tabelle einen Primärschlüssel
besitzt, wird dieser Clustered Index automatisch erstellt. Im Gegensatz dazu kann man
auch einen Non-Clustered Index erstellen. Er betrifft nicht die physikalische Speicherung bzw. Sortierung der Daten. Stattdessen befindet sich die Sortierung in einer zusätzlichen Baumstruktur, die wie die schon erwähnten Techniken Lexikon, Wörterbuch oder
Stichwortverzeichnis fungieren. Neben diesen beiden Hauptformen lassen sich noch
weitere Arten unterscheiden.
2
1
3
Abbildung 1.10: Indizes
18. Wählen Sie den Ordner Statistiken aus, um die Entscheidungsgrundlage für
die Verwendung eines Index zu sehen. Für die Bearbeitung einer Anfrage
37
Grundlagen
stehen meistens unterschiedliche Vorgehensweisen zur Verfügung, die
unter dem Namen Ausführungsplan bekannt sind. Welcher konkreter
Ausführungsplan und damit auch welcher Index tatsächlich genutzt wird,
um eine Anfrage zu beantworten, beeinflusst die so genannte
Indexselektivität.
1
2
Abbildung 1.11: Statistiken
Je höher diese Indexselektivität ist, desto höher ist die Wahrscheinlichkeit, dass der
entsprechende Ausführungsplan zum Einsatz kommt. Der Optimierer ermittelt den
benötigten Wert anhand der erstellten Statistiken. Sie enthalten solche Informationen
wie die Anzahl der Tabellenzeilen, die Verteilungsschritte oder die physikalischen Speicherseiten.
38
Grundlagen
1.2.2
Abfragen direkt ausführen
Das Management Studio bietet die Möglichkeit, über einen einfachen Texteditor Abfragen an die Datenbank zu senden bzw. T-SQL-Programme wie die erwähnten Trigger
und Prozeduren zu erstellen. Neben diesem Programm gibt es auch noch rein kommandozeilenbasierte Werkzeuge. Im Wesentlichen sollen Sie in diesem Buch lernen, welche
verschiedenen Syntaxanweisungen ein Programmierer (im Gegensatz zu einem Administrator) in dieses Textfeld eingetragen soll. Empfehlenswert ist natürlich immer, sowohl Datenbankadministrationskenntnisse als auch Programmierkenntnisse zu erwerben.
1. Um eine Abfrage oder allgemein eine DB-Anweisung auszuführen, wählen
Sie die Schaltfläche NEUE ABFRAGE.
2. In der Auswahlliste, welche die verschiedenen Datenbanken enthält, die
gerade innerhalb des Datenbankservers enthalten sind, wählen Sie die
AdventureWorks-Datenbank aus, da die Anweisung in dieser DB
ausgeführt werden soll.
3. Tragen Sie dann Ihre SQL-Anweisungen wie bspw. SELECT * FROM
Production.Product in das sich öffnende Textfenster ein.
4. Wählen Sie die AUSFÜHREN-Schaltfläche oder drücken Sie die F5-Taste.
5. Das Ergebnis erscheint standardmäßig in der so genannten Raster-Ansicht
(engl. Grid View) im Bereich ERGEBNISSE. Zusätzliche Meldungen oder
natürlich Fehlermeldungen erscheinen dagegen im Bereich MELDUNGEN.
Die vorher genannten Schritte sind neben der Anmeldung die nahezu wichtigsten für die
Arbeit mit der Datenbank. Sie lassen sich noch um weitere Schritte wie das Öffnen
einer fertigen SQL-Datei mit Anweisungen oder die Speicherung von Anweisungen und
Ergebnissen vervollständigen, doch für den Einstieg in die Arbeit mit dem SQL Server
sind dies die wesentlichen Aktivitäten. Im Bereich der Administration und natürlich der
Erstellung von Datenbankobjekten bietet das Management Studio noch weitere Möglichkeiten.
39
Grundlagen
Um eine Folge von SQL-Anweisungen wie bspw. eine Abfrage, die Sie später noch
einmal ausführen wollen, als Textdatei zu speichern, wählen Sie DATEI /
SQL1QUERY.SQL SPEICHERN oder DATEI / SQL1QUERY.SQL SPEICHERN ALS... aus. Die
Endung der Datei ist für die Funktionstüchtigkeit der Datei bedeutungslos, d.h. die Endung .txt wäre genauso sinnvoll. Allerdings können Sie die .sql-Endung mit dem Management Studio verknüpfen und ist sie die gebräuchliche Endung für Datenbankanweisungen.
Eine solchermaßen gespeicherte Datei lässt sich dann über Datei / Öffnen wieder laden
und zur Ausführung bringen.
1
4
2
3
Abbildung 1.12: Ausführen einer Abfrage
Bisweilen hat man allerdings gerade nicht nur eine einzige Datei, sondern für einen
bestimmten Arbeitsbereich eine ganze Reihe von zusammenhängenden Skripten. Zu
40
Grundlagen
ihrer Organisation lässt sich ein ganzes Projekt erstellen. Es enthält neben den Skripten
auch die Verbindungsangaben zur Datenbank.
1. Wählen Sie DATEI / NEU / PROJEKT.
2. Wählen Sie aus den drei verschiedenen Projektarten den Eintrag SQL
SERVER-SKRIPTS aus. Geben Sie einen neuen Namen für das Projekt sowie
auch über die DURCHSUCHEN-Schaltfläche einen Speicherort an. Bestätigen
Sie alle Einstellungen mit OK.
1
2
3
Abbildung 1.13: Erstellen eines Projekts
3. Richten Sie mit dem Kontextmenü unter Verbindungen eine neue
Verbindung ein oder nutzen Sie die bereits bestehende. Fügen Sie neue
41
Grundlagen
Skripte, Abfragen und Anweisungen mit Hilfe des Kontextmenüs unter
Abfragen hinzu. Es öffnet sich in diesem Fall ein neues Abfragefenster,
welche Sie direkt im Projekt speichern können.
Die Abbildung zeigt, wie eine einfache Abfrage ausgeführt wurde und dieses Skript
inklusive der benutzten Datenbankverbindung in der Projektmappe gespeichert wurde.
Beachten Sie bitte, dass Sie die Möglichkeit haben, in einer Projektmappe mehrere
Projekte zu speichern. In diesem Beispiel haben sowohl die Projektmappe als auch das
Projekt den selben Namen. Dies ist allerdings nicht notwendig. Stattdessen könnte man
sich für AdventureWorks-Datenbank eine große Projektmappe vorstellen, in der Ihre im
Rahmen des Buch erstellten Skripte nach Kapiteln sortiert enthalten sind.
Abbildung 1.14: Projektmappe im Einsatz
Um dann neue Elemente in einer Projektmappe hinzuzufügen, stehen die diversen Kontextmenüs zur Verfügung. Im Kontextmenü der Projektmappe fügen Sie neue oder
vorhandene Projekte hinzu, während sie im Kontextmenü des Projekts sowohl neue
Datenbankverbindungen als auch neue Abfragen hinzufügen. Diese beiden letzteren
Elemente lassen sich auch in den jeweiligen Projektabteilungen neu erstellen.
42
Grundlagen
Abbildung 1.15: Projekte und Abfragen einer Projektmappe hinzufügen
1.2.3
Abfragen im Editor ausführen
Eigentlich müsste man gar kein T-SQL lernen, um einfache Daten aus dem MS SQL
Server zu extrahieren oder solche Operationen wie Daten einfügen, löschen oder aktualisieren durchzuführen. Mit einigem Geschick, wenngleich auch nicht mit demselben
professionellen Effekt, könnte man sich diese vier typischen Operationen einfach auch
im Abfrage-Editor grafisch zusammenstellen. Dabei handelt es sich um eine einfach zu
bedienende Oberfläche, wie sie in ähnlicher Weise auch bspw. in MS Access vorhanden
ist. Leider ist dies auf einfachste Abfragen beschränkt, sodass man wohl doch besser mit
T-SQL arbeitet.
Für die Nutzung des Abfrage-Editors müssen Sie zunächst ein leeres Abfrage-Fenster
öffnen, da ansonsten der Menü-Eintrag für den Editor nicht erscheint.
Um eine Abfrage auszuführen sind folgende Schritte notwendig:
1. Wählen Sie ABFRAGE / ABFRAGE IN EDITOR ENTWERFEN. Im sich
öffnenden großen Dialogfenster sieht man drei Bereiche. Im oberen
Bereiche befindet sich der Tabellenbereich, in welchem die verschiedenen
43
Grundlagen
ausgewählten Tabellen mit ihren Spalten und Verknüpfungen angezeigt
werden. Im mittleren Teil befindet sich der Bearbeitungsbereich, in dem
man Spalten, ihre Aliasnamen, Werte für Filter, Sortierungen etc. angeben
kann. Einige Werte lassen sich aus Listenfeldern abrufen, andere müssen
dagegen tatsächlich eingegeben werden.
1
2
3
Abbildung 1.16: Tabellen im Editor auswählen
2. Zunächst wählen Sie aus der sich öffnenden Tabellenliste die von Ihnen
gewünschte Tabelle aus. In diesem Beispiel handelt es sich um die beiden
Tabellen Production.Product und Production.ProductSubcategory,
wobei der erste Namensteil für das so genannte Datenbankschema
(übergeordnete Struktur) und der zweite Teil für den Tabellennamen steht.
Beide Tabellen sind über eine Primärschlüssel-Fremdschlüssel-
44
Grundlagen
Verknüpfung verbunden, sodass automatisch ein entsprechende Pfeil
gezogen wird. Dabei gibt das Schlüsselsymbol an, dass der Primärschlüssel
in der Tabelle ProductSubcategory gespeichert ist, während das Symbol
für die Unendlichkeit angibt, dass dieser Schlüssel beliebig oft in der
Product-Tabelle referenziert wird. N Produkte aus der Product-Tabelle
stehen mit einem Datensatz aus der ProductSubcategory-Tabelle in
Beziehung. Möchten Sie später weitere Tabellen auswählen, öffnen Sie das
Kontextmenü im Tabellenbereich.
1
2
3
Abbildung 1.17: Spalten(namen), Filter und Sortierung vorgeben
3. Wählen Sie danach die Spalten aus den benötigten Tabellen aus. Dies
gelingt entweder im Tabellenbereich, indem Sie die Spalten markieren, oder
45
Grundlagen
Sie können unten im Eingabebereich die Spalten aus einer Auswahlliste
auswählen. Die markierten Spalten aus den Tabellen werden automatisch
auch als anzuzeigende Spalten in der Ergebnismenge verwendet.
Es ist allerdings nicht nowendig, tatsächlich alle Spalten, auf die Sie im
Laufe der Abfrage zugreifen, auch tatsächlich auszugeben. Wenn eine
Spalte nur für einen Filter verwendet werden soll, besteht keine
Notwendigkeit, sie auch tatsächlich auszugeben. Dies ist dann umso
wichtiger, wenn der Filter immer zum gleichem Wert führen würde und
dieser Wert dann in jeder Spalte wieder genannt wird. In der Spalte
AUSGABE im Bearbeitungsbereich können Sie daher noch einmal genau
angeben, dass zwar diese Spalte bspw. für eine Sortierung oder für einen
Filter verwendet werden, aber gerade nicht ausgegeben werden soll.
4. In der Spalte ALIAS im Bearbeitungsbereich geben Sie dann bei den
Spalten, welche in der Ergebnismenge erscheinen sollen, einen möglichen
Ersatznamen für die Spaltenköpfe an. Dieser Name wird als Alias(name)
bezeichnet, und sollte einer guten Bezeichnung dienen, wenn der originale
Tabellenspaltenname nicht für eine Ausgabe geeignet ist, weil bspw. eine
Abkürzung verwendet wurde. Im aktuellen Beispiel sollen bspw. der Name
des Produktkategorie und der Produktname ausgegeben werden. Da beide
Spalte in der Tabelle Name heißen, erhalten Sie für die Abfrage einen
Aliasnamen.
5. Sofern das Ergebnis nach einer Spalte sortiert werden soll, geben Sie
einfach in der Spalte SORTIERUNGSART für die zu sortierende Spalte die
entsprechende Reihenfolge mit Aufsteigend oder Absteigend an. Die
Sortierung erfolgt automatisch so, dass sie für den Spaltendatentyp korrekt
ist, d.h. von A-Z, 1-x oder auch nach dem Kalender.
6. Beim Auswahl der Sortierung werden automatisch in der Reihenfolge der
Auswahl die Werte für die Spalte SOTIERREIHENFOLGE gesetzt. Wenn man
also erst nach dem Produktnamen und dann nach der Kategorie sortieren
46
Grundlagen
möchte, dann trägt man in die Spalte für den Produktnamen den Wert 1 und
in der Spalte für den Kategorienamen den Wert 2 ein.
7. Schließlich möchte man möglicherweise gar nicht alle Werte aus der
Tabelle abrufen, sondern einen so genannten Filter anwenden, d.h. nur alle
Produkte einer bestimmten Kategorie, mit einer bestimmten
Produktnummer oder einem bestimmten Preis. Es ist dabei auch möglich,
mehrere Bedingungen anzugeben, da neben der Spalte FILTER noch weitere
Oder-Spalten sind, in denen zusätzliche Bedingungen eingetragen werden
können. Die Angabe einer solchen Bedingung gelingt über einen Ausdruck,
der die Vergleichsoperatoren >, >=, <, <=, =, != sowie die besonderen SQLOperatoren, die in einem späteren Kapitel vorgestellt werden, erwartet. Im
aktuellen Beispiel soll der Produktnamen kleiner C und der Preis größer 100
sein.
Abbildung 1.18: Ausgabe und Einsatz der Abfrage
8. Nachdem alle Angabe getroffen sind, klicken Sie auf OK, verlassen dadurch
den Abfrage-Editor und gelangen mit dem fertig erstellten Quelltext in das
Abfragefenster. Dort führen Sie die Abfrage so aus, als hätten Sie sie selbst
nicht nur erstellt, sondern sogar selbst nieder geschrieben.
47
Grundlagen
Neben Abfragen werden Sie in diesem Buch auch lernen, wie Sie Daten über SQL aktualisieren können. Dies gelingt nicht über den SELECT-Befehl, den Sie gerade unter
Einsatz des Abfrage-Editors erstellt haben, sondern über UPDATE. Einige Grundbestandteile in dieser Anweisung sind allerdings gleich denen der Abfrage.
1. Wählen Sie ABFRAGE / ABFRAGE IN EDITOR ENTWERFEN.
2. Ändern Sie den Typ der zu erstellenden Abfrage über das Kontextmenü
über TYP ÄNDERN / AKTUALISIEREN in eine Aktualisierungsabfrage.
1
2
3
Abbildung 1.19: Aktualisierungsabfrage
3. Wählen Sie wie zuvor eine Tabelle aus, die Sie aktualisieren möchten. Dies
gelingt über das Kontextmenü mit dem Eintrag TABELLE HINZUFÜGEN. In
diesem Beispiel wählen Sie die Product-Tabelle aus.
48
Grundlagen
4. Wählen Sie im Bearbeitungsbereich die Spalten aus, die Sie für die Abfrage
benötigen. Dies sind im Normalfall zwei Spaltentypen, nämlich zum einen
eine Spalte, die einen neuen Wert erhalten soll, und zum anderen eine
Spalte, auf die ein Filter angewendet wird, um die entsprechenden zu
aktualisierenden Datensätze zu finden. Geben Sie keinen Filter an, werden
alle Datensätze aktualisiert. Möchten Sie allerdings einen Filter verwenden,
können Sie sich aussuchen, ob der Filter auf eine Spalte als die zu
aktualisierende angewandt werden soll, oder ob Filter und neuer Wert auf
die gleiche Spalte angewendet werden sollen. In diesem Fall soll der
ListPrice den Wert 100 erhalten, wenn das zuvor ausgewählte Produkt
gefunden wird. Daher ist der Filter in der Name-Spalte auf den Ausdruck
='All-Purpose Bike Stand'. Das zusätzlich eingefügte N ist für die
Abfrage nicht von Bedeutung, sondern kennzeichnet die Zeichenkette.
Weitere Bedingungen können Sie wie zuvor in die ODER-Spalten einfügen.
5. Wählen Sie OK und führen Sie, wenn Sie wollen, die Abfrage auch
tatsächlich aus. Um die Änderungen nachher wieder ungeschehen zu
machen, geben Sie zusätzlich noch die Anweisungen GO und ROLLBACK ein.
Neben Abfragen und Aktualisierungen lässt sich auch das SQL erstellen, mit dessen
Hilfe Datensätze aus der Datenbank gelöscht werden können. Es wird letztendlich mit
den gleichen Mechanismen erstellt, die auch schon bei den vorherigen Aktionen angewandt wurden.
1. Wählen Sie ABFRAGE / ABFRAGE IN EDITOR ENTWERFEN.
2. Ändern Sie den Typ der zu erstellenden Abfrage über das Kontextmenü
über TYP ÄNDERN / LÖSCHEN in eine Löschabfrage.
3. Wählen Sie wie zuvor eine Tabelle aus, aus der Sie Daten löschen möchten.
Dies gelingt über das Kontextmenü mit dem Eintrag TABELLE
HINZUFÜGEN. In diesem Beispiel wählen Sie die Product-Tabelle aus.
4. Wählen Sie im Bearbeitungsbereich die Spalten aus, die Sie für die Abfrage
benötigen. Dies sind bei einer Löschabfrage nur die Spalten, für die ein
49
Grundlagen
Filter vorgegeben werden soll. Bei einer Abfrage waren es im Vergleich
dazu Filter- und Anzeigespalten, bei einer Aktualisierungsabfrage Spalten,
die einen neuen Wert erhalten sollten, und natürlich auch Filter. Da einfach
der gesamte Datensatz entfernt werden soll, benötigt man nur Filter-Spalten.
In diesem Fall sollen dies Datensätze mit einer Produktnummer BETWEEN
879 AND 882 sein.
5. Wählen Sie OK und führen Sie, wenn Sie wollen, die Abfrage auch
tatsächlich aus. Um die Änderungen nachher wieder ungeschehen zu
machen, geben Sie zusätzlich noch die Anweisungen GO und ROLLBACK ein.
1
2
3
Abbildung 1.20: Löschabfrage
Die beiden Möglichkeiten, eine Tabelle zu erstellen und eine so genannte Einfügeabfrage zu erstellen, sind untereinander sehr ähnlich und ähneln auch sehr der gewöhnlichen
Abfrage. Während diese einfach nur eine flüchtige Ergebnismenge erzeugt, erlauben es
50
Grundlagen
die beiden anderen diese Ergebnismenge entweder in einer neu zu erstellenden Tabelle
oder in einer bereits vorhandenen zu speichern.
1. Wählen Sie ABFRAGE / ABFRAGE IN EDITOR ENTWERFEN.
2. Ändern Sie den Typ der zu erstellenden Abfrage über das Kontextmenü
über TYP ÄNDERN / TABELLE ERSTELLEN in eine Erstellungsabfrage.
3. Geben Sie den Namen der zu erstellenden Tabelle ein.
4. Geben Sie die Namen der zu erstellenden Spalten vor. Die Datentypen
werden direkt aus der Abfrage bzw. aus der befragten Tabelle übernommen.
Die Strukturen von Abfrage und Zieltabelle müssen also übereinstimmen.
Alternativ können Sie auch zunächst wie zuvor eine Tabelle bestimmen,
aus der Sie Daten auswählen möchten. Dies gelingt über das Kontextmenü
mit dem Eintrag TABELLE HINZUFÜGEN.
5. Geben Sie mögliche Abfrageeigenschaften wie Filter, Aliasnamen für die
Spaltennamen der zu erzeugenden Tabelle und Sortierungen vor.
6. Wählen Sie OK und führen Sie, wenn Sie wollen, die Abfrage auch
tatsächlich aus.
Da die Tabelle tatsächlich sofort angelegt wird, folgt im nächsten Quelltext eine Erweiterung zu dem automatisch erzeugten Quelltext. Er enthält zu Beginn noch eine Löschanweisung für die gesamte Tabelle mit Hilfe des DROP-Befehls von SQL und am Ende
noch eine Testabfrage der gesamten erstellten Tabelle. Dies soll nachweisen, dass sie
erstens existiert und zweitens auch Daten übernommen worden. Die einzelnen Anweisungen werden durch das GO-Schlüsselwort getrennt.
DROP TABLE [Shirts-Produkte]
GO
SELECT ProductID, Name, ListPrice
INTO
[Shirts-Produkte]
51
Grundlagen
FROM
Production.Product
WHERE
(Name LIKE N'%Shorts%')
GO
SELECT * FROM [Shirts-Produkte]
selectinto.sql: Löschen, Neuerstellung und Abfrage
1
2
3
4
5
Abbildung 1.21: Erstellungsabfrage
Man erhält als Ergebnis der gesamten Abfrage folgende Daten in der erzeugten Tabelle:
ProductID
52
Name
ListPrice
Grundlagen
----------- -------------------------------------- --------841
Men's Sports Shorts, S
59,99
849
Men's Sports Shorts, M
59,99
868
Women's Mountain Shorts, M
69,99
869
Women's Mountain Shorts, L
69,99
…
Ein letztes Wort soll noch zu dem sehr schönen Werkzeug des Abfrage-Editors verloren
werden. In den zurückliegenden Beispielen wurden immer nur Tabellen ausgewählt, in
denen Daten zu bearbeiten oder auszuwählen waren. Im Dialogfenster Tabelle hinzufügen, welches sich im Kontextmenü des Editors des Tabellenbereichs öffnet, sind allerdings vier verschiedene Reiter angebracht, die - je nach Operation - auch alle genutzt
werden können. Die drei zusätzlichen Möglichkeiten, um Daten auszuwählen oder überhaupt Zugriff auf Daten zu haben, sollen hier nur kurz erwähnt werden. Sie sind
allerdings Themen späterer Kapitel:
ƒ
Tabellen (Relationen) bilden in jeder Datenbank den zentralen Speicherort für
Daten bzw. bieten auch für die anderen Möglichkeiten die eigentliche Datenquelle.
Sie enthalten die Daten tatsächlich und erlauben bei entsprechender Berechtigung
Zugriff auf den gesamten Datenbestand.
ƒ
Ansichten oder auch Sichten stellen gespeicherte Abfragen dar. Sie enthalten also
nicht tatsächlich die Daten, sondern nur das SQL der Abfrage, welches zu den
Abfragen führt. Dies wird auch oft als virtuelle Tabelle bezeichnet, da man die
Sicht in SQL wie eine gewöhnliche Tabelle nutzen kann, sie in Wirklichkeit aber
nur eine gespeicherte Abfrage mit einem eigenen Namen darstellt. Sofern eine
Sicht wiederum indiziert ist, werden dann auch die Daten in der Sicht gespeichert.
53
Grundlagen
Abbildung 1.22: Mögliche Datenquellen
ƒ
Funktionen stellen nicht nur wie Prozeduren eine Möglichkeit dar, um
Softwarebausteine in der Datenbank zu entwickeln, sondern erlauben auch die
Erstellung von parametrisierten gespeicherten Abfragen. Dabei enthält die Funktion
die SQL-Anweisung für die Abfrage, während die möglichen Filter-Werte über die
Funktionsparameter angegeben werden können. Dieser Parameter ist bei Sichten
nicht möglich, sodass Funktionen als zusätzliche Möglichkeit dienen, Abfragen aus
Zeitersparnis- oder auch Sicherheitsgründen vorzubereiten.
ƒ
Synonyme stellen einen Spitz- oder Aliasnamen für DB-Objekte dar. Im Fall des
Datenabrufs ist dies vor allen Dingen für Tabellen wichtig. So hat man die
Möglichkeit, eine Abstraktionsschicht zwischen den so genannten Basis-Objekten
54
Grundlagen
und den DB-Klienten einzuziehen. Unterliegende Namen können sich verändern,
wobei das Synonym seinen Namen behält. Ein weiterer Vorteil liegt darin, dass
lange Namenskonstrukte wie Server1.AdventureWorks. Person.Employee für
server.datenbank.schema.[tabelle|sicht] überflüssig werden, da das kürzere
Synonym zum Einsatz kommen kann.
1.2.4
Vorlagen-Editor
Für häufig wiederkehrende Aufgaben, die in Form von Skripten ausgeführt werden
können, gibt es die Möglichkeit, so genannte Vorlagen zu erstellen. Dies sind halb fertige T-SQL-Skripte, in denen Textbereiche wie Objektnamen oder einfache Werte angegeben und beim konkreten Aufruf gegen konkrete Werte ausgetauscht werden können.
Verschiedene fertige Vorlagen sind ebenfalls bereits vorhanden.
Gehen Sie für die Erstellung und den Aufruf solcher Vorlagen folgendermaßen vor:
1. Wählen Sie ANSICHT / VORLAGEN-EXPLORER.
2. In diesem Vorlagen-Explorer können Sie entweder eine bereits vorhandene
und/oder von Ihnen erstellte Vorlage aus einem von Ihnen benannten
Ordner auswählen. Alternativ können Sie aber auch eine neue Vorlage
erstellen. Wählen Sie dazu aus dem Kontextmenü NEU / VORLAGE.
Möchten Sie einen neuen Ordner für die Sortierung Ihrer Vorlagen
einfügen, wählen Sie an dieser Stelle den Eintrag ORDNER.
3. Die eingefügte Vorlage können Sie mit Befehlen aus dem Kontextmenü
bearbeiten. Wählen Sie UMBENENNEN für einen neuen Namen, LÖSCHEN
zum Löschen etc. Wählen Sie BEARBEITEN, um T-SQL-Quelltext
anzugeben.
55
Grundlagen
1
2
3
4
5
6
7
Abbildung 1.23: Vorlagen erstellen und nutzen
56
Grundlagen
4. Die Erstellung eines Vorlagen-Textes ist relativ simpel. Die wichtigste
Überlegung besteht darin, den Quelltext so zu formulieren, dass er nachher
durch die Vorgabe von einigen Parameterwerten in möglichst vielen Fällen
eingesetzt werden kann. Diese Parameter werden mit Hilfe von spitzen
Klammern in der Form <Parametername, Datentyp, Vorgabewert>
angegeben. Sie müssen nicht extra angemeldet werden, sondern werden
automatisch anhand dieser Syntax aus dem Vorlagentext extrahiert und
nachher beim Verwenden dieser Vorlage in einem Dialogfenster angegeben,
um dort ersetzt zu werden. An dieser Stelle soll nicht auf die T-SQL-Syntax
eingegangen werden, da dies ja Thema der vielen nachfolgenden Kapitel
des Buchs ist. Wesentlich ist daher nur die Erstellung, Speicherung und
Verwendung einer Vorlage neben der Angabe und Verwendung eines
Parameters.
5. Wenn Sie die Vorlage erstellt, Parameter hinzugefügt und die Vorlage
gespeichert haben, wollen Sie sie beizeiten aufrufen. Dazu öffnen Sie
einfach die gewünschte Vorlage aus dem Vorlagen-Explorer und wählen
dann ABFRAGE / WERTE FÜR VORLAGENPARAMETER ANGEBEN.
6. In dem sich öffnenden Dialogfenster stehen die verschiedenen Parameter
mit ihren Datentypen und ihren möglichen Vorgabewerten. Ersetzen Sie
ggf. die Werte und bestätigen Sie dieses Dialogfenster. Dadurch ersetzen
Sie die Parameter durch die Vorgabewerte oder die an ihrer Stelle
eingegebenen Werte im Vorlagentext.
7. Führen Sie die geänderte Anweisung aus und erfreuen Sie sich an der
gesparten Arbeitszeit durch Ihre hervorragende Vorlage.
1.2.5
Dokumentation
Trotz der MS SQL Server-Literatur, die Sie bspw. in Form dieses Buchs oder anderer
Bücher dieser Reihe in der Hand halten bzw. im Schrank besitzen, werden Sie letztendlich in der Dokumentation noch mehr Informationen finden. Diese sind vielleicht nicht
57
Grundlagen
mit so vielen Beispielen erläutert wie in einem Buch, doch dafür sind sie sehr detailreich hinsichtlich der verwendeten Technik.
Abbildung 1.24: Dokumentation
Verwenden Sie die Dokumentation folgendermaßen:
1. Rufen Sie die Dokumentation auf, indem Sie START / PROGRAMME / MS
SQL SERVER 2005 / DOKUMENTATION UND LERNPROGRAMME / SQL
SERVER-ONLINEDOKUMENTATION auswählen.
2. Sofern Sie einen bestimmten SQL-Befehl suchen, ist dies eigentlich
besonders einfach, denn dann sollten Sie einfach unter dem Reiter SUCHEN
diesen SQL-Befehl eingeben und die SUCHEN-Schaltfläche betätigen.
3. Wählen Sie innerhalb der verschiedenen Reiter auf der linken Seite einen
Bereich aus, indem Sie die Hilfetexte anzeigen wollen. Dies kann die lokale
Hilfe, die MSDN-Online-Dokumentation auf Deutsch oder Englisch oder
sonstige Informationsplattformen sein. Innerhalb dieser einzelnen Reiter
erscheinen dann im mittleren Bereich verschiedene Artikel, wobei für den
T-SQL-Programmierer in vielen Fällen der erste Artikel bereits der richtige
ist, da hier die Suchfunktion anhand eines SQL-Schlüsselworts aus
verständlichen Gründen besonders gut funktioniert.
58
Grundlagen
1.3
Programmierbarkeit
Unter dem sehr ungewöhnlichen deutschen Wort Programmierbarkeit versteht der MS
SQL Server nicht nur die Fähigkeit, überhaupt nützlichen Quelltext in verschiedenen
Sprachen in der Datenbank zu speichern oder - wie im Fall von T-SQL - direkt auszuführen, sondern auch die verschiedenen programmierten Objekte. Die Syntax, mit der
diese Objekte erstellt werden können, ist neben der sehr umfangreichen Darstellung von
Abfragen und Analysen ein zentrales Thema dieses Buchs. Daher dient dieser Abschnitt
nur als Appetithappen. Sie sollen also sehen, welche Objekte programmiert und sogar in
der Datenbank gespeichert werden können.
Die AdventureWorks-Datenbank ist bereits mit sehr vielen programmierten Objekten
und damit, um im Jargon von MS SQL Server zu bleiben, mit sehr viel Programmierbarkeit ausgestattet. Daher kann es sehr hilfreich sein, diese verschiedenen Objekte
aufzurufen und versuchen, auf ihre Einsatzweise hin zu verstehen.
1.3.1.1
Prozeduren
Eine Prozedur stellt ein kleines Programm dar, das in der Datenbank gespeichert ist und
das innerhalb der Datenbank über SQL oder außerhalb der Datenbank über eine beliebige Programmiersprache aufgerufen werden kann. Typische Einsatzbereiche von Prozeduren sind vereinfachte Datenbearbeitungsroutinen. In diesem Fall verwendet man in
der Anwendung, welche die Datenbank nutzt, nicht den entsprechenden SQL-Befehl für
die Erfassung, Löschung oder Aktualisierung von Daten, sondern ruft die entsprechende
Prozedur auf und übergibt die Daten. Dadurch kann man Validierungen oder beliebige,
über die einfache Datenbearbeitung hinausgehende Anweisungen automatisiert ausführen, ohne in der äußeren Anwendung darauf Rücksicht zu nehmen. Ein weiterer
Einsatzbereich, der gerade für den MS SQL Server sehr wichtig ist, stellt gespeicherte
Abfrage in Form von Prozeduren dar, wobei hier Filterwerte als Parameter übergeben
werden können und dadurch der Datenabruf besonders einfach gestaltet wird. Eine
solche Prozedur liefert eine Ergebnismenge wie eine Abfrage in SQL zurück.
59
Grundlagen
1
2
Abbildung 1.25: Prozeduren untersuchen
Rufen Sie vorhandene Prozeduren folgendermaßen auf. Sie können über den folgenden
Weg auch neue Prozeduren erstellen, sofern Sie nicht einfach den dazu notwendigen TSQL-Quelltext in ein leeres Abfragefenster eingeben. Interessant ist das gleich erwähnte
Kontextmenü noch für den Aufruf der Objekte, die von der Prozedur abhängig sind.
Sofern solche abhängigen Objekte existieren, kann diese Prozedur nicht einfach gelöscht werden, weil die abhängigen Objekte dadurch ungültig würden.
1. Öffnen Sie im Objekt-Explorer im Knoten PROGRAMMIERBARKEIT einer
Datenbank den Knoten GESPEICHERTE PROZEDUREN. Dort sind die
verschiedenen Datenbankprozeduren aufgelistet. Im Knoten GESPEICHERTE
SYSTEMPROZEDUREN befinden sich dagegen für das gesamte
Datenbanksystem nutzbare Prozeduren.
60
Grundlagen
2. Wählen Sie für eine Sie interessierende Prozedur aus dem Kontextmenü den
Eintrag SKRIPT FÜR GESPEICHERTE PROZEDUREN ALS und wählen Sie dann
aus der sich öffnenden Liste einen der Einträge. CREATE steht für
Erstellung, ALTER für Änderung, DROP für Löschung und EXECUTE für
Ausführung. Jeder Befehl kann in einem neuen Abfrage-Fenster geöffnet in
eine Datei bzw. in die Zwischenablage kopiert werden. Um sich also
einfach das Erstellungsskript einer solchen Prozedur anzusehen, wählen Sie
den Eintrag CREATE IN / NEUES ABFRAGE-EDITORFENSTER.
3. Sofern Sie über die Schaltfläche CREATE den Quelltext der Prozedur in
einem neuen Abfragefenster geöffnet werden, sehen Sie die Prozedur so,
wie Sie sie in T-SQL nach der Lektüre des Buchs ebenfalls hätten vorgeben
können. Mit dem sich öffnenden Quelltext lässt sich eine Prozedur in der
Datenbank speichern. Er sieht im Falle einer Änderung bis auf die erste
Zeile genauso aus, wobei in dieser ersten Zeile dann die ALTERAnweisung steht. Diese Ansicht erhalten Sie über die entsprechende
ALTER-Schaltfläche im vorher beschriebenen Kontextmenü. Eine
Änderung löscht eine Prozedur nicht, sodass abhängige Objekte Schaden
nehmen könnten, sondern verändert ihren Quelltext, um bspw. neue
Anforderungen zu berücksichtigen oder Fehler zu korrigieren. Während
diese Schatlflächen mehr für einen Leser des Quelltextes interessant sind,
aber nicht besonders viele Möglichkeiten bieten, mit der Prozedur kreativ
umzugehen, ist die Schaltfläche EXECUTE dagegen darauf ausgerichtet, in
einem T-SQL-Skript die Prozedur auch tatsächlich auszuführen. Wenn die
Verwendung von T-SQL nicht gewünscht wird, obwohl nur noch die
Parameterwerte vorgegeben werden müssen, dann kann man auch die
Schaltfläche GESPEICHERTE PROZEDUR AUSFÜHREN aus dem Kontextmenü
verwenden. Es führt nicht zu einem T-SQL-Skript, sondern vielmehr zu
einem Dialogfenster, in welchem die benötigten bzw. gewünschten
Parameterwerte eingetragen werden können. Dies ist eine vereinfachte
grafische Darstellung der in diesem Schritt beschriebenen T-SQL-Lösung.
61
Grundlagen
1
2
2
3
Abbildung 1.26: Ausführen einer Prozedur
4. Es öffnet sich wiederum ein neues Fenster, in dem nun allerdings nicht die
Erstellung der Prozedur und damit auch ihr Quelltext angegeben wird,
sondern die Ausführung derselben. Das T-SQL-Skript stellt nicht das
bereits zuvor kurz vorgestellte Standard-SQL dar, sondern bietet eine
Variablendeklaration und die Ausführung der Prozedur über die EXECAnweisung, wobei die erstellten Variablen als Parameter übergeben
62
Grundlagen
werden. Die Parameter werden nur deklariert, erhalten allerdings noch
keinen Wert. Dies ist vom Benutzer durchzuführen, wobei hier jede
beliebige T-SQL-Anweisung zum Einsatz kommen kann. Dies schließt
einfache und direkte Wertvorgaben genauso ein wie auch komplexe
Ausdrücke, den Abruf von Abfrageergebnissen, die Verwendung von
Funktionen oder Berechnungen. Im Beispiel, das im Bildschirmfoto für die
Nachwelt festgehalten wurde, handelt es sich zum einen um den klassischen
Fall einer Wertvorgabe und zum anderen um den Abruf eines Aggregats
(größtes Datum) aus der von der Prozedur abgefragten Tabelle.
-- TODO: Set parameter values here.
SET @StartProductID = 970
SET @CheckDate = (SELECT MAX(StartDate)
FROM Production.BillOfMaterials)
5. Schließlich kann man die ausgewählte Prozedur über das bearbeitete Skript
starten, indem man die AUSFÜHREN-Schaltfläche wählt. Im Fall der
ausgewählten Prozedur uspGetBillOfMaterials erhält man für eine StartProduktnummer und ein Vergleichsdatum die zugehörigen Materiallisten in
Form eines Abfrageergebnisse zurück. In diesem Fall hat man also eine in
einer Prozedur versteckte parametrisierte Abfrage ausgeführt. In einem
anderen Fall hat man möglicherweise Daten- oder Systemänderungen
vorgenommen.
1.3.1.2
Funktionen
Eine Funktion hat viele Gemeinsamkeiten mit einer Prozedur, was sich insbesondere
auch in ihrer Darstellung in der grafischen Oberfläche und in diesem einleitenden Niveau dieses Abschnitts deutlich widerspiegelt. Es handelt sich ebenfalls um ein kleines
Programm, das in der Datenbank gespeichert ist und das einen klar begrenzten Verantwortungsbereich im Rahmen der Datenbankbenutzung ausfüllt. Eine Funktion hat ebenfalls die Fähigkeit, Übergabeparameter anzunehmen, kann über das gleiche Kontextme-
63
Grundlagen
nü verschiedentlich in ihrem Quelltext betrachtet oder ausgeführt werden. Daher soll
aus Platzgründen auf eine erneute Darstellung dieses Kontextmenüs verzichtet werden.
Man findet im vorherigen Abschnitt ausreichendes Bildmaterial dazu.
Im Gegensatz zu einer Prozedur besitzt eine Funktion allerdings einen so genannten
Rückgabewert, sodass man sie mit Methoden oder Funktionen einer gewöhnlichen Programmiersprache vergleichen kann, wenn in diesem Vergleich vorausgesetzt wird, dass
die erwähnte Funktion oder Methode ebenfalls einen Rückgabewert liefert. Einige Programmiersprachen unterscheiden ja auch mit Hilfe verschiedener syntaktischer Elemente, ob sich eine Funktionalität eher als Prozedur oder eher als Funktion bezeichnen lassen würde - auch dann, wenn die Programmiersprache an sich diese Unterscheidung
nicht trifft. Entweder handelt es sich um das Schlüsselwort void, um anzugeben, dass
diese Methode keinen Rückgabewert liefert, oder nur um die Verwendung der returnAnweisung für die tatsächliche Rückgabe eines Wertes. Eine Prozedur kann über einen
Ausgabeparameter ebenfalls einen Wert an das aufrufende Programm zurückgeben,
doch ein Rückgabewert zeichnet sich dadurch aus, dass man den Funktionsaufruf auf
die rechte Seite einer Zuweisung bzw. überall dort platzieren kann, wo ein Ausdruck
erwartet wird. Eine Prozedur ist kein solcher Ausdruck, da man den Ausgabeparameter
zunächst abrufen und dann die Variable mit dem abgerufenen Wert wieder als Ausdruck
verwenden könnte.
Wenn eine Funktion sich dadurch auszeichnet, einen Rückgabewert zu haben und als
Ausdruck verwendet werden zu können, dann kann man sie so gestalten, dass sie auch
direkt in SQL zum Einsatz kommen können. Dies bedeutet, dass sie neben solchen
Standardfunktionen wie COUNT, MIN oder SUM in einer SQL-Anweisung stehen und
Werte für einen Filter oder eine Berechnung liefern können. Sie ermöglichen es damit
genauso wie Prozeduren, die Arbeit mit der Datenbank sehr zu vereinfachen, wobei in
einem solchen Fall allerdings ganz gewöhnliches SQL dadurch vereinfacht werden
kann, weil die selbst erstellte Funktion komplexe Berechnungen, Auswertungen oder
Verknüpfungen selbst vornimmt und nur noch die gewünschten, vielleicht sogar parametrisierten Werte zurückliefert.
64
Grundlagen
Wie gerade schon gesehen, ist eine Prozedur in der Lage, ein Abfrageergebnis zurückzuliefern. Diese Fähigkeit besitzt eine Funktion auch, wobei sie allerdings in der FROMKlausel einer Abfrage erscheinen kann, die normalerweise eine Tabellen- oder Sichtreferenz erwartet. Über eine solche Funktion ist es möglich, fertige Teilabfragen mit
bspw. komplexen, sicherheitsrelevanten Bedingungen sowie Verknüpfungen parametrisiert aufzurufen.
1.3.1.3
Trigger
Trigger sind ein weiteres programmierbares Schema-Objekt. Es wird allerdings nur in
ganz wenigen Beispielen genutzt und im Rahmen des Buchs nicht weiter vertieft. Im
Wesentlichen ist die Erstellung von Triggern zwar mit den T-SQL-Fähigkeiten, die in
diesem Buch vermittelt werden, zu bewerkstelligen. Allerdings handelt es sich um ein
hauptsächlich administratives Thema, sodass es besonders gut im Administrationsbuch
aufgehoben ist.
Während eine Prozedur ausdrücklich über ihren Namen aufgerufen wird, ist ein Trigger
entweder einem Schema-Objekt wie einer Tabelle oder einer Sicht zugeordnet oder
wartet auf die Ausführung bestimmter DDL (Data Defintion Language)-Befehle. Die
eine Trigger-Art wird als DML (Data Manipulation Language)-Trigger bezeichnet, da
sie auf die SQL-Anweisungen INSERT, UPDATE und DELETE wartet, welche den
Trigger auslösen und damit seine Anweisungen zur Ausführung bringen. Die andere Art
bezeichnet man als DDL-Trigger, weil diese Trigger auf Anweisungen wie CREATE,
ALTER oder DROP warten, welche zur DDL gehören. Innerhalb eines solchen Triggers
lassen sich nahezu beliebige Anweisungen wie auch in einer Prozedur oder Funktion
angeben.
Die Besonderheit von Trigger liegt tatsächlich ausschließlich in der automatischen Ausführung auf Basis von anderen Befehlen. Dadurch ist es möglich, bestimmte Sicherheits- oder Datenkonsistenzregeln zu programmieren, die mit gewöhnlichem SQL
oder sonstigen Datenbankeinstellungen administrativer Art nicht abgebildet werden
können. Da innerhalb eines Triggers die gesamte T-SQL-Syntax zur Verfügung steht,
65
Grundlagen
stellen Trigger eine wesentliche Fähigkeit von Datenbanken ab, um sicher zu sein und
konsistent zu bleiben.
1.3.1.4
Assemblies
Mit Hilfe der Anweisung CREATE ASSEMBLY name FROM 'C:\assembly.dll'
lassen sich .NET-Assemblies in der Datenbank verankern. Dies eröffnet für den MS
SQL Server ganz neue Möglichkeiten der Datenbank- und Softwareentwicklung. Bislang konnte besonders Oracle neben der datenbankeigenen Programmiersprachen
PL/SQL auch noch zusätzlich anbieten, kompilierte Klassen einer so umfangreichen
Programmiersprache wie Java für die Entwicklung von Datenbankmodulen zu verwenden. Dies ist nun für den MS SQL Server auch in Form der .NET-Technologie möglich
geworden. Assemblies aus den Sprachen C#.NET oder VB.NET sowie natürlich anderen .NET-fähigen Sprachen lassen sich nun direkt in die Datenbank laden. Dies eröffnet
Möglichkeiten, objektrelational zu arbeiten, indem komplexe Datenstrukturen in Form
von Klassen mit mehreren Eigenschaften/Feldern und Methoden abgebildet werden, als
auch Prozeduren, Funktionen und Trigger nicht mehr über T-SQL, sondern direkt über
.NET zu erstellen und sie dann wie gewöhnliche, in T-SQL erstellte Module zu verwenden. T-SQL-Vorwissen ist dennoch notwendig, weil die Organisation und Verwaltung
der Assemblies über T-SQL funktioniert und Abfragen sowie die Erstellung von sonstigen Schema-Objekten weiterhin über T-SQL erfolgt.
1.4
Beispieldatenbank AdventureWorks
Im Normalfall haben wir für die verschiedenen Bücher im Bereich Datenbanken immer
auch eigene Datenbanken entwickelt. In diesem Fall allerdings hat sich dieses Vorgehen
als nicht sonderlich weltverbessernd herausgestellt. Da die AdventureWorks-Datenbank
die Schwächen der sehr vereinfachten Nordwind-Datenbank vollständig überwindet und
sich seit längerer Zeit auch steigender Beliebtheit erfreut sowie, was eigentlich sehr viel
wichtiger ist, derart umfangreich ist, dass eine selbst erstellte Datenbank (sogar im Vergleich zu unseren vorherigen Datenbanken) nicht besser sein kann, haben wir uns ent-
66
Grundlagen
schlossen, zum ersten Mal auf eine vom Hersteller bereit gestellte Beispieldatenbank
zurückzugreifen.
1.4.1
Allgemeine Design-Prinzipien
Die Datenbank ist in unterschiedliche Schemata eingestellt, die mit ihren wesentlichen
Tabellen im nachfolgenden kurz vorgestellt werden. Es werden in diesem Buch aus
Gründen der Übersichtlichkeit nicht alle Tabellen genutzt. Der Umfang der gesamten
Datenbank wäre dafür ein wenig zu groß, doch um keine Langeweile aufkommen zu
lassen, sollte auch nicht nur solch klassische Tabellen wie solche zur Produkt- und
Kundendaten genutzt werden. Bis auf wenige Fälle werden keine eigenen Tabellen
erstellt oder benötigt, sodass die zusammen mit dem Datenbanksystem installierte AdventureWorks-DB unmittelbar mit den bereitgestellten Skripten dieses Buchs Ergebnisse produzieren sollte.
Daten von Angestellten befinden sich im Schema HumanResources. Dabei ist ganz
wesentlich, dass solche typischen Spalten für Name und Adresse gerade nicht in einer
Tabelle wie Employee gespeichert werden. Stattdessen existieren eigene Tabellen namens Contact und Address im Schema Person, welche sämtliche Personen wie
Verkäufer und Kunden jeweils verknüpfen. Dies ist eine Design-Entscheidung, die es
verhindert, die gesamten Kontaktattribute für jedes Objekt des Weltmodells jeweils neu
zu erfassen. Es erschwert allerdings auch gleichzeitig Abfragen, da bereits eine Verknüpfung durchgeführt werden muss, um für Daten aus einer Tabelle wie Employee,
SalesOrderHeader oder VendorContact auch die entsprechenden Kontaktinformationen abzurufen.
Eine weitere wichtige Designentscheidung, die in der AdventureWorks-DB vorgestellt
wird, ist in der Tabelle Address zu erkennen. Auch sie sammelt für alle möglichen
Objekte im Weltmodell von AdventureWorks, die Adresse besitzen können, zentral
diese große Anzahl von Adressen. Eine andere Möglichkeit wäre gewesen, die Felder
dieser Tabelle ebenfalls wie die Felder von der Contact-Tabelle in die einzelnen Tabellen der referenzierenden Tabellen einzufügen. Da im Gegensatz zu den ContactFeldern allerdings auch mehrere Adressen pro Objekt gespeichert werden sollten, d.h.
67
Grundlagen
eine 1:n- (ein Objekt hat mehrere Adressen) oder sogar eine n:m-Beziehung (mehrere
Objekte teilen sich eine Adresse bzw. mehrere Objekte teilen sich mehrere Adressen)
notwendig ist, hätte man neben den Beziehungstabellen, die nun von den einzelnen
Objekten zu dieser zentralen Address-Tabelle führen, die gesamte Address-Tabelle
für die einzelnen Objekte neu erstellen müssen. Entstanden wären dann solche Tabellen
wie EmployeeAddress oder VendorAddress.
Darüber hinaus zeigt die Datenbank genau in diesem Bereich auch noch die Technik
einer so genannten Werteliste. Bei wiederholenden Begriffen, die allerdings nur in einem einzigen Feld einer Tabelle auftauchen, ist es oftmals eine wichtige Frage des Datenbankdesigns, inwieweit hier noch einmal eine eigene Tabelle für diese sich wiederholenden Werte entwickelt werden sollen. Mit Blick auf solche Tabellenkalkulationsprogramme wie MS Excel, in denen Werte, die bereits in der Spalte benutzt wurden,
automatisch angezeigt werden, sobald die ersten identifizierenden Buchstaben eingegeben werden, findet man oftmals kleinere Datenbanklösungen, die solche Wertelisten
(oder besser gesagt: Kandidaten für Wertelisten) nicht eigens auslagern, sondern stattdessen direkt in vollem Wortlaut in der Spalte speichern. Auswahlmenüs in einer äußeren Anwendung lassen sich dann hervorragend mit SELECT DISTINCT-Abfragen ermitteln, die mögliche Duplikate ausblenden und daher quasi genau die Werte anzeigen,
die in einer solchen Wertelistentabelle erscheinen könnten. Allerdings besteht immer
auch die Gefahr, dass unkorrekte Schreibweisen (zusätzliche Leerzeichen, Bindestriche,
allgemeine Rechtschreibfehler sowie korrekte Schreibvarianten oder Singular-/PluralUnterschiede) dazu führen, dass gleichartige Werte verschieden gespeichert werden und
daher Filter schlecht funktionieren oder inkonsistente Daten entstehen. Dies kann teilweise durch eine weitere Normalisierung (Prozess der Bildung eines guten relationalen
Modells) verhindert werden, was konkret bedeutet, Wertelistentabellen zu erstellen.
Solche Tabelle sind die ganzen Type-Tabelle, d.h. Tabellen wie ContactType oder
AddressType. Im klassischen Fall besteht eine solche Tabelle wie ein Array in einer
beliebigen Programmiersprache nur aus dem Primärschlüsselfeld und dem gespeicherten Wert für diesen Schlüssel. Im beschriebenen Fall von AdventureWorks gibt es noch
weitere Spalten wie bspw. das Änderungsdatum, die allerdings für das grundlegende
Verständnis unwichtig sind. Eine solche Werteliste kann man ebenfalls hervorragend in
68
Grundlagen
einem Auswahlmenü in einer äußeren Anwendung abrufen. Man kann darüber hinaus
allerdings auch für konsistente Daten sorgen, sofern Einträge in dieser Wertelistentabelle nicht einfach unkontrolliert von jedem Benutzer vorgenommen werden können und
plötzlich doch wieder die erwähnten verschiedenen Schreibweise für den gleichen Begriff erscheinen. Neben diesem Vorteil bietet dieses Vorgehen den Nachteil, dass ein
Datenabruf mit Textinhalten und nicht nur den Schlüsselwerten zu vielen Verknüpfungen führt, um die ganzen Primärschlüssel-Fremdschlüssel-Beziehungen wieder aufzulösen, die nötig sind, um aus den Schlüsselwerten wieder die eigentlich gemeinten Werte
zu erzeugen.
Ob solche Lösungen gut oder schlecht sind, kann abschließend nicht beurteilt werden.
Wesentlich ist vielmehr, dass man im Rahmen des DB-Designs (was nicht Thema dieses Buchs ist, sondern eines anderen Buchs im Bereich Datenbanken von Comelio Medien sein wird) verschiedene typische Lösungsansätze kennt und sich bewusst für den
einen oder anderen Weg entscheidet. Im Fall von AdventureWorks muss man davon
ausgehen, dass bei einer so großen Datenstruktur dieses Vorgehen dafür sorgt, dass von
jeder Person die gleichen Kontaktdaten gespeichert werden können, ohne dass die Feldstrukturen in mehreren Tabellen erscheinen müssen.
Auch wenn objektrelationale Techniken aus Gründen der Didaktik in der AdventureWorks umgesetzt sind und auch in diesem Buch diskutiert werden, so zeigt dieses Datenmodell bereits Grenzen des relationalen Modells. Dies soll keinesfalls als Technologiekritik verstanden werden, denn die vielen so genannten Alternativen und Verbesserungen konnten sich bislang in keiner Weise durchsetzen. Im Rahmen von Beratungsveranstaltungen und Seminaren gibt es auch an uns oft Fragen mit fast schon spionageartigen Unterton, ob wir denn Datenbanken bzw. Unternehmen wüssten, welche neue
Techniken des relationalen Modells einsetzen. Ab und an treffen wir tatsächlich auf
solche Ansätze, doch im Großen und Ganzen bleibt es bei einigen Buchkapiteln oder
Zeitschriftenbeiträgen, die sich diesem Thema immer wieder annehmen.
Was ist damit gemeint? Ohne bereits die Vorstellung der Technik umfassend vorweg zu
nehmen, soll dennoch kurz auf dieses Thema eingegangen werden, weil die Tabellen, an
denen man es sehr gut erläutern kann, bereits genant wurden. Darüber hinaus handelt es
69
Grundlagen
sich dazu auch noch um solche eigentlich trivialen Datenstrukturen wie Kontaktdaten.
Diese müssten doch selbstverständlich hervorragend und vor allen Dingen problemlos
mit geschlossenen Augen diskussionsfrei modelliert werden können. Interessanterweise
bieten sie allerdings gerade das Paradebeispiel für die scheinbare Notwendigkeit von
weiter gehenden Techniken der Datenbankmodellierung.
Das traditionelle relationale Modell kennt keine Vererbung und keine Möglichkeit,
gleichartige Feldstrukturen mehrfach zu benutzen. Wenn Kunden, Angestellte und Verkäufer die gleichen zehn Felder für Kontaktdaten (Name und Adresse mit jeweiligen
Unterfeldern) benutzen, verwendet man folgendes Vorgehen: jede Tabelle für die drei
genannten Objekte erhält die gleichen zehn Felder. Der Aspekt, dass zusätzlich auch
noch mehrere Adressen einem Objekt zugeordnet werden können, kann vernachlässigt
werden, da er nur ein Spezialproblem darstellt. Wenn man sich eine solche Datenbankstruktur näher ansieht, scheint man auch nicht zufrieden, vor allen Dingen dann nicht,
wenn ähnliche (Teil-)SQL-Befehle für die Datenerfassung notwendig sind oder Änderungen an den zehn Feldern (Datentypen oder Namen) an allen betroffenen Objekten
gleichzeitig vorgenommen werden müssen, um das gesamte Modell wieder konsistent
zu machen. Möglicherweise wird sogar eine Änderung vergessen, sodass die eigentlich
gleich strukturierten zehn Felder doch in einigen wenigen Punkten in den einzelnen
Objekten fehlerhaft verschieden modelliert sind.
Schaut man sich in einem solchen Fall dagegen eine mögliche objektorientierte Softwaremodellierung an, so gibt es eine ganze Reihe von Techniken, um mit solchen
gleichartigen (Unter-)Strukturen umzugehen. Man könnte eine Elternklasse oder eine
abstrakte Klasse erstellen, welche die gemeinsamen Felder/Eigenschaften besitzt und
welche als Elternklasse für die erwähnten Objekte dient. Änderungen in der abstrakten
Klasse oder der Elternklasse würden sich dadurch auf die Kindklassen auswirken. Um
die Vererbungshierarchie nicht auf Basis einer vermutlich nicht so zentralen Struktur
wie den Kontaktdaten aufzubauen (Mehrfachvererbung nicht möglich), kann man stattdessen eine eigene Klasse für die Kontaktfelder entwickeln, welche als Typ in einer/m
Eigenschaft/Feld der erwähnten Objekte genutzt wird. So wirken sich Änderungen der
Oberstruktur weiterhin in den Klienten aus. Als Ergänzung könnte man sich auch noch
70
Grundlagen
eine Schnittstelle denken, welche Methoden anbietet, um Klassen mit Kontakt(unter)feldern zu nutzen und bspw. Adresszeilen oder zusammen gesetzte Namensbestandteile zu liefern.
In einer relationalen Datenbank ist dies alles nicht möglich. Weder Vererbung von Tabellenstrukturen noch Datentypen mit mehreren Unterfeldern sind möglich. Erst die
objektrelationalen sowie natürlich hierarchischen Techniken bieten hier einen Ansatz
an, wobei insbesondere die objektrelationalen oft diskutiert werden. Dies bedeutet
nichts Anderes, als dass man in einer nahezu beliebigen .NET-Sprache für den MS SQL
Server (andere Datenbanksysteme wie Oracle nutzen hier PL/SQL oder Java) die erwähnte Klasse erstellt und sie mit den benötigten Feldern ausstattet, um die in verschiedenen Objekten verwendeten gleichen Feldstrukturen gut und zentral abzubilden. Eine
Alternative stellt dagegen eine zentrale Tabelle dar, welche von allen betroffenen Objekten referenziert wird. Dies ermöglicht es, die gleichartigen Strukturen auszulagern
und dadurch zentral ggf. sogar zu verändern. Es erfordert allerdings Verknüpfungen
zwischen den Tabellen, um auf diese Strukturen zuzugreifen. Dieses Verfahren wurde
in der AdventureWorks-DB verwendet.
1.4.2
Darstellung einzelner Tabellenbereiche
Innerhalb der Personaldaten werden die Angestellten in einer Employee-Tabelle gespeichert. Sie arbeiten in einer Abteilung, die in einer Department-Tabelle dargestellt
ist. Da ein Angestellter nicht notwendigerweise permanent in der gleichen Abteilung
arbeitet, sondern von Zeit zu Zeit auch wechselt, ist keine direkte Beziehung zwischen
Department und Employee vorhanden, sondern stattdessen eine Beziehungstabelle
EmployeeDepartmentHistory eingefügt, welche insbesondere den Eintritt in eine
neue Abteilung und den möglichen Austritt enthält. Darüber hinaus arbeiten die Mitarbeiter in Schichten, wobei die Schichteinteilung in der Shift-Tabelle angegeben ist.
Betreten Sie eine Abteilung werden sie ebenfalls zu einer Schicht zugeordnet, sodass in
der erwähnten EmployeeDepartmentHistory-Tabelle auch eine Verknüpfung zu
dieser Shift-Tabelle existiert.
71
Grundlagen
Abbildung 1.27: Schema HumanResources
Die Kontaktdaten aller Objekte des modellierten Weltausschnitts, die Kontaktdaten
aufweisen können, sind zentral in zwei Tabellen gespeichert. Die Contact-Tabelle
enthält die allgemeinen persönlichen Daten wie Vorname, Nachname etc. Die
Address-Tabelle dagegen enthält Addressinformationen mit Straße, Stadt und PLZ.
Für beide Tabellen existieren dann auch noch jeweils zwei Type-Tabelle, d.h. eine Tabelle namens ContactType und eine AddressType-Tabelle, durch die die jeweiligen
Datensätze kategorisiert werden können. Länder und allgemeine geografische Bereiche,
die außerhalb von konkreten Adressen liegen bzw. die wie Wertelisten angesehen wer-
72
Grundlagen
den können, sind dann in verschiedenen weiteren Tabellen untergebracht. Dies sind
solche Tabellen wie CountryRegion oder StateProvince.
Abbildung 1.28: Schema Person
Die Produkte werden im Schema Production in eine Vielzahl von Tabellen eingeteilt.
Davon werden in den Beispielen in diesem Buch nicht alle verwendet, da die Gesamtzahl der Informationen nicht für alle Beispiele didaktisch wertvoll ist, sofern man nicht
gerade ein Beispiel benötigt, um zehn Tabellen zu verknüpfen. Die zentrale Tabelle ist
Product, in der die wesentlichen Informationen eines Produkts wie Nummer, Bezeichnung, Farbe, Sicherheitsbestand im Lager, Standardkosten und Listenpreis enthalten
sind. Für die beiden Attribute Größe und Verkaufseinheit gibt es ausgelagerte Tabellen
mit Wertelisten. Kunden bewerten die Produkte in einer ProductReview-Tabelle.
Fotos zu den Produkten speichert die Tabelle ProductPhoto in Form eines Pfads. Die
historischen Preise werden in einer ProductListPriceHistory gespeichert. Die
Kategorien schließlich, die wieder für verschiedene Beispiele sehr hilfreich sind, um
Produkte zu kategorisieren und über diese Kategorien Aggregate abzubilden, sind in
73
Grundlagen
zwei Tabellen gespeichert. Man unterscheidet zwei Kategorieebenen: zum eine die
ProductSubcategory und zum anderen die ProductCategory, wobei die Product-Tabelle zunächst die Unterkategorie verknüpft und diese Unterkategorie wiederum die Oberkategorien.
Abbildung 1.29: Schema Production
Die Verkaufsinformationen befinden sich im Schema Sales, wobei aus den sehr vielen
Tabellen in den Beispielen im Wesentlichen nur die SalesOrderHeader-Tabelle benutzt wird, da sie die zusammenfassenden Informationen für einen Verkauf (Kopfdaten)
enthält. Sie referenziert in sehr vielen Fremdschlüsselspalten eine große Menge an anderen Tabellen, da sie als typische Buchungstabelle die Geschäftsaktivitäten der Firma
abbildet. Zu den Kopfdaten eines Auftrags gehören solche Informationen wie drei verschiedene Zeiten (Bestell-, Auslieferungs-, Fälligkeitsdatum), Preise (Netto, Steuer,
Total, Fracht) und die erwähnten Referenzen wie Adressen (Liefer-, Rechnungsadresse),
Kunden- und Kontaktinformationen sowie Verkaufsgebiet.
74
Grundlagen
Abbildung 1.30: Schema Sales
Neben diesen Kopfdaten, die ja bereits umfangreich genug sind, wenn man nur die
ganzen Referenzen auflöst, gibt es noch weitere Tabellen in diesem Schema. Jede Bestellung enthält natürlich auch einzelne Positionen, die in SalesOrderDetail gespeichert werden, welche insbesondere eine Referenz zur Product-Tabelle enthält. Die
Bestelldetails werden in fast allen Beispielen außer Acht gelassen.
Ähnliches gilt auch für die verschiedenen Währungen, die man eigentlich für die Bildung von Umsatzaggregaten mit Wechselkursen berücksichtigen müsste. Dazu gibt es
die Tabellen CurrencyRate-Tabelle mit den zeitbezogenen Wechselkursdaten und die
Currency-Tabelle mit dem Währungsnamen als Werteliste. Die ermittelten Umsatzzahlen, die in den verschiedenen Aggregatbeispielen genutzt werden, müssten also
eigentlich noch jeweils in eine gemeinsame Währung umgerechnet werden, was jedoch
zu nebensächlichem Zusatzquelltext führt, der das eigentliche Beispiel nur vernebelt.
Die einzelnen Bestellungen, sofern Sie nicht über den Webshop eingegangen sind, können einem Verkäufer zugeordnet werden, die Personen darstellen, einem Gebiet zugeordnet sind, Verkaufszahlengeschichte besitzen und Quoten erfüllen müssen. Dieser
Bereich wird nur in wenigen Beispielen betrachtet.
75
Grundlagen
Abbildung 1.31: Schema Purchasing
Die Produkte werden teilweise hergestellt, teilweise aus verschiedenen Bestandteilen
montiert und teilweise nur weiterverkauft. Für die verschiedenen Fertigteile, die beschafft werden müssen, existiert ein eigenes Schema namens Purchasing, in dem
diese Einkäufe abgebildet werden. Dieses Schema wird in den verschiedenen Beispielen
nur wenig genutzt, kann allerdings hervorragend für eigenes Arbeiten auf Basis der
Beispiele zum Sales-Schema genutzt werden, da der Einkauf von den Datenstrukturen
her ähnlich aufgebaut ist.
Die zentrale Tabelle ist hier PurchaseOrderHeader, welche die Kopfdaten einer
Bestellung enthält. Die eine Bestellung konstituierenden Posten befinden sich in der
PurchaseOrderDetail-Tabelle, welche eine Referenz zur Product-Tabelle enthält,
da diese Tabelle sowohl die zu kaufenden als auch die zu verkaufenden Produkte enthält. Die Kopfdaten enthalten wiederum Zeitinformationen (Bestell-, Lieferdatum)
76
Grundlagen
sowie Referenzen wie den Angestellten, der die Bestellung ausgelöst hat, und den Verkäufer.
Die letzte Abbildung enthält eine Komplettansicht des Datenmodells, welches zwar
völlig unleserlich ist, aber kurz zeigen soll, wie umfangreich die Datenstruktur in Wirklichkeit ist. Auch wenn in den Beispielen bereits deutlich mehr Tabellen genutzt werden
als bei einem Buch, welches die Nordwind- oder die Pubs-DB einsetzen, so gibt es in
Wirklichkeit noch viel mehr interessante Daten und Strukturen zu entdecken. Für Beispiele stehen damit sehr viel mehr Möglichkeiten zur Verfügung als früher, sodass es
für uns nur logisch schien, bei einer neu beginnenden Reihe zum MS SQL Server auch
die neue, sehr viel bessere Datenbank zu verwenden.
Abbildung 1.32: Komplettansicht
77
Grundlagen
78
Einfache Abfragen
2 Einfache Abfragen
79
Einfache Abfragen
80
Einfache Abfragen
2 Einfache Abfragen
Dieses Kapitel stellt den ersten Teil der beiden Kapitel zu Abfragen dar. Der SELECTBefehl ist sicherlich der wichtigste SQL-Befehl, sobald überhaupt eine Datenbank vorhanden (schon erledigt) und Daten eingetragen (ebenfalls schon eledigt) sind. Er ermöglicht den Zugriff auf die einzelnen Tabellen und Sichten in Form einfacher Abfragen
oder innerhalb eines vollständigen Transact SQL-Programms wie ein Skript oder eine
Prozedur/Funktion bzw. ein Trigger. Sofern man die Datenbank verlässt, taucht SQL
wiederum in beliebigen Programmiersprachen wie C#.NET auf und besitzt dort den
gleichen Aufbau wie innerhalb der Datenbank.
Es steht zu befürchten, dass einige Leser nicht zum ersten Mal eine Datenbank befragen, sodass der Beginn bei der berühmten Hallo-Welt-Abfrage SELECT * FROM tabelle womöglich zu einfach scheint. Beide Kapitel sollen allerdings auch die Klauseln
zeigen, die möglicherweise nicht so bekannt sind, sodass Fortgeschrittene vielleicht nur
10 oder 20 Seiten überschlagen sollten, ehe man wieder mit dem Text beginnt.
2.1
Grundstruktur von SELECT
Wie schon erwähnt, stellt der SELECT-Befehl einen der mächtigsten bzw. wenigstens
umfangreichsten SQL-Befehle dar, weil er im Gegensatz zu vielen anderen derart viele
Klauseln aufweist, dass die allgemeine Syntax fast schon eine ganze Buchseite füllen
kann. Im Gegensatz zu anderen ebenfalls umfangreichen Anweisungen verhält es sich
bei SELECT zudem auch noch so, dass man tatsächlich für kniffelige Fragestellungen in
die Randbereiche der Syntax kriechen muss, sofern man nicht über Umwege das gleiche
Ergebnis erhalten möchte.
Die allgemeine Syntax für einfache Abfragen hat folgendes Format:
SELECT spaltenliste
[ FROM tabelle ]
[ WHERE bedingung ]
81
Einfache Abfragen
[ GROUP BY gruppierungsausdruck ]
[ HAVING bedingung ]
[ ORDER BY sortierungsausdruck [ ASC | DESC ] ]
Wie man sehen kann, sind alle Klauseln, welche direkt nach SELECT folgen, optional
und haben zusammen mit SELECT selbst folgende Bedeutung:
ƒ
Spaltenliste: Hier zählt man die einzelnen Spalten auf, die im Abfrageergebnis
wieder erscheinen sollen. Die Reihenfolge der Spalten entscheidet darüber, in
welcher Reihenfolge sie in der Ergebnismenge erscheinen. Auch nicht angegebene
Spalten können in anderen Klauseln referenziert werden. Zusätzlich können in
dieser Liste so genannte Aliasnamen für Spalten angegeben werden.
ƒ
Tabellenangabe: Nach der FROM-Klausel schließt sich wenigstens eine Tabellenoder Sichtangabe an, aus der die Daten abgerufen werden sollen und in der die
genannten Spalten auftreten müssen. Es besteht mit Blick auf die komplexen
Abfragen auch die Möglichkeit, mehrere Tabellenreferenzen zu nennen, um Daten
aus mehreren Tabellen abzurufen.
ƒ
Suchbedingung: Nach der WHERE-Klausel folgt wenigstens eine Suchbedingung,
welche die Anzahl der Werte in der Ergebnismenge auf diejenigen verringert,
welche die Bedingung erfüllen. Innerhalb dieser Klausel können mehrere
Bedingungen aufeinander folgen und über Gruppenbildung zusammengefügt
werden, um komplexe Bedingungen zu formulieren.
ƒ
Gruppierung: Nach der GROUP BY-Klausel folgt ein Gruppierungsausdruck, mit
der gleichartige Werte anhand eines Spaltennamens (Standardfall) oder eines
anderen Ausdrucks wie bspw. eines Funktionsaufrufs zusammengefügt / gruppiert
werden können. Dies wird im Normalfall in Zusammenhang mit so genannten
Aggregatfunktionen wie Zählen oder Summieren in der Spaltenliste genutzt und
liefert einfache analytische Ergebnisse.
ƒ
Gruppensuchbedingung: Nach der HAVING-Klausel ist es möglich, Bedingungen
für die Gruppen anzugeben, um nur solche Gruppen in die Ergebnisliste
82
Einfache Abfragen
aufzunehmen, welche die Bedingung erfüllen. Syntaktisch entspricht die HAVINGKlausel der WHERE-Klausel und bietet auch die Möglichkeit, komplexe
Bedingungen zu formulieren.
ƒ
Sortierung: Nach der ORDER BY-Klausel folgt wenigstens eine Spalte oder ein
Funktionsaufruf mit der optionalen Angabe ASC (Standardwert, aufsteigende
Sortierung) oder DESC (absteigender Sortierung), um anhand dieser Spalte die
gesamte Ergebnismenge zu sortieren. Mehrere Spaltenangaben führen dazu, dass
die Ergebnisse mehrfach sortiert werden.
2.1.1
Spaltenauswahl
Die einfachste Abfrage enthält anstelle einer einzigen Spalte oder einer Spaltenliste nur
den Asteriskus, das Sternchen. Sie entspricht quasi einem Hallo-Welt-Beispiel in einer
beliebigen Programmiersprache und ist in jeder Datenbank lauffähig. Sie liefert alle
Spalten und alle Zeilen der Tabelle zurück. Aus diesem Grund sollte in einer realen
Datenbank mit unbekanntem Datenbestand dieser Befehl nicht aufs Geratewohl zum
Einsatz kommen sollen. Stattdessen sollte man die Ergebnismenge mit Hilfe der COUNTFunktion zählen bzw. wenigstens anhand des Primärschlüssels einschränken.
Die nachfolgende Abfrage ruft die gesamte Employee-Tabelle aus dem HumanResources-Schema ab. Da einige Spalten sehr viele Zeichen zulassen und sie auch aufgrund ihrer Anzahl nicht abgedruckt werden können, gibt es keinen Ergebnisausdruck.
SELECT * FROM HumanResources.Employee
211_01.sql: Auswahl aller Spalten
Die nachfolgende Abfrage grenzt die Spaltenliste auf die drei Spalten EmployeeID,
Gender (Geschlecht mit den Werten m und f) und Title (Positionstitel in der Firma)
ein.
SELECT EmployeeID, Gender, Title
FROM HumanResources.Employee
83
Einfache Abfragen
211_02.sql: Auswahl weniger Spalten
Man erhält als Ergebnis eine hier deutlich gekürzte Ergebnismenge, in der die Spalten in
der in der Spaltenliste angegebenen Reihenfolge ausgegeben werden. Eine andere Anordnung in der Spaltenliste führt auch zu einer anderen Reihenfolge in der Ergebnismenge.
EmployeeID
Gender Title
----------- ------ ----------------------------------------1
M
Production Technician - WC60
2
M
Marketing Assistant
2.1.2
Aliasnamen
Es ist möglich, so genannte Aliasnamen für Spalten anzugeben. In einer einfachen Abfrage dienen diese Aliasnamen zunächst nur der schöneren Ausgabe in der Ergebnisliste. Dies ist insbesondere dann sehr wichtig, wenn ein Spaltenwert über einen Funktionsaufruf erzeugt wird und ansonsten der Funktionsname auch im Spaltennamen erscheint.
In einer komplexen Abfrage dagegen sind sie teilweise sogar notwendig, wenn die Ergebnisse der Abfrage in einem anderen Zusammenhang von einer so genannten äußeren
Abfrage weiter verwendet werden und dann gewöhnliche Bezeichner benötigt werden.
SELECT EmployeeID AS [Pers-Nr],
Gender
AS Geschlecht,
Title
AS [Titel in der Firma]
FROM HumanResources.Employee
211_03.sql: Aliasnamen
Man erhält im Ergebnis tatsächlich die Aliasnamen inkl. Bindestrich und Leertaste in
den Spaltenköpfen:
84
Einfache Abfragen
Pers-Nr
Geschlecht Titel in der Firma
----------- ---------- -------------------------------------1
M
Production Technician - WC60
2
M
Marketing Assistant
SELECT EmployeeID AS [Pers-Nr],
Gender
AS Geschlecht,
Title
AS [Titel in der Firma]
FROM HumanResources.Employee
Pers-Nr
Geschlecht Titel in der Firma
----------- ---------- --------------------------------------1
M
Production Technician - WC60
2
M
Marketing Assistant
Abbildung 2.1: Zusammenhang zwischen Anweisung und Ausgabe
Für Tabellen ist die Vorgabe eines Aliasnamen ebenfalls möglich. Sie verändert nicht
die Ausgabe, sondern die Möglichkeit, wie Tabellen angesprochen werden können.
Diese Aliasnamen eignen sich bei so genannten qualifizierten Spaltennamen, um sie bei
sehr langen Tabellennamen als Abkürzung, Selbstverknüpfungen sowie so abgeleiteten
Tabellen zu verwenden. Die letzten beiden Themen werden in einem späteren Abschnitt
behandelt.
FROM HumanResources.Employee AS emp
211_04.sql: Aliasnamen für Tabellen
Zusätzlich ist es möglich, das AS-Schlüsselwort sowohl bei Spalten- wie auch bei Tabellenaliasnamen wegzulassen. Dies ändert natürlich nur die Syntax und nicht die Ausgabe
des Ergebnisses. Allerdings wird die Lesbarkeit durch die Verwendung von AS und
85
Einfache Abfragen
natürlich die hübsche Formatierung erhöht, was die Mühe, zwei zusätzliche Buchstaben
zu schreiben, möglicherweise wieder ausgleicht.
SELECT EmployeeID [Pers-Nr],
Gender
Geschlecht,
Title
[Titel in der Firma]
FROM HumanResources.Employee emp
211_05.sql: Aliasnamen ohne AS-Schlüsselwort
2.1.3
Qualifizierte Spaltennamen
Die Spaltennamen können qualifiziert auftreten, was bedeutet, dass Schema- und Tabellename vor den Spaltennamen geschrieben werden. Dabei kann man entweder den tatsächlichen Namen oder den innerhalb der Abfrage vergebenen Aliasnamen verwenden.
Bei einfachen Abfragen ergibt sich dadurch kein besonderer Vorteil und besteht auch
keine Verpflichtung, qualifizierte Spaltennamen zu verwenden. Die Qualifizierung ist
dagegen zu verwenden, wenn mehrere Tabellen miteinander verknüpft werden und
gleiche Spaltennamen in den einzelnen Tabellennamen auftreten. Um diese dann wiederum zu unterscheiden, ist die Qualifizierung notwendig.
SELECT HumanResources.Employee.EmployeeID,
HumanResources.Employee.Gender,
HumanResources.Employee.Title
FROM HumanResources.Employee
SELECT emp.EmployeeID,
emp.Gender,
Title
86
Einfache Abfragen
FROM HumanResources.Employee AS emp
211_06.sql: Qualifizierte Spaltennamen
2.2
Bedingungen
Im Normalfall ist es nicht sonderlich interessant, einfach alle Daten einer Tabelle abzurufen.
2.2.1
Einfache Bedingungen und Operatoren
Die einfachste Bedingung besteht aus einem Vergleich auf Gleichheit. Bei diesem Operator wie auch bei anderen Operatoren ist zu unterscheiden, ob man einen Vergleich für
einen Zahlendatentyp oder einen Zeichenkettendatentyp formuliert. Bei Zahlen tritt der
zu vergleichende Wert ohne Anführungszeichen auf. Bei Zeichenketten dagegen muss
der entsprechende Wert innerhalb von Anführungszeichen gesetzt werden.
2.2.1.1
Prüfung auf Gleichheit
Die nachfolgende Abfrage ruft nur die Datensätze aus der Contact-Tabelle ab, welche
den Wert Mr. in der Spalte Title enthalten.
SELECT Title, FirstName, LastName
FROM Person.Contact
WHERE Title = 'Mr.'
221_01.sql: Einfache Bedingung
Die nächste Abfrage nimmt zusätzlich noch die Spalte ContactID hinzu, welche den
Primärschlüssel enthält. Da hier eine Ganzzahl gespeichert ist, entfallen – wie oben
erwähnt – die Anführungszeichen.
SELECT ContactID, Title, FirstName, LastName
FROM Person.Contact
87
Einfache Abfragen
WHERE ContactID = 15
221_02.sql: Einfache Bedingung
Man erhält als Ergebnis im ersten Fall eine längere Liste und im zweiten Fall nur noch
einen einzigen Datensatz, da ja eine Gleichheitsbedingung auf den Primärschlüssel
ausgeführt werden.
ContactID
Title
FirstName
LastName
----------- -------- ----------------- ----------15
Ms.
Kim
Akers
Neben dem Gleichheitszeichen gibt es die auch in anderen Sprachen und natürlich Datenbanken vorhandenen Operatoren für Vergleiche, die in nachfolgender Tabelle aufgelistet sind.
Operator
Bedeutung
>
größer als
<
kleiner als
>=
größer gleich als
!<
nicht kleiner als
<=
kleiner gleich als
!>
nicht größer als
=
gleich
!= oder <>
Ungleich
BETWEEN ausdruck1 AND ausdruck2
größer gleich ausdruck1 und kleiner
gleich ausdruck2
IS [NOT] NULL
(un)gleich NULL
[NOT]
88
IN
(ausdruck1,
(un)gleich ausdruck1 oder (un)gleich
Einfache Abfragen
ausdruck2,...)
ausdruck2
Operatoren
Neben den gewöhnlichen Vergleichsoperatoren, die exakt so funktionieren wie in den
bekannten Programmiersprachen sind insbesondere die in der Tabelle zum Schluss
aufgeführten Vergleichsoperatoren sehr nützlich. Sie ermöglichen die Verkürzung von
ansonsten nur in Kombination einzusetzenden Operatoren. Die nachfolgenden Beispiele
zeigen einige dieser Operatoren im Einsatz.
2.2.1.2
Prüfung auf Verhältnisse
Die erste Abfrage prüft darauf, ob der LastName kleiner gleich dem Buchstaben B ist.
-- Nachname kleiner B
SELECT ContactID, Title, FirstName, LastName
FROM Person.Contact
WHERE LastName < 'B'
221_03.sql: Verhältnisse
2.2.1.3
Prüfung auf Wertebereiche
Die zweite Abfrage prüft analog darauf, ob der LastName zwischen B und D liegt,
wobei die beiden Grenzen eingeschlossen sind.
-- Nachname zwischen B und D (jeweils eingeschlossen)
SELECT ContactID, Title, FirstName, LastName
FROM Person.Contact
WHERE LastName BETWEEN 'B' AND 'D'
221_04.sql: Wertebereiche
89
Einfache Abfragen
2.2.1.4
Prüfung auf NULL
Die dritte Abfrage prüft dagegen mit IS [NOT] NULL darauf, ob die Title-Spalte den
Wert NULL enthält. Dies ist nicht dasselbe wie der Einsatz der gewöhnlichen Operatoren
zum Test auf Gleichheit und Ungleichheit. NULL gilt als eigenständiger Wert, der auch
nicht dasselbe ist wie eine leere Zeichenkette oder gar der Zahlwert 0. Stattdessen beschreibt der Wert NULL den Feldzustand, dass kein Wert vorhanden ist bzw. kein Wert
bekannt ist. Dabei kann NULL für jeden Datentyp eintreten, sofern in der jeweiligen
Spalte der NULL-Wert zulässig ist.
-- Anrede unbekannt (gleich NULL)
SELECT ContactID, Title, FirstName, LastName
FROM Person.Contact
WHERE Title IS NULL
221_05.sql: NULL-Werte
2.2.1.5
Prüfung auf Wertelisten
Die vierte Abfrage setzt dagegen den [NOT] IN-Operator ein. Er ersetzt eine längere
Abfrage, in der jeweils die einzelnen Werte in der Werteliste auf Gleichheit geprüft
werden. Daher untersucht die nachfolgende Abfrage diejenigen Angestellten mit den in
der IN-Klausel angegebenen Namen.
-- Nachname gleich Adams, Miller, Perez
SELECT Title, FirstName, LastName
FROM Person.Contact
WHERE LastName IN ('Adams', 'Miller', 'Perez')
221_06.sql: Wertelisten
90
Einfache Abfragen
2.2.2
Boolesche Operatoren
Oftmals genügt es überhaupt nicht, nur auf eine einzige Bedingung zu prüfen. Bedingungen können gleichzeitig oder auch nur alternativ zutreffen; sie können sogar in unterschiedlichen Kombinationen auftreten, wobei diese Kombinationen wiederum gleichzeitig oder alternativ gelten. Mit Hilfe der booleschen Operatoren ist es möglich, solche
komplexen WHERE-Bedingungen zu formulieren.
2.2.2.1
Einfache Bedingungen
Im einfachsten Fall möchte man zwei Bedingungen gleichzeitig überprüfen, d.h. diese
beiden Bedingungen müssen von den Datensätzen erfüllt sein, welche in die Ergebnismenge übernommen werden. Dabei schließt man weitere Bedingungen mit Hilfe von
AND an die ursprüngliche Bedingung n der WHERE-Klausel an. Im nachfolgenden Beispiel sind nur solche Produkte gesucht, die die Größe S aufweisen und von gelber Farbe
sind.
-- Produkte in Größe S und Gelb
SELECT Name, Size, Color
FROM Production.Product
WHERE Size = 'S'
AND Color = 'Yellow'
222_01.sql: UND-Verknüpfung
Man erhält genau einen Datensatz zurück, auf den beide Bedingungen gleichzeitig zutreffen.
Name
Size
Color
----------------------------------- ----- -------Short-Sleeve Classic Jersey, S
S
Yellow
91
Einfache Abfragen
Anders verhält es sich mit einer Alternative. In diesem Fall ist die Ergebnismenge im
Normalfall deutlich größer als bei der Verwendung von AND. So kehrt man im nachfolgenden Beispiel ganz einfach die Suche um und interessiert sich für Produkte von Größe S oder gelber Farbe. Dies drückt man mit Hilfe des OR-Operators anstelle von AND
für die Bedingungen nach der ersten Bedingung aus.
-- Produkte in Größe S oder Gelb
SELECT Name, Size, Color
FROM Production.Product
WHERE Size = 'S'
OR Color = 'Yellow'
222_01.sql: ODER-Verknüpfung
Man erhält eine größere Menge, für die der eine oder der andere Wert zutrifft. Wie man
leicht sehen kann, ist in den Datensätzen die Größe S vorhanden, wenn die Farbe möglicherweise gerade nicht Gelb ist. Andererseits ist in den Datensätzen die Farbe Gelb
vorhanden, wenn die Größe möglicherweise gerade nicht S ist. Lediglich bei dem Datensatz, der auch schon in der vorherigen Abfrage gefunden wurde, treffen beide Bedingungen zu. Er ist zwar nicht noch einmal abgedruckt, gehört aber auch zur Ergebnismenge.
Name
Size
Color
---------------------------------- ----- -------Long-Sleeve Logo Jersey, S
S
Multi
Road-550-W Yellow, 38
38
Yellow
ML Road Frame-W - Yellow, 48
48
Yellow
Men's Sports Shorts, S
S
Black
Women's Tights, S
S
Black
92
Einfache Abfragen
2.2.2.2
Verschachtelte Bedingungen
Auf Basis der einfachen kombinierten Bedingungen lassen sich durch Einsatz von
Klammern auch erweiterte Kombinationen bzw. Gruppen von Bedingungen bilden. So
soll in der nachfolgenden Abfrage die Größe fixiert werden und in jedem Fall den Wert
S aufweisen. Mit Blick auf die Farbe ist dies allerdings nicht so. Hier genügt es, dass die
Farbe entweder Gelb oder Schwarz ist, wobei nun der OR-Operator zwischen den beiden
Bedingungen für die Farbe sitzt und diese gesamt Gruppe mit Hilfe des AND-Operators
an die Bedingungen zur Größensuche angeschlossen wird.
-- Produkte in Größe S und Gelb oder Schwarz
SELECT Name, Size, Color
FROM Production.Product
WHERE Size = 'S'
AND (Color = 'Yellow' OR Color = 'Black')
222_02.sql: Innere UND-Verknüpfung
Durch die Fixierung auf die Größe findet man in der Ergebnismenge ausschließlich
Produkte der Größe S. In der Spalte für die Farbe allerdings befinden sich entweder die
beiden Werte für Gelb oder Schwarz, die mit Hilfe des AND-Operators angeschlossen
und in Kombination über den OR-Operator angeschlossen wurden.
Name
Size
Color
-------------------------------- ----- --------------Men's Sports Shorts, S
S
Black
Women's Tights, S
S
Black
Short-Sleeve Classic Jersey, S
S
Yellow
93
Einfache Abfragen
WHERE Size = 'S'
AND (Color = 'Yellow' OR Color = 'Black')
Name
Size Color
------------------------------------------ ----- --------------Men's Sports Shorts, S
S
Black
Women's Tights, S
S
Black
Short-Sleeve Classic Jersey, S S
Yellow
Abbildung 2.2: Funktionsweise verschiedener Operatoren
Sofern man diese Operatoren zum ersten Mal im Einsatz sieht, kann es sein, dass man
sich im Kurs zu einer Frage oder beim Lesen (und natürlich Ausprobieren!) eines Buchs
zur Frage verführt sieht, was wohl passieren würde, wenn statt des OR-Operators zwischen den beiden Bedingungen für die Farbe ein AND stünde. Sobald man die Antwort
hört, ist man dann typischerweise ganz verblüfft, dass man sich so hat verwirren lassen.
Selbstverständlich ist dann die Ergebnismenge mehr als übersichtlich, weil sie keine
Zeilen enthält. Es gibt kein Produkt, das gleichzeitig zwei verschiedene Werte im selben
Feld besitzen kann. Für eine Datenbank weist jedes Produkt nur eine Farbe auf.
Wie man sich darüber hinaus denken kann, lässt sich die Formulierung von solchen
Ausdrücken nahezu beliebig komplex aufbauen, indem ganz einfach weitere Bedingungen angehängt und kombiniert werden. Das nachfolgende Beispiel zeigt eine erweiterte
Suche, die eine Größe von S oder M fordert und gleichzeitig eine Kategorie von 21 und
18. Gleichzeitig soll die Farbe entweder reines Gelb oder gemischt sein. Der Quelltext
zeigt darüber hinaus auch noch die alternative Formulierung von OR-Ketten über den
[NOT] IN-Operator, der in diesen Fällen immer die richtige Wahl darstellt.
-- Produktkategorie T-Shirt oder kurze Hose
-- UND Größe S oder M
-- UND Farbe Gelb oder gemischt
94
Einfache Abfragen
SELECT Name, Size, Color, ProductSubcategoryID
FROM Production.Product
WHERE Size IN ('S', 'M')
AND (ProductSubcategoryID = 21
OR ProductSubcategoryID = 18)
AND (Color = 'Yellow' OR Color = 'Multi')
-- Alternative Formulierung
SELECT Name, Size, Color, ProductSubcategoryID
FROM Production.Product
WHERE Size IN ('S', 'M')
AND ProductSubcategoryID IN (21, 18)
AND Color IN ('Yellow', 'Multi')
222_02.sql: Komplexe Verknüpfung und Alternative
Als Ergebnis erhält man eine Liste mit Produkten, die allen Bedingungen gleichzeitig
genügen. Die Größe ist nur S oder M, und die Farbe ist Gelb oder gemischt, und die
Kategorie ist 18 oder 21.
Name
Size
Color
Product
SubcategoryID
------------------------------ ----- --------- -------------Long-Sleeve Logo Jersey, S
S
Multi
21
Long-Sleeve Logo Jersey, M
M
Multi
21
Men's Bib-Shorts, S
S
Multi
18
95
Einfache Abfragen
Men's Bib-Shorts, M
M
Multi
18
Short-Sleeve Classic Jersey, S S
Yellow
21
Short-Sleeve Classic Jersey, M M
Yellow
21
Zum Schluss folgt noch ein Beispiel, das zeigen soll, wie auch alle anderen Operatoren
für den Vergleich zum Einsatz kommen können. Neben der Forderung, dass die Größe
S oder XL sein soll, darf der Listenpreis nur zwischen 30 und 50 mit eingeschlossenen
Grenzen liegen. Alternativ lässt sich ein solchermaßen beschriebenes Werteintervall
auch mit Hilfe von BETWEEN…AND beschreiben, dem in jedem Fall der Vorzug zu geben
ist.
-- Größe nicht S oder XL UND Listenpreis zwischen 30 und 50
SELECT Name, ListPrice
FROM Production.Product
WHERE Size NOT IN ('S', 'XL')
AND (ListPrice >= 30 AND ListPrice <= 50)
-- alternative Formulierung
-- AND ListPrice BETWEEN 30 AND 50
222_02.sql: Komplexe Verknüpfung und Alternative
Die stark gekürzte Ergebnisliste zeigt, dass die Größen und die beiden Preise genau zu
den gewünschten Bedingungen passen.
Name
ListPrice
--------------------------------- --------------------Long-Sleeve Logo Jersey, M
49,99
Full-Finger Gloves, L
37,99
96
Einfache Abfragen
2.2.3
Mathematische Operatoren:
Neben den Vergleichsoperatoren und den gerade gezeigten booleschen Operatoren gibt
es auch noch mathematische Operatoren, die besonders interessante Ergebnismengen
ermöglichen. Diese ergeben teilweise sogar Werte, die in dieser Art gar nicht in der
Datenbank gespeichert waren, sondern aufgrund von Berechnungen entstanden sind.
2.2.3.1
Berechnete Spalten
Es ist möglich, in einer Abfragemenge auch Spalten auszugeben, deren Werte aus dynamischen Berechnungen auf Basis einer oder mehrerer Spalten und weiteren festen
Werten beruhen. Ein Schulbeispiel in diesem Zusammenhang ist die Ausgabe eines
Netto-Preises, des Mehrwertsteueranteils und des Bruttorpreises. Als Beispiel für eine
solche berechnete Spalte soll die Tabelle PRODUCT untersucht werden. Sie enthält eine
Spalte ListPrice mit dem Listenpreis, eine Spalte StandardCost mit den Kosten für
einen Artikel und eine weitere Spalte SafetyStockLevel mit dem Sicherungs- oder
Reservebestand auf dem Lager. Diese drei Spalten kann man zu interessanten Rechnungen verknüpfen. Es ist syntaktisch nicht in jedem Fall notwendig, Aliasnamen zu vergeben, doch erfordert es eine gute Ausgabe durchaus, keine Spalten ohne Spaltennamen
auszugeben.
In der nachfolgenden Abfrage bspw. ermittelt man den Bruttogewinn eines Produkts aus
der Differenz zwischen ListPrice und StandardCost. Dazu muss man lediglich die
beiden Spalten wie zwei Variablen in einer Programmiersprache durch ein MinusZeichen verbinden, um dann für jeden einzelnen Datensatz die entsprechende Differenz
ausrechnen lassen. Eine Voraussetzung für diese und andere Rechnungen ist lediglich,
dass die Datentypen gleich sind oder wenigstens miteinander genutzt werden können.
Um den Warenwert dieses Sicherungsbestandes auf dem Lager zu ermitteln, genügt es,
die Werte der SafetyStockLevel-Spalte mit denen der ListPrice-Spalte zu multiplizieren.
Schließlich folgt noch eine weitere berechnete Spalte, in der die Punkt-vor-StrichRechnung-Regel berücksichtigt werden muss und deswegen sogar auf Klammern zu-
97
Einfache Abfragen
rückgegriffen werden muss, um die korrekte Berechnung vornehmen zu können. Sie
ermittelt den Bruttowarengewinn, der sich aus dem Sicherungsbestand ermittelt. Dies
bedeutet, dass die Differenz aus dem Listenpreis und den Standardkosten mit der Anzahl der Produkte im Sicherungsbestand multipliziert werden muss.
SELECT ProductNumber
AS Nr,
ListPrice
AS Liste,
SafetyStockLevel
AS Sicherung,
ListPrice - StandardCost
AS BruttoGewinn,
SafetyStockLevel * ListPrice AS Warenwert,
(ListPrice - StandardCost)
* SafetyStockLevel
AS BruttoWarenGewinn
FROM production.product
WHERE ListPrice != 0
223_01.sql: Diverse Berechnungen
Wie schon oben erwähnt, ist es nicht so, dass die Aggregate, d.h. die Gesamtwerte einer
Spalte multipliziert werden, sondern dass stattdessen die angegebenen Berechnungen
für jeden einzelnen Datensatz durchgeführt werden, sodass demzufolge für die einzelnen Datensätze der Ergebnismenge unterschiedliche Ergebnisse zu erwarten sind.
Nr
Liste
Sicherung
------------------------- --------------------- --------SA-M198
133,34
500
SA-M237
147,14
500
BruttoGewinn
BruttoWarenGewinn
98
Warenwert
Einfache Abfragen
--------------------- --------------------- ---------------34,57
66670,00
17285,00
38,15
73570,00
19075,00
2.2.3.2
Zeichenketten verknüpfen
Es besteht die Möglichkeit, die Ausgabe von vorneherein bereits durch die Verknüpfung
von Zeichenketten so aufzubereiten, dass eine Verarbeitung in Form eines Berichts
besonders einfach gelingt. Das Schulbeispiel in diesem Bereich ist die Erzeugung eines
Feldes in der Ergebnisliste, in dem Anrede, Vorname und Nachname durch Leerzeichen
getrennt in einer einzigen Zeichenkette erscheinen. Im nachfolgenden Beispiel setzt
man mit Hilfe des Plus-Operators, der offensichtlich für verschiedene Operationen einsetzbar ist, die Felder für den Produktnamen, die Größe und die Farbe zusammen, wobei
zusätzlich auch noch Größe und Farbe in runden Klammern gesetzt und durch ein
Komma mit Leertaste getrennt werden.
SELECT Name + ' (' + Size + ', ' + Color + ')' AS Produkt,
ListPrice AS Preis
FROM Production.Product
WHERE Size IS NOT NULL
223_02.sql: Zeichenketten verknüpfen
Wenngleich beim Verknüpfen von Zeichenketten sehr einfach Fehler geschehen, weil
man mit Plus- und Anführungszeichen gleichzeitig operieren muss, so ist die Ausgabe
wie auch in diesem Fall doch besonders hübsch anzusehen.
Produkt
Preis
--------------------------------------------- --------HL Road Frame - Black, 58 (58, Black)
1431,50
Mountain Bike Socks, M (M, White)
9,50
99
Einfache Abfragen
2.2.3.3
Berechnete Bedingungen
Schließlich müssen die mathematischen Operatoren nicht nur für Berechnungen eingesetzt werden, deren Ergebnis der Benutzer in der Abfrage lesen kann, sondern es ist
auch möglich, jene einfach nur innerhalb der WHERE-Klausel zu verwenden. Sie lassen
sich also überall dort sinnvoll einsetzen, wo ein gültiger Ausdruck erwartet wird.
In nachfolgender Abfrage sucht man also die Produkte heraus, deren Bruttogewinn, d.h.
die Differenz zwischen Listenpreis und Standardkosten größer als 30 sind. Um dies zu
kontrollieren, wird zwar auch das Ergebnis dieser Berechnung in der Ergebnisliste ausgegeben, aber wesentlich ist hier die Verwendung der Berechnung als Bedingung in der
WHERE-Klausel.
-- Bruttogewinn größer 30
SELECT ProductNumber
AS Nr,
ListPrice - StandardCost
AS BruttoGewinn,
ListPrice
AS Liste
FROM production.product
WHERE ListPrice != 0
AND ListPrice - StandardCost > 30
223_03.sql: Linksseitige Berechnungen
Man erhält eine entsprechende Liste, der man es vom reinen Aufbau oder Format her
nicht ansieht, dass sie eine solch spektakuläre Bedingung besitzt.
Nr
BruttoGewinn
Liste
------------------------- --------------------- -----------SA-M198
34,57
133,34
SA-M237
38,15
147,14
100
Einfache Abfragen
SA-M687
51,05
196,92
Viele SQL-Benutzer können sich nur langsam daran gewöhnen, dass sehr viele Formulierungen in SQL ein gültiger Ausdruck sind und daher an höchst ungewöhnlichen oder
für den Laien an unvermuteten Orten erscheinen. Das nächste Beispiel zeigt nämlich
nun, wie eine solche Berechnung auch auf der linken Seite untergebracht werden kann.
Rechnet man einmal selbst den entsprechenden Ausdruck im Kopf aus oder stellt sich
vor, dass auf der linken Seite bislang immer nur Spaltennamen standen, in Wirklichkeit
dort aber auch eine einfach Zahl erscheinen kann, dann gewinnt diese Abfrage auch für
Zwecke, in denen diese Zahl fest vorgegeben wird, an Bedeutung.
Die Abfrage prüft also darauf, ob der Bruttogewinn (Listenpreis weniger Standardkosten) größer als die halben Standardkosten sind und ob die prozentualen Kosten (Standardkosten pro Listenpreis) größer als 50% sind. Über den Sinn und Zweck einer solchen Abfrage mag man sich streiten, aber für eine gute Formulierung muss man schon
einmal tief in die Trickkiste der ungelesenen Managementberichte greifen. Die Berechnungen für das Verhältnis und die Differenz der beiden Werte erscheint bei dieser Abfrage links vom Vergleichsoperator, ändert sich bei jedem neuen Datensatz und kann
daher für einen Vergleich sinnvoll genutzt werden.
-- Mehr als 50% Bruttogewinn
-- UND Bruttogewinn mehr als die Hälfte der Kosten
SELECT ProductNumber
AS Nr,
ListPrice - StandardCost
AS BruttoGewinn,
ListPrice
AS Liste,
StandardCost / ListPrice * 100 AS [%-Gewinn],
StandardCost
AS Kosten,
StandardCost / 2
AS [Halbe Kosten]
FROM production.product
101
Einfache Abfragen
WHERE ListPrice != 0
AND ListPrice - StandardCost >
StandardCost / 2
AND StandardCost / ListPrice * 100 > 50
223_03.sql: Llinksseitige Berechnungen
Zur Kontrolle gibt man die errechneten Werte in der Ergebnismenge auch aus.
Nr
BruttoGewinn
Liste
------------------------- --------------------- --------FR-R92R-62
562,8658
1431,50
FR-R38R-44
150,0629
337,22
%-Gewinn
Kosten
Halbe Kosten
--------------------- --------------------- ----------------60,68
868,6342
434,3171
55,50
187,1571
93,5785
2.2.4
Mengen-Operatoren
Die einzelnen Tabellendaten, die man mit oder ohne Bedingung abruft, können genauso
wie das Ergebnis selbst als Menge betrachtet werden. Sofern man sich mit den Grundlagen der relationalen Datenbanktheorie beschäftigt, ist dies eine wesentliche Erkenntnis,
die aber auch bei einer allgemeinen Beschäftigung mit SQL quasi wie von selbst entsteht. Insbesondere die Kenntnis um die Mengen-Operatoren zeigt diese Tatsache noch
einmal deutlich. Sie erlauben nämlich, zwei Abfragen des gleichen Aufbaus zu verbinden, zu schneiden oder voneinander abzuziehen, also genau typische Mengenoperationen durchzuführen.
102
Einfache Abfragen
Eine (Teil-)Ergebnismenge gilt dabei als passend für eine andere, wenn man sich vorstellen kann, dass eine sinnvolle neue Ergebnismenge auf der Grundlage der beiden
einzelnen Mengen erstellt werden kann. Dies betrifft die Spaltenzahl, welche gleich sein
sollte, und die Datentypen, die zueinander passen müssen, d.h. die ineinander konvertiert werden können. Die Konvertierung erfolgt dabei so, dass die zweite Teilmenge auf
die Datentypen der ersten angepasst wird. Zusätzlich sollte natürlich auch ein gewisser
inhaltlicher Bezug zwischen den verschiedenen Spalten bestehen, d.h. eine Produktnummer mit der Anzahl der Kinder eines Angestellten sind zwar vom Datentyp möglicherweise überaus geeignet, in einer Spalte in der Ergebnismenge zu erscheinen, bieten
aber keine sinnvolle Kombinationsmöglichkeit. Die Spaltennamen rekrutieren sich bei
allen drei Operatoren aus den Spalten(alias)namen der ersten Teilmenge.
Die allgemeine Syntax mit den Operatoren
ƒ
UNION für die Verbindung von zwei Ergebnismengen mit Ausschluss von
Duplikaten
ƒ
UNION ALL für die Verbindung mit Übernahme der Duplikate
ƒ
EXCEPT für die Differenzbildung zweier Mengen
ƒ
INTERSECT für die Schnittmengenbildung zweier Mengen
ist nachfolgend aufgelistet:
{ <Abfrageangabe> oder (<Abfrageausdruck>) }
UNION [ ALL ]
<Abfrageangabe> oder (<Abfrageausdruck>)
[ UNION [ ALL ] <Abfrageangabe> oder (<Abfrageausdruck>)
[ ...n ] ]
{ <Abfrageangabe> oder (<Abfrageausdruck>) }
103
Einfache Abfragen
{ EXCEPT | INTERSECT }
{ <Abfrageangabe> oder (<Abfrageausdruck>) }
2.2.4.1
Abfragemengen verbinden
Die Schulbeispiele für den sinnvollen Einsatz einer Verknüpfung von Abfrageergebnissen sind folgende zwei: 1. Die einzelnen möglichen Abfrage-SQL-Zeichenketten liegen
in verschiedenen einzelnen Variablen in einer beliebigen Programmiersprache vor.
Durch Benutzerinteraktion oder einen sonstigen Anstoß wählt man im Programm immer
nur eine Teilmenge der möglichen SQL-Anweisungen aus, deren Ergebnismengen zu
einer gemeinsamen verbunden werden sollen. 2. Die Daten, auf die sich die Abfrage
bezieht, liegen in verschiedenen Tabellen vor, gehören aber inhaltlich zusammen. Dies
kann bspw. auch dann möglich sein, wenn eine Sicht Daten wieder zusammenfügt, die
aus Gründen der Zugriffs- und Speicheroptimierung in mehrere einzelne/partitionierte
Tabellen aufgeteilte wurden.
Das nachfolgende Beispiele ist im Gegensatz zu den beiden gerade genannten gut für
das Verständnis des Einsatzbereichs von UNION geeignet, aber doch deutlich trivialerer
Natur. Die erste Abfrage ermittelt die Produkte der Größe S, die zweite die Produkte der
Farbe Gelb. Beide Abfragen liefern die gleichen Spaltenstruktur, die daher von Anzahl
und Datentypen übereinstimmen, wobei die Aliasnamen der ersten Abfrage die Spaltennamen der Ergebnismenge bestimmen.
-- Gelbe T-Shirts in S und M
SELECT Name
Size
AS Bezeichnung,
AS Größe,
Color AS Farbe
FROM Production.Product
WHERE Size = 'S'
AND Color = 'Yellow'
104
Einfache Abfragen
UNION
SELECT Name, Size, Color
FROM Production.Product
WHERE Size = 'M'
AND Color = 'Yellow'
224_01.sql: Verbindung von Ergebnissen
S
Yellow
S
Yellow
S
Yellow
M
Yellow
S
Yellow
S
Yellow
M
Yellow
M
Yellow
S
Yellow
M
Yellow
UNION ALL
UNION
M
Yellow
M
Yellow
S
Yellow
S
Yellow
UNION
M
Yellow
M
Yellow
M
Yellow
M
Yellow
M
Yellow
S
Yellow
M
Yellow
S
Yellow
M
Yellow
M
Yellow
Abbildung 2.3: Funktionsweise von UNION [ALL]
Man erhält die gleiche Ausgabe wie bei einer Formulierung mit [NOT] IN.
Bezeichnung
Größe Farbe
-------------------------------- ----- -------Short-Sleeve Classic Jersey, S
S
Yellow
105
Einfache Abfragen
Short-Sleeve Classic Jersey, M
M
Yellow
Während die vorherige Abfrage mögliche Duplikate, die in beiden Ergebnismengen
erschienen, ausblendete, sorgt UNION ALL dafür, dass diese Duplikate erhalten bleiben.
Die nächste Abfrage prüft daher in der ersten Teilabfrage darauf, ob die Größe S oder M
und die Farbe Gelb ist, während die zweite Teilabfrage erneut auf die Größe M und die
Farbe Gelb prüft. Beide Mengen liefern Duplikate, die ebenfalls in der Ergebnismenge
angezeigt werden.
-- Gelbe T-Shirts in 'S', 'M' mit 'M'
SELECT Name, Size, Color
FROM Production.Product
WHERE Size IN ('S', 'M')
AND Color = 'Yellow'
UNION ALL
SELECT Name, Size, Color
FROM Production.Product
WHERE Size = 'M'
AND Color = 'Yellow'
224_02.sql: Duplikate zusätzlich ausgeben
2.2.4.2
Abfragemengen abziehen
Mit Hilfe des Operators EXCEPT ist es leicht möglich, die Ergebnisse der ersten Teilmenge anzuzeigen, die nicht in der Ergebnismenge der zweiten Abfrage erscheinen. So
ruft die erste Abfrage die Produkte mit verschiedenen Größen, darunter auch M, ab und
prüft in beiden Fällen auf gelbe Farbe. Das bedeutet, dass die gelben Produkte in Größe
M, die in der zweiten Abfrage gefunden werden, im gemeinsamen Ergebnis nicht mehr
erscheinen, da sie von der ersten Menge abgezogen werden.
106
Einfache Abfragen
-- Gelbe T-Shirts in 'S', 'L', 'XL', 'M' ohne 'M'
SELECT Name, Size, Color
FROM Production.Product
WHERE Size IN ('S', 'L', 'XL', 'M')
AND Color = 'Yellow'
EXCEPT
SELECT Name, Size, Color
FROM Production.Product
WHERE Size = 'M'
AND Color = 'Yellow'
224_03.sql: Abziehen von Ergebnissen
Im Ergebnis erscheinen eine Menge gelber Produkte, aber keiner in Größe M.
Name
Size
Color
----------------------------------- ----- ------Short-Sleeve Classic Jersey, S
S
Yellow
Short-Sleeve Classic Jersey, L
L
Yellow
Short-Sleeve Classic Jersey, XL
XL
Yellow
2.2.4.3
Abfragemengen schneiden
Schließlich lassen sich zwei Ergebnismengen auch schneiden, das bedeutet, es werden
nur die Datensätze, welche in beiden Ergebnismengen erscheinen, in der gemeinsamen
Ergebnismenge angezeigt. Diese Schnittmenge erstellt man mit Hilfe des Operators
INTERSECT. Im nachfolgenden Beispiel ruft die erste Ergebnismenge die gelben Pro-
107
Einfache Abfragen
dukte mit Größe S oder M ab, die zweite die gelben Produkte in M, sodass im Ergebnis
nur die gelben Produkte in M erscheinen.
-- Gelbe T-Shirts in 'S', 'M' verglichen mit 'M'
SELECT Name, Size, Color
FROM Production.Product
WHERE Size IN ('S', 'M')
AND Color = 'Yellow'
INTERSECT
SELECT Name, Size, Color
FROM Production.Product
WHERE Size = 'M'
AND Color = 'Yellow'
224_04.sql: Ergebnismengen schneiden
Das Ergebnis besteht aus gelben Produkten in Größe M.
Name
Size
Color
-------------------------------- ----- ------Short-Sleeve Classic Jersey, M
108
M
Yellow
Einfache Abfragen
M
Yellow
S
Yellow
L
Yellow
EXCEPT
M
Yellow
S
Yellow
M
Yellow
S
Yellow
INTERSECT
M
Yellow
L
Yellow
S
Yellow
Abbildung 2.4: Funktionweise von INTERSECT und EXCEPT
2.3
Ergebnisse aufbereiten
In diesem Abschnitt werden einige Techniken vorgestellt, die dazu dienen, Ergebnismengen zu erzeugen, die weniger durch die Verwendung von Bedingungen erstellt
werden, sondern die durch Sortierungs- oder Gruppierungsanweisungen bzw. durch das
Ausblenden von Duplikaten erstellt werden.
2.3.1
Duplikate ein-/ausblenden
Das Schlüsselwort DISTINCT ermöglicht es, Duplikate einer Ergebnismenge bzw. einer
Tabelle auszublenden. Im Normalfall sollten in einer Tabelle natürlich keine Duplikate
vorhanden sein, aber dieser Fall ist syntaktisch mit DISTINCT zu lösen. Dennoch können Duplikate in einer Ergebnismenge erscheinen. Da sich die SQL-Abfrage nicht doppelte Daten ausdenken kann, kann es nur daran liegen, dass die Abfrage so formuliert
109
Einfache Abfragen
ist, dass durch das Auslassen der unterscheidenden (Schlüssel-)Spalten doppelte Werte
abgerufen werden. Dies geschieht immer dann, wenn in der zu Grunde liegenden Datenmenge die Werte durch einen Schlüssel unterschieden werden können. Dies muss in
keinem Fall der tatsächliche Primärschlüssel der gesamten Spalte sein, sondern kann
auch eine andere Spalte sein, die allerdings für die abgerufenen Werte die Schlüsselfunktion übernehmen kann. Durch das Weglassen bzw. durch den fehlenden Abruf
dieser Spalte entstehen Duplikate in der Ergebnismenge.
Das Schulbeispiel für die Entstehung und schließlich das Ausblenden von Duplikaten ist
die Erzeugung von Wertelisten. Beim Zivilstand bspw. gibt es in der gesamten Tabelle
Employees nur die beiden Werte M (verheiratet) oder S (unverheiratet). Würde man
die Werte ohne DISTINCT abrufen, dann würde man eine lange Liste mit wechselnden
Werten für M oder S erhalten, die den Werten und der Zeilenanzahl der gesamten
Employees-Tabelle entspräche.
-- Nur verschiedene Werte für Zivilstatus
SELECT DISTINCT MaritalStatus
FROM HumanResources.Employee
231_01.sql: Duplikate ausblenden
SELECT MaritalStatus
FROM HumanResources.Employee
M
S
S
M
S
SELECT DISTINCT MaritalStatus
FROM HumanResources.Employee
M
S
S
M
S
Abbildung 2.5: Funktionweise von DISTINCT
Durch das Ausblenden der Duplikate entsteht eine Liste, in der nur noch die verschiedenen Werte für die Spalte MaritalStatus abgerufen werden. Solche Abfragen kann
man leicht an einer Formulierung wie „Welche verschiedenen Zivilstände gibt es überhaupt in der Tabelle?“ erkennen.
110
Einfache Abfragen
MaritalStatus
------------M
S
Nimmt man noch eine weitere Spalte wie das Geschlecht hinzu, dann erhält man die
verschiedenen Kombinationsmöglichkeiten von Geschlecht und Zivilstand, sofern sie in
der Tabelle auftreten. Die DISTINCT-Klausel wirkt sich also hier auf die gesamte Ergebnismenge aus und ist als Anhängsel zu SELECT und nicht als zu einer bestimmten
Spalte gehörend zu betrachten. Dies ist insoweit auch sehr einleuchtend, da in einer
tabellenorientierten Ergebnismenge schlecht in der einen Spalte die verschiedenen Werte und in der anderen Spalte eine lange Liste an sich wiederholend auftretender Werte
entstehen kann.
-- Nur verschiedene Werte für Zivilstatus und Geschlecht
SELECT DISTINCT MaritalStatus, Gender
FROM HumanResources.Employee
231_02.sql: Duplikate von kombinierten Werten ausblenden
In diesem Fall sind in der Employees-Tabelle alle möglichen Kombinationen enthalten,
sodass sowohl Männer als auch Frauen verheiratet oder ledig sein können.
MaritalStatus Gender
------------- -----M
F
M
M
S
F
S
M
111
Einfache Abfragen
2.3.2
Ergebnisse sortieren
Ergebnismengen zu sortieren ist eine ganz häufige Aufgabe, die teilweise für die schöne
oder sinnvolle Ausgabe und teilweise auch für die Kontrolle nach größtem oder kleinstem Wert in einer Ergebnismenge durchgeführt werden muss. Die Anweisung für die
Sortierung lautet ORDER BY und erwartet wenigstens eine Spalte. Die aufsteigende
Sortierung lässt sich über das zusätzliche Schlüsselwort ASC (engl. ascending) ausdrücken, das allerdings der Standardwert ist. Eine absteigende Sortierung richtet man dann
über DESC (engl. descending) ein. Beide Schlüsselwörter folgen dem Spaltennamen, der
übrigens auch ein Aliasnamen sein kann. Sofern kein Spaltenalias vergeben wurde und
eine Funktion oder Berechnung den Spaltenwert erzeugt, die man nicht noch einmal in
der ORDER BY-Klausel einfügen möchte, oder wenn die SQL-Anweisungen dynamisch
aus Zeichenketten zusammen gesetzt werden, kann man auch die 1-basierte Positionsnummer der Spalte verwenden. Schließlich werden mehrere Spaltennamen wie LastName, FirstName so abgearbeitet, dass erst nach der ersten und dann innerhalb der
ersten Spalte nach der zweiten sortiert wird. Das Prinzip ist sehr einfach, und das Ergebnis dementsprechend schnell erreicht.
Im nachfolgenden Beispiel ruft man aus der Contact-Tabelle Personen mit den Nachnamen Adams und Navarro ab, um die Ergebnismenge zunächst nach dem Nachnamen
und dann nach dem Vornamen zu sortieren. Dabei sind im Quelltext verschiedene Varianten angegeben, welche alle die gleiche aufsteigende Sortierung von LastName und
dann FirstName ergeben. In der Standardvariante nennt man die beiden Spaltennamen
ohne ASC, in der Alternative folgt nach jeder Spalte oder nach einer einzigen Spalte die
ausdrückliche ASC-Anweisung für die aufsteigende Sortierung. In einer dritte Variante
spart man die Nennung der Spaltennamen und ersetzt sie durch die Positionsnummern,
welche natürlich auch wieder um ASC ergänzt werden könnten.
-- Nachname, Vorname von A-Z geordnet
SELECT LastName, FirstName
FROM Person.Contact
WHERE LastName IN ('Adams', 'Navarro')
112
Einfache Abfragen
ORDER BY LastName, FirstName
-- ORDER BY LastName ASC, FirstName ASC
-- ORDER BY 1, 2
232_01.sql: Alphabetische Ordnung A-Z
In der Ergebnismenge erscheint zunächst Adams und dann Navarro, wobei innerhalb
der beiden Namen die Vornamen jeweils wieder aufsteigend sortiert sind.
LastName
FirstName
-------------- ---------Adams
Aaron
Adams
Adam
Adams
Alex
Navarro
Adrienne
Navarro
Albert
Navarro
Alberto
Im nächsten Beispiel sollen die Nachnamen weiterhin aufsteigend, die Vornamen allerdings absteigend sortiert werden. Neben den beiden Varianten, die Spaltennamen oder
die Positionsnummern der Spalten zu nennen, ist hier wichtig, dass die absteigend zu
sortierende Spalte um die Anweisung DESC ergänzt wird, welche den Vorgabewert
überschreibt.
-- Nachname, Vorname von Z-A geordnet
SELECT LastName, FirstName
FROM Person.Contact
WHERE LastName IN ('Adams', 'Navarro')
113
Einfache Abfragen
ORDER BY LastName, FirstName DESC
-- ORDER BY LastName ASC, FirstName DESC
-- ORDER BY 1 ASC, 2 DESC
232_02.sql: Alphabetische Ordnung gemischt
In der Ergebnismenge bleibt die Reihenfolge der Nachnamen unverändert, lediglich die
Vornamen sind nun von Z-A sortiert.
LastName
FirstName
---------- -----------Adams
Xavier
Adams
Wyatt
Adams
Thomas
2.3.3
Standard-Aggregate
Standard-SQL bietet eine Reihe von Funktionen, die in jeder Datenbank nutzbar sind
und als so genannte Standard-Aggregate bekannt sind. Sie verdienen diesen Namen
aufgrund der Universalität ihres Einsatzes in den verschiedenen Datenbanksystemen,
die man auch außerhalb des SQL Servers nutzen könnte (aber wer wollte das schon?).
Die nachfolgende Tabelle listet diese universellen Funktionen kurz auf, wobei die
COUNT_BIG-Funktion nicht zum Standard gehört, aber ebenso zählt wie COUNT, wobei
sehr große Werte auch gezählt werden können.
Funktion
Bedeutung
AVG
Durchschnitt der Spalte
MIN
Minimum der Spalte
MAX
Maximum der Spalte
SUM
Summe der Spalte
114
Einfache Abfragen
COUNT
Anzahl der Einträge einer Spalte
COUNT_BIG
Standardaggregate
Die nächsten Beispiele erzeugen entweder jeweils einen einzigen Wert und stellen damit eine ganz klassische Form der Aggregatabfrage dar, oder werden in Verbund mit
anderen Aggregaten (bzw. im nächsten Abschnitt mit einer Gruppierung) verwendet.
Wie man sieht, ist die Verwendung nicht weiter schwierig:
ƒ
Die COUNT-Funktion zählt die Anzahl der Datensätze in einer Spalte, wobei sie
auch noch in einer COUNT(DISTINCT *)-Variante auftritt und dabei die Duplikate
ausblendet. Im einfachsten Fall verwendet man einen Asteriskus, ansonsten kann
man auch einen Spaltennamen einsetzen.
ƒ
Die Funktionen für die Summen- und Durchschnittsberechnung können nicht mit
dem Asteriskus zusammen eingesetzt werden. Stattdessen ist hier eine numerische
Spalte notwendig, die also eine entsprechende Berechnung erlaubt.
ƒ
Die beiden Funktionen zur Ermittlung Maximum und Minimum werden in den
meisten Fällen mit numerischen Werten eingesetzt, können allerdings auch für
Zeichenketten zum Einsatz kommen. In diesem Fall würde eine Abfrage wie
SELECT MAX(lastname) FROM Person.Contact den schönen Wert Zwilling
liefern.
-- Wieviele Datensätze sind in Contact?
SELECT COUNT(*) AS Anzahl
FROM Person.Contact
-- Wie hoch sind die Verkäufe?
SELECT SUM(SalesYTD)
AS Verkäufe,
SUM(SalesLastYear) AS LetzteVerkäufe
115
Einfache Abfragen
FROM Sales.SalesPerson
WHERE TerritoryID < 5
-- Wie hoch sind die Durchcshnittsverkäufe?
SELECT AVG(SalesYTD)
AS Verkäufe,
AVG(SalesLastYear) AS LetzteVerkäufe,
AVG(SalesYTD) - AVG(SalesLastYear) AS Differenz
FROM Sales.SalesPerson
-- Wie hoch waren die maxi-/minimalen Verkäufe?
SELECT MAX(SalesYTD)
AS Maxi,
MIN(SalesLastYear) AS Mini
FROM Sales.SalesPerson
233_01.sql: Anwendung von Aggregatfunktionen
Die verschiedenen angekündigten Ergebnisse der Aggregate sind nachfolgend aufgelistet. Interessant ist insbesondere die Abfrage, welche mehrere Aggregate einer Tabelle
zugleich ermittelt.
Anzahl
----------19972
Verkäufe
LetzteVerkäufe
-------------- --------------22152408,0054
116
10558949,205
Einfache Abfragen
Verkäufe
LetzteVerkäufe
Differenz
-------------- --------------- -------------2605530,949
Maxi
1393291,9779
1212238,9711
Mini
------------- ------5200475,2313
2.3.4
0,00
Gruppieren
Eine besondere Technik der Ergebnisaufbereitung sorgt für Abfragemöglichkeiten, die
bereits sehr informative Ergebnisse über Aggregate in der Datenbank liefern und zusätzlich deutlich anspruchsvollere Daten erzeugen als eine einfache Ausgabe der gespeicherten Werte. Die hierbei verwendete Klausel lautet GROUP BY und ermöglicht die
Gruppierung von Werten nach einem bestimmten Kriterium, wobei in den meisten Fällen auch eine oder gar mehrere Aggregatfunktionen auf diese Gruppen angewandt werden.
2.3.4.1
Einfache Gruppierung
In einer der vorherigen Abfragen rief man die verschiedenen Werte ab, die in der Spalte
MaritalStatus von Employee gespeichert sind. In einer anderen Abfrage wurden
diese Werte wiederum zusätzlich mit den Werten der Spalte Gender verglichen, wobei
das nicht sonderlich überraschende Ergebnis entstand, dass sowohl Männer als auch
Frauen in verheirateter und lediger Form auftreten. Interessant wäre nun in diesem Zusammenhang zu erfahren, wie viele Angestellten denn jeweils diese verschiedenen Werte aufweisen. Genau eine solche Untersuchung lässt sich mit Hilfe der Gruppierung
durchführen.
117
Einfache Abfragen
Als Regel gilt, dass alle Spalten in der Spaltenliste, die nicht durch den Einsatz einer
(Standard-)Aggregatfunktion gebildet werden, in der GROUP BY-Klausel erscheinen
müssen. Je mehr Spalten also abgerufen werden, desto mehr verschiedene Werte können eine Gruppe bilden, was natürlich die Aggregate sehr beeinflusst und zu kleineren
aggregierten Werten führt, da ja die zu betrachtenden Merkmale zunehmen.
Um das vorher erwähnte Beispiel aufzugreifen, indem der Zivilstand mit oder ohne
Bezug auf das Geschlecht ermittelt wurde, sollen nun in den nächsten beiden Beispielen
die Anzahl pro Zivilstand und die Anzahl von Männern und Frauen pro Zivilstand ermittelt werden. Es entstehen also zunächst die gleichen bzw. schon bekannten Wertegruppen, die nun allerdings um eine Spalte für die Anzahl ergänzt wurden. Dabei ruft
man weiterhin die Spalte MaritalStatus ab, kann allerdings auf das DISTINCT verzichten, da die Spalte mit Hilfe von GROUP BY gruppiert und damit auch auf die überhaupt vorhandenen Werte reduziert wird. In der zweiten Spalte der Ergebnismenge
sollen dann die verschiedenen Werte pro Gruppe ermittelt werden, wozu lediglich die
COUNT(*)-Funktion zum Einsatz kommt. Sobald wie in der zweiten Abfrage weitere
Merkmale bzw. Spalten in die Gruppen aufgenommen werden, entstehen im Normalfall
mehr Gruppen und müssen diese Spalten auch zusätzlich in der GROUP BY-Klausel
erscheinen.
-- Anzahl pro Zivilstand
SELECT MaritalStatus AS Zivilstand,
COUNT(*)
AS Anzahl
FROM HumanResources.Employee
GROUP BY MaritalStatus
-- Anzahl pro Zivilstand und Geschlecht
SELECT MaritalStatus AS Zivilstand,
Gender
118
AS Geschlecht,
Einfache Abfragen
COUNT(*)
AS Anzahl
FROM HumanResources.Employee
GROUP BY MaritalStatus, Gender
ORDER BY Gender
234_01.sql: Gruppierung für eine Spalte und für zwei
Man erhält als Ergebnis der ersten Abfrage nur die beiden Gruppen M (verheiratet) und
S (ledig) mit den jeweiligen Zahlen. In der zweiten Ergebnismenge jedoch erfährt man
zusätzlich, dass es überhaupt Männern und Frauen in beiden Gruppen gibt und wie hoch
die Anzahl in den beiden Gruppen ist. Verloren geht nun die Aggregatinformation, wie
viele Werte es in jeder Gruppe insgesamt (nur Männer oder nur verheiratet) gibt. Dazu
sind noch diverse Erweiterungen der Gruppierung nötig.
Zivilstand Anzahl
---------- ----------M
146
S
144
Zivilstand Geschlecht Anzahl
---------- ---------- ----------M
F
49
S
F
35
M
M
97
S
M
109
Alle Aggregatfunktionen lassen sich mit GROUP BY sinnvoll kombinieren und zu interessanten Ergebnissen führen. So ermittelt die nächste Abfrage bspw. für jede Bestel-
119
Einfache Abfragen
lung die Summe der Posten und den Durchschnittswert der Postenwerte sowie die Anzahl der bestellten Posten. Damit werden drei verschiedene Aggregatfunktionen für die
Gruppenbildung auf Basis der Bestellnummer angewandt.
-- Summe einer Bestellung,
-- Durchschnitt/Anzahl von Posten
SELECT SalesOrderID
AS Nr,
SUM(LineTotal) AS Summe,
AVG(LineTotal) AS Schnitt,
COUNT(*)
AS Anzahl
FROM Sales.SalesOrderDetail
GROUP BY SalesOrderID
ORDER BY Summe DESC
234_02.sql: Mehrere Aggregatfunktionen
Man erhält die angekündigten Aggregate pro Bestellung.
Nr
Summe
Schnitt
Anzahl
-------- --------------- -------------- ------51131
163930.394292
2732.173238
60
55282
160378.391334
3144.674339
51
46616
150837.438737
2320.575980
65
2.3.4.2
Gruppenbedingungen
Bisweilen kann es passieren, dass eine ganze Reihe von Gruppen mit nur einem einzigen Datensatz in der Datenbank entsteht und sie daher trotz geringerer Zeilenzahl im
Vergleich zu den Originaldaten immer noch zu viele Daten liefert. Dann kann ebenso
120
Einfache Abfragen
der Fall eintreten, dass ganz einfach eine zusätzliche inhaltliche Bedingung auf die
Aggregatwerte angewandt werden soll. Dies lässt sich mit Hilfe der HAVING-Klausel
abbilden, die der GROUP BY-Klausel folgt und als WHERE-Klausel der Gruppierung
angesehen werden kann. Sie greift eines der Aggregate auf und verknüpft mehrere Bedingungen durch die booleschen Operatoren, die hier genauso wie in der WHERE-Klausel
gelten, um zusätzliche Bedingungen für die Gruppenaggregate anzugeben.
Die nächste Abfrage ermittelt die Anzahl der Produkte pro Kategorie und als Aggregate
den durchschnittlichen, maximalen und minimalen Bedarf an Produktionstagen. Es
sollen allerdings nur solche Kategorien in die Ergebnismenge übernommen werden,
welche mehr als 10 Produkte aufweisen und deren Produktionszeit ungleich 0 ist. Der
Gruppierung folgt dann – wie in diesem Beispiel zu sehen ist – wiederum der Sortierung, die stets die letzte Klausel bildet.
-- Anzahl von Produkten pro Subkategorie
-- Durchschnittliche, maxi-/minimale Produktionsdauer
SELECT ProductSubcategoryID
COUNT(*)
AS Kategorie,
AS Anzahl,
AVG(DaysToManufacture) AS [Prod-Tage],
MAX(DaysToManufacture) AS Maxi,
MIN(DaysToManufacture) AS Mini
FROM Production.Product
GROUP BY ProductSubcategoryID
HAVING COUNT(*) > 10
AND AVG(DaysToManufacture) != 0
ORDER BY ProductSubcategoryID
234_03.sql: Bedingungen für Aggregate
121
Einfache Abfragen
Als Ergebnis erhält man eine auf die Bedingung reduzierte Menge.
Kategorie
Anzahl
Prod-Tage
Maxi
Mini
----------- ----------- ----------- ----------- ----------1
32
4
4
4
33
1
2
1
...
14
2.3.5
Zufällige Datenauswahl
Um zufällige Daten auszuwählen, gibt es eine eigene Klausel, die innerhalb der FROMKlausel in den Befehlen SELECT, UPDATE und DELETE zusätzlich angegeben werden
kann. Der Zufallsalgorithmus oder die Genauigkeit sind nicht besonders raffiniert, aber
für eine einfache Lösung eines solchen Problems reicht sie völlig aus. Ihre allgemeine
Syntax lautet:
TABLESAMPLE [SYSTEM] (
sample_number [ PERCENT | ROWS ] )
[ REPEATABLE ( repeat_seed ) ]
Die einzelnen Bestandteile haben folgende Bedeutung:
ƒ
SYSTEM weist die Klausel an, eine implementationsbedingte, d.h. vorgegebene,
Stichprobenmethode zu verwenden. Dieser Zusatz ist von Standard-SQL
übernommen und bezieht sich dort auf verschiedene Datenbanken. Für die Version
2005 ist nur eine Stichprobenfunktion vorhanden, die eine zufällige Anzahl von
Datenseiten auswählt.
ƒ
sample_number enthält einen festen Wert oder eine Prozentangabe, welche die
Anzahl der Zeilen, die zufällig ermittelt werden sollen, angibt. Wenn zusätzlich
PERCENT gesetzt wird, dann gilt die Zahl als float, ansonsten als bigint.
122
Einfache Abfragen
ƒ
PERCENT legt fest, dass die prozentuale Menge aus der Tabelle ermittelt werden
soll. Der Wertebereich liegt zwischen 0 und 100.
ƒ
ROWS (bigint größer 0) legt fest, dass die angegebene Zahl in Reihen ausgegeben
werden soll.
ƒ
REPEATABLE legt fest, dass die zuvor ausgewählte Zeilenmenge noch einmal
ermittelt werden darf, d.h. dass zwei aufeinander folgende Ergebnisse gleich sein
dürfen. Sofern der derselbe repeat_seed (bigint größer 0) für den Zufallsgenerator
verwendet wird, ist dies immer die gleiche Datenmenge, sofern keine Änderungen
im Datenbestand erfolgt sind.
Die nachfolgenden Beispiele zeigen die verschiedenen Einsatzmöglichkeiten dieser
zusätzlichen Angabe in FROM. Dabei muss man sich darauf einstellen, dass bei sehr
kleinen Werte und kleinem Datenbestand auch schon einmal gar keine Ergebnisse ausgegeben werden sollen. Bei Testläufen und entsprechendem Testdatenbedarf kann dies
manchmal erwünscht sein. Sofern man einen nachvollziehbaren und wiederholbaren
Test ausführen will, sollte man in jedem Fall die REPEATABLE-Anweisung zusätzlich
mit einem festen Wert als Zufallszahl anfügen.
-- 20% abrufen
SELECT *
FROM Production.Product TABLESAMPLE SYSTEM (20 PERCENT)
-- 20 Reihen abrufen
SELECT *
FROM Production.Product TABLESAMPLE SYSTEM (20 ROWS)
-- 200 Reihen wiederholbar abrufen
SELECT *
FROM Production.Product TABLESAMPLE SYSTEM (200 ROWS)
REPEATABLE(50)
123
Einfache Abfragen
235_01.sql: Zufällige Ergebnismenge
2.4
Eingebaute Funktionen
Jede relationale Datenbank besitzt einen Blumenstrauß an so genannten eingebauten
Funktionen. Einige wie SUM() oder COUNT() sind darüber hinaus nicht nur in einer
Datenbank verfügbar, sondern werden als Standardaggregatfunktionen auch im SQLStandard definiert und können getrost in jeder Datenbank als unterstützt vorausgesetzt
werden. Neben diesen Funktionen sind solche wie für die Bearbeitung von Zeichenketten oder mathematischen Aufgaben in der einen oder anderen Form ebenfalls in den
meisten Datenbanken verfügbar, wenngleich sie auch in jeder einzelnen Datenbank vom
Namen her neu gelernt werden müssen. Viele andere Funktionen jedoch sind nicht einfach so verfügbar, sondern müssen teilweise sogar selbst implementiert werden oder
durch eine Kombination von vorhandenen Funktionen realisiert werden. Dieser Abschnitt stellt die im MS SQL Server 2005 vorhandenen Funktionen vor, wobei der
Schwerpunkt auf Abfrage und Umwandlung von Ergebnissen oder Berechnungen und
weniger auf administrative Aufgaben liegt. Diese so genannten Systemfunktionen werden zwar auch erwähnt, sind allerdings im Buch zur Datenbankadministration genauer
erläutert und dort auch grundsätzlich besser aufgehoben.
Folgende Funktionsgruppen werden von der Dokumentation vorgestellt:
ƒ
Konfigurationsfunktionen liefern Informationen zur Konfiguration der Datenbank.
ƒ
Cursorfunktionen liefern Informationen zu Cursorn.
ƒ
Datums- und Zeitfunktionen ermöglichen Operationen für Datums- und Zeitwerte
und bieten als Rückgabewerte Zeichenketten, Zahlen-, Datums- oder Zeitwerte.
ƒ
Mathematische Funktionen ermöglichen Berechnungen auf der Grundlage der
Funktionsparameter und liefern einen numerischen Wert.
ƒ
Metadatenfunktionen
Datenbankobjekten.
124
liefern
Informationen
zur
Datenbank
und
zu
Einfache Abfragen
ƒ
Sicherheitsfunktionen liefern Informationen über Benutzer und Rollen zurück.
ƒ
Zeichenkettenfunktionen ermöglichen Operationen für Parameter vom Typ char
oder varchar und liefern eine Zeichenfolge oder einen numerischen Wert.
ƒ
Systemfunktionen ermöglichen Operationen bezüglich Werten, Objekten und
Konfigurationseinstellungen der Datenbank aus und liefern diesbezügliche
Informationen.
ƒ
Statistische Systemfunktionen liefern statistische Informationen zum System.
ƒ
Text- und Imagefunktionen ermöglichen Operationen zu Text- bzw. Image-Daten
aus und liefern Informationen zu diesen Daten.
In den nachfolgenden Listen taucht die Unterscheidung deterministisch und nicht deterministisch auf. Eine deterministische Funktion liefert bei gleichen Eingabewerten die
gleichen Ausgabewerte, eine nicht deterministische nicht. Ein typisches Beispiel für
eine nicht deterministische Funktion ist der Abruf der aktuellen Uhrzeit oder der Abruf
des aktuellen Benutzernamens.
2.4.1
Datums- und Zeitfunktionen
Datums- und Zeitwerte stellen besondere Datenstrukturen dar, von denen in jeder Datenbank Teile extrahiert werden sollen oder die in irgendeiner Weise formatiert werden
müssen. Sollten solche Funktionen fehlen, bleibt nichts anders übrig als die Datumswerte in Zeichenketten umzuwandeln und Extraktionen, Formatierungen und natürlich auch
Sortierungen und Gruppierungen auf der Ebene von Zeichenketten vorzunehmen. Dies
ist allerdings aufgrund des mathematischen Systems (24 Stunden für einen Tag, 7 Tage
für eine Woche etc.) bei Berechnungen überaus kompliziert, sodass entsprechende
Funktionen, die dies alles berücksichtigen, überaus wichtig sind.
2.4.1.1
Referenz
Die verschiedenen Funktionen erwarten ab und an Parameter, die für einzelne Zeitbestandteile stehen. In der nachfolgenden Tabelle sind diese Namen sowie ihre Abkürzungen zusammen gefasst.
125
Einfache Abfragen
Datumseinheit
Abkürzungen
Bedeutung
year
yy, yyyy
Jahr
quarter
qq, q
Quartal
month
mm, m
Monat
dayofyear
dy, y
Tag des Jahres (fortlaufend)
day
dd, d
Tag
week
wk, ww
Woche, Kalenderwoche
Hour
hh
Stunde
minute
mi, n
Minute
second
ss, s
Sekunde
millisecond
ms
Millisekunde
Zeiteinheiten
Folgende Funktionen sind in dieser Kategorie vorhanden:
ƒ
DATEADD (deterministisch) berechnet einen neuen datetime-Wert, welche als
Summe eines Datums und eines Zeitraums ermittelt wird.
Syntax: DATEADD (datepart , number, date )
ƒ
DATEDIFF (deterministisch) berechnet einen neuen datetime-Wert, welche als
Differenz eines Datums und eines Zeitraums ermittelt wird.
Syntax: DATEDIFF ( datepart , startdate , enddate )
ƒ
DATENAME (nicht deterministisch) liefert eine Zeichenkette zurück, die den
Textwert eines Datumsteils angibt.
Syntax: DATENAME ( datepart ,date )
ƒ
DATEPART (deterministisch mit Ausnahme von DATEPART (dw, date), wobei
dw den Datumsteil für den Wochentag bestimmt, welcher wiederum von dem durch
126
Einfache Abfragen
ersten Tag der Woche abhängt) liefert eine
Ganzzahl, welche den abgefragten Teil des Datums angibt.
SET
DATEFIRST festgelegten
Syntax: DATEPART ( datepart , date )
ƒ
DAY (deterministisch) liefert eine Ganzzahl, welche den Tagesanteil des Datums
angibt.
Syntax: DAY ( date ) oder DATEPART(dd,date).
ƒ
GETDATE (nicht deterministisch) liefert das aktuelle Systemdatum inkl. Uhrzeit
zurück, wobei das eingestellte SQL Server 2005-Format zum Einsatz kommt.
Syntax: GETDATE ( )
ƒ
GETUTCDATE (nicht deterministisch) liefert die aktuelle UTC-Zeit (Coordinated
Universal Time oder Greenwich Mean Time) zurück, wobei die
Zeit(zonen)einstellungen des Rechners zum Einsatz kommen, welcher den MS
SQL Server 2005 enthält.
Syntax: GETUTCDATE()
ƒ
MONTH (deterministisch) liefert eine Ganzzahl, welche den Monatsanteil des
Datums angibt.
Syntax: MONTH ( date ) anstelle von DATEPART(mm, date).
ƒ
YEAR (deterministisch) liefert eine Ganzzahl, welche den Jahressanteil des Datums
angibt.
Syntax: YEAR ( date ) oder DATEPART(yy, date).
2.4.1.2
Beispiele
Das nächste Beispiel addiert zu verschiedenen Datumswerten weitere Zeiteinheiten
hinzu. Hierbei sind insbesondere einige „gefährliche“ Datumswerte ausgewählt, die
dennoch zu richtigen Ergebnissen führen.
-- Datumsteil mit vollem Namen
127
Einfache Abfragen
SELECT DATEADD(day, 21, '01.01.2006') AS TagPlus,
DATEADD(month, 1, '28.02.2006') AS MonatPlus,
DATEADD(month, 1, '30.01.2006') AS MonatPlus,
DATEADD(minute, 1, '30.01.2006 01:10:30.000')
AS MinutePlus
-- Datumsteil mit abgekürztem Namen
SELECT DATEADD(q, 2, '09.01.2006') AS QuartalPlus,
DATEADD(dy, 1, '28.02.2006') AS TagPlus,
DATEADD(wk, 1, '30.01.2006') AS WochePlus,
DATEADD(mi, 1, '30.01.2006 01:10:30.000') AS
MinutePlus
241_01.sql: Berechnungen mit der Zeit
Man erhält als Ergebnis die folgenden Datumswerte.
TagPlus
MonatPlus
----------------------- ----------------------2006-01-22 00:00:00.000 2006-03-28 00:00:00.000
MonatPlus
MinutePlus
----------------------- ----------------------2006-02-28 00:00:00.000 2006-01-30 01:11:30.000
QuartalPlus
128
TagPlus
Einfache Abfragen
----------------------- ----------------------2006-07-09 00:00:00.000 2006-03-01 00:00:00.000
WochePlus
MinutePlus
----------------------- ----------------------2006-02-06 00:00:00.000 2006-01-30 01:11:30.000
Das nächste Beispiel ruft Namen von Datumswerten ab.
-- Datumsteil mit vollem Namen
SELECT DATENAME(day, '01.01.2006') AS Tag,
DATENAME(month, '28.02.2006') AS Monat,
DATENAME(quarter, '30.01.2006') AS Quartal,
DATENAME(year, '30.01.2006 01:10:30.000') AS Jahr
-- Datumsteil mit abgekürztem Namen
SELECT DATENAME(q, '09.01.2006') AS Quartal,
DATENAME(dy, '28.02.2006') AS Tag,
DATENAME(dw, '28.02.2006') AS Tag,
DATENAME(wk, '30.01.2006') AS Woche,
DATENAME(mi, '30.01.2006 01:10:30.000') AS Minute
241_01.sql: Namen auslesen
Man erhält als Ergebnis folgende Werte:
Tag
Monat
Quartal
Jahr Quartal Tag Tag
Woche
Minute
129
Einfache Abfragen
----- -------- -------- ---- ------- --- -------- ----- ----1
Februar
1
2006 1
59
Dienstag 6
10
Die Funktion DATEPART() ruft einzelne Zeitbestandteile eines Datums ab.
-- Datumsteil mit vollem Namen
SELECT DATEPART(day, '01.01.2006') AS Tag,
DATEPART(month, '28.02.2006') AS Monat,
DATEPART(quarter, '30.01.2006') AS Quartal,
DATEPART(year, '30.01.2006 01:10:30.000') AS Jahr
-- Datumsteil mit abgekürztem Namen
SELECT DATEPART(q, '09.01.2006') AS Quartal,
DATEPART(dy, '28.02.2006') AS Tag,
DATEPART(dw, '28.02.2006') AS Tag,
DATEPART(wk, '30.01.2006') AS Woche,
DATEPART(mi, '30.01.2006 01:10:30.000') AS Minute
241_03.sql: Datumsteil abrufen
Man erhält als Ergebnis:
Tag
Monat
Quartal
Jahr Quartal Tag Tag
Woche Minute
----- -------- -------- ---- ------- --- ---- ----- ------1
2
1
2006 1
59
2
6
10
Das nächste Beispiel verwendet alternativ verschiedene spezielle Funktionen:
SELECT DAY('01.01.2006')
130
AS Tag,
Einfache Abfragen
MONTH('28.02.2006') AS Monat,
YEAR('30.01.2006')
AS Jahr
241_04.sql: Spezialfunktionen verwenden
Man erhält als Ergebnis die folgenden Werte:
Tag
Monat
Jahr
----------- ----------- ----------1
2
2006
Schließlich zeigt das nächste Beispiel, wie aktuelle Zeitinformationen abgerufen werden
können.
SELECT DAY(GETDATE ())
GETDATE ()
AS Tag,
AS [System-Datum],
YEAR(GETUTCDATE()) AS Jahr,
GETUTCDATE()
AS [Greenwich Mean Time]
241_05.sql: Zeit abrufen
Man erhält als Ergebnis:
Tag
System-Datum
Jahr
Greenwich Mean Time
---- ----------------------- ----- ----------------------4
2.4.2
2005-08-04 11:20:19.967 2005
2005-08-04 09:20:19.967
Mathematische Funktionen
Die mathematischen Funktionen erlauben diverse Berechnungen, aus denen auch komplexe Berechnungen in SQL zusammengesetzt werden können. Sofern diese Funktionen
auch in Kombination nicht die gewünschten Ergebnisse liefern, bleibt dazu – wie auch
bei den Zeit- oder Zeichenkettenfunktionen – immer noch die Alternative, die Um-
131
Einfache Abfragen
wandlungen und Berechnungen nicht in SQL sondern in der abrufenden Programmiersprache durchzuführen.
2.4.2.1
ƒ
Referenz
ABS liefert den absoluten Wert einer Zahl, d.h. die Zahl ohne Vorzeichen.
Syntax: ABS ( numeric_expression )
ƒ
ACOS liefert für den angegebenen Kosinuswert den Winkel im Bogenmaß.
Syntax: ACOS ( float_expression )
ƒ
ASIN liefert für den angegebenen Arkussinuswert den Winkel im Bogenmaß.
Syntax: ASIN ( float_expression )
ƒ
ATAN liefert für den angegebenen Arkustangenswert den Winkel im Bogenmaß.
Syntax: ATAN ( float_expression )
ƒ
ATN2 liefert für den Quotienten der beiden angegebenen
(Arkustangenswert) den Winkel im Bogenmaß.
Syntax: ATN2 ( float_expression ,float_expression )
ƒ
CEILING liefert die nächstgrößere ganze Zahl, größer oder gleich dem angegeben
Ausdruck.
Syntax: CEILING ( numeric_expression )
ƒ
COS liefert den Kosinus des als Bogenmaß angegebenen Winkels zurück.
Syntax: COS ( float_expression )
ƒ
COT liefert den Kotangens des als Bogenmaß angegebenen Winkels zurück
Werten
Syntax: COT ( float_expression )
ƒ
132
DEGREES liefert den Winkel als Gradzahl für den als Bogenmaß angegebenen
Winkel zurück.
Syntax: DEGREES ( numeric_expression )
Einfache Abfragen
ƒ
EXP liefert den exponentiellen Wert des Ausdrucks zurück.
Syntax: EXP ( float_expression )
ƒ
FLOOR liefert die nächstkleinere Ganzzahl zurück, kleiner oder gleich dem
Ausdruck ist.
Syntax: FLOOR ( numeric_expression )
ƒ
LOG liefert den natürlichen Logarithmus des Ausdrucks zurück.
Syntax: LOG ( float_expression )
ƒ
LOG10 liefert den dekadischen (Basis 10) Logarithmus des Ausdrucks zurück.
Syntax: LOG10 ( float_expression )
ƒ
PI liefert den konstanten Wert von PI (3.14159...).
Syntax: PI ( )
ƒ
POWER liefert den um y potenzierten Wert des Ausdrucks zurück.
Syntax: POWER ( numeric_expression , y )
ƒ
RADIANS liefert das Bogenmaß für einen in Grad angegebenen Winkel zurück.
Syntax: RADIANS ( numeric_expression )
ƒ
RAND liefert eine Zahl zwischen 1 und 0 zurück. Der Parameter seed erlaubt die
Angabe eines Startwertes, der ansonsten zufällig ermittelt wird.
Syntax: RAND ( [ seed ] )
ƒ
ROUND liefert einen Wert zurück, der auf die in length angegebene Genauigkeit
gerundet ist. Der Parameter function erlaubt bei einer Zahl ungleich 0 die Kürzung
und nicht Rundung der Zahl.
Syntax: ROUND ( numeric_expression , length [ ,function ] )
ƒ
SIGN liefert das Vorzeichen positiv (+1), Null (0) oder negativ (-1) der
angegebenen Zahl zurück.
Syntax: SIGN ( numeric_expression )
ƒ
SIN liefert den Sinus des im Bogenmaß angegebenen Winkels zurück.
Syntax: SIN ( float_expression )
133
Einfache Abfragen
ƒ
SQRT liefert die Quadratwurzel des Ausdrucks.
Syntax: SQRT ( float_expression )
ƒ
SQUARE liebert die Quadratzahl (Potenz 2) der Ausdrucks zurück.
Syntax: SQUARE ( float_expression )
ƒ
TAN liefert den Tangens des Ausdrucks zurück.
Syntax: TAN ( float_expression )
Für die Erläuterung der trigonometrischen Funktionen ist folgendes rechtwinklige Dreieck gegeben:
b = Ankathete
a = Gegenkathete
ß
a
c = Hypothenuse
Abbildung 2.6: Rechtwinkliges Dreieck
Es gelten die in nachfolgender Liste angegebenen Beziehungen:
ƒ
Sinus Į = a/c = Gegenkathete / Hypothenuse
ƒ
Cosinus ȕ = b/c = Ankathete / Hypothenuse
ƒ
Tangens Į = a/b = Gegenkathete / Ankathete = Sinus Į / Cosinus ȕ
ƒ
Kotangens Į = b/a = Ankathete / Gegenkathete
= Cosinus Į / Sinus ȕ = 1 / Tangens Į
134
Einfache Abfragen
Für die Funktionen des Bogenmaßes gelten auf dem Einheitskreis folgende Beziehungen. Der Einheitskreis besitzt einen Radius r = 1 und einen Umfang vom U = 2ʌ. Die
Umrechnungsformel von Grad in Bogenmaß lautet x / 2 ʌ = ij / 360 oder x = ʌ / 180 *
ij.
Bogenmaß
x = x (f )
f
-f
r=1
Bogenmaß:
x = -x (f )
Abbildung 2.7: Einheitskreis und Bogenmaße
2.4.2.2
Beispiele
Die mathematischen Funktionen bedürfen weitestgehend keiner Erklärung. Wie bei
einem Taschenrechner werden die den Funktionen zu Grunde liegenden Rechnungen
auf die übergebenen Werte angewendet.
SELECT ABS(-0.5) AS Betrag,
ROUND(-0.7345, 2) AS Rund1,
FLOOR(2.2)
AS AbRund,
135
Einfache Abfragen
CEILING(2.2) AS AufRund,
PI() AS [Pi] AS [PI],
SQUARE(5)
AS Quadrat,
POWER(5,2) AS Potenz,
POWER(25, 0.5) AS Wurzel,
EXP(1) AS [Eulersche Zahl],
LOG(2) AS [Nat. Logarithmus],
LOG(EXP(1)) AS [1],
EXP(LOG(2)) AS [2]
242_01.sql: Einfache mathematische Berechnungen
Man erhält die nachfolgenden Werte:
Betrag
Rund1
AbRund
AufRund Pi
----------- -------- ------- ------- ---------------0.5
-0.7300
Quadrat Potenz Wurzel
2
3
3,14159265358979
Eulersche Zahl Nat. Logarithmus
1
2
------- ------ -------- -------------- ----------------- --25
25
5
2,718281828
0,69314718055
1
2
Nach dem Satz von Pythagoras gilt a² + b² = c², d.h. ein rechtwinkliges Dreieck wird
bspw. durch 3² + 4² = 5², d.h. 9 +16 = 25, gebildet. Diese Zahlenfolge wird zudem auch
136
Einfache Abfragen
noch als pythagoreisches Zahlentripel bezeichnet, weil aufeinander folgende Ganzzahlen die Seitenlängen bilden und dies schon seit der Antike als formschön gilt und für
diverse tägliche Anwendungen bspw. in der Landvermessung genutzt wurde. Im nachfolgenden Beispiel sollen diese Zahlen ebenfalls genutzt werden.
DECLARE @a int, @b int, @c int
SELECT @a = 3, @b = 4, @c = 5
SELECT SIN(@a/@c) AS Sinus,
COS(@b/@c) AS Kosinus,
TAN(@a/@b) AS Tangens,
COT(@b/@a) AS Kotangens
242_02.sql: Trigonometrische Funktionen
Als Ergebnis erhält man die nachfolgenden Werte:
Sinus
Kosinus
Tangens
Kotangens
--------- --------- -------- -----------------0
1
0
0,642092615934331
Die Zufallszahlenfunktion RAND() bietet im Gegensatz zu den anderen Funktionen
sicherlich einen größeren Einsatzbereich, wenn es bspw. darum geht, Testdaten zu
erzeugen oder sonstiges zufälliges Verhalten zu implementieren. Sie tritt in zwei Varianten auf. In der parameterlosen Variante liefert die Zufallszahlenfunktion Werte auf
Basis eines vom System zufällig bestimmten Ausgangswertes. Dieser Ausgangswert ist
wichtig, da die Zufallszahlenfunktion auf Basis desselben Ausgangswertes auch die
gleichen Ergebnisse liefert. In der Variante mit Parameter kann dieser Ausgangswert
vorgegeben werden.
Für die Verwendung ist zu beachten, dass der gleiche Aufruf mit gleichem Ausgangswert immer die gleichen Zufallszahlen liefert, sodass das Zufallsexperiment wiederhol-
137
Einfache Abfragen
bar bleibt und die gleiche Zahlenfolge liefert. Dies lässt sich sehr schön am ersten Teil
des Beispiels erkennen, wenn man nur diesen Teil mehrfach ausführt.
Im zweiten Teil des Beispiels erstellt man ein kleines Transact-SQL-Programm. Zunächst gibt es zwei Wertespeicher für Zufallszahlen bis 10 und 100 sowie einen Zählerwert, der bei 1 beginnt. Die beiden Wertespeicher stellen einfache Zeichenketten dar,
in die nacheinander die Zufallszahlen abgelegt werden sollen. Um Zufallszahlen bis 10
zu erzeugen, muss man die durch RAND() erzeugte Zahl mit 10 multiplizieren, genauso
wie bei einer Zufalls im Bereich zwischen 1 und 100 den Faktor 100 verwenden muss.
Dies verschiebt eine erzeugte Zufallszahl zwischen 0 und 1 in den Bereich 1 bis 100.
Um dann nur noch Ganzzahlen zu erhalten, muss man in jedem Fall einfach runden. Um
die Zahlen überhaupt als Zeichenkette verarbeiten zu können, ist es zudem notwendig,
sie mit der STR()-Funktion in eine Zeichenkette umzuwandeln.
SELECT RAND(100) AS Zufall1,
RAND(100) AS [Zufall2=1],
RAND()
AS Z3,
RAND()
AS Z4
DECLARE @i int, @bisZehn VARCHAR(100),
@bisHundert VARCHAR(100)
SELECT @i = 1, @bisZehn = '', @bisHundert = ''
WHILE @i < 10
BEGIN
SET @i = @i+1
SET @bisZehn = @bisZehn + LTRIM(STR(ROUND(RAND()*10,0)))
+ ' | '
138
Einfache Abfragen
SET @bisHundert = @bisHundert +
LTRIM(STR(ROUND(RAND()*100,0))) + ' | '
END
PRINT @bisZehn
PRINT @bisHundert
242_03.sql: Zufallszahlen
Man erhält als Ergebnis die nachfolgenden Zeilen. Bemerkenswert ist die Tatsache, dass
gleicher Startwert zu gleichen Zufallszahlen fügt, was man sehr schön in der ersten
Ausgabe in den beiden ersten Spalten erkennen kann. Die letzten beiden Zeilen stellen
die ermittelten und in einer Zeichenkette gespeicherten Zufallszahlen zwischen 1 und 10
sowie 1 und 100 dar.
Zufall1
Zufall2=1
Z3
---------------------- ---------------------- --------------0,715436657367485
0,715436657367485
0,28463380767982
Z4
---------------------0,0131039082850364
3 | 3 | 4 | 7 | 2 | 4 | 4 | 9 | 2 |
10 | 87 | 58 | 30 | 82 | 29 | 62 | 23 | 68 |
Die nachfolgenden Beispiele erstellen aus Bogenmaß wieder Gradzahlen und beschreiten den umgekehrten Weg.
139
Einfache Abfragen
SELECT DEGREES((PI()/4)) AS [Bogen->Grad]
SELECT RADIANS(90) AS [Grad->Bogen],
RADIANS(50) AS [Grad->Bogen]
242_04.sql: Grad und Bogenmaß
2.4.3
Zeichenkettenfunktionen
Während die gerade gezeigten trigonometrischen Funktionen sicherlich in einer kaufmännischen Anwendung nicht gerade zu den absoluten Schlaglichtern im Rahmen von
Abfragen gehören, sondern ihr Einsatz eher für technische Daten und Spezialfälle aufgespart bleibt, sind die Zeichenkettenfunktionen wesentlich häufiger im Einsatz. Sie
erlauben die Formatierung von Zeichenketten sowie die Auswahl von Teilen eines Zeichenkettenwerts. Neben diesem sehr häufigen Anwendungsfall lässt sich auch noch an
die Entfernung oder das Hinzufügen von Leerzeichen sowie das Ersetzen von Werten
denken.
2.4.3.1
Referenz
Folgende Funktionen sind in dieser Kategorie verfügbar:
ƒ
ASCII liefert den ASCII-Codewert des ersten Zeichens einer Zeichenkette.
Syntax: ASCII ( character_expression )
ƒ
CHAR liefert aus einem ASCII-Codewert als Ganzzahl ein Zeichen.
Syntax: CHAR ( integer_expression )
ƒ
liefert die Startposition des Suchausdrucks innerhalb einer
Zeichenkette zurück.
CHARINDEX
Syntax: CHARINDEX ( expression1 ,expression2 [ , start_location
] )
ƒ
140
DIFFERENCE liefert den Unterschied zweier SOUNDEX-Werte als Ganzzahl.
Einfache Abfragen
Syntax:
DIFFERENCE
(
character_expression
,
character_expression )
ƒ
LEFT liefert den linken Teil einer Zeichenfolge mit der angegebenen Menge an
Zeichen.
Syntax: LEFT ( character_expression , integer_expression )
ƒ
LEN liefert die Zeichenanzahl einer Zeichenkette ohne nachfolgende Leerzeichen.
Syntax: LEN ( string_expression )
ƒ
LOWER wandelt die Zeichenkette in Kleinbuchstaben um.
Syntax: LOWER ( character_expression )
ƒ
LTRIM liefert eine Zeichenkette ohne führende Leerzeichen.
Syntax: LTRIM ( character_expression )
ƒ
NCHAR liefert das Unicode-Zeichen auf Basis eines ganzzahligen Codes.
Syntax: NCHAR ( integer_expression )
ƒ
PATINDEX liefert die Position des ersten Auftretens des angegebenen Musters in
der Zeichenkette.
Syntax: PATINDEX ( '%pattern%' , expression )
ƒ
QUOTENAME liefert eine Unicode-Zeichenkette mit zusätzlichen Trennzeichen,
sodass ein gültig begrenzter Bezeichner entsteht.
Syntax: QUOTENAME ( 'character_string' [ , 'quote_character' ]
)
ƒ
REPLACE liefert eine Zeichenkette, in der die Fundstellen der zweiten Zeichenkette
in der ersten durch die dritte Zeichenkette ersetzt wurden.
Syntax: REPLACE ( 'string_expression1' , 'string_expression2' ,
'string_expression3' )
141
Einfache Abfragen
ƒ
REPLICATE wiederholt eine Zeichenkette.
Syntax: REPLICATE ( character_expression ,integer_expression )
ƒ
REVERSE kehrt eine Zeichenkette um.
Syntax: REVERSE ( character_expression )
ƒ
RIGHT liefert den rechten Teil einer Zeichenfolge mit der angegebenen Menge an
Zeichen.
Syntax: RIGHT ( character_expression , integer_expression )
ƒ
RTRIM liefert eine Zeichenkette ohne folgende Leerzeichen.
Syntax: RTRIM ( character_expression )
ƒ
SOUNDEX liefert eine vierstellige Zeichenkette, welche die Ähnlichkeit von zwei
Zeichenketten angibt.
Syntax: SOUNDEX ( character_expression )
ƒ
SPACE liefert eine Zeichenfolge aus mehreren Leerzeichen.
Syntax: SPACE ( integer_expression )
ƒ
STR wandelt Daten in Zeichenkette um.
Syntax: STR ( float_expression [ , length [ , ] ] )
ƒ
STUFF ersetzt in einer Zeichenkette einen Bereich durch eine andere Zeichenkette.
Syntax: STUFF
(
character_expression
,
start
,
length
,character_expression )
ƒ
SUBSTRING liefert eine Teilzeichenausdruck ab der Startposition für eine
angegebene Länge zurück.
Syntax: SUBSTRING ( expression ,start , length )
ƒ
142
UNICODE liefert den Unicode-Codewert des ersten Zeichens einer Zeichenkette.
Einfache Abfragen
Syntax: UNICODE ( 'ncharacter_expression' )
ƒ
UPPER wandelt die Zeichenkette in Großbuchstaben um.
Syntax: UPPER ( character_expression )
2.4.3.2
Beispiele
Die Zeichenkettenfunktionen sind überaus einfach zu verwenden und bieten eine große
Anwendungsvielfalt. Die nachfolgenden Beispiele erklären sich weitestgehend selbst.
Es wird eine längere Zeichenkette in einer Variable gespeichert, aus der ein Teil ausgewählt und in einer anderen Zeichenkette gespeichert wird. Dann gibt man die kürzere
Zeichenkette in Groß- und Kleinbuchstaben aus, kehrt die Reihenfolge der Buchstaben
um, ersetzt das kleine M durch ein kleines N, löscht die Leertasten auf der rechten Seite
der längeren Zeichenkette und ersetzt schließlich ab der Stelle 5 die nächsten 20 Zeichen durch die Zeichenkette wo. Insbesondere die beiden Ersetzungsmethoden sollte
man vergleichen, denn während REPLACE() jedes einzelne Vorkommen einer Testzeichenkette ersetzt, ist es mit STUFF() möglich, einen numerisch beschriebenen Bereich
durch eine Zeichenkette verschiedener Länge zu ersetzen.
DECLARE @text1 varchar(200),
@text2 varchar(200),
@zahl int
SET @text1 = 'Ene, mene, muh, und weg bist du.
'
SET @text2 = SUBSTRING(@text1, 11, 4)
SET @zahl = 12345
SELECT LEN(@text1)
AS Länge,
LOWER(@text2) AS Klein,
UPPER(@text2) AS GROSS,
REVERSE (@text2) AS Spiegel,
143
Einfache Abfragen
REPLACE ( @text2 , 'm' , 'n' ) AS Ersatz1,
RTRIM(@text1) AS LZweg,
@text2 + SPACE(3) + @text2
AS LZhinzu,
STUFF (@text1, 5, 20, 'wo ') AS Ersatz2
243_01.sql: Verwendung einfacher Zeichenkettenfunktionen
Man erhält die verschiedenen malträtierten und umgewandelten Zeichenketten in der
Ergebnismenge zurück.
Länge
Klein
GROSS
Spiegel
Ersatz1
LZweg
------ ------ ------ --------- --------
----------------
32
... weg bist du.
muh
LZhinzu
MUH
hum
nuh
Ersatz2
--------- ---------muh
muh Ene,wo bist du.
Bisweilen kann es interessant sein, Buchstaben in ASCII-Werten vorliegen zu haben
und aus diesen Werten wieder Buchstaben zu generieren oder aus existierenden Buchstaben ASCII-Werte zu erstellen. Dies zeigt das nachfolgende Beispiel, welches ein
ähnliches Beispiel aus der Dokumentation aufgreift. Die Zeichenkette Hallo Welt soll
mit Hilfe einer Schleife durchwandert werden, wobei jeder Buchstaben in einen ASCIIWert umgewandelt wird, um diesen in einer anderen Zeichenkette zu speichern. Gleichzeitig soll aber auch gezeigt werden, wie man aus einem solchen Wert wieder einen
Buchstaben macht, sodass in einem anderen Schritt an diese Umwandlung wieder eine
Rückumwandlung in einen Buchstaben angeschlossen wird. Die beiden Funktionen
ASCII() für die Umwandlung in einen ASCII-Wert und CHAR() für die Rückumwand-
144
Einfache Abfragen
lung sowie die Funktionen STR() für die Umwandlung einer Zahl in eine Zeichenkette
wie auch LEN() für die Ermittlung der Länge einer Funktion sind hierbei notwendig.
DECLARE @text varchar(200),
@ausgabeASCII varchar(200),
@ausgabeVARCHAR varchar(200),
@position int
SELECT @text = 'Hallo Welt.', @ausgabeASCII = '',
@ausgabeVARCHAR = '', @position = 1
-- Wanderung durch einzelne Zeichen
WHILE @position <= LEN(@text)
BEGIN
-- Umwandlung in ASCII
SET @ausgabeASCII = @ausgabeASCII +
LTRIM(STR(ASCII(SUBSTRING(@text, @position, 1)))) +
'|'
-- Umwandlung von Zeichen -> ASCII -> Zeichen
SET @ausgabeVARCHAR = @ausgabeVARCHAR +
CHAR(ASCII(SUBSTRING(@text, @position, 1)))
SET @position = @position + 1
END
-- Ausgabe
PRINT @ausgabeASCII
PRINT @ausgabeVARCHAR
145
Einfache Abfragen
243_02.sql: Umwandlung nach ASCII und zurück
Man erhält schließlich zwei Zeichenketten mit dem ursprünglichen Wert der bearbeiteten Zeichenkette und den einzelnen Buchstaben in ASCII-Nummern.
72|97|108|108|111|32|87|101|108|116|46|
Hallo Welt.
Schließlich folgt noch einmal eine Reihe von Zeichenkettenfunktionen, die aus Platzgründen nicht in den vorherigen Beispielen untergebracht werden konnten. Dabei ist
QUOTENAME() besonders hervorzuheben und möglicherweise häufiger im Einsatz als
die beiden Ähnlichkeitsfunktionen. QUOTENAME() erstellt aus einer beliebigen Zeichenkette mit bspw. Leertaste oder eckigen Klammern einen gültigen Bezeichner, d.h.
umschließt die Bestandteile der Zeichenkette mit entsprechenden Entwerterzeichen. Die
Funktion CHARINDEX() kann auch nützliche Dienste leisten, da sie innerhalb einer
Zeichenkette oder erst ab einer bestimmten Position in einer Zeichenkette die Position
eines angegebenen Zeichens ermittelt.
SOUNDEX() bietet die Möglichkeit, eine Zeichenkette in eine alphanumerische Zeichenfolge umzuwandeln, welche es für die englische Sprache ermöglichen soll, Wörter aufgrund ihrer ähnlichen Aussprache zu vergleichen. Der Algorithmus besteht aus folgenden Schritten: 1. Der erste Buchstabe bleibt erhalten. 2. Die Buchstaben a, e, h, i, o, u,
w, y werden entfernt, sofern es sich nicht um den ersten Buchstaben handelt. 3. Den
Buchstaben werden folgende Zahlen zugeordnet: b, f, p, v ersetzt man durch 1, c, g, j, k,
q, s, x, z ersetzt man durch 2, d, t ersetzt man durch 3, l ersetzt man durch 4, m und n
ersetzt man durch 5, r ersetzt man durch 6. 4. Doppelte Werte sollen gestrichen werden.
5. Nur die ersten vier Bytes werden zurückgeliefert und werden mit 0-Werten aufgefüllt.
Wie man sich denken kann, gibt es viele verschiedene Varianten und Verbesserungen
dieses Verfahrens, um ähnliche Zeichenketten leicht zu identifizieren oder überhaupt
die Ähnlichkeit abzubilden. Die beiden Entwickler dieser Methode sind Robert Russel
und Margaret Odell im Jahre 1918.
146
Einfache Abfragen
Die DIFFERENCE()-Funktion ermittelt eine Ganzzahl, der den Unterschied ausdrückt,
der zwischen zwei SOUNDEX-Werten vorhanden ist. Diese Zahl liegt zwischen 0 und 4,
wobei 0 auf eine geringe und 4 auf eine hohe Ähnlichkeit zwischen zwei Zeichenketten
hinweist.
SELECT QUOTENAME('Bez{}eich ner[]')
AS Bezeichner,
CHARINDEX ('l', 'Hallihallo' , 5 ) AS Position,
DIFFERENCE ( 'Welle' , 'Kelle' ) AS U1,
DIFFERENCE ( 'Eins' , 'Zwei' )
AS U2,
DIFFERENCE ( 'x' , 'u' )
AS U3,
SOUNDEX('Welle') AS U4,
SOUNDEX('Kelle') AS U5,
SOUNDEX('Eins')
AS U6,
SOUNDEX('Zwei')
AS U7
243_03.sql: Weitere Zeichenkettenfunktionen
Man erhält die nachfolgenden Werte.
Bezeichner
Pos
U1
U2
U3
U4
U5
U6
U7
------------------- ------ ------ --- --- ----- ----- ---- [Bez{}eich ner[]]]
2.4.4
8
3
1
3
W400
K400
E520 Z000
Systemfunktionen
Die Systemfunktionen sind zwar sehr zahlreich, werden allerdings hier mehr der Vollständigkeit halber erwähnt. Sie sind zwar für den Programmierer, der administrative
Aufgaben mit T-SQL erledigt, sehr nützlich, doch ist dies nicht die Perspektive dieses
Buches.
147
Einfache Abfragen
Folgende Funktionen sind u.a. in dieser Kategorie verfügbar:
ƒ
APP_NAME (nicht deterministisch) prüft, ob die aktuelle Sitzung vom Typ SQL
Server Management Studio ist.
Syntax: APP_NAME ( )
ƒ
CAST und CONVERT (deterministisch außer bei datetime, smalldatetime oder
sql_variant) wandelt einen Ausdruck in einen anderen Datentyp um.
Syntax: CAST ( expression AS data_type [ (length ) ]) und
CONVERT ( data_type [ ( length ) ] , expression [ , style ] )
ƒ
COALESCE (deterministisch) prüft n verschiedene Ausdrücke darauf, ob sie den
Wert NULL haben. Ist dies bei einem der aufgelisteten Ausdrücke der Fall, wird der
für diesen Fall angegebene Ausdruck ausgeführt. Sollte kein Ausdruck NULL sein,
dann liefert diese Funktion NULL zurück.
Die gleiche Fallunterscheidung lässt sich daher auch mit einer CASEAnweisung ausdrücken:
CASE
WHEN (ausdruck1 IS NOT NULL) THEN ausdruck1
WHEN (ausdruckN IS NOT NULL) THEN ausdruckN
ELSE NULL
END
Syntax: COALESCE ( expression [1,...n ] )
ƒ
148
COLLATIONPROPERTY (nicht deterministisch) liefert den Namen der in der
Datenbank verwendeten Sortierung im Datentyp nvarchar(128) zurück. Mögliche
Werte sind: CodePage für Nicht-Unicode-Codepage, LCID für Windows-LCID,
ComparisonStyle für die Windows-Vergleichstechnik, Version für die
Sortierungsversion mit den Werten 1 für MS SQL Server 2005-Sortierverfahren
und 0 für frühere Sortierverfahren.
Einfache Abfragen
Syntax: COLLATIONPROPERTY( collation_name , property )
ƒ
COLUMNS_UPDATED (nicht deterministisch)
Syntax: COLUMNS_UPDATED ( )
ƒ
(nicht deterministisch) ist ein Alias der Funktion
GETDATE() und liefert das aktuelle Datum und die aktuelle Uhrzeit.
CURRENT_TIMESTAMP
Syntax: CURRENT_TIMESTAMP
ƒ
CURRENT_USER (nicht deterministisch) liefert den Namen des aktuellen Benutzers.
Syntax: CURRENT_USER
ƒ
DATALENGTH (Deterministisch) liefert die Byteanzahl zurück, die für die Ausgabe
und Darstellung des angegebenen Ausdrucks notwendig sind.
Syntax: DATALENGTH ( ausdruck )
ƒ
fn_servershareddrives (nicht deterministisch) liefert eine Übersicht über
freigegebene Laufwerke, welche zur selben Clustergruppe gehören, zu der auch der
aktuelle MS SQL Server gehört.
149
Einfache Abfragen
150
Komplexe Abfragen
3 Komplexe Abfragen
151
Komplexe Abfragen
152
Komplexe Abfragen
3 Komplexe Abfragen
Während die Abfragen des letzten Kapitels ausschließlich immer eine einzige Tabelle
bearbeiteten und von den syntaktischen Möglichkeiten der Abfragegestaltung bei Weitem noch nicht alles ausgeschöpft haben, was überhaupt an SQL-Syntax im SQL Server
in spezieller Weise oder im SQL-Standard als solchem bereit steht, soll dieses Kapitel
nun zeigen, wie man Daten aus mehreren verknüpften Tabellen abruft, Unterabfragen
formuliert und erweiterte Aggregate erzeugt.
3.1
Verknüpfungen
Das Prinzip des relationalen Modells liegt gerade darin, dass die Objekte der realen
Welt mit einzelnen Entitäten (Relationen) abgebildet werden. Sie besitzen jeweils einen
Primärschlüssel, der aus einem oder mehreren Feldern besteht. Die Verbindungen zwischen den einzelnen Tabellen richtet die so genannte Primärschlüssel-FremdschlüsselBeziehung ein. Dabei erscheint der Schlüsselwert einer Primärschlüsselspalte in einer
anderen Tabelle als so genannter Fremdschlüssel und erlaubt über die Verknüpfung mit
der anderen Tabelle bzw. einer Lektüre der zu diesem Schlüsselwert passenden Zeile
der anderen Tabelle, die zusätzlichen Informationen dieser anderen Tabelle abzurufen.
Dieses Prinzip ist in allen relationalen Datenbanken umgesetzt und führt dazu, dass
möglichst keine redundanten Daten gespeichert werden. Dies bezieht sich auf die verknüpften Daten, die ja in ihrer jeweiligen Merkmalsausprägung für die einzelnen Spalten nur einmal erscheinen, aber in der Tabelle, die den Fremdschlüssel enthält, mehrfach verknüpft sein können.
3.1.1
Manuelle Verknüpfungen
Es gibt zwei Möglichkeiten, Tabellen miteinander zu verknüpfen, von denen die in
diesem Abschnitt beschriebene manuelle Verknüpfung die ungünstigere von beiden
Varianten ist, da sie weniger gut lesbar ist und insgesamt fehleranfälliger ist. Da allerdings die ANSI-SQL-Verknüpfungstechnik in vielen bestehenden Anwendungen immer
153
Komplexe Abfragen
noch diese Verknüpfungstechnik verwendet wird, da sie vor einigen Jahren noch nicht
so verbreitet im Einsatz war oder von Datenbanken nicht unterstützt wurde, soll zunächst diese Technik vorgeführt werden. Er soll allerdings nur dazu dienen, ein allgemeines Verständnis für die Verknüpfungstechnik als solche zu fördern und die Lektüre
bestehender SQL-Skripte zu ermöglichen, aber in keinem Fall dazu führen, sie auch zu
verwenden.
3.1.1.1
Einfache Verknüpfung
Bislang war es nicht möglich, zu einem Angestellten auch Vor- und Nachname abzurufen, weil in der Tabelle Employee nur allgemeine Informationen zum Angestellten als
solchen gespeichert waren, und die Informationen zu seiner Person, die er mit anderen
Personenarten wie bspw. externem Verkaufspersonal gemeinsam hatte, in einer zentralen Tabelle Contact gespeichert waren. Zwischen diesen beiden Tabellen besteht eine
so genannte 1:1-Verknüpfung, da jeweils ein Datensatz aus der Employee-Tabelle mit
genau einem Datensatz aus der Contact-Tabelle über die Spalte ContactID der
Employee-Tabelle verbunden ist. Diese ContactID ist in der Employee-Tabelle der
Fremdschlüssel und in der Tabelle Contact dagegen der Primärschlüssel. Bei einer
Verknüpfung von beiden Tabellen ist also genau der Datensatz aus Contact auszuwählen, der den gleichen Wert in der ContactID-Spalte besitzt wie in der EmployeeTabelle. Dies ist das Grundprinzip des so genannten Equijoins, da hier über die Gleichheit abgefragt wird.
Syntaktisch richtet man eine solche Verknüpfung manuell in der FROM- und WHEREKlausel ein. In der Tabellenliste der FROM-Klausel schreibt man ganz einfach die Tabellen, aus denen gleichzeitig Daten abgerufen werden sollen, in einer Komma-getrennten
Liste nieder. Da in diesem Datenmodell die Tabellen einen langen Namen aufweisen
und zudem in einzelnen Schemata untergebracht sind, ist es hier lohnenswert, so genannte Tabellenaliase zu vergeben, die einen deutlich kürzeren Namen aufweisen. Sie
ermöglichen dann später in der WHERE-Klausel einen deutlich vereinfachten Zugriff auf
die Primärschlüssel-/Fremdschlüssel-Spalten, welche als Bedingung gleichgesetzt werden.
154
Komplexe Abfragen
In natürlicher Sprache könnte man sich dieses Vorgehen so vorstellen: „Wähle die Spalten FirstName, LastName und HireDate aus den beiden Tabellen Employee, die mit
der Abkürzung emp angesprochen werden soll, und der Tabelle Contact, die mit der
Abkürzung con angesprochen werden soll, für die Datensätze aus, welche in beiden
Tabellen die gleiche ContactID aufweisen. Als Filter soll gelten, dass nur Mitarbeiter
mit einem Nachname kleiner B ausgewählt werden sollen.“
Innerhalb der WHERE-Klausel besitzt man also zwei verschiedene Arten von Bedingungen: Zum einen eine Verknüpfungsbedingung, welche die ContactID-Spalten beider
Tabellen gleich setzt, und zum anderen eine inhaltliche Filterung/Einschränkung wie er
auch in einer gewöhnlichen Abfrage mit Blick auf eine Tabelle erscheinen würde.
-- Angestellte mit Kontaktdaten
SELECT FirstName, LastName, HireDate
FROM HumanResources.Employee AS emp, Person.Contact AS con
WHERE emp.ContactID = con.ContactID
AND LastName < 'B'
311_01.sql: Verknüpfen von zwei Tabellen
Als Ergebnis erhält man neben den Informationen aus der Employee-Tabelle auch die
Informationen aus der Contact-Tabelle. Die Anzahl der Datensätze entspricht dabei
aufgrund der 1:1-Verknüpfung der Anzahl der auch ansonsten beim Filter auf den
Nachnamen ausgewählten Datensätzen. Allerdings müsste man dafür in der ContactTabelle wissen, welche Kontakte für Mitarbeiter gelten.
FirstName
LastName
HireDate
------------- ------------- ----------Greg
Alderson
1999-01-03
Sean
Alexander
1999-01-29
155
Komplexe Abfragen
ContactID HireDate
----------- ----------------------1076
1998-02-07 00:00:00.000
1173
1999-01-03 00:00:00.000
1270
1999-01-29 00:00:00.000
1050
1999-01-30 00:00:00.000
ContactID FirstName
LastName
----------- ------------------------ --------------------1173
Greg
Alderson
1270
Sean
Alexander
1304
Sean
Alexander
FirstName
LastName
HireDate
------------------------ ------------------------ ---------------------------------Greg
Alderson
1999-01-03 00:00:00.000
Sean
Alexander
1999-01-29 00:00:00.000
Abbildung 3.1: Verknüpfung von Tabellen
3.1.1.2
Qualifizierte Spaltennamen
Im vorherigen Kapitel wurden die qualifizierten Spaltennamen eingeführt, die aus
Gründen der Syntaxvorstellung sehr gut an die dort gewählte Stelle passten, aber leider
keinen besonderen Nutzen stifteten, außer einen Spannungsbogen aufzubauen, der nun
seinen Höhepunkt findet. Sollten nämlich durch die Verknüpfung zwei Spalten den
gleichen Namen erhalten und beide in der Ergebnismenge ausgegeben werden bzw.
werden gleichnamige Spalten verknüpft, dann muss man angeben, aus welcher Tabelle
diese Spalten stammen. Dies hat man auch schon im vorherigen Beispiel in der Zeile
WHERE emp.ContactID = con.ContactID gesehen. Im nachfolgenden Beispiel
verhält es sich so, dass jeweils eine Title-Spalte in der Employee- und in der Contact-Tabelle vorhanden ist, von denen die eine die Anrede und die andere die Position
im Unternehmen bezeichnet. Um nun in der Spaltenliste anzugeben, welche von beiden
Spalten im Ergebnis angezeigt werden soll, oder um in der WHERE-Klausel eine bestimmte Einschränkung auf eine von beiden Spalten vorzunehmen, muss man die jeweiligen Spaltennamen qualifizieren. Dies gelingt entweder über den gewöhnlichen Tabellennamen oder bei sehr langen Namen über den Tabellenalias.
-- Angestellte mit Kontaktdaten
SELECT emp.Title, con.Title, LastName
156
Komplexe Abfragen
FROM HumanResources.Employee AS emp, Person.Contact AS con
WHERE emp.ContactID = con.ContactID
AND con.Title IS NOT NULL
311_02.sql: Doppelte Spaltennamen
Title LastName
ContactID
-------- -------------------------- ----------Ms.
Erickson
1005
Mr.
Goldberg
1006
Ms.
Galvin
1008
Mr.
Welcker
1010
NULL Saraiva
1020
NULL Brown
1030
Ms.
Williams
1035
Mr.
Ting
1088
ContactID Title
----------- -------------------------------------------------1005
Design Engineer
1006
Design Engineer
1035
Marketing Specialist
1032
Marketing Specialist
1088
Production Technician - WC20
1008
Tool Designer
1025
Sales Representative
Title Title
ContactID LastName
-------- -------------------------------------------------- ------------- --------------------------Ms.
Design Engineer
1005
Erickson
Mr.
Design Engineer
1006
Goldberg
Ms.
Marketing Specialist
1035
Williams
Mr.
Production Technician - WC20
1088
Ting
Ms.
Tool Designer
1008
Galvin
Mr.
Vice President of Sales
1010
Welcker
Mr.
Sales Representative
1027
Mensa-Annan
Mr.
Pacific Sales Manager
1012
Abbas
Abbildung 3.2: Funktionsweise der qualifizierten Spaltennamen
Im Ergebnis erhält man die beiden unterschiedlichen Title-Spalten im Ergebnis.
Title
Title
LastName
--------------------- -------- ---------Design Engineer
Ms.
Erickson
Design Engineer
Mr.
Goldberg
157
Komplexe Abfragen
Sollte man die Qualifizierung in der WHERE-Klausel oder in der Spaltenliste vergessen,
dann ist die Fehlermeldung überaus deutlich und lautet:
Meldung 209, Ebene 16, Status 1, Zeile 2
Mehrdeutiger Spaltenname 'Title'.
3.1.1.3
Mehrfache Verknüpfung
Für gewöhnlich reicht bei einem großen Datenmodell die Verknüpfung von nur zwei
Tabellen überhaupt nicht aus, um die interessanten Daten zu beschaffen. Daher müssen
meistens mehr als zwei Tabellen verbunden werden. Dies gelingt ganz einfach dadurch,
indem man die Tabellenliste in der FROM-Klausel um weitere Tabellen und ihre Aliasnamen ergänzt und in der WHERE-Klausel die entsprechenden Bedingungen mit AND
anhängt. So verknüpft die nachfolgende Abfrage insgesamt vier Tabellen, nämlich
Employee, Contact, EmployeeDepartmentHistory und Shift, um herauszufinden, welcher Angestellte auf welcher Schicht eingesetzt ist.
-- Angestellte mit Kontaktdaten, Schicht
SELECT emp.EmployeeID, emp.Title, LastName, StartTime,
EndTime
FROM HumanResources.Employee AS emp,
Person.Contact
AS con,
HumanResources.EmployeeDepartmentHistory AS edh,
HumanResources.Shift
AS sh
WHERE emp.ContactID = con.ContactID
AND emp.EmployeeID = edh.EmployeeID
AND edh.ShiftID = sh.ShiftID
AND emp.EmployeeID = 119
158
Komplexe Abfragen
311_03.sql: Verknüpfung mit mehreren Tabellen
Man erhält für den Mitarbeiter 119 seine Arbeitszeiten und seinen Vor- und Nachnamen
in einer einzigen Ergebnismenge.
EmployeeID
Title
LastName
StartTime
EndTime
----------- -------------- ----------- ------------ -------119
3.1.1.4
Marketing ...
Williams
07:00:00 15:00:00
Ungewollte Kreuzverknüpfung
Die manuelle Verknüpfung ist zwar von der Syntax her einfacher zu verstehen als die
im nächsten Abschnitt vorgestellte ANSI-SQL-Verknüpfung, doch birgt sie die Gefahr
einer ungewollten Kreuzverknüpfung, die auch unter dem Namen kartesisches Produkt
berühmt und berüchtigt ist. Bisweilen möchte man sogar ein solches kartesisches Produkt ausdrücklich einrichten, doch die Anwendungsfälle dafür sind deutlicher seltener
als die ungewollte Erzeugung.
Der Fehler entsteht, wenn ganz einfach in der WHERE-Klausel eine der notwendigen
Verknüpfungsbedingungen fehlt. Im Normalfall hat man bei n Tabellen n-1 Verknüpfungsbedingungen, sofern immer nur ein Primärschlüssel-/Fremdschlüsselfeld an jeder
Verknüpfung teilnehmen, ansonsten natürlich dementsprechend mehr Bedingungen.
Vergisst man eine von diesen Bedingungen, dann enthält die Ergebnismenge die Kombinationen von allen Werten der korrekt verknüpften Tabellen mit den Datensätzen der
nicht korrekt verknüpften Tabellen also bei fehlendem sonstigen Filter das Produkt aus
Zeilenzahl (Kardinalität) der einen Zwischenergebnismenge oder Tabelle und der Zeilenzahl der aufgezählten, aber nicht verknüpften Tabellen. Es entstehen also ungewollt
besonders umfangreiche Ergebnismengen, die syntaktisch richtig, aber inhaltlich völlig
falsch, da sie Werte enthalten, die nicht einmal in der Datenbank so gespeichert waren.
Wie man sich leicht denken kann, ist eine solche syntaktisch richtige und logisch falsche Ergebnismenge sehr gefährlich, da man bei Unkenntnis des ungefähren Umfangs
der Ergebnismenge durch Schätzung zu völlig falschen Ergebnissen kommt. Insbesondere bei großen Datenbanken mit umfangreicher Tabellenzahl, die zudem auch noch
159
Komplexe Abfragen
sehr gut normalisiert sind, hat man oft nur noch Zahlen in der Ergebnismenge und wenige Zeichenketten, die möglicherweise helfen würden, zu erkennen, dass die Anzahl
der Datensätze für einen bestimmten Sachverhalt der abgebildeten Welt völlig ausgeschlossen ist.
Der Grund, warum zu Beginn dieses Abschnitts die manuellen Verknüpfung nicht empfohlen wurde, liegt nun gerade darin, dass man natürlich bei sehr vielen Tabellen und
möglicherweise umfangreichen Filtern immer schneller in die Gefahr gerät, tatsächlich
eine Verknüpfungsbedingung zu vergessen. Es findet kein syntaktischer Schutz statt
bzw. man kann im Gegensatz zu den ANSI-SQL-Verknüpfungen auch gar nicht ausdrücklich angeben, mit welcher Art von Verknüpfung nun die Tabellen verbunden werden sollen. Durch die Vermischung von Verknüpfungsbedingungen und einschränkenden/filternden Bedingungen in der WHERE-Klausel kann man nicht ausreichend unterscheiden, welche Bedingung zur einen oder anderen Gruppe gehört bzw. ob alle notwendigen Verknüpfungsbedingungen vorhanden sind.
Die nächste Abfrage erzeugt eine solche ungewollte Kreuzverknüpfung, um das deutliche und unschöne Ergebnis vorzuführen. Dabei listet die FROM-Klausel ganz einfach die
beiden Tabellen EmployeDepartmentHistory und Shift auf, ohne überhaupt eine
Verknüpfungsbedingung anzugeben. Stattdessen begrenzt die WHERE-Klausel die Datensätze nur auf die Angestelltennummer kleiner drei.
SELECT edh.EmployeeID, edh.ShiftID, sh.ShiftID, sh.StartTime
FROM HumanResources.EmployeeDepartmentHistory AS edh,
HumanResources.Shift
AS sh
WHERE edh.EmployeeID <3
311_04.sql: Ungewollte Kreuzverknüpfung
Man erhält ein Ergebnis, in dem man deutlich sehen kann, wie die Gruppe aus EmployeeID und Schichtnummer aus der EmployeeDepartmentHistory-Tabelle mit allen
160
Komplexe Abfragen
verfügbaren Schichtnummern aus der Shift-Tabelle verknüpft werden. Dies ist genau
das kartesische Produkt, das sicherlich inhaltlich völlig unnütz ist.
EmployeeID
ShiftID ShiftID StartTime
----------- ------- ------- -----------1
1
1
07:00:00.000
1
1
2
15:00:00.000
1
1
3
23:00:00.000
2
1
1
07:00:00.000
2
1
2
15:00:00.000
2
1
3
23:00:00.000
3.1.2
ANSI-SQL-Verknüpfungen
Während die manuellen Verknüpfungen nur für das Leseverständnis von älteren Skripten vorgestellt wurden, sollen nun die ANSI-SQL-Verknüpfungen auch als richtige
Technik vorgestellt und ausführlicher behandelt werden. Die besondere Verbesserung
der ANSI-SQL-Verknüpfungen besteht nun daraus, dass sowohl die Tabellen, aus denen die Daten übernommen werden sollen, als auch die Verknüpfungsbedingungen
komplett in der FROM-Klausel stehen. Dadurch enthält die WHERE-Bedingung nur noch
die einschränkenden Bedingungen. Dies verhindert ein ungewolltes Vergessen von
Verknüpfungsbedingungen, da diese vollständig in der FROM-Klausel erwartet werden.
3.1.2.1
Innere Verknüpfung
Die zuvor vorgestellten Verknüpfungen lassen sich in ANSI-SQL-Syntax mit einer so
genannten inneren Verknüpfung und Gleichheitsverbindung (häufigster Fall) abbilden.
Dabei prüft die innere Verknüpfung darauf, dass nur die Datensätze in die Ergebnismenge übernommen werden, die in beiden verknüpften Tabellen einen Treffer haben,
d.h. die Verknüpfungsbedingung erfüllen. Die Syntax für die innere Verknüpfung er-
161
Komplexe Abfragen
wartet, dass zunächst die erste Tabelle genannt und möglicherweise umbenannt wird.
Dann folgt die Klausel INNER JOIN und die zweite Tabelle mit optionalem Aliasnamen. Die ansonsten auch in der manuellen Verknüpfung angegebene Verknüpfungsbedingung folgt dann nach dem Schlüsselwort ON.
Das nachfolgende Beispiel verknüpft die beiden Tabellen SalesPerson und SalesTerritory miteinander, um die Nummer des Verkäufers und seine Verkaufsregion
sowie das übergeordnete Gebiet dieser Verkaufsregion herauszufinden. Dabei müssen
die beiden TerritoryID-Spalten in beiden Tabellen miteinander verbunden werden.
-- Verkäufer, Gebiete und Länder
SELECT sp.SalesPersonID, Name, [Group]
FROM Sales.SalesPerson AS sp
INNER JOIN Sales.SalesTerritory AS st
ON sp.TerritoryID = st.TerritoryID
312_01.sql: Einfache Verknüpfung
Man erhält die angekündigte Liste mit Verkäufernummer und Verkaufsgebiet.
SalesPersonID Name
Group
------------- -------------- ---------------275
Northeast
North America
276
Southwest
North America
Möchte man mehrere Tabellen miteinander verknüpfen, dann schließt man mit weiteren
INNER JOIN-Klauseln die benötigten Tabellen an. Die genaue Verknüpfungsbedingung
spezifiziert man dann jeweils nach ON. Grundsätzlich gibt es keine Notwendigkeit, die
Reihenfolge der Verknüpfung von der Reihenfolge der Tabellen oder von inhaltlichen
Erwägungen abhängig zu machen. Es empfiehlt sich zwar, eine korrekte Reihenfolge
162
Komplexe Abfragen
aus Gründen der besseren Lesbarkeit und Kontrolle einzuhalten, doch letztendlich zählt
nur, dass alle benötigten Tabellen aufgerufen werden.
Im nächsten Beispiel nimmt man zur gerade vorher gezeigten Abfrage noch die Tabelle
SalesTerritoryHistory hinzu und schränkt die Ergebnisse auf eine einzige Ver-
kaufsregion ein, um herauszufinden, welche verschiedenen Verkäufer diese Region
bereisten oder bearbeiten.
-- Verkäufer, Gebiete und Zeiten
SELECT sp.SalesPersonID, Name, StartDate, EndDate
FROM Sales.SalesPerson AS sp
INNER JOIN Sales.SalesTerritoryHistory AS sth
ON sp.SalesPersonID = sth.SalesPersonID
INNER JOIN Sales.SalesTerritory AS st
ON sth.TerritoryID = st.TerritoryID
WHERE Name = 'Northwest'
ORDER BY StartDate
312_02.sql: Mehrfache Verknüpfung
Man erhält nun für die ausgewählte Region mehrere Verkäufer mit unterschiedlichen
und teilweise anschließenden Einstiegs- und Ausstiegsdaten.
SalesPersonID Name
StartDate
EndDate
------------- ---------------- ----------- ----------280
Northwest
2001-07-01
2002-10-31
283
Northwest
2001-07-01
NULL
287
Northwest
2002-11-01
NULL
163
Komplexe Abfragen
3.1.2.2
Äußere Verknüpfung
Während die innere Verknüpfung nur die Datensätze aus der einen Tabelle übernimmt,
die in der anderen Tabelle einen Partnerdatensatz finden, der über die Primärschlüssel/Fremdschlüssel-Verknüpfung zu finden ist, betrachtet die äußere Verknüpfung auch die
Datensätze, die gerade keinen Partnerdatensatz finden. Die Abfragen, die u.a. mit einer
äußeren Verknüpfung zu lösen sind, interessieren sich immer für alle Datensätze einer
Tabelle, unabhängig davon, ob sie mit der anderen Tabellen verknüpft sind, oder sie
fordern, nur die Datensätze anzuzeigen, die gerade keine Verknüpfung mit der anderen
Tabellen aufweisen. Als syntaktische Alternative stehen in unterschiedlicher Weise
auch die Mengenoperatoren zur Verfügung.
Die natürlich-sprachliche Frage für das nächste Beispiel lautet daher: „Zeige alle Produkte an, die überhaupt gespeichert sind, und gebe – falls verfügbar – ihre zugehörigen
Lagerplatznummer an.“ Hier kann man sich vorstellen, dass in der Tabelle Product
eine umfangreiche Liste an Produkten enthalten ist, die überhaupt nicht mehr angeboten
ist oder aus anderen Gründen nicht mehr auf Lager gehalten wird. Die Abfrage soll
allerdings nicht nur die Produkte abrufen, die auch einen Lagerplatz und damit eine
Verknüpfung zur Tabelle ProductInventory haben, sondern einfach alle Produkte,
ganz unabhängig vom Lagerplatz. Ist allerdings ein solcher Lagerplatz verfügbar, dann
soll er selbstverständlich angezeigt werden. Im direkten Vergleich zu einer inneren
Verknüpfung erscheinen in der Ergebnismenge also mehr Datensätze. Das sind diejenigen, die in der Spalte ProductID der Tabelle ProductInventory nicht erscheinen.
Sie erhalten als Wert eine NULL in der Ergebnismenge, da ja tatsächlich kein Wert für
dieses Feld vorhanden ist.
Die Abfrage wird als äußere Verknüpfung formuliert, indem man die allgemeine Syntax
tabelle [LEFT | RIGHT] OUTER JOIN verwendet. In Seminaren ist nicht nur die
äußere Abfrage an sich häufig ein großes Gesprächsthema und teilweise mit einigem
Mythos umgeben, weil es schon einmal einen Kollegen gibt, der diese Abfragetechnik
tatsächlich beherrscht und man sich erstaunlich viel unter einer äußeren Abfrage vorstellt. Eine gewisse Enttäuschung ist leider hier regelmäßig zu bemerken. So auch bei
der Information, dass die Angabe LEFT oder RIGHT wirklich nichts anderes bedeutet als
164
Komplexe Abfragen
die Information, welche von beiden Tabellen (die linke oder die rechte) die Wertedominanz erhalten soll, d.h. welche ihre Daten trotz fehlender Verknüpfung komplett in die
Ergebnismenge übergeben soll. Im aktuellen Beispiel sollen alle Daten aus der Tabelle
Product in der Ergebnismenge erscheinen. Daher hat die Tabelle Product Wertedominanz und man formuliert Product LEFT OUTER JOIN ProductInventory,
wenn Product links steht, und ProductInventory RIGHT OUTER JOIN Product,
wenn Product rechts steht. Um es also – wie im Seminar – noch einmal ganz deutlich
zu formulieren: lediglich die Position der wertedominanten Tabelle entscheidet darüber,
ob es LEFT oder RIGHT ist.
Möglicherweise entsteht nun die Frage, wie man denn die wertedominante Tabelle
erkennen könne. Sofern man die Abfrage so deutlich formuliert wie zuvor, dann sollte
in dieser natürlich-sprachlichen Frage bereits deutlich zu erkennen sein, dass man überhaupt alle Produkte (und nicht nur die gekauften), alle Kunden (und nicht nur die, die
auch in der Telefonsupport-Tabelle erscheinen) oder alle Mitarbeiter (und nicht nur die,
die an einem Projekt beteiligt sind) abrufen will. Sollte dies nicht reichen, dann hilft ein
kritischer Blick auf das Datenmodell. Im aktuellen Fall würde man die ProductTabelle auch als Eltern-Tabelle bezeichnen, weil die ProductInventory-Tabelle
ihren Primärschlüssel als Fremdschlüssel enthält und mehrere Einträge in der ProductInventory-Tabelle Bezug zum gleichen Produkt haben können. Eine wertedominante
ProductInventory-Tabelle ist eigentlich bei einer korrekten Funktionsweise der
referenziellen Integrität (Primärschlüssel-Fremdschlüssel-Verknüpfung) nicht denkbar.
Eine Abfrage würde hier bspw. lauten: „Zeige alle Lagerplätze, und – falls verfügbar –
auch die zugehörigen Produkte an.“ Das klingt zunächst ganz logisch, wenn man voraussetzt, dass die ProductInventory-Tabelle alle möglichen Lagerplätze enthält. In
Wirklichkeit zeigt aber ein Blick auf das Datenmodell, dass es sich hierbei um die Beziehungstabelle zwischen Product und Location handelt, wobei Location die überhaupt verfügbaren Lagerplätze speichert. Sofern also ein Datensatz in der ProductInventory-Tabelle erscheint und kein passendes Produkt mehr finden kann, stellt sich
die Frage, wie überhaupt das zugehörige Produkt gelöscht werden könnte und warum
bspw. eine automatische Lösch-Aktion in der ProductInventory-Tabelle nicht implementiert wurde und fehlgeschlagen ist. Dies ist allerdings eine Frage des Datenmo-
165
Komplexe Abfragen
dells bzw. der Einrichtung der konkreten Tabellen und Thema eines ganz anderen
Buchs (nämlich Administration und Grundlagen zu relationalen Datenbanken).
-- Alle Produkte und mögliche Lagerplätze
SELECT p.ProductID
AS [P-ID],
piv.ProductID AS [PIV-ID],
LocationID
AS [Loc-ID],
Shelf
AS Regal,
Bin
AS Kasten
FROM Production.Product AS p
LEFT OUTER JOIN Production.ProductInventory AS piv
ON p.ProductID = piv.ProductID
ORDER BY piv.ProductID
312_03.sql: Äußere Verknüpfung mit Anzeige aller Daten
Wie angekündigt, enthält die Ergebnismenge sowohl die Produkte, die auch einem Lagerplatz zugewiesen sind, und diejenigen, die keinem oder nicht mehr einem Lagerplatz
zugewiesen sind. Dies bedeutet, dass für diese Produkte natürlich die Spalten aus der
ProductInventory-Tabelle einen Standardwert erhalten müssen. Dies ist der Wert
NULL, da ja keine Information für die entsprechenden Felder in der Ergebnismenge
vorliegt.
P-ID
PIV-ID
Loc-ID Regal
Kasten
----------- ----------- ------ ---------- -----717
NULL
NULL
NULL
NULL
718
NULL
NULL
NULL
NULL
...
166
Komplexe Abfragen
1
1
1
A
1
1
1
6
B
5
Der nächste Schritt kann dann in vielen Fällen die Frage sein, welche Datensätze denn
nun genau keinen Partner in der anderen Tabelle finden. In natürlich-sprachlicher Formulierung wäre dies für das aktuelle Beispiel: „Zeige die Produkte, die keinem Lagerplatz zugewiesen sind.“ Der grundsätzliche Aufbau der Abfrage bleibt derselbe wie in
der vorherigen Abfrage. Es tritt nur eine einzige zusätzliche Anweisung noch hinzu,
nämlich die einschränkende Bedingung, dass gerade die Fremdschlüsselspalte in der
Kind-Tabelle – in diesem Fall ProductInventory – den Wert NULL enthalten muss.
Dies lässt sich mit Hilfe des IS [NOT] NULL-Operators einrichten.
-- Nur Produkte ohne Lagerplatz
SELECT p.ProductID, piv.ProductID, LocationID
FROM Production.Product AS p
LEFT OUTER JOIN Production.ProductInventory AS piv
ON p.ProductID = piv.ProductID
WHERE piv.ProductID IS NULL
312_04.sql: Anzeige nur nicht verknüpfter Daten
Man erhält die für die vorherige Abfrage ermittelte Ergebnismenge, die allerdings nur
noch die unverbundenen Datensätze der Eltern-Tabelle Product enthält. Im Normalfall
würde man sich natürlich hier gerade nicht für die vielen NULL-Felder interessieren,
sondern vielmehr einen größeren Teil der Spalten der Product-Tabelle ausgeben.
ProductID
ProductID
LocationID
----------- ----------- ---------680
NULL
NULL
706
NULL
NULL
167
Komplexe Abfragen
717
NULL
NULL
Wie oben kurz erwähnt, ist die äußere Abfrage eine syntaktische Möglichkeit unter
verschiedenen Varianten. Die zweite Alternative, die ebenfalls noch keine Lösung in
irgendeiner äußeren Programmiersprache erfordert, stellt ganz einfach der MengenOperator EXCEPT dar. Dieser ermöglicht es, zunächst eine Abfrage zu formulieren,
welche auf die Product-Tabelle zugreift und alle Produkte abruft. Von dieser Menge
zieht man genau die Produkte ab, welche in der ProductInventory-Tabelle referenziert werden. Insgesamt erhält man dann die gleichen Daten wie durch eine äußere Abfrage.
-- Nur Produkte ohne Lagerplatz
SELECT ProductID
FROM Production.Product
EXCEPT
SELECT ProductID
FROM Production.ProductInventory
312_05.sql: Einsatz eines Mengen-Operators
Wie gerade erwähnt, erhält man die gleichen Informationen wie bei einer Formulierung
mit Hilfe einer äußeren Abfrage.
ProductID
----------680
706
717
168
Komplexe Abfragen
Wie schon bei der inneren Verknüpfung ist es auch bei der äußeren Verknüpfung möglich, mehr Tabellen als nur zwei zu verknüpfen. Dabei ist grundsätzlich nur zu beachten,
dass eine an einer Stelle geforderte äußere Verknüpfung auch mit jeder weiteren Tabelle
wieder als äußere Verknüpfung fortgesetzt wird. Vergisst man dies, erhält man keine
Fehlermeldung und hat auch keinen Fehler verursacht. Stattdessen werden nur die ganzen NULL-Werte, die bislang erfolgreich eingerichtet wurden, durch die nun folgende
innere Verknüpfung wieder gelöscht, weil die beiden Mengen so verarbeitet werden,
dass ausschließlich die Partnerdatensätze in die weitere Ergebnismenge übernommen
werden. Man verliert also bei einer einmaligen Nennung von INNER JOIN die über
OUTER JOIN zusätzlich abgerufenen Datensätze.
Im nachfolgenden Beispiel hängt man also an die schon zuvor verwendete Abfrageanweisung noch die Tabelle Location an, welche die überhaupt zur Verfügung stehenden
Lagerplätze enthält. Spätestens jetzt sollte man erkennen, dass die ProductInventory-Tabelle eine Beziehungstabelle mit einigen zusätzlichen Attribute wie bspw. der
gelagerten Produktmenge darstellt, welche die Produkte den verfügbaren Lagerplätzen
zuordnet. Da die Product-Tabelle weiterhin auf der linken Seite steht, werden nun alle
weiteren Tabellen ebenfalls mit LEFT OUTER JOIN angeschlossen.
-- Alle Produkte und mögliche Lagerplätze mit Namen
SELECT p.ProductID
AS [P-ID],
piv.ProductID AS [PIV-ID],
l.LocationID
AS [Loc-ID],
Shelf
AS Regal,
Bin
AS Kasten,
l.Name
FROM Production.Product AS p
LEFT OUTER JOIN Production.ProductInventory AS piv
169
Komplexe Abfragen
ON p.ProductID = piv.ProductID
LEFT OUTER JOIN Production.Location AS L
ON l.LocationID = piv.LocationID
ORDER BY piv.ProductID
312_06.sql: Mehrfache äußere Verknüpfung
Als Ergebnis erhält man wie zuvor sowohl Produkte, die einen Lagerplatz haben bzw.
einem solchen zugeordnet sind, als auch diejenigen, die nur in der Product-Tabelle
gespeichert sind und nicht gelagert und möglicherweise auch nicht angeboten werden.
Als zusätzliche Spalte wird aus der Location-Tabelle noch der Name des Lagerortes
im Lager abgerufen.
P-ID
PIV-ID
Loc-ID Regal
Kasten Name
----------- ----------- ------ ---------- ------ -----------925
NULL
NULL
NULL
NULL
NULL
902
NULL
NULL
NULL
NULL
NULL
1
1
1
A
1
Tool Crib
1
1
6
B
5
50
A
5
...
Miscellaneous
1
3.1.2.3
1
Subassembly
Selbstverknüpfung
Eine inhaltlich andere Form der Verknüpfung, die allerdings mit den gleichen syntaktischen Mitteln eingerichtet wird wie eine innere oder äußere Verknüpfung, ist die so
genannte Selbstverknüpfung. Dabei verknüpft man die gleichen Tabelle mit Hilfe einer
170
Komplexe Abfragen
oder mehrere Spalten, die durch einen Aliasnamen unterschieden werden, miteinander
und fügt einen Filter hinzu, der auf unterschiedliche Werte bei gleichem Schlüssel überprüfen soll. Dabei muss der Schlüssel, der für die Verknüpfung verwendet wird, nicht
notwendigerweise auch der Tabellenschlüssel sein, sondern kann jede beliebige Spalte
sein, die für die Beantwortung einer Abfrage sinnvoll genutzt werden kann.
Die nächsten Beispiele sollen die Selbstverknüpfung über mehrere Stufen hinweg erklären. Zunächst ruft die nachfolgende Anweisung die Verkaufsgebiete mit ihrem Verkäufer und dem Enddatum ihrer Geschäftstätigkeit in dieser Region ab.
-- Verkaufsgebiete und ihre Verkäufer
SELECT TerritoryID, SalesPersonID, EndDate
FROM Sales.SalesTerritoryHistory
ORDER BY TerritoryID
312_07.sql: Abfrage aller Daten
Man erhält also eine Liste mit Gebieten, wobei es bspw. solche Gebiete wie 1 und 4
gibt, die von mehreren Verkäufern gleichzeitig oder nacheinander betreut wurden, während andere Gebiete wie 5 nur von einem einzigen Verkäufer betreut werden und dieser
zudem auch noch der erste in diesem Gebiet ist.
TerritoryID SalesPersonID EndDate
----------- ------------- ----------------------1
280
2002-10-31 00:00:00.000
1
283
NULL
1
287
NULL
276
NULL
...
4
171
Komplexe Abfragen
4
281
NULL
5
279
NULL
(17 Zeile(n) betroffen)
Interessant ist es nun herauszufinden, welche Gebiete nun ausschließlich von mehreren
Verkäufer bearbeitet wurden oder werden. Dies ist nicht die gleiche Fragestellung wie
herauszufinden, welches Gebiet wie viele Verkäufer hat oder welches davon eine Verkäuferanzahl größer 1 aufweist. Dies ist zwar eine sehr schöne Aggregationsabfrage mit
der Funktion COUNT und einer Gruppensuchbedingung per HAVING, doch liefert sie
nicht unmittelbar die gewünschten Ergebnisse. Die Liste der Verkaufsgebiete könnte
dann allerdings in einer Unterabfrage über den IN-Operator angeschlossen werden.
Die nachfolgende Abfrage zeigt nun die Lösung mit Hilfe einer Selbstverknüpfung,
wobei zunächst die so genannten Rohdaten ermittelt werden. Diese Daten sind also
noch nicht für die Beantwortung der Frage zu verwenden, sondern zeigen nur die Funktionsweise der Selbstverknüpfung und wie ihre Daten für die eigentliche Antwort zu
verwenden sind. Man ruft also in der FROM-Klausel jeweils die gleiche Tabelle auf und
vergibt einen passenden Aliasnamen, der im einfachsten Fall nummeriert wird. Als
Verknüpfungsbedingung verwendet man die Spalten, welche tatsächlich miteinander
verglichen werden sollen, was nicht notwendigerweise der Primärschlüssel der Tabelle
sein muss. In diesem Fall sollen die Verkaufsgebiete gleich sein. In der WHERE-Klausel
filtert man dann den ungleichen Spaltenwert heraus. In diesem Fall ist dies der Verkäufer, welche ungleich sein soll. Um zu verstehen, wie die Daten aufbereitet werden, gibt
die Spaltenliste jeweils die beiden verknüpften Gebiete und die darin gefundenen Verkäufer aus.
-- Verkaufsgebiete mit verschiedenen Verkäufern (Rohdaten)
SELECT sth1.TerritoryID
AS [Terr-ID 1],
sth2.TerritoryID
AS [Terr-ID 2],
sth1.SalesPersonID AS [P-ID 1],
172
Komplexe Abfragen
sth2.SalesPersonID AS [P-ID 2]
FROM Sales.SalesTerritoryHistory AS sth1
INNER JOIN Sales.SalesTerritoryHistory AS sth2
ON sth1.TerritoryID = sth2.TerritoryID
WHERE sth1.SalesPersonID != sth2.SalesPersonID
ORDER BY sth1.TerritoryID
312_07.sql: Abfrage der Rohdaten (mit Kreuzbeziehungen)
Man erhält also im Ergebnis vier Spalten, die beide jeweils aus der gleichen Tabelle
bzw. aus unterschiedlich benannten Kopien der gleichen Tabelle stammen. Dabei wird
insbesondere deutlich, dass zu viele Reihen ausgegeben werden, da die Daten miteinander per Kreuzverknüpfung kombiniert werden. Aus der vorbereitenden Abfrage ist bekannt, dass die beiden Gebiete 1 und 4 jeweils drei Datensätze aufweisen, die unterschiedliche Verkäufer bezeichnen. Hier ist nun allerdings die doppelte Menge entstanden, da der Wert 283 und 280 noch einmal als 280 und 283 erscheinen. Die Ausgabe
dieser Rohdaten hilft allerdings, sich zu vergewissern, dass die Selbstverknüpfung erfolgreich verlief und man nun die passende Anpassung für das Endergebnis vornehmen
kann.
Terr-ID 1
Terr-ID 2
P-ID 1
P-ID 2
----------- ----------- ----------- ----------1
1
283
280
1
1
287
280
1
1
280
287
1
1
283
287
1
1
280
283
173
Komplexe Abfragen
1
1
287
283
4
4
281
276
4
4
276
281
...
(18 Zeile(n) betroffen)
Für diese endgültige Lösung nun wählt man nur die Daten einer der beiden Tabellenkopien aus und setzt noch ein DISTINCT vor die Spaltenliste, um die Duplikate auszublenden.
-- Verkaufsgebiete mit verschiedenen Verkäufern
-- (ohne Duplikate)
SELECT DISTINCT sth1.TerritoryID AS [Terr-ID 1],
sth1.SalesPersonID AS [P-ID 1]
FROM Sales.SalesTerritoryHistory AS sth1
INNER JOIN Sales.SalesTerritoryHistory AS sth2
ON sth1.TerritoryID = sth2.TerritoryID
WHERE sth1.SalesPersonID != sth2.SalesPersonID
ORDER BY sth1.TerritoryID
312_07.sql: Duplikatlose Lise mit gewünschten Daten
Schließlich erhält man nur noch die Gebiete mit mehreren Verkäufern.
Terr-ID 1
P-ID 1
----------- ----------1
174
280
Komplexe Abfragen
1
283
1
287
…
4
276
4
281
(12 Zeile(n) betroffen)
3.2
Unterabfragen
Im letzten Beispiel des vorherigen Abschnitts fiel schon die Bemerkung, dass es überhaupt Unterabfragen gibt und dass bspw. mit ihnen auch die letzte Fragestellung zu
beantworten wäre. Unterabfragen sind eigenständige Abfragen, die an Stelle von anderen Ausdrücken nach unterschiedlichen Operatoren, innerhalb von FROM und sogar
innerhalb der Spaltenliste erscheinen können. Sie ermöglichen überaus komplexe Abfragen, die teilweise die Verwendung von mehreren einzelnen Abfragen und bspw. die
Zwischenspeicherung der von ihnen gelieferten Werte in Variablen vermeiden. Teilweise stellen sie auch nur Alternativen zu Lösungen mit anderen Techniken dar.
3.2.1
Einfache Unterabfragen
Als einfache Unterabfrage kann man solche bezeichnen, die einen einzelnen Wert oder
eine ganze Werteliste zurückliefern und die daher innerhalb der WHERE-Klausel eingesetzt werden können. Die einzige Alternative zu einer solchen Unterabfrage ist, es die
entsprechenden Werte in einer Variable oder einem Cursor zu vorzuhalten und dann
bspw. mit „richtigem“ Transact SQL ein kleines Programm zu schreiben, in dem die
gleiche Abfrage als Programm abgebildet wird. Alternativ kann man natürlich auch
zuerst die Werte ermitteln und dann in der nächsten Abfrage direkt im Quelltext vorgeben, doch dann ist die gesamte Abfrage nicht mehr so dynamisch wie beim Einsatz
einer Unterabfrage oder eines kleinen Programms.
175
Komplexe Abfragen
3.2.1.1
Einzelne Werte
Unterabfragen eignen sich also dazu, einzelne Werte, wie sie bspw. von einer Aggregatfunktion geliefert werden, direkt in die umschließende Abfrage hinein zu übermitteln.
Das nächste Beispiel ermittelt zunächst den Durchschnittspreis aller Produkte in der
Tabelle Product. Den ermittelten Werte würde man im Normalfall ohne Unterabfrage
einfach so als festen, nicht dynamisch ermittelten Wert in die WHERE-Klausel übernehmen. Da allerdings jeder gültige Ausdruck links oder rechts vom Vergleichoperator
erwartet wird, kann man in runden Klammern auch sofort die gesamte Aggregatabfrage
verwenden. Bei den Vergleichoperatoren ist in diesem Fall wichtig, dass sie nur einen
einzigen Wert (eine Reihe, eine Spalte) liefern.
-- Durchschnittspreis (= 438,6662)
SELECT AVG(ListPrice) AS Schnitt
FROM Production.Product
-- Produkte teurer als Durchschnitt
SELECT ProductID, Name, ListPrice
FROM Production.Product
WHERE ListPrice > (SELECT AVG(ListPrice) AS Schnitt
FROM Production.Product)
ORDER BY ListPrice ASC
321_01.sql: Ersatz von Wertvorgaben
176
Komplexe Abfragen
SELECT ProductID, Name, ListPrice
FROM Production.Product
WHERE ListPrice > (SELECT AVG(ListPrice) AS Schnitt
438,6662
FROM Production.Product)
ORDER BY ListPrice ASC
Abbildung 3.3: Funktionsweise der einfachen Unterabfrage
Man erhält trotz spektakulär neuer Syntax das erwartete Ergebnis, das man auch bei
herkömmlicher direkter Wertvorgabe erhalten hätte.
ProductID
Name
ListPrice
----------- -------------------------------- ---------977
Road-750 Black, 58
539,99
989
Mountain-500 Black, 40
539,99
3.2.1.2
Wertelisten
Der Operator [NOT] IN kann nicht nur mit einem einzigen Wert umgehen, sondern
stattdessen mit einer ganzen Werteliste. Die nächste Abfrage liefert die Nummern von
Produkten, die an Sonderaktionen teilgenommen haben. Diese Liste wird dann in der
darauf folgenden Anweisung verwendet, um anstelle einer Verknüpfung die Detailinformationen von Produkten herauszufinden, die an Sonderaktionen teilnahmen. Das
Beispiel zeigt allerdings auch, wie man die gleiche Information mit Hilfe einer INNER
JOIN-Anweisung ermitteln kann.
-- Produkte, die an Sonderaktionen teilnahmen
SELECT DISTINCT ProductID
FROM Sales.SpecialOfferProduct
177
Komplexe Abfragen
-- Produkte in Sonderaktionen
SELECT ProductID, Name, ListPrice
FROM Production.Product
WHERE ProductID IN (SELECT DISTINCT ProductID
FROM Sales.SpecialOfferProduct)
SELECT DISTINCT p.ProductID, Name, ListPrice
FROM Production.Product AS p
INNER JOIN Sales.SpecialOfferProduct AS sop
ON p.ProductID = sop.ProductID
321_03.sql: Einsatz von Unterabfragen für Wertelisten
Man erhält in beiden Fällen die Produkte aus Sonderaktionen.
ProductID
Name
ListPrice
----------- ---------------------------------- ----------680
HL Road Frame - Black, 58
1431,50
706
HL Road Frame - Red, 58
1431,50
707
Sport-100 Helmet, Red
178
34,99
Komplexe Abfragen
SELECT ProductID, Name, ListPrice
FROM Production.Product
WHERE ProductID IN (SELECT DISTINCT ProductID
FROM Sales.SpecialOfferProduct)
680
706
707
Abbildung 3.4: Funktionsweise der Unterabfrage einer Liste
Zum Schluss soll noch ein Beispiel folgen, welches zeigt, dass wirklich alle Operatoren
die Möglichkeit bieten, Ergebnisse aus Unterabfragen zu verarbeiten. Dies liegt weniger
am Operator als mehr an der Tatsache, dass eine Unterabfrage ein erlaubter gültiger
Ausdruck ist, der einem direkt vorgegebenen Wert oder einer Werteliste entspricht. So
liefert die nachfolgende Anweisung die Produkte mit einem Preis zwischen Durchschnitt und Maximum. Hierzu setzt man den Operator BETWEEN…AND ein, der zwei
einzeilige Ausdrücke erwartet. Beide lassen sich unter Einsatz von Unterabfragen bilden.
-- Produkte mit Preis zwischen Durchschnitt und Maximum
SELECT ProductID, Name, ListPrice
FROM Production.Product
WHERE ListPrice BETWEEN (SELECT AVG(ListPrice) AS Schnitt
FROM Production.Product)
AND (SELECT MAX(ListPrice) AS Maxi
FROM Production.Product)
ORDER BY ListPrice ASC
321_02.sql: Unterabfrage bei BETWEEN und AND
179
Komplexe Abfragen
3.2.2
Spaltenunterabfragen
Während die gerade gezeigten Unterabfragen besonders häufig vertreten sind und
schnell verständlich sind, gibt es verschiedene andere Unterabfragenarten, die etwas
mehr Beschäftigung mit ihnen erfordern, um ihren Sinn und Zweck zu verstehen. Dabei
besteht eine Spaltenunterabfrage tatsächlich aus einer Unterabfrage, die innerhalb der
Spaltenliste erscheint. Mit dieser Technik lassen sich zusätzliche Werte in einer Spalte
ausgeben, die entweder mit anderen Daten korrelieren (korrelierte Spaltenunterabfragen) oder die ganz einfach eine Übersichtsabfrage bilden.
Die nachfolgende Abfrage liefert ein einfaches Aggregat, nämlich die Anzahl von Mitarbeitern pro Geschlecht. Hierbei handelt es sich natürlich um keine Spaltenunterabfrage, sondern um eine gewöhnliche Abfrage mit Aggregatfunktion und Gruppierung.
-- Anzahl von Männern und Frauen in Belegschaft
SELECT Gender AS Geschlecht,
COUNT(*) AS Anzahl
FROM HumanResources.Employee
GROUP BY Gender
322_01.sql: Aggregatermittlung mit Gruppierung
Man erhält zwei Reihen mit den beiden Gruppen und den dazugehörigen Aggregatwerten.
Geschlecht Anzahl
---------- ----------F
84
M
206
(2 Zeile(n) betroffen)
180
Komplexe Abfragen
Möchte man aber nicht mehrere Reihen erzeugen, sondern viel lieber nur eine einzige,
welche die verschiedenen gerade ermittelten Werte enthält, dann kann man mehrere
Spaltenunterabfragen verwenden. Sie ermöglichen es, jede interessante Aggregatabfrage, welche genau einen Wert liefert, als neuen Spaltenwert auszugeben, wobei nicht
einmal die gleiche Tabelle befragt werden muss. So liefert das nächste Beispiel die
Anzahl von Frauen und Männern in der Belegschaft in einem einzeiligen Ergebnis mit
zwei Spalten. Dazu werden die einzelnen Abfragen in runden Klammern pro Spalte
angegeben. Um die entstehenden Duplikate zu unterdrücken, setzt man noch das
Schlüsselwort DISTINCT vor beide Abfragen.
-- Anzahl von Männern und Frauen in Belegschaft
SELECT DISTINCT
(SELECT COUNT(*)
FROM HumanResources.Employee
WHERE Gender = 'M') AS [Anzahl M],
(SELECT COUNT(*)
FROM HumanResources.Employee
WHERE Gender = 'F') AS [Anzahl F]
FROM HumanResources.Employee
322_01.sql: Aggregatermittlung als Übersichtsabfrage
Man erhält die beiden Aggregate in einer Zeile und zwei Spalten.
Anzahl M
Anzahl F
----------- ----------206
84
(1 Zeile(n) betroffen)
181
Komplexe Abfragen
3.2.3
Abgeleitete Tabellen
Eine weitere ungewöhnliche, aber für viele komplexe Fragestellungen sehr nützliche
und die Arbeit sogar vereinfachende Technik ist die der abgeleiteten Tabellen. Dabei
befindet sich die Unterabfrage innerhalb der FROM-Klausel und stellt quasi die lokale
Tabelle dar, welche die äußere Abfrage abfragt.
3.2.3.1
Standardfall
Um das Prinzip und die Syntax zu zeigen, folgt zunächst der Standardfall, den man
inhaltlich auch viel einfacher hätte formulieren können. Doch hier soll es nun darum
gehen, die allgemeine Funktionsweise zu demonstrieren. Innerhalb der FROM-Klausel
platziert man zunächst eine funktionstüchtige Abfrage, die also auch alleine syntaktisch
(und inhaltlich) korrekt ist. Die bereits in den zu Grunde liegenden Tabellen vorhandenen Spaltennamen bzw. die optional vorhandenen Aliasnamen bilden von dieser lokalen
temporären Tabellen die neuen Spaltennamen, welche in der äußeren Abfrage referenziert werden können. Jede dieser abgeleiteten Tabellen benötigt einen Aliasnamen, der
ganz einfach an die Unterabfrage angeschlossen wird. Die Spaltennamen können dann
bei Bedarf qualifiziert werden.
So liefert die Unterabfrage im nächsten Beispiel zwei Spalten aus der EmployeeTabelle zurück, die auch genauso von der äußeren Abfrage ausgegeben bzw. abgerufen
werden. Zusätzlich erstellt die Unterabfrage allerdings auch noch eine Spalte, die sich
aus den beiden Spalten FirstName und LastName der Person-Tabelle sowie einem
Leerzeichen zusammensetzt. Diese neue zusammengesetzte Spalte erhält den Namen
Name und steht für weitere Bearbeitung in der äußeren Abfrage zur Verfügung. Man
kann sich hier bereits gut vorstellen, wie sinnvoll die abgeleiteten Tabellen eingesetzt
werden können, wenn es um Funktionen und Berechnungen geht, die in der abgeleiteten
Tabelle erstellt und dann als neue Rohdaten für weitere Bearbeitungszwecke der äußeren Tabelle zur Verfügung gestellt werden.
-- Liste aller weiblichen Angestellten
SELECT Name, Phone
182
Komplexe Abfragen
FROM (SELECT FirstName + ' ' + LastName AS Name,
Phone,
Gender
FROM HumanResources.Employee AS emp
INNER JOIN Person.Contact AS c
ON emp.ContactID = c.ContactID) AS emp
WHERE Gender = 'F'
323_01.sql: Einfache abgeleitete Tabelle
SELECT Name, Phone
FROM (SELECT FirstName + ' ' + LastName AS Name,
Phone,
Gender
FROM HumanResources.Employee AS emp
INNER JOIN Person.Contact AS c
ON emp.ContactID = c.ContactID) AS emp
WHERE Gender = 'F'
Abbildung 3.5: Funktionsweise der abgeleiteten Tabellen
In diesem Fall wählt man aus den zur Verfügung gestellten Spalten der abgeleiteten
Tabelle nur die beiden Spalten Name und Phone aus, wobei zusätzlich ein Filter auf die
weiblichen Angestellten gelegt wird. Dieser zeigt, dass man vom Blickwinkel der äuße-
183
Komplexe Abfragen
ren Abfrage genauso auf die Daten einer abgeleiteten Tabelle zugreifen kann sowie auf
die Daten jeder beliebigen anderen Tabelle.
Name
Phone
-------------------------- ---------------JoLynn Dobney
903-555-0145
Ruth Ellerbrock
145-555-0130
Gail Erickson
849-555-0139
3.2.3.2
Mehrstufige Verschachtelung
Wie man sich vorstellen kann, ist es erlaubt, innerhalb von abgeleiteten Tabellen wiederum abgeleitete Tabellen zu verwenden. Ein Beispiel soll dieses Prinzip illustrieren.
Zunächst ruft die nachfolgende Anweisung die überhaupt vorhandenen ManagerIDWerte in der Employee-Tabelle ab. Damit erhält man eine Liste der Mitarbeiter, die als
Führungskraft referenziert werden. Da sie typischerweise mehrere Mitarbeiter als Untergebene besitzen, ist hier wiederum DISTINCT notwendig, um die Duplikate auszublenden.
-- Liste aller ManagerIDs
SELECT DISTINCT ManagerID AS EmployeeID
FROM HumanResources.Employee
WHERE ManagerID IS NOT NULL
323_02.sql: Einfache Auflistung
Man erhält eine längere Liste, die u.a. die nachfolgenden Werte enthalten.
EmployeeID
-----------
184
Komplexe Abfragen
3
6
7
Mit Hilfe dieser Liste ist es nun möglich, die ContactID-Werte zu ermitteln, welche
die solchermaßen identifizierten Mitarbeiter besitzen. Dazu verknüpft man ganz einfach
die gerade erstellte Abfrage in Form einer abgeleiteten Tabelle über INNER JOIN mit
der Employee-Tabelle. Interessant an der Syntax dieser Abfrage ist natürlich, dass auf
der einen Seite eine tatsächlich vorhandene Tabelle wie Employee mit einer nur als
abgeleiteter Tabelle erstellten Tabelle so verbunden wird wie bei einer ganz gewöhnlichen Tabelle.
-- Liste der EmployeeIDs und ContactIDs
SELECT emp.EmployeeID, ContactID
FROM (SELECT DISTINCT ManagerID AS EmployeeID
FROM HumanResources.Employee
WHERE ManagerID IS NOT NULL) AS man
INNER JOIN HumanResources.Employee AS emp
ON emp.EmployeeID = man.EmployeeID
323_02.sql: Ermittlung von ContactID auf Basis von EmployeeIDs
Diese Abfrage liefert nun wiederum eine Liste der EmployeeID-Werte von als Führungskräften erkannten Mitarbeitern sowie deren ContactID.
EmployeeID
ContactID
----------- ----------3
1002
6
1028
185
Komplexe Abfragen
7
1070
Zu guter letzt wollte man natürlich weniger eine Liste mit Schlüsselwerten erhalten,
sondern vielmehr die tatsächlichen Kontaktdaten der Führungskräfte. Dies lässt sich
wiederum mit Hilfe einer abgeleiteten Tabelle einrichten. Sie dient in diesem Fall dazu,
die ContactID bereitzustellen, welche für die Verknüpfung mit der Contact-Tabelle
notwendig ist. Dabei wird das gerade gezeigte Abfrageergebnisse, bestehend aus
EmployeeID und ContactID, zur abgeleiteten Tabelle und dient als Verknüpfungspartner für die Contact-Tabelle, aus der man FirstName und LastName abruft und
zusammen mit der Mitarbeiternummer ausgibt.
-- Liste aller Führungskräfte
SELECT EmployeeID, FirstName, LastName
FROM Person.Contact AS c
INNER JOIN
(SELECT emp.EmployeeID, ContactID
FROM (SELECT DISTINCT ManagerID AS EmployeeID
FROM HumanResources.Employee
WHERE ManagerID IS NOT NULL) AS man
INNER JOIN HumanResources.Employee AS emp
ON emp.EmployeeID = man.EmployeeID) AS emp
ON c.ContactID = emp.ContactID
323_02.sql: Verwendung der Unterabfragen für Manager-Liste
Man erhält für die drei schon zuvor gezeigten Mitarbeiter nun auch die Kontaktdaten
neben ihrer Mitarbeiternummer in der Ergebnisliste.
186
Komplexe Abfragen
EmployeeID
FirstName
LastName
----------- ------------------ -----------3
Roberto
Tamburello
6
David
Bradley
7
JoLynn
Dobney
3.2.3.3
Vereinfachte Berechnungen
Ganz zu Beginn, als die abgeleiteten Tabellen vorgestellt wurden, wurde bereits erwähnt, dass insbesondere dann ein großer Vorteil dieser Technik besteht, wenn man
Berechnungen und Funktionen einsetzt. Hier erstellt man die eigentlichen Rohdaten, auf
die man nachher noch weitere Untersuchungen, Filter oder zusätzliche Berechnungen
anwenden möchte, in Form einer abgeleiteten Tabelle und erspart sich dadurch die doppelte Nennung von Berechnungsvorschriften und Funktionsaufrufen. Die Abfrage kann
man in den meisten Fällen auch völlig ohne abgeleitete Tabelle erstellen, doch vereinfacht sie die spätere Weiterverwendung der einmal erstellten Ergebnisse.
So erstellt hier die abgeleitete Tabelle zunächst die benötigten Rohdaten, indem die
Tabellen Product, ProductInventory und ProductSubcategory verknüpft und
die benötigten Spalte von Produkt- und Kategoriename sowie Menge und Sicherheitsbestand übersetzt werden, um dann in der äußeren Abfrage diese Aliasnamen aufzurufen und mit ihnen rechnen zu können. Als Alternative kann man sich vorstellen, dass
innerhalb der abgeleiteten Tabelle ebenfalls schon Berechnungen und Funktionsaufrufe
durchgeführt werden.
-- Produkt und LagerDifferenz-Ermittlung
SELECT Produkt, Sicherheit - Menge AS [Lager-Differenz]
FROM
(SELECT p.ProductID,
p.Name
AS Produkt,
187
Komplexe Abfragen
psc.Name
AS Kategorie,
piv.Quantity AS Menge,
p.SafetyStockLevel AS Sicherheit
FROM Production.Product AS p
INNER JOIN Production.ProductInventory AS piv
ON p.ProductID = piv.ProductID
INNER JOIN Production.ProductSubcategory AS psc
ON p.ProductSubcategoryID = psc.ProductSubcategoryID)
AS
lager
ORDER BY [Lager-Differenz]
323_03.sql: Vereinfachung von Berechnungen
Man erhält eine Produktliste mit der Differenz zwischen Sicherheit und vorhandener
Menge, die für Bestellungen genutzt werden könnte.
Produkt
Lager-Differenz
-------------------------------- --------------Sport-100 Helmet, Black
-320
...
Mountain-100 Silver, 48
-2
Mountain-100 Black, 44
0
...
Mountain-100 Silver, 44
188
25
Komplexe Abfragen
3.2.4
Korrelierte Unterabfragen
Mit Hilfe einer korrelierten Unterabfrage lassen sich zwei Mengen zueinander in Beziehung setzen und Werte der äußeren Abfrage dazu verwenden, in der inneren Abfrage als
eingehende Parameter für weitere Werteermittlung genutzt zu werden. Man erkennt sie
oftmals daran, dass ein Aggregat für eine bestimmte Gruppe benötigt wird und das
Wörtchen „pro“ in der Abfrage erscheint.
3.2.4.1
Standardfall
Das nachfolgende Beispiel zeigt den typischen Standardfall einer korrelierten Unterabfrage. Es sucht für jede Kategorie das billigste Produkt. Hier soll also genau ein Aggregat für ermittelte Gruppenwerte gefunden werden. Dies geschieht in einer ganz typischen Syntax, die am besten über Aliasnamen verstanden wird, die deutlich machen,
dass hier eine äußere Abfrage Werte an eine innere liefert. In der äußeren Abfrage ermittelt man zunächst die Namen von Produktunterkategorien. Zusätzlich gibt man den
Produktnamen des billigsten Produkts aus. Die Ermittlung dieses billigsten Produkts
lässt sich durchführen, indem man den Listenpreis mit dem Ergebnis einer Unterabfrage
vergleicht, die eine Spalte und eine Reihe zurückliefert. Dabei sucht die MIN-Funktion
aber gerade nicht das billigste Produkt in der ganzen Tabelle, sondern vergleicht in einer
WHERE-Klausel den in der äußeren Abfrage gerade verarbeiteten Subkategorienamen.
Dies geschieht durch den Aufruf des Tabellenalias der außen genutzten ProductTabelle. In diesem Fall greift man auch in der korrelierten Unterabfrage auf diese Tabelle zu, was allerdings nicht notwendig ist. Im Vordergrund steht vielmehr, dass man
überhaupt Werte aus der äußeren mit Werten der inneren Abfrage in Beziehung setzt
und sie dadurch korreliert.
-- Pro Kategorie das billigste Produkt
SELECT psc.Name, a.Name, ListPrice
FROM Production.Product AS a
INNER JOIN Production.ProductSubcategory AS psc
ON a.ProductSubcategoryID = psc.ProductSubcategoryID
189
Komplexe Abfragen
WHERE ListPrice = (SELECT MIN(ListPrice)
FROM Production.Product AS b
WHERE a.ProductSubcategoryID =
b.ProductSubcategoryID)
ORDER BY ListPrice
324_01.sql: Standardfall der Korrelierten Unterabfrage
Patch Kit/8 Patches
Water Bottle - 30 oz.
SELECT psc.Name, a.Name, ListPrice Bike Wash - Dissolver
FROM Production.Product AS a INNER JOIN Production.ProductSubcategory AS psc
ON a.ProductSubcategoryID = psc.ProductSubcategoryID
WHERE ListPrice = (SELECT MIN(ListPrice)
Tires and Tubes
Bottles and Cages
Cleaners
FROM Production.Product AS b
2,29
4,99
7,95
WHERE a.ProductSubcategoryID = b.ProductSubcategoryID)
ORDER BY ListPrice
Abbildung 3.6: Funktionsweise der korrelierten Unterabfrage
Man erhält eine Liste, in der Kategorien und Produkte mit dem billigsten Preis innerhalb dieser Kategorie erscheinen. Dies muss nicht bedeuten, dass jede Kategorie nur
einmal erscheint, da es sein kann, dass mehrere Produkte einer Kategorie den gleichen
Preis besitzen, der zufällig auch der kleinste innerhalb der Kategorie ist.
Name
Name
ListPrice
--------------------- ----------------------------- ---------
190
Komplexe Abfragen
Tires and Tubes
Patch Kit/8 Patches
2,29
Bottles and Cages
Water Bottle - 30 oz.
4,99
Cleaners
Bike Wash - Dissolver
7,95
3.2.4.2
Korrelierte Spaltenunterabfragen
Die Technik der korrelierten Unterabfrage ist auch beim Einsatz von Spaltenunterabfragen möglich. Dabei verwendet man wiederum den gerade in der äußeren Abfrage bearbeiteten Datensatz als Filter für die Unterabfrage. Da nun allerdings nicht ein Filter wie
einer gewöhnlichen korrelierten Unterabfrage verwendet wird, sondern eine neue Spalte
in die Ergebnismenge gelangen soll, befindet sich die Unterabfrage in der Spaltenliste,
weist allerdings die gleiche Korrelationstechnik wie eine gewöhnliche korrelierte Unterabfrage auf.
Im Beispiel sollen die Mitarbeiternummern und Namen ausgegeben werden, die zu
einer Führungskraft gehören. In der korrelierten Spaltenunterabfrage ermittelt man
zunächst den zusammengesetzten Namen aus FirstName und LastName der Contact-Tabelle für den Datensatz, der gerade in der äußeren Abfrage bearbeitet wird.
Dies geschieht durch die Korrelation WHERE c.ContactID = emp.ContactID. Die
Filterung der Mitarbeiter auf die Führungskräfte geschieht dann in einer Unterabfrage,
welche für den IN-Operator angegeben wird,
-- Liste aller Führungskräfte
SELECT EmployeeID,
(SELECT FirstName + ' ' + LastName
FROM Person.Contact AS c
WHERE c.ContactID = emp.ContactID) AS Name
FROM HumanResources.Employee AS emp
WHERE EmployeeID IN (SELECT DISTINCT ManagerID AS
EmployeeID
191
Komplexe Abfragen
FROM HumanResources.Employee
WHERE ManagerID IS NOT NULL)
ORDER BY EmployeeID
342_01.sql: Korrelation in der Spaltenunterabfrage
Die nachfolgende Abbildung versucht, die verwendete Syntax noch einmal aufzubereiten.
Roberto Tamburello
David Bradley
JoLynn Dobney
SELECT EmployeeID,
(SELECT FirstName + ' ' + LastName
FROM Person.Contact AS c
3
6
7
WHERE c.ContactID = emp.ContactID) AS Name
FROM HumanResources.Employee AS emp
WHERE EmployeeID IN (SELECT DISTINCT ManagerID AS EmployeeID
FROM HumanResources.Employee
WHERE ManagerID IS NOT NULL)
Abbildung 3.7: Funktionsweise der korrelierten Spaltenunterabfrage
Man erhält eine Liste mit Führungskräften, bestehend aus ihrer Mitarbeiternummer und
ihrem Namen.
EmployeeID
Name
----------- --------------------3
192
Roberto Tamburello
Komplexe Abfragen
6
David Bradley
7
JoLynn Dobney
3.2.5
Operatoren für Unterabfragen
Neben den schon bekannten Operatoren wie den Vergleichsoperatoren, BETWEEN...AND und [NOT] IN gibt es noch weitere Operatoren, welche ausschließlich
mit einer Unterabfrage genutzt werden können und daher in der Beschreibung der möglichen Operatoren von WHERE fehlten. Dies soll nun im Rahmen der Unterabfragen
geschehen. Teilweise ermöglichen sie die vereinfachende Formulierung von Anweisungen, teilweise jedoch können sie genauso gut durch andere Techniken, die meistens
beliebter sind, vermieden werden.
3.2.5.1
Operator ALL
Der Operator ALL führt einen Vergleich zwischen einem skalaren Wert und einer einzelnen Spalten durch und ähnelt damit dem [NOT] IN-Operator. Man erhält dann den
Wert TRUE, wenn der verwendete Vergleich für alle einzelnen Paare des skalaren Ausdrucks und der in der Unterabfrage ermittelten Werteliste. Anderenfalls wird FALSE
zurückgegeben.
Die allgemeine Syntax hat die Form:
skalarerAusdruck
{ = | <> | != | > | >= | !> | < | <= | !< }
ALL ( unterabfrage )
Die nachfolgende Abfrage ermittelt in der Unterabfrage die Preise von allen Produkten.
Diese Werteliste wird dann mit Hilfe von >= ALL verwendet, um die teuersten Produkte
herauszufinden. Das sind nämlich gerade die, welche gleich teurer oder teurer als die
angegebenen Preise sind. In diesem Fall lässt sich das gleiche Ergebnis mit einer einfachen Aggregatunterabfrage über MAX(ListPrice) herleiten.
-- Artikel, die teurer als ALLE anderen sind
193
Komplexe Abfragen
-- Der teuerste Artikel
SELECT ProductID, Name, ListPrice
FROM Production.Product
WHERE ListPrice >= ALL (SELECT ListPrice
FROM Production.Product)
325_01.sql: Einsatz von ALL
Man erhält mehrere Ergebniszeilen, da verschiedene Produkte aufgrund unterschiedlicher Größen den gleichen Preis haben.
ProductID
Name
ListPrice
----------- --------------------- -----------749
Road-150 Red, 62
3578,27
750
Road-150 Red, 44
3578,27
Die nachfolgende Abfrage ermittelt, welche Kunde die Bestellung mit der größten
Fracht aufgegeben hatte. Auch hier kann man entweder eine einfache Unterabfrage mit
MAX(Freight) verwenden, oder man ruft zunächst sämtliche Frachten ab und fordert,
dass der gesuchte Kunde eine Bestellung aufgegeben hat, deren Fracht größer gleich
allen Frachtwerten in den Bestellungen ist.
SELECT c.CustomerID, Freight
FROM Sales.Customer AS c
INNER JOIN Sales.SalesOrderHeader AS soh
ON c.CustomerID = soh.CustomerID
WHERE Freight >= ALL (SELECT Freight
FROM Sales.SalesOrderHeader)
194
Komplexe Abfragen
/* WHERE Freight = (SELECT MAX(Freight)
FROM Sales.SalesOrderHeader)*/
325_02.sql: Ersatz einer einfachen Unterabfrage
Man erhält den entsprechenden Kunden mit der höchsten Fracht zurück.
CustomerID
Freight
----------- ----------599
5608,9121
3.2.5.2
Operator [NOT] EXISTS
Der Operator [NOT] EXISTS prüft darauf, ob in einer Unterabfrage Zeilen vorhanden
sind. Die allgemeine Syntax ist besonders einfach und lautet nur [NOT] EXISTS unterabfrage. Auch hier lassen sich andere Operatoren wie [NOT] IN oder auch =ANY
verwenden.
Im nachfolgenden Beispiel ermittelt man mit Hilfe von IN und einer direkten Angabe
von Größe wie S oder M verschiedene Bekleidungsprodukte. Im nächsten Schritt wird
dann versucht, die gleiche Abfrage mit Hilfe von EXISTS einzurichten. Dabei muss
man sogar eine korrelierte Unterabfrage einsetzen, da man natürlich nur die Größen
zum Vergleich in der Unterabfrage heranziehen möchte, die überhaupt zu den Werten
der äußeren Abfrage gehören. Für die Datensätze, für die also ein Wert in der Unterabfrage existiert, erhält man den entsprechenden Wert der äußeren Abfrage.
-- Produkte mit bestimmten Größen
SELECT Name, Size, Color, ProductSubcategoryID
FROM Production.Product
WHERE Size IN ('S', 'M', 'XL')
AND productSubcategoryID = 21
195
Komplexe Abfragen
SELECT Name, Size, Color, p1.ProductSubcategoryID
FROM Production.Product AS p1
WHERE EXISTS (SELECT DISTINCT Size
FROM Production.Product AS p2
WHERE p2.ProductSubcategoryID = 21
AND p1.ProductSubcategoryID =
p2.ProductSubcategoryID)
AND Size >= 'M'
325_03.sql: Verwendung von EXISTS statt IN
Im Ergebnis erhält man eine Reihe von Pullovern in den bereits in der ersten Abfrage
fest vorgegebenen Größen, die nun allerdings dynamisch ermittelt werden.
Name
Size
Color
ProductSubc~ID
------------------------------ ----- ------- ---------------Long-Sleeve Logo Jersey, S
S
Multi
21
Long-Sleeve Logo Jersey, M
M
Multi
21
3.2.5.3
Operatoren SOME und ANY
Auch die beiden Operatoren SOME und ANY führen einen Vergleich durch, bei dem ein
skalarer Wert mit den in einer Unterabfrage ermittelten Werten einer einzigen Spalte
paarweise in Beziehung gesetzt wird. Beide Operatoren geben dann den Wert TRUE
zurück, sobald irgendein (any) Paar bzw. einige (some) Paare bei diesem paarweisen
Vergleich den Wert TRUE liefern.
196
Komplexe Abfragen
Die allgemeine Syntax lautet:
skalarerAusdruck
{ = | < > | ! = | > | > = | ! > | < | < = | ! < }
{ SOME | ANY } ( unterabfrage)
Die Unterabfrage liefert eine Reihe von Versanddaten von Bestellungen eines angegebenen Bereichs. Die äußere Abfrage dagegen liefert nur die Bestellungen und ihre Versanddaten, wenn das Bestelldatum gleich oder später als das einer der in der Unterabfrage ausgewählten Bestelldaten liegt.
-- Bestellungen mit späterem Versanddatum
-- als in wenigstens einer Zeile der Unterabfrage
SELECT SalesOrderID, ShipDate
FROM Sales.SalesOrderHeader
WHERE OrderDate > SOME (SELECT DISTINCT ShipDate
FROM Sales.SalesOrderHeader
WHERE SalesOrderID BETWEEN 68806
AND 71613)
325_04.sql: Überprüfung auf (irgend)einen Wert
3.3
Verzweigungen
Für die Ausgabe anhand von Untersuchungen in Form einer Fallunterscheidung wie sie
auch in gängigen Programmiersprachen erscheint, steht in SQL eine CASE-Anweisung
zur Verfügung. Sie erlaubt es, einen Ausdruck direkt vorzugeben oder eine Berechnungsvorschrift, Funktion oder Unterabfrage zu verwenden, um in Abhängigkeit einer
Untersuchung unterschiedliche Spaltenwerte in der Ergebnisliste zu erzeugen.
197
Komplexe Abfragen
3.3.1
CASE mit Selektor
Die erste Variante dieser Fallunterscheidung mit Hilfe des CASE-Ausdrucks wird „Case
mit Selektor“ genannt. Der Selektor ist dabei der Ausdruck, welcher in mehreren Fällen,
die auf Gleichheit prüfen, untersucht wird. Hier ist es also nur möglich, tatsächlich
exakt so vorhandene Werte abzuprüfen oder einen Standardfall vorzugeben. Trotz der
komplexen Syntax handelt es sich bei der CASE-Anweisung um einen Spaltenausdruck,
d.h. um die Erzeugung eines einzigen Spaltenwerts.
Der Datentyp, welcher durch die Anweisungsausdrücke in den einzelnen Fällen vorgegeben wird, muss natürlich immer derselbe sein. Sollte es zufällig geschehen, dass bei
unterschiedlichen Datentypen in den einzelnen Fällen im Endergebnis doch immer nur
die Fälle gleichen Datentyps ausgewählt werden, erhält zwar man keine Fehlermeldung,
doch läuft die gesamte Abfrage natürlich dann schief, sobald ein Fall mit anderem Anweisungsdatentyp ausgewählt wird.
Auch wenn in den meisten Fällen sicherlich ausdrücklich nur feste Werte als Anweisungsausdruck zum Einsatz kommen, so ist jeder beliebige gültige Ausdruck richtig.
Dies kann sowohl eine Funktion oder Berechnung als auch eine ganze Unterabfrage
sein, welche genau einen Wert zurückliefert.
Die allgemeine Syntax hat folgendes Aussehen:
CASE Selektor
WHEN Testausdruck THEN Ergebnisausdruck
[ ...n ]
[
ELSE Standardausdruck
]
END
198
Komplexe Abfragen
Das Standardbeispiel für die Verwendung von CASE besteht daraus, einen Spaltenwert,
der für die Ausgabe nicht geeignet ist, weil er bspw. einen verschlüsselten Wert oder
einen Ja-/Nein-Wert enthält, mit einer verständlichen Übersetzung zu füllen. Als Ergänzung lässt sich diese Technik auch auf Aggregate anwenden, wobei hier die bspw. als
Zahlenwerte ermittelten Aggregatwerte in Texte umgewandelt werden wie „Kategorie
A“ oder „Sehr wichtig.“ Als Beispiel soll nun also die Spalte ActiveFlag in der noch
gar nicht so häufig verwendeten Vendor-Tabelle für die Lieferanten in Textwerte übersetzt werden. Die Spalte enthält den Wert 1, wenn der Lieferant noch aktiv ist, d.h. von
ihm Waren bezogen werden, und sie enthält den Wert 0, wenn er inaktiv ist, d.h. von
ihm keine Werte bezogen werden. Die CASE-Anweisung nimmt nun den Spaltennamen
als Selektor, sodass die einzelnen Fälle darauf prüfen, ob der Spaltenwert gleich eines
der angegeben Fallwerte ist. Da nur zwei Werte möglich sind, gibt es einen WHENAusdruck, der unmittelbar von einem ELSE-Ausdruck gefolgt wird. Ansonsten sind
auch mehrere WHEN-Ausdrücke möglich. Nach den WHEN-Ausdrücken folgt der Testwert, der in diesem Fall gleich einem der Spaltenwerte ist und von einem THEN mit
Anweisungsausdruck gefolgt wird. Diese Anweisung bildet ganz schlicht die in der
Ergebnismenge benötigte Ausgabe mit den Wörtern „Aktiv“ und „Inaktiv“ ab. Beide
sind vom Datentyp her eine Zeichenkette, sodass die gesamte entstehende Spalte von
diesem Datentyp ist. Es ist, wie schon oben erwähnt, nicht möglich, verschiedene Datentypen in einer Spalte auszugeben. Sofern allerdings für eine gesamte Abfrage immer
der gleiche Datentyp ausgewählt wird, ist dies auch korrekt.
-- Übersetzung der Status-Angabe
SELECT VendorID,
Name,
CASE ActiveFlag
WHEN 1 THEN 'Aktiv'
ELSE 'Inaktiv'
END AS Status
199
Komplexe Abfragen
FROM Purchasing.Vendor
331_01.sql: Übersetzung von ausgabeuntauglichen Werten
Als Ergebnis erhält man eine Aufstellung der Lieferanten mit Nummer und Name sowie
ihrem Status mit Hilfe der Werte „Aktiv“ und „Inaktiv“.
VendorID
Name
Status
----------- ----------------------------------- ------1
International
Aktiv
2
Electronic Bike Repair & Supplies
Aktiv
48
Gardner Touring Cycles
Inaktiv
Wie schon oben erwähnt, muss sich der Anweisungsausdruck durchaus nicht auf einen
einfachen Wert begrenzen, sondern kann neben Funktionen und Berechnungen auch
gleich eine ganze Unterabfrage umfassen. Dies soll im nachfolgenden Beispiel einmal
demonstriert werden. Hierbei geht es ganz einfach darum, dass bei aktiven Lieferanten
die Stadt aus der Address-Tabelle ermittelt werden soll, während bei inaktiven Lieferanten weiterhin das Wort „Inaktiv“ erscheinen soll. Dazu muss man lediglich die entsprechend korrelierte Unterabfrage nach dem Schlüsselwort THEN platzieren. In diesem
Fall testet also CASE zunächst darauf, ob der Spaltenwert 1 ist. Ist dies der Fall, verknüpft man Vendor mit VendorAddress und Address, wobei dies nur für den Datensatz der Fall sein soll, der in der äußeren Abfrage gerade bearbeitet wird.
-- Ermittlung der Stadt bei "aktiv"
SELECT VendorID,
Name,
CASE ActiveFlag
WHEN 1
THEN (SELECT City
200
Komplexe Abfragen
FROM Purchasing.Vendor AS v2
INNER JOIN Purchasing.VendorAddress AS va
ON v1.VendorID = va.VendorID
INNER JOIN Person.Address AS a
ON a.AddressID = va.AddressID
WHERE v1.VendorID = v2.VendorID)
ELSE 'Inaktiv'
END AS Stadt
FROM Purchasing.Vendor AS v1
331_02.sql: Unterabfrage innerhalb von CASE
Man erhält wiederum eine Ergebnisliste mit Lieferantennummer und Namen, wobei
allerdings in der dritten Spalte entweder die Stadt bei aktiven Lieferanten oder das Wort
„Inaktiv“ bei inaktiven Lieferanten ausgegeben wird.
VendorID
Name
Stadt
----------- -------------------------------- -----------1
International
Salt Lake City
2
Electronic Bike Repair
Tacoma
48
Gardner Touring Cycles
Inaktiv
3.3.2
Selektorlose CASE-Anweisung
Im vorherigen Format der CASE-Anweisung war es nur möglich, auf Gleichheit zu prüfen und einen Standardfall anzugeben. Dies ist allerdings nicht sehr flexibel, weil ganz
einfach nicht jede (aggregierte) Spalte zu einer überschaubaren Menge an Werten führt
oder man ganz einfach eine Menge von Werten zu Bereichen zusammenfassen möchte.
201
Komplexe Abfragen
In diesem Fall spricht man von einer „selektorlosen Case-Anweisung“, weil nach dem
CASE-Schlüsselwort gerade kein zu untersuchender Ausdruck fällt, sondern ganz gewöhnliche Vergleiche nach den WHEN-Schlüsselwörtern folgen. Dies ermöglicht es,
gewöhnliche if-/else if-/else-Konstruktionen von gängigen Programmiersprachen abzubilden und erhöht den Nuzen der CASE-Anweisung um ein Vielfaches.
Die allgemeine Syntax hat die Form:
CASE
WHEN Vergleichsausdruck THEN Ergebnisausdruck
[ ...n ]
[
ELSE Standardausdruck
]
END
Im nachfolgenden Beispiel kombiniert man diese selektorlose Case-Aweisung noch mit
einer abgeleiteten Tabelle, in welcher die zu untersuchenden Rohdaten für die äußere
Abfrage ermittelt werden. Diese abgeleitete Tabelle erstellt eine Liste von Lieferantennummern und Namen sowie eine Spalte namens Orders mit der Anzahl der erhaltenen
Bestellungen. Die Fallunterscheidung in der äußeren Abfrage fragt nun in zwei Fällen
und einem Standardfall ab, ob die Bestellungen bei diesem Lieferanten größer, kleiner
oder gleich 50 sind liefert einen Zahlwert zur Angabe der Bedeutung des Lieferanten als
Statuswert.
SELECT Name,
CASE
WHEN Orders > 50 THEN 1
WHEN Orders = 50 THEN 2
202
Komplexe Abfragen
ElSE 3
END AS Status
FROM (SELECT v.VendorID,
Name,
COUNT(PurchaseOrderID) AS Orders
FROM Purchasing.Vendor AS v
INNER JOIN Purchasing.PurchaseOrderHeader AS poh
ON v.VendorID = poh.VendorID
GROUP BY v.VendorID, Name) AS d
332_01.sql: Einsatz von Vergleichsoperatoren
Man erhält die angekündigte Liste mit den Statusinformationen, die sich auf die Anzahl
der Bestellungen beziehen.
Name
Status
------------------------------------- --------International
1
Electronic Bike Repair & Supplies
1
Premier Sport, Inc.
2
3.4
Zusätzliche Aggregate
Neben den schon bekannten Standardaggregatfunktionen und ihre Einsatzmöglichkeiten
für die Erstellung von Summen, Zählungen und Ermittlung von Extremwerten gibt es
weitere Syntaxtechniken, die eine weitere Aggregierung oder besondere Auswahl von
Werten ermöglichen. Diese sollen in diesem Abschnitt vorgestellt werden. Dabei sind
insbesondere die Möglichkeiten hervorzuheben, mit denen erweiterte Gruppierungen
203
Komplexe Abfragen
und die Erstellung von Gruppensummen oder Berichtssummen möglich sind. Einfache
Anforderungen lassen sich mit den hier vorgestellten Techniken bereits umsetzen, wobei sie allerdings nur die Tür zu einem ganz anderen Universum darstellen, das mit
einer umfassenden Technik im SQL Server 2005 und .NET aufwartet und unter den
Begriffen Reporting Services und Analysis Services bekannt ist.
3.4.1
Rangfolgen
Bisweilen möchte man Rangfolgen oder auch Hitparaden erstellen. Dazu gibt es im
SQL Server eine sehr schöne Syntax, mit der solche Hitparade abgerufen werden können. Die einfachste Möglichkeit, überhaupt Rangfolgen zu erstellen, ist natürlich der
Einsatz einer Sortierung mit Hilfe der Klausel ORDER BY. Sie dient auch als Basis für
die zusätzliche Syntax. Im nächsten Beispiel ruft man zunähst die Kundennummern und
ihre Anzahl an Bestellungen ab. Die Ergebnismenge sortiert man dabei absteigend anhand der Bestellungszahl.
-- Kunden und ihre Anzahl an Bestellungen
SELECT c.CustomerID, COUNT(soh.SalesOrderID) AS Orders
FROM Sales.Customer AS c
INNER JOIN Sales.SalesOrderHeader AS soh
ON c.CustomerID = soh.CustomerID
GROUP BY c.CustomerID
ORDER BY Orders DESC
341_01.sql: Einfache Sortierung
Als Ergebnis erhält man sämtliche ermittelten Werte. Um nun eine weitere Einschränkung vorzunehmen, könnte man eine WHERE-Klausel einfügen, welche einen Wert aus
der Anzahl der Bestellungen zum Vergleich nimmt und beispielsweise nur die Kundennummern ausgibt, welche eine Bestellung größer als 20 ausgelöst haben.
204
Komplexe Abfragen
CustomerID
Orders
----------- ----------11091
28
11176
28
11185
27
11223
27
Die Vorgehensweise, eine WHERE-Klausel einzusetzen, ermöglicht es allerdings überhaupt nicht, eine Hitparade mit den ersten 10 Kunden auszugeben. Dazu setzt man ganz
einfach eine so genannte TOP-n-Abfrage ein. Sie hat ihren Namen von der allgemeinen
Syntax SELECT TOP n spaltenliste. Dabei gibt die Zahl nach dem TOPSchlüsselwort an, wie viele Datensätze in der Hitparade enthalten sein sollen. Die Anzahl der ausgewählten Werte lässt sich dabei entweder numerisch direkt vorgegeben
(TOP 10) oder prozentual (TOP 10 PERCENT). Die allgemeine Syntax lautet:
SELECT [ TOP (zahlausdruck) [PERCENT]
[ WITH TIES ]] spaltenliste
Ändert man die oben verwendete Abfrage wie in folgendem Ausschnitt um, indem man
noch TOP 5 angibt, dann erhält man nur die ersten fünf Datensätze der ursprünglichen
Ergebnismenge.
SELECT TOP 5 c.CustomerID, COUNT(soh.SalesOrderID) AS Orders
...
341_01.sql: Einfache Rangfolge
Man erhält – wie angekündigt – fünf Datensätze.
CustomerID
Orders
----------- -----------
205
Komplexe Abfragen
11091
28
11176
28
11185
27
11223
27
11200
27
(5 Zeile(n) betroffen)
Wie die gerade ausgegebene Ergebnismenge zeigt, haben viele Kunden die gleichen
Werte, d.h. die gleiche Anzahl an Bestellungen. Um diese gleichen Werte auch in der
Hitparade zu berücksichtigen und mehr Zeilen zurückzugeben, wenn gleiche Werte
ermittelt werden, dann schließt man an die TOP n-Klausel noch WITH TIES an.
SELECT TOP 5 WITH TIES c.CustomerID,
COUNT(soh.SalesOrderID) AS Orders
...
341_01.sql: Einfache Rangfolge mit Duplikaten
Diese Anweisung sorgt dafür, dass insgesamt 13 ausgegeben werden und damit deutlich
mehr als fünf. Dies geschieht nur dann, wenn in der Hitparade mehrfach die gleichen
Werte ausgegeben werden wie bspw. die mehrfach auftretenden Bestellzahlen. Alle
Kunden mit 27 und 28 Bestellungen werden ausgegeben, wobei weitere Kunden nicht
mehr ausgegeben werden, da hier nur ein Datensatz für 25 Bestellungen gefunden wird.
CustomerID
Orders
----------- ----------11091
28
11176
28
11185
27
206
Komplexe Abfragen
11223
27
11200
27
...
(13 Zeile(n) betroffen)
Schließlich lässt sich TOP n auch in DML-Anweisungen für einfügen (INSERT), löschen (DELETE) und aktualisieren (UPDATE) von Datensätzen verwenden, wobei allerdings hier der Wert in runden Klammern angegeben werden muss.
3.4.2
Untersummen und Würfel
Zwischen gewöhnlichen Abfragen und komplexen Berichten, die auch nicht mehr mit
einfachem SQL erstellt werden können und Thema eines anderen Buchs sind, gibt es
noch eine weitere Möglichkeit, erweiterte Abfragen zu formulieren. Sie erlauben es
bspw., Untersummen zu ermitteln und für Gruppen oder den gesamten Bericht auszugeben. Eine weitere Möglichkeit ergibt sich durch die Erzeugung von Würfeln, die
quasi verschiedene Abfragen mit unterschiedlich vielen Spalten vereinen und die Ergebnisse in so genannten Dimensionen ausgeben. Diese Form ist eine sehr vereinfachte
Form von Datawarehouse-Abfragen, die ansonsten nur mit entsprechenden SQL ServerWerkzeugen umgesetzt werden können. SQL erlaubt allerdings, einfache Aggregate
bereits ohne besonderen Technikeinsatz zu erzeugen.
3.4.2.1
Untersummen bilden
Wie schon erwähnt, hat man die Möglichkeit, unter einem Bericht bzw. unter Gruppen
von Werten Untersummen auszugeben. Die nachfolgende Abfrage ermittelt zunächst
die Rohdaten, welche für dieses und die nachfolgenden Beispiele verwendet werden
sollen. Es handelt sich um eine Liste, welche Kunden, Gebiete und Jahre gruppiert und
für diese drei Dimensionen die Anzahl der Bestellungen ausgibt. Alle drei Spalten bilden jeweils eine Gruppe, was durch verschiedene Techniken in diesem Kapitel geändert
werden soll, ohne mehrere Abfragen nacheinander zu formulieren.
SELECT c.CustomerID
AS Kunde,
207
Komplexe Abfragen
c.TerritoryID
AS Gebiet,
YEAR(OrderDate)
AS Jahr,
COUNT(soh.SalesOrderID) AS Bestellungen
FROM Sales.Customer AS c
INNER JOIN Sales.SalesOrderHeader AS soh
ON c.CustomerID = soh.CustomerID
GROUP BY c.CustomerID, c.TerritoryID, YEAR(OrderDate)
ORDER BY Jahr, Gebiet, Bestellungen DESC
342_01.sql: Einfache Aggregate
Man erhält – wie schon gerade erwähnt – die Kombination aus Kunden- und Gebietsnummer sowie Jahr mit der entsprechenden Anzahl der Bestellungen. Dies beantwortet
die Frage „Wie viele Bestellungen hat ein Kunde in seinem Gebiet in einem Jahr aufgegeben?“.
Kunde
Gebiet
Jahr
Bestellungen
----------- ----------- ----------- -----------344
1
2001
2
218
1
2001
2
401
1
2001
2
73
1
2001
2
Diese Abfrage liefert allerdings keine Informationen darüber, wie viele Verkäufe überhaupt zu verzeichnen sind oder wie sich diese Verkäufe auf die einzelnen Kunden auswirken. Dies gelingt allerdings mit dem sehr einfachen WITH ROLLUP, das man an die
GROUP BY-Klausel anschließt. Die nachfolgende Abfrage wiederholt die Anweisung
der vorherigen, ergänzt sie allerdings genau um diese Klausel.
208
Komplexe Abfragen
...
GROUP BY c.CustomerID, c.TerritoryID, YEAR(OrderDate)
WITH ROLLUP
ORDER BY Jahr, Gebiet, Bestellungen DESC
342_01.sql: Untersummen bilden
Die WITH ROLLUP-Klausel gibt die Aufforderung, zusätzlich zu den ohnehin ermittelten Zeilen auch noch eine zusammenfassende Summe für die Aggregatspalte zu ermitteln und darüber hinaus die Gesamtverkäufe der Kunden und die Gesamtverkäufe der
Kunden in ihrem Gebiet auszugeben. Diese Zeilen sind an den NULL-Werten in den
jeweils anderen Spalten zu erkennen, da ja für diese Felder gerade kein Wert ausgegeben werden kann.
Kunde
Gebiet
Jahr
Bestellungen
----------- ----------- ----------- -----------NULL
NULL
NULL
6
1
NULL
NULL
4
2
NULL
NULL
2
1
1
NULL
4
2
1
NULL
2
1
1
2001
2
1
1
2002
2
2
1
2002
2
209
Komplexe Abfragen
3.4.2.2
Würfel
Die Erzeugung eines Würfels stellt eine Erweiterung dar, die im Vergleich zu WITH
ROLLUP für jede Spalte, die als Dimension bezeichnet wird, eine solche Gesamtsumme
ausgibt und zusätzlich auch noch die Kombinationen von ihnen berücksichtigt. Dies
bedeutet eine erhöhte Zeilenanzahl durch die verschiedenen ermittelten Aggregate, die
bisweilen die benötigten Werte erheblich übersteigt. Die nachfolgende Abfrage entspricht der vorherigen und wird allerdings aus Gründen der Übersichtlichkeit noch einmal abgedruckt. Zur Erzeugung des Würfels schließt man die WITH CUBE-Klausel noch
an GROUP BY an.
SELECT c.CustomerID
AS Kunde,
c.TerritoryID
AS Gebiet,
YEAR(OrderDate)
AS Jahr,
COUNT(soh.SalesOrderID) AS Bestellungen
FROM Sales.Customer AS c
INNER JOIN Sales.SalesOrderHeader AS soh
ON c.CustomerID = soh.CustomerID
GROUP BY c.CustomerID, c.TerritoryID, YEAR(OrderDate)
WITH CUBE
ORDER BY Jahr, Gebiet, Bestellungen DESC
342_02.sql: Bildung von Würfeln
Um die Funktionsweise und den Nutzen sowie den Aufbau des Ergebnisses zu verstehen, folgt für eine Einschränkung auf wenige Kunden das vollständig ausgegebene
Ergebnis. Die erste Gruppe entspricht der Gesamtanzahl aller Bestellungen, d.h. einer
Summe über alle gruppenweise aggregierten Werte. Dann folgen die Bestellungen für
die Dimensionen Kunde, Gebiet, Kunde und Gebiet, Kunde und Jahr, Kunde Gebiet und
210
Komplexe Abfragen
Jahr sowie Jahr. Die einzelnen zusätzlichen Spalten kann man an den NULL-Werten
erkennen, wobei grundsätzlich natürlich auch NULL-Werte aus der Datenbank hier erscheinen können. Dies ist allerdings hier nicht der Fall, da beim Löschen der WITH
CUBE-Klausel die Rohdaten keine NULL-Werte anzeigen. Dies hätte der Fall sein können, wenn nicht alle Kunden einem Gebiet zugeordnet wären und daher das Gebiet
NULL auch ein Wert in der Ergebnismenge gewesen wäre.
Kunde
Gebiet
Jahr
Bestellungen
----------- ----------- ----------- -----------NULL
NULL
NULL
6
1
NULL
NULL
4
2
NULL
NULL
2
NULL
1
NULL
6
1
1
NULL
4
2
1
NULL
2
1
NULL
2001
2
NULL
NULL
2001
2
NULL
1
2001
2
1
1
2001
2
NULL
NULL
2002
4
1
NULL
2002
2
2
NULL
2002
2
NULL
1
2002
4
1
1
2002
2
211
Komplexe Abfragen
2
1
3.4.2.3
2002
2
GROUPING-Funktionen
Wie schon eben bei der Vorstellung des Ergebnisses erläutert, stellt sich die wesentliche
Frage, welche Felder den NULL-Wert enthalten, d.h. ob sie durch die Dimensionierung
und Würfelbildung leer bleiben müssen oder ob der Wert tatsächlich so in der Datenbank gespeichert war. Dies ist die eigentliche Funktion der Funktion GROUPING, welche
auf die Nicht-Aggregat-Spalten, d.h. auf die Dimensionsspalten angewandt werden
kann. Sie liefert den Wert 1, wenn der NULL-Wert durch die Würfelbildung zu Stande
kommt, und den Wert 0, wenn er ein Datenbankwert ist.
SELECT c.CustomerID
AS Kunde,
c.TerritoryID
AS Gebiet,
YEAR(OrderDate)
AS Jahr,
COUNT(soh.SalesOrderID)
AS [Best.],
GROUPING(YEAR(OrderDate)) AS J,
GROUPING(c.TerritoryID)
AS G,
GROUPING(c.CustomerID)
AS K
FROM Sales.Customer AS c
INNER JOIN Sales.SalesOrderHeader AS soh
ON c.CustomerID = soh.CustomerID
GROUP BY c.CustomerID, c.TerritoryID, YEAR(OrderDate)
WITH CUBE
ORDER BY Jahr, Gebiet, [Best.] DESC
342_03.sql: Markierung der erzeugten NULL-Felder
212
Komplexe Abfragen
Der zweite, für Programmierer sehr nützliche Einsatzbereich, liegt darin, dass man
durch eine Sortierung der Spalten die einzelnen Dimensionen in sehr guter Sortierung
erhält und auch bei einem automatischen Abruf der Daten in einer beliebigen Programmiersprache in der Ergebnismenge erkennen kann, wann eine neue Dimension beginnt
und endet bzw. welche Reihen zu einer bestimmten Dimension gehören. Wie man an
der Ergebnismenge sehen kann, entstehen solche Gruppen wie (1,1,0) oder (1,0,1). Jede
Gruppe entspricht einem solchen Schlüssel, wobei die Gruppen durch die Sortierung der
GROUPING-Spalten entstehen und dann in C#.NET oder VB.NET verarbeitet bzw. zunächst überhaupt erkannt werden können.
Kunde
Gebiet
Jahr
Best.
J
G
K
----------- ----------- ----------- ----------- ---- ---- --NULL
NULL
NULL
6
1
1
1
1
NULL
NULL
4
1
1
0
2
NULL
NULL
2
1
1
0
NULL
1
NULL
6
1
0
1
1
1
NULL
4
1
0
0
2
1
NULL
2
1
0
0
1
NULL
2001
2
0
1
0
NULL
NULL
2001
2
0
1
1
NULL
1
2001
2
0
0
1
1
1
2001
2
0
0
0
NULL
NULL
2002
4
0
1
1
1
NULL
2002
2
0
1
0
2
NULL
2002
2
0
1
0
213
Komplexe Abfragen
NULL
1
2002
4
0
0
1
1
1
2002
2
0
0
0
2
1
2002
2
0
0
0
(16 Zeile(n) betroffen)
3.4.2.4
Berichts- und Gruppenuntersummen
Bisweilen interessiert man sich bei sehr umfangreichen Ergebnismengen, d.h. bei sehr
vielen Spalten und damit möglichen Dimensionen nicht für alle Aggregatwerte bzw.
Kombinationen von ihnen, sondern begnügt sich mit einigen wenigen zusätzlichen Ergebnissen. Dies gelingt über die COMPUTE [BY]-Anweisung, welche eine Untersumme
für genau benannte Spalten ermittelt. Die allgemeine Syntax ist:
[ COMPUTE
{ { AVG | COUNT | MAX | MIN |
STDEV | STDEVP | VAR | VARP | SUM }
( ausdruck) } [ ,...n ]
[ BY ausdruck) [ ,...n ] ]
]
Wie man sehen kann, lässt sich auch die Art und Weise, wie das zusätzliche Berichtsoder Gruppenaggregat ermittelt wird, mit einer der bekannten Aggregatfunktionen angeben und besteht nicht nur aus einer Summenbildung. Die COMPUTE [BY]-Anweisung
folgt der Sortierung und erzeugt – was ein gewisser Nachteil ist – eine zusätzliche Ergebnismenge, die nicht so einfach in einer beliebigen Programmiersprache verarbeitet
werden kann wie die zusätzlichen Zeilen und NULL-Felder von WITH ROLLUP und
WITH CUBE.
214
Komplexe Abfragen
Die nachfolgende Variante der schon mehrfach verwendeten Abfrage fügt am Ende der
ersten Ergebnismenge noch zusätzlich die Gesamtanzahl aller Bestellungen und Kunden
ein.
SELECT c.CustomerID
AS Kunde,
c.TerritoryID
AS Gebiet,
YEAR(OrderDate)
AS Jahr,
COUNT(soh.SalesOrderID)
AS [Best.]
FROM Sales.Customer AS c
INNER JOIN Sales.SalesOrderHeader AS soh
ON c.CustomerID = soh.CustomerID
GROUP BY c.CustomerID, c.TerritoryID, YEAR(OrderDate)
ORDER BY Jahr, Gebiet, [Best.] DESC
COMPUTE SUM(COUNT(soh.SalesOrderID)),
COUNT(c.CustomerID)
342_04.sql: Erzeugung von Berichtsuntersummen
Man erhält zunächst das gewöhnliche Ergebnis. Danach folgen erst mögliche Aggregate
in einer eigenen Ergebnismenge.
Kunde
Gebiet
Jahr
Best.
----------- ----------- ----------- ----------1
1
2001
2
1
1
2002
2
2
1
2002
2
7
1
2002
2
215
Komplexe Abfragen
2
1
2003
4
7
1
2003
2
sum
cnt
----------- ----------14
6
Die vorherige Variante der schon bekannten Abfrage ermittelte eine so genannte Berichtsuntersumme. Die allgemeine Syntax der Klausel lautet allerdings COMPUTE [BY],
was es ermöglicht, nach dem BY-Schlüsselwort noch die Spalte anzugeben, welche für
die Gruppenuntersummen genutzt werden soll. In der nächsten Variante soll also für die
einzelnen Jahre jeweils die Summe der gezählten Bestellungen ermittelt werden. Dies
ist nur möglich, wenn die Spalte mit der Jahreszahl auch sortiert wird.
…
GROUP BY c.CustomerID, c.TerritoryID, YEAR(OrderDate)
ORDER BY Jahr ASC
COMPUTE SUM(COUNT(soh.SalesOrderID)) BY Jahr
342_05.sql: Erzeugung von Gruppenuntersummen
Man erhält tatsächlich jeweils eine Untersumme mit der Anzahl der Bestellungen für die
einzelnen Jahre.
Kunde
Gebiet
Jahr
Best.
----------- ----------- ----------- ----------1
sum
216
1
2001
2
Komplexe Abfragen
----------2
Kunde
Gebiet
Jahr
Best.
----------- ----------- ----------- ----------1
1
2002
2
2
1
2002
2
7
1
2002
2
sum
----------6
Diese Möglichkeiten schöpfen den Sprachumfang von SQL aus, wobei allerdings zusätzliche Techniken über die Reporting Services und die Analysis Services zur Verfügung stehen, mit denen weitaus komplexere Abfragen und ganze Abfrageanwendungen
erzeugt werden können.
217
Komplexe Abfragen
218
Datenmanipulation
4 Datenmanipulation
219
Datenmanipulation
220
Datenmanipulation
4 Datenmanipulation
Dieses Buch geht grundsätzlich davon aus, dass die Datenbank, die Sie verwenden, auf
die eine oder andere Art vom Himmel gefallen ist. Das bedeutet, dass Sie, wie es viele
Teilnehmer in den entsprechenden T-SQL-Seminaren berichten, eine bestehende Datenbank abfragen oder mit Funktionen und Prozeduren versehen sollen. Dazu gehört in
vorrangiger Weise gerade nicht die Administration oder die Erstellung neuer Datenstrukturen. Daher ist in diesem Kapitel die Beschreibung von SQL DDL (Data Definition Language) im Gegensatz zu den vielen Beispielen zu SQL DML (Data Manipulation
Language) zur Abfrage und Bearbeitung von Daten auf die absoluten Grundlagen beschränkt. Im Band zur Administration dagegen beschäftigen sich die Beispiele ausdrücklich mit dem vielschichtigen und wichtigen Aspekt der Erstellung und Verwaltung
von Schema-Objekten in der Datenbank.
In einem ersten Teil dieses Kapitels soll also nun in Grundzügen dargestellt werden, wie
Sie Tabellen und Sichten über die grafische Oberfläche oder direkt in T-SQL anlegen.
An dieser Stelle sei bereits darauf verwiesen, dass die Erstellung von Tabellen auch
innerhalb eines T-SQL-Programms von großer Bedeutung sein kann, weil T-SQL keine
eigenen Array- oder Collection-Strukturen verwendet, sondern für die Abbildung von
Listenwerten mit einer oder mehreren Spalten auf so genannte temporäre Tabellen zurückgreift. Diese müssen in T-SQL weitestgehend genauso erstellt werden wie gewöhnliche Tabellen, die dauerhaft in der Datenbank verbleiben.
In einem zweiten Teil stellen wir dann vor, wie Sie Daten in bestehende oder neue Tabellen eintragen, aus ihnen löschen oder Daten aktualisieren. Dies sind Standardanweisungen aus SQL, welche lediglich in T-SQL genutzt werden können und ansonsten in
ähnlicher Weise auch in beliebigen anderen relationalen Datenbanken zum Einsatz
kommen können.
221
Datenmanipulation
4.1
Datenstrukturen anlegen
Eine relationale Datenbank zeichnet sich dadurch aus, dass die Daten in Tabellen gespeichert werden. Diese sind meistens nach Objekten (Entitäten), welche in der zu abzubildenden Modellwelt auftreten, geordnet, wobei die Entwicklung von normalisierten,
d.h. gut strukturierten und für eine Weiterentwicklung der Datenbank ohne Datenredundanzen (doppelte Datenspeicherung) und gute Leistungsfähigkeit notwendigen Datenstrukturen eine Tätigkeit darstellt, die sowohl Programmierer als auch Administratoren
betrifft. In diesem Fall ist die AdventureWorks-Datenbank bereits vollständig erstellt
und zeigt ein sehr umfassendes Bild einer sehr weit normalisierten Datenbank.
Tabellen dienen also im MS SQL Server selbstverständlich der Datenspeicherung, wie
dies auch in allen anderen relationalen Datenbanken der Fall ist. Zusätzlich weisen sie
allerdings noch die Besonderheit auf, dass sie nur temporär angelegt werden können.
Eine solche temporäre Tabelle, mit den gleichen SQL-Anweisungen erstellt wie eine
dauerhaft gespeicherte, bildet dann Array-Strukturen in T-SQL-Programmen ab. Während die dauerhaft gespeicherten Tabellen sehr gut über die Oberfläche erstellt werden
können, ist dies bei den temporären Tabellen nicht möglich, sofern man aus der grafisch
modellierten Tabelle nicht einfach das sie beschreibende SQL automatisch generieren
lässt, um dieses dann in seinem eigenen Programm zu verwenden.
Neben den Tabellen, die sich nachher mit dem schon hinreichend bekannten SELECTBefehl abfragen lassen, gibt es eine zweite Struktur für die Abbildung von Daten. Dies
sind Sichten, die bisweilen auch schon einmal in der MS SQL Server-Dokumentation
oder sogar in der grafischen Oberfläche als Ansichten bezeichnet werden. Eine solche
Sicht oder View, wenn man das auch weit verbreitete englische Wort verwenden möchte, entspricht einer gespeicherten Abfrage, auf die man genauso wie auf einer Tabelle
mit dem SELECT-Befehl zugreifen kann. Diese Technik soll ebenfalls in diesem Abschnitt erläutert werden.
4.1.1
Tabellen
Tabellen werden im MS SQL Server Management Studio im Objekt Explorer unterhalb
des DATENBANKEN-Knotens aufgelistet, wenn man eine der Datenbanken öffnet. In
222
Datenmanipulation
diesem Fall ist das der Knoten der AdventureWorks-Datenbank. Öffnet man hier wiederum den Ordner Tabellen, dann erhält man eine Liste der Tabellen, sortiert nach dem
Schema (übergeordnete Strukturierung), in dem sie angelegt wurden. Dabei gibt es
neben den vom Datenbankentwickler der AdventureWorks-DB angelegten Schemata
auch das Standardschema dbo (database owner), in dem automatisch alle Strukturen
erstellt werden, sofern man keine Vorkehrungen trifft, sie in einem neuen oder bereits
bestehenden Schema zu speichern.
Es gibt zwei Möglichkeiten, neue Tabellen zu erstellen. Sofern man sehr viele komplexe
Einstellungen zu treffen hat, wird man dies sicherlich mit Hilfe der grafischen Oberfläche erledigen. Sofern man allerdings die Einstellungen in Textform erfassen möchte,
um sie möglicherweise besser planen zu können, schreibt man die über die grafische
Oberfläche automatisch erstellten SQL-Anweisungen direkt auf. Diesen Weg muss man
ohnehin beschreiten, sobald man etwas anspruchsvollere T-SQL-Programme schreiben
möchte.
4.1.1.1
Tabellen grafisch anlegen
Führen Sie folgende Schritte durch, um eine neue Tabelle anzulegen:
1. Öffnen Sie die Datenbank, in der Sie eine neue Tabelle anlegen wollen.
Dies ist im aktuellen Beispiel immer die AdventureWorks-Datenbank.
2. Öffnen Sie das Kontextmenü des Ordners TABELLEN mit der rechten
Maustaste und wählen sie den Eintrag NEUE TABELLE.
Es erscheint eine tabellarische Darstellung, in der Sie im oberen Bereich
Spaltenname und Datentyp angegeben bzw. aus einer Auswahlliste
auswählen können. In der dritten Spalte geben Sie an, ob der Wert NULL in
die Spalte eingetragen werden darf, d.h. ob die Zelle leer sein kann
(Kontrollkästchen markiert) oder nicht. Dieser leere Wert ist dann nicht
etwa die leere Zeichenkette oder irgendein magischer Standardwert,
sondern der Wert NULL, den Sie bereits mit Hilfe des IS [NOT] NULL in
einer Abfrage abgeprüft haben. Die Datentypen haben neben ihrem Namen,
223
Datenmanipulation
der sie charakterisiert, auch ab und an eine Länge, die sie über die Tastatur
vorgeben können, sobald Sie überhaupt einen Datentyp ausgewählt haben.
In diesem Beispiel handelt es sich um eine Tabelle, welche einige Spalte
aus der Employee-Tabelle mit dem Primärschlüsselfeld der DepartmentTabelle enthält. Daher gibt es in dieser Tabelle die Spalten ID mit dem
Primärschlüsseldatentyp uniqueidentifier. Dieser Datentyp stellt einen
komplex aufgebauten eindeutigen Wert für den Datensatz dar und hat bspw.
die Form 6F9619FF-8B86-D011-B42D-00C04FC964FF. Ein neuer Wert
wird über die NEWID()-Funktion erzeugt. Möglich wären auch Ganzzahlen
wie int gewesen, die auch als Alternative bei 1 beginnend schrittweise
automatisch um 1 erhöht werden könnten. Die Spalte Name bezieht sich auf
den Abteilungsnamen, während DepID die Abteilungsnummer enthält. Sie
tritt als smallint-Wert auf, während der Name in nvarchar(50)
gespeichert wird. Die nächsten beiden Spalten sind FirstName und
LastName des Mitarbeiters.
3. Um eine Spalte einzufügen oder zu löschen, markieren Sie die zu löschende
Spalte bzw. die Spalte, vor der eine andere Spalte einzuügen ist. In diesem
Fall zeigt die Abbildung, wie vor der Spalte FirstName noch eine Spalte
eingefügt werden soll, welche die Anrede speichern soll.
4. Spalten können eine Vielzahl von zusätzlichen Eigenschaften neben
Namen, Datentyp und der Zulassung oder dem Verbot von NULL-Werten
haben. Drei wichtige Eigenschaften für den Anfänger sind in jedem Fall die
Angabe, ob die Spalte den Primärschlüssel enthält, d.h. die Tabelle
eindeutig referenziert, ob die Tabelle den Wert einer anderen Spalte enthält
und daher einen Fremdschlüssel abbildet und ob es einen Standardwert gibt.
224
Datenmanipulation
2
1
3
4
5
6
Abbildung 4.1: Tabellen über die grafische Oberfläche erstellen
225
Datenmanipulation
Diesen Standardwert trägt man unterhalb der tabellarischen Darstellung im
Bereich SPALTENEIGENSCHAFTEN ein. Sobald kein Wert für diese Spalte
angegeben wird, wenn neue Daten eingetragen werden sollen, würde dieser
Standardwert verwendet werden. In diesem Fall soll in der Title-Spalte für
die Anrede der Wert Mr vorab eingetragen werden, da die meisten
Angestellten (wenngleich auch nur mit einer leichten Überzahl) Herren
sind. Sobald man die Eingabetaste betätigt, wird die Zeichenkette noch als
solche gekennzeichnet, indem ein N vor den Text und einfache
Hochkommata zur Begrenzung um den Vorgabewert gesetzt werden.
5. Möglicherweise haben Sie sich gewundert, dass Sie bislang noch gar nicht
nach dem Tabellennamen gefragt wurden. Sie müssen über DATEI /
TABELLE_1.SQL SPEICHERN das aktuelle Skript und damit auch die Tabelle
speichern. Erst in diesem Moment kann man den Namen vergeben. In
diesem Fall handelt es sich um EmpDep für Employee-Department als
Zusammenstellung. Den Wert tragen Sie im sich öffenden Dialogfenster
ein. Möchten Sie zusäztlich ein spezielles Schema auswälen, in dem die
Tabelle gespeichert werden soll, dann können Sie dieses hier auch
eintragen. Ansonsten wird das Standardschema dbo verwendet.
6. Schließlich erhalten Sie die neu erstellte Tabelle in der Liste von Tabellen,
die für die ausgewählte Datenbank im Objekt-Explorer angezeigt werden.
Die Informationen zu einer Spalte kann man sich in einem umfassenden Dialogfenster
anschauen, welches große Gemeinsamkeit mit dem Bereich SPALTENEIGENSCHAFTEN
beim Anlegen der Tabelle hat. Wählen Sie hierzu aus dem Kontextmenü der Spalte den
Eintrag EIGENSCHAFTEN.
226
Datenmanipulation
2
1
Abbildung 4.2: Spalten-Eigenschaften anzeigen
Möglicherweise möchten Sie die Eigenschaften der gesamten Tabelle untersuchen. Dies
betrifft allerdings im Wesentlichen Einstellungen, welche in den Bereich der Administration hineinragen. Um die Eigenschaften sehen zu können, wählen Sie im Kontextmenü der Tabelle ganz einfach den Eintrag EIGENSCHAFTEN, so wie sie dies auch schon im
Kontextmenü für die Spalte gemacht haben, um die Spalteneigenschaften zu sehen. In
der Abbildung können Sie erkennen, dass dieses Kontextmenü überhaupt sehr interessant ist, um mit der Tabelle zu arbeiten. Neben der Anzeige von Eigenschaften kann
man auch eine neue Tabelle anlegen, die Daten einer Tabelle anzeigen, d.h. eine Tabelle
öffnen sowie andere Operationen ausführen.
227
Datenmanipulation
2
1
Abbildung 4.3: Tabellen-Eigenschaften anzeigen
Um die Spalteneigenschaften nicht nur zu lesen, sondern tatsächlich zu ändern, sind
folgende Schritte notwendig, wobei in diesem Fall ein Primärschlüssel festgelegt wird.
1. Wählen Sie für eine der Tabellenspalten, die im Objekt-Explorer angezeigt
werden, aus dem Kontextmenü den Eintrag ÄNDERN.
2. Wählen Sie in der Spalte, in welcher der Primärschlüssel angelegt werden
soll, das Kontextmenü. Wenn Sie andere Änderungen durchführen wollen,
können Sie dies natürlich genauso in der tabellarischen Aufbereitung der
Spalten durchführen, die Sie ja schon vorhin genutzt haben, um die Spalten
erstmalig anzulegen.
228
Datenmanipulation
2
1
3
4
Abbildung 4.4: Spalten-Eigenschaften ändern
3. Um die Änderungen in der Tabelle festzushreiben, müssen Sie noch die
Tabelel speichern. Dies entspricht dem erstmaligen Speichervorgang, wenn
man die Tabelle gerade vollständig neu erstellt. Dies können Sie entweder
im Menu DATEI durchführen oder im Kontextmenü des Reiters, unter dem
die tabellarische Darstellung angeboten wird.
4. Im Normalfall werden die Änderungen nicht sofort auch in der
Eigenschaftenliste im Objekt-Explorer angezeigt. In diesem Fall wählen Sie
aus dem Kontextmenü der Tabelle den Eintrag AKTUALISIEREN.
229
Datenmanipulation
Eine wichtige Eigenschaft von Tabellen (Relationen) in einem relationalen Datenbanksystem sind die Beziehungen zwischen den Tabellen. So steht bspw. die neue erstellte
Tabelle EmpDep mit der Tabelle Department in einer 1:n-Beziehung, da n Datensätze
der EmpDep-Tabelle mit einem Datensatz aus der Department-Tabelle in Beziehung
stehen. In der abgebildeten Welt bedeutet dieser Umstand, dass n Angestellte in einer
Abteilung arbeiten. Grundsätzlich ist es für die Benutzung der Datenbank und den in ihr
gespeicherten Tabelle unwichtig, ob diese Beziehungen tatsächlich auch ausformuliert
werden. Man kann sie in der Anwendung oder auch nur im Kopf genau so berücksichtigen. Es ist allerdings möglich, sie explizit als Eigenschaften der Tabelle zu formulieren
und darüber hinaus mit dieser Angabe dafür zu sorgen, dass bspw. nur solche Fremdschlüsselwerte in der EmpDep-Tabelle erscheinen, die auch tatsächlich in de Department-Tabelle vorhanden sind. Eine andere Regel lautet, dass nur dann ein Datensatz in
der Department-Tabelle gelöscht werden kann, wenn kein Datensatz in der EmpDepTabelle auf diesen Datensatz verweist. Aus Sicht der Department-Tabelle ist die EmpDep-Tabelle eine Kindtabelle der Elterntabelle Department, weil die Datensätze der
EmpDep-Tabelle sich auf Informationen in der Department-Tabelle beziehen.
Diese gesamte Technik lässt sich auf verschiedene Art und Weise einstellen und konfigurieren. Dies betrifft bspw. die Frage, was denn in der Kindtabelle geschehen soll,
wenn ein Datensatz doch in der Elterntabelle gelöscht oder aktualisiert wird. Man nennt
dies die referenzielle Integrität, was im DBA-Band dieser Reihe beschrieben wird. Für
das aktuelle Beispiel soll nur eine einfache Verknüpfung angelegt werden, damit auch
nachher ein Datenbankdiagramm automatisch erzeugt werden kann.
1. Wählen Sie aus dem Kontextmenü der Tabelle den Eintrag ÄNDERN.
2. Wählen Sie aus dem Kontextmenü der Spaltenliste den Eintrag
BEZIEHUNGEN.
230
Datenmanipulation
2
1
3
4
5
6
7
8
Abbildung 4.5: Beziehung / Fremdschlüssel hinzufügen
231
Datenmanipulation
3. Es öffnet sich ein umfangreiches Dialogfenster, in dem die Beziehungen
und ihre Eigenschaften angezeigt und verwaltet werden können. Wenn eine
Beziehung schon vorhanden ist, können Sie sich im rechten Bereich über
ihre Eigenschaften informieren. Die für dieses Beispiel interessanten
Basisangaben sind leider in der Standardansicht ausgeblendet und müssen
erst über die zweite Plus-Schaltfläche eingeblendet werden. Wählen Sie die
Schaltfläche HINZUFÜGEN.
4. Es öffnet sich ein weiteres Dialogfenster, in dem die Eltern- und
Kindtabelle sowie die Primärschlüsselspalte der Eltern- und die
Fremdschlüsselspalte der Kindtabelle ausgewählt werden. Hier ist
unbedingt auf die richtige Zuordnung zu achten. Für das aktuelle Beispiel
ist unter dem Auswahlmenü PRIMÄRSCHLÜSSELTABELLE die
Schlüsselspalte DepartmentID der Tabelle Department auszuwählen. Im
rechten Bereich wählt man dann als Kindtabelle aus dem Auswahlmenü
FREMDSCHLÜSSELTABELLE
die
EmpDep-Tabelle
und
ihre
Fremdschlüsselspalte DepID. Es ist möglich, einen Schlüssel über mehrere
Spalten zu verteilen, sodass an dieser Stelle auch mehrere Einträge denkbar
sind. Bestätigen Sie alles mit OK.
5. Unter der zweiten Plus-Schaltfläche namens TABELLEN- UND SPALTENSPEZIFIKATION sollten nun die richtigen Zuordnungen von Tabellen und
Spalten erscheinen. Dies betrifft insbesondere die korrekte Zuweisung von
Eltern- und Kind-Eigenschaft. Dies ist am besten genauestens zu
kontrollieren. Weitere DBA-Tätigkeiten schließen sich dann in diesem
Bereich an wie bspw. die erwähnten Regelungen zur referenziellen
Integrität.
6. Bestätigen Sie das sich öffnende Dialogfenster mit OK.
7. Eigentlich ist nun für das aktuelle Beispiel die Spalte Name nicht mehr
notwendig, denn ihr Wert hängt von der DepID ab, welche wiederum eine
Fremdschlüsselbeziehung zur Department-Tabelle enthält. Mit Blick auf
232
Datenmanipulation
das Datenbankdesign würde man feststellen müssen, dass die Tabelle nicht
normalisiert ist, weil eine Spalte von einer anderen direkt abhängt, diese
Spalte aber nicht der Primärschlüssel der Tabelle ist. Dies ist ja nicht DepID,
sondern vielmehr ID. Um hier keine Fehler bei Aktualisierungen zu
riskieren, sollte man eine solche Spalte löschen. Sofern in einem anderen
Arbeitsgang eine solche Spalte überhaupt nicht vorhanden ist, sollte man
nur aus dem Kontextmenü der Tabelle oder Spalte den Eintrag
AKTUALISIEREN wählen, um zu kontrollieren, ob alle Zuordnungen korrekt
sind.
In umfangreichen Datenbanken ist es nicht besonders einfach, den Überblick zu bewahren. Sofern man kein fertiges Datenbankdiagramm wie von der AdventureWorks-DB
besitzt, kann man sich mit Hilfe von MS Visio oder natürlich dem MS SQL Server
Management Studio selbst ein solches Datenbankdiagramm erstellen. Dies ist sehr einfach, sodass Sie diese fünf Minuten Arbeit in jedem Fall investieren sollten. Für die
beiden verknüpften Tabellen soll gezeigt werden, wie man ein solches Diagramm erstellen kann.
1. Wählen Sie den ersten Order unterhalb des Datenbankknotens namens
DATENBANKDIAGRAMME aus und wählen Sie im Kontextmenü den Eintrag
NEUES DATENBANKDIAGRAMM.
2. Sie erhalten sofort eine Liste der in dieser Datenbank vorhandenen
Tabellen, aus der Sie diejenigen auswählen, welche im Diagramm
erscheinen sollen. Im aktuellen Beispiel sind das Department und EmpDep.
Über HINZUFÜGEN können Sie diese Tabellen Ihrem Diagramm
hinzufügen.
233
Datenmanipulation
1
2
3
4
5
6
7
8
6
7
Abbildung 4.6: Diagramm erstellen
234
Datenmanipulation
3. Die beiden Tabellen werden nicht nur hinzugefügt, sondern werden auch
sofort aufgrund der Einstellungen in der Datenbank verbunden. Der
Schlüssel am Ende der Beziehungslinie zeigt an, dass hier der
Primärschlüssel und damit die Elterntabelle vorhanden ist, während das
Unendlichkeitszeichen für die Kindtabelle und den Umstand steht, dass hier
mehrere Primärschlüsselreferenzen in Form von Fremdschlüsseln
auftauchen.
4. Sie können das Diagramm dann über das DATEI-Menü speichern und
nachher im DATENBANKDIAGRAMME-Knoten wiederfinden.
Unabhängig von den beschriebenen Schritten, ein Diagramm zu erstellen, welche ja
tatsächlich sehr leichtfüßig zu bewerkstelligen sein müssten, enthält die Abbildung noch
zwei Kontextmenüs.
Das größere ist das Kontextmenü einer Tabelle. Hier befinden sich insbesondere die
Einstellungen für die Tabellenansicht. Sollte man nur am allgemeinen Schema interessiert sein, ist die Ansicht mit den Spaltennamen sicherlich die beste, weil sie besonders
übersichtlich ist. Wie man an der Abbildung erkennen kann, gibt es aber auch eine reine
Tabellendarstellung für sehr große Modelle bzw. sehr oberflächliche Darstellungen der
Datenbank und auch die Möglichkeit, sehr viele Details wie bspw. die Datentypen anzuzeigen und dies auch als benutzerdefinierte Sicht abzuspeichern.
An den verschiedenen Einträgen in diesem Kontextmenü sieht man allerdings auch,
dass man die Tabellen aus dem Diagramm oder sogar aus der Datenbank löschen kann,
dass man die Tabelle genauso bearbeiten kann wie in der gewöhnlichen EigenschaftenAnsicht und dass man automatisch die direkt verknüpften Tabelle zu den ausgewählten
Tabellen hinzufügen kann. Dies ist insbesondere dann nützlich, wenn man ein Diagramm für eine unbekannte Datenbank erstellt.
Das kleinere Kontextmenü zeigt die Operationen, die bei einem ganzen Datenbankdiagramm durchgeführt werden können. Hier ist es möglich, neue Tabellen zum Diagramm
hinzuzufügen und aus der Datenbank abzurufen, bzw. sogar eine ganz neue Tabelle zu
erstellen. Als Alternative für eine strukturierte Erstellung innerhalb des Objekt-
235
Datenmanipulation
Explorers kann man diese Arbeit also auch unmittelbar im Diagramm durchführen.
Dieses Diagramm kann dann zusätzlich auch noch mit Textanmerkungen versehen
werden und dadurch also auch kommentiert werden. Für einen Ausdruck eines großen
Modells stellt sich immer die Frage, ob man durch Skalierung versucht, alles auf eine
DIN A4-Seite zu quetschen oder ob man schon - falls vorhanden - DIN A3 oder mehrere A4-Seiten drucken möchte, um diese dann zusammenzukleben. Eine Vorschau und
eine Einstellung zu den Seitenumbrüchen sind in diesem Kontextmenü ebenfalls erreichbar genauso wie ein Zoom.
Nachdem man mögliche neue Tabellen für eine bestehende, vom Himmel gefallene
Datenbank erstellt hat, möchte man – vielleicht auch aus Gründen des Lernens oder der
Sicherung – ein SQL-Skript generieren, das genau die gleiche Tabelle wieder einrichten
kann. Hätte man das Skript selbst geschrieben, wäre es möglicherweise in einfacherer
Syntax entstanden, doch dies ist ja bei derartigen Abrufen immer der Fall.
1. Öffnen Sie den Knoten einer Tabelle.
2. Wählen Sie aus dem Kontextmenü Skript für TABELLE ALS / CREATE IN.
Je nachdem, ob man das SQL-Skript im Abfrage-Editor oder gleich als
Datei speichern bzw. in die Zwischenablage kopieren möchte, wählt man
einen entsprechenden Eintrag.
3. Als Alternative sehen Sie schon in der Abbildung, dass man die
Löschanweisung über DROP IN ebenfalls erstellen kann. Nach der
Trennungslinie kann man dann die Operationen abfragen, einfügen,
aktualisieren und löschen über die gleiche grafische Oberfläche ausführen,
die schon vom Abfrage-Fenster aus abzurufen ist und bereits erklärt wurde.
236
Datenmanipulation
Abbildung 4.7: SQL-DDL erstellen
Man erhält das nachfolgende Skript, dessen Syntax im nächsten Abschnitt erläutert
wird.
CREATE TABLE [dbo].[EmpDep](
[ID] [uniqueidentifier] NOT NULL,
[Name] [nvarchar](50)
COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[DepID] [smallint] NULL,
[Title] [nchar](8)
COLLATE SQL_Latin1_General_CP1_CI_AS NULL
CONSTRAINT [DF_EmpDep_Title]
DEFAULT (N'Mr'),
[FirstName] [nvarchar](50)
COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[LastName] [nvarchar](50)
COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
237
Datenmanipulation
CONSTRAINT [PK_EmpDep] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX
= OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
411_01.sql: Erzeugtes SQL-DDL
4.1.1.2
Tabellen mit SQL erstellen
Mit Hilfe von SQL lässt sich unter Einsatz des CREATE-Befehls eine Tabelle anlegen,
wobei über CREATE sämtliche so genannte Schema-Objekte, d.h. Objekte in der Datenbank, angelegt werden können. Dazu zählen auch Sichten, also gespeicherte Abfragen,
und auch die für diesen Band sehr wichtigen Funktionen und Prozeduren. Der Befehl ist
wie der SELECT-Befehl von erschreckendem Umfang und bietet eine Vielzahl an Optionen und Angaben, die sehr tief in die Administration und den technischen Aufbau der
Datenbank eingreifen.
238
Datenmanipulation
CREATE TABLE
1
[ database_name . [ schema_name ] . | schema_name . ] table_name
( { <column_definition> | <computed_column_definition> }
[ <table_constraint> ] [ ,...n ] )
[ ON { partition_scheme_name ( partition_column_name ) | filegroup
| "default" } ]
[ { TEXTIMAGE_ON { filegroup | "default" } ]
[;]
<data type> ::=
3
[ type_schema_name . ] type_name
[ ( precision [ , scale ] | max |
[ { CONTENT | DOCUMENT } ]
xml_schema_collection ) ]
<column_definition> ::=
2
column_name <data_type>
[ COLLATE collation_name ]
[ NULL | NOT NULL ]
[
[ CONSTRAINT constraint_name ] DEFAULT constant_expression ]
| [ IDENTITY [ ( seed ,increment ) ] [ NOT FOR REPLICATION ]
]
[ ROWGUIDCOL ] [ <column_constraint> [ ...n ] ]
Abbildung 4.8: Basisstruktur für die Tabellenerstellung
Die Abbildung zeigt den grundlegenden Aufbau der CREATE-Anweisung, wie er auch
für diesen Band notwendig ist. Er zeigt die allgemeine Syntax der Tabellenerstellung in
einer zusammengefassten Darstellung und zwei Erweiterungen, die in der allgemeinen
Syntax nur über Ausdrücke in spitzen Klammern wie <column_definition> notiert
werden. Dies ist eine in der Dokumentation oft genutzte Möglichkeit, eine große Syntaxstruktur zu vereinfachen.
So enthält der mit der Nummer 1 versehene Bereich den gesamten CREATE-Befehl in
einer sehr stark vereinfachten Fassung. Anstelle von der in Nummer 2 angegebenen
Spaltendefinition erscheint in Nummer 1 schlichtweg <column_definition>. Gleiches gilt für die in Nummer 2 referenzierte Abkürzung <data_type>. Die Datentypangabe ist wiederum in Nummer 3 angegeben.
239
Datenmanipulation
CREATE TABLE
1
[ database_name . [ schema_name ] . | schema_name . ] table_name
( { <column_definition> | <computed_column_definition> }
[ <table_constraint> ] [ ,...n ] )
[ ON { partition_scheme_name ( partition_column_name ) | filegroup
| "default" } ]
<computed_column_definition> ::=
[ { TEXTIMAGE_ON { filegroup | "default" } ]
column_name AS computed_column_expression
[;]
[ PERSISTED [ NOT NULL ] ]
2
[
[ CONSTRAINT constraint_name ]
{ PRIMARY KEY | UNIQUE }
[ CLUSTERED | NONCLUSTERED ]
[
WITH FILLFACTOR = fillfactor
| WITH ( <index_option> [ , ...n ] )
]
| [ FOREIGN KEY ]
REFERENCES referenced_table_name [ ( ref_column ) ]
[ ON DELETE { NO ACTION | CASCADE } ]
[ ON UPDATE { NO ACTION } ]
[ NOT FOR REPLICATION ]
| CHECK [ NOT FOR REPLICATION ] ( logical_expression )
[ ON { partition_scheme_name ( partition_column_name )
| filegroup | "default" } ]
]
Abbildung 4.9: Berechnete Spalten
Wie man in der Nummer 1 sehen kann, folgt nach CREATE TABLE der Name der Tabelle, wobei dieser auch den Namen der Datenbank und des Schemas, in dem die Tabelle
erstellt werden soll, enthalten kann. Diese drei Bereiche werden jeweils durch einen
Punkt getrennt. Im einfachsten Fall genügt allerdings der Tabellenname. Nach der Nennung des Namens folgt in zwei runden Klammern die Angabe der einzelnen Spalten und
danach weitere allgemeine Angaben zur Tabelle wie Einschränkungen und Integritätsregeln. Die Erstellung der Spalten ist neben den administrativen Angaben der wesentliche Bereich der gesamten SQL-Anweisung. Hier gibt es insgesamt zwei Möglichkeiten,
welche in den beiden Abbildungen angezeigt werden.
240
Datenmanipulation
ƒ
Unter <column_definition> versteht man die Angabe eines Spaltennamens und
eines Datentyps für diese Spalte sowie weitere Angaben für die Spalte, die ihre
Eigenschaften
betreffen:
zulassen
von
NULL-Werten,
Zeichensatz,
Standardwertangabe, Angabe der Einzigartigkeit des Wertes für Primärschlüssel.
ƒ
Unter <computed_column_definition> versteht man die Angabe eines
Spaltennamens und des Schlüsselwortes AS, der zu einem Ausdruck führt, der eine
so genannte berechnte Spalte erstellt. In diese Spalte lässt sich später kein Wert
eintragen, sondern er wird automatisch und dynamisch auf Basis der angegebenen
Berechnungsvorschrift ermittelt.
Die nachfolgenden Beispiele sollen zeigen, wie man direkt in SQL eine Tabelle erstellen kann. Neben der Speicherung in der Datenbank sind die Beispiele von ihrem grundsätzlichen Aufbau her auch relevant für die Erstellung von so genannten temporären
Tabellen. Dies sind Tabellen, die in zwei Ausprägungen erscheinen. Die gewöhnliche
temporäre Tabelle kennzeichnet man mit einem Rautenzeichen vor ihrem Namen, was
angibt, dass sie temporär und nur für die aktuelle Sitzung gespeichert wird. Eine globale
temporäre Tabelle dagegen kennzeichnet man durch zwei Rautenzeichen, was wiederum angibt, dass sie zwar nicht fix in der Datenbank gespeichert ist, aber für alle Sitzungen – also global – verfügbar ist.
Um eine Tabelle zu löschen, verwendet man den DROP-Befehl, der nicht nur für Tabelle,
sondern für alle Schema-Objekte zur Verfügung steht und vor Nennung des SchemaObjektnamens noch die Art des Schema-Objekts erwartet. In diesem Fall ist das TABLE,
im Falle einer Prozedur PROCEDURE und im Falle einer Funktion FUNCTION. Um das
Beispiel mehrfach auszuführen, ist es notwendig, die bereits erstellte Tabelle zu löschen. Die beiden Anweisungen DROP und CREATE sind durch GO getrennt, was ankündigt, dass der eine Befehl abgeschlossen ist und ein neuer beginnt.
Auf Basis der schon in der Datenbank vorhandenen Tabelle Address erstellt das nachfolgende Beispiel eine ähnliche Tabelle Address2. Man erkennt sehr gut den Aufbau,
mit der die Spalten erstellt werden: name datentyp [NOT] NULL.
241
Datenmanipulation
Dabei zeigt bereits die erste Zeile, dass die AddressID-Spalte zusätzlich auch den
Primärschlüssel bildet und daher zusätzlich noch das Schlüsselwort PRIMARY KEY
enthält. Die Eigenschaft IDENTITY [ (seed , increment) ] bietet die Möglichkeit, einen automatisch aufsteigenden Schlüssel zu verwenden, der mit jeder neuen
Einfügen-Operation bei seed beginnend um den Wert, der in increment angegeben
ist, erhöht wird.
Die optionale [NOT] NULL-Angabe legt fest, ob NULL-Werte in der Spalte zulässig sind
oder nicht. Auch wenn diese Angabe nicht verpflichtend ist, so stellt sie doch eine sehr
wichtige Angabe dar, um die Funktionsweise der Spalte genau anzugeben.
Während der Großteil der Spalten sich nach dem gerade beschriebenen Schema richtet,
fällt nur noch die letzte Spalte namens ModifiedDate wieder aus dem Rahmen. Sie
enthält zusätzlich noch eine DEFAULT-Angabe mit einem Standardwert. Sollte beim
Einfügen kein Wert für diese Spalte angegeben werden, so würde dieser Standardwert
verwendet werden. Im Normalfall ist dieser Standardwert fix vorgegeben, d.h. der
wahrscheinlichste und damit häufigste Wert, der für neue Datensätze gilt.
IF OBJECT_ID ('Person.Address2', 'T') IS NOT NULL
DROP TABLE Person.Address2
GO
CREATE TABLE Person.Address2 (
AddressID
int IDENTITY(1,1) NOT NULL PRIMARY KEY ,
AddressLine1
nvarchar(60)
NOT NULL,
AddressLine2
nvarchar(60)
NULL,
City
nvarchar(30)
NOT NULL,
StateProvinceID int
NOT NULL,
PostalCode
NOT NULL,
242
nvarchar(15)
Datenmanipulation
ModifiedDate
datetime
NOT NULL
DEFAULT (getdate()))
GO
411_02.sql: Erstellen einer Tabelle
Sobald man die neue Address2-Tabelle angelegt hat, sollte sie nach dem Aktualisieren
im Objekt-Explorer ebenfalls erscheinen. Da es ja bereits eine ähnliche Tabelle gibt,
kann man diese beiden gut vergleichen.
Abbildung 4.10: Tabellen mit automatischen / eigenen Bezeichnern
Die Spalten sind weitestgehend dieselben bzw. nur um wenige abgekürzt. Da allerdings
im gerade gezeigten Skript die verschiedenen Einstellungen wie Primärschlüssel und
Einschränkungen wie ein Standardwert nicht ausdrücklich einen eigenen Namen erhalten haben, wurden automatische Standardnamen vergeben. Dies ist bei einer gespeicherten Tabelle keine so gute Lösung, weil in Fehlermeldungen diese Namen wieder erscheinen und man sie dann nicht so leicht zuordnen kann wie ein selbst definierter,
weitestgehend selbst-erklärender und -sprechender Name. Bei den erwähnten temporären Tabellen ist dies oft nicht so bedeutsam, wenn sie mehr wie eine Array-Struktur
genutzt werden und vielleicht auch gar keine solchen Beziehungen zu anderen Tabellen
enthalten.
243
Datenmanipulation
Zusätzlich wurde die OBJECT_ID()-Funktion eingesetzt. Sie hat die allgemeine Syntax:
OBJECT_ID (
'[ database_name . [ schema_name ] . | schema_name . ]
object_name' [ ,'object_type' ] )
Sie dient dazu, die so genannte Objekt-ID zurückliefern, wie es der Name der Funktion ja schon nahe legt. Ein Ausdruck wie SELECT OBJECT_ID('Person.Address')
AS Tabelle ermittelt den Wert 53575229 für die im Testsystem vorhandene Datenbankinstallation. Sollte man dagegen ein nicht vorhandenes Schema-Objekt aufrufen, so
erhält man den Wert NULL zurück, sodass dies ein einfacher Test ist, ob ein SchemaObjekt vorhanden ist oder nicht. Bei einer temporären Tabelle muss man in jedem Fall
den Namen der Datenbank für die temporären Objekte vor den Namen des Objekts
setzen.
Hier
ändert
sich
dann
die
Abfrage
zu
SELECT
OBJECT_ID('tempdb..#temptable') um. Mit Hilfe der Abfrage SELECT DISTINCT
type FROM sys.objects erhält man die derzeit verfügbaren möglichen Abkürzungen für Schema-Objekte wie bspw.:
AF = Aggregatfunktion (CLR)
SN = Synonym
C = CHECK-Einschränkung
SQ = Dienstwarteschlange
D = DEFAULT (Einschränkung oder
eigenständig)
TA = Assembly-DML-Trigger (CLR)
F = FOREIGN KEY-Einschränkung
PK = PRIMARY KEY-Einschränkung
IF = Inline-Tabellenwertfunktion von
SQL
P = Gespeicherte SQL-Prozedur
TF = Tabellenwertfunktion von SQL
PC = Gespeicherte Assemblyprozedur
(CLR)
U = Tabelle (benutzerdefiniert)
FN = SQL-Skalarfunktion
V = Sicht
FS = Assemblyskalarfunktion (CLR)
X = Erweiterte gespeicherte Prozedur
FT
244
=
Assembly-Tabellenwertfunktion
TR = SQL-DML-Trigger
UQ = UNIQUE-Einschränkung
Datenmanipulation
(CLR)
IT = Interne Tabelle
R = Regel (vom alten Typ, eigenständig)
RF = Replikationsfilterprozedur
Im nächsten Beispiel wird die Address2-Tabelle noch einmal abgewandelt. In Ergänzung zum vorherigen Beispiel werden nun sowohl selbst benannte Einschränkungen als
auch Angaben zu Zeichensätzen verwendet, die in den Zeichenkettenspalten genutzt
werden sollen. Dabei folgt der Name der [NOT] NULL-Angabe nach dem Schlüsselwort
CONSTRAINT. Diese Namen erscheinen dann auch im Objekt-Explorer, sobald man die
Anzeige aktualisiert und zu den Eigenschaften der erstellten Tabelle navigiert.
CREATE TABLE Person.Address2 (
AddressID
int IDENTITY(1,1) NOT NULL
CONSTRAINT PK_Address_AddressID2 PRIMARY KEY
,
AddressLine1
nvarchar(60)
COLLATE SQL_Latin1_General_CP1_CI_AS NOT
NULL,
AddressLine2
nvarchar(60)
COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
City
nvarchar(30)
COLLATE SQL_Latin1_General_CP1_CI_AS NOT
NULL,
StateProvinceID int NOT NULL,
PostalCode
nvarchar(15)
COLLATE SQL_Latin1_General_CP1_CI_AS NOT
NULL,
245
Datenmanipulation
ModifiedDate
datetime NOT NULL
CONSTRAINT DF_Address_ModifiedDate2
DEFAULT (getdate()))
411_03.sql: Erweiterte Tabellendefinition
Nachdem eine typische Tabelle erzeugt wurde, die zwar als Referenz und damit als
Elterntabelle für andere Tabellen dienen kann, erstellt wurde, soll nun im nachfolgenden
Beispiel eine Tabelle definiert werden, die neben den üblichen Eigenschaften auch
Fremdschlüsselbeziehungen zu anderen Tabellen hat. Dazu eignen sich im Schema der
AdventureWorks-Datenbank sehr viele Tabellen, da das Schema für eine Beispieldatenbank durchaus sehr komplex ist und daher viele Verknüpfungen zwischen den Tabellen
besitzt.
Tabellenbeziehungen lassen sich über die FOREIGN KEY-Klausel angeben, die entweder
direkt in der Spaltendefinition erscheint oder nach der Spaltendefinition angeschlossen
wird. In einem automatisch erzeugten Skript ist auch zu erkennen, dass keine von beiden Varianten, die in den nächsten Beispielen benutzt werden, tatsächlich umgesetzt
werden. In einem erzeugten CREATE-Skript wird zunächst die Tabelle erstellt, ehe dann
mit diversen ALTER-Anweisungen die Tabelle geändert und weitere Angaben angehängt
werden, nachdem die eigentliche Tabelle erstellt wurde. Ein Vorteil dieser Technik
besteht darin, dass ein Teil der Anweisungen im Fehlerfall durchaus ausgeführt werden
kann, während innerhalb der umfassenden Reihe an zusätzlichen Anweisungen auch
bisweilen ein Fehler entstehen kann, der zwar zu einer Fehlermeldung führt, das gesamte Skript aber zunächst nicht beendet.
Im nachfolgenden Beispiel erstellt man eine Variante der Product-Tabelle, die weniger
Spalten als die originale Tabelle in der DB besitzt. Der Primärschlüssel wird wiederum
automatisch erzeugt, weil die ProductID-Spalte die IDENTITY-Eigenschaft besitzt.
Nach einigen im zuvor beschriebenen Verfahren definierten Spalten für die Abbildung
von Produkten folgen die Fremdschlüsselverknüpfungen. Sie gelten für die Verknüpfungen zu den beiden Tabellen UnitMeasure, welche Maßeinheiten sammelt, die im
246
Datenmanipulation
Rahmen der Datenbank an verschiedenen Stelle genutzt werden, und ProductSubcategory, welche zusammen mit der von ProductSubcategory aus verknüpften Tabelle ProductCategory die Produkte in Kategorien und Unterkategorien einteilt.
Hierbei muss natürlich zunächst die ProductSubcategory für die Zuweisung zu einer
Untergruppe verbunden werden, weil die Untergruppen dann wiederum zu wenigen
Kategorien zusammengefasst werden.
Es sind zwar nur zwei Tabellen verbunden, dennoch findet man insgesamt drei Fremdschlüsselbeziehungen. Neben der Spalte SizeUnitMeasureCode verknüpft nämlich
auch die Spalte WeightUnitMeasureCode die Spalte UnitMeasure bzw. sogar die
gleiche Spalte UnitMeasureCode, da einfach die beiden Spalten für Größe und Gewicht eine Maßeinheit benötigen, die in der umfassenden Tabelle mit standardisierten
Maßeinheiten gespeichert werden. An anderer Stelle wurde bereits erwähnt, dass es
möglich ist, in mehreren Spalten die gleiche Tabelle zu referenzieren. In diesem Fall
handelt es sich allerdings nicht um den viel häufigeren Fall eines zusammen gesetzten
Schlüssels, der aus mehreren Feldern erstellt wird, sondern stattdessen sind tatsächlich
verschiedene Datensätze aus der UnitMeasure-Tabelle verbunden.
CREATE TABLE Production.Product2 (
ProductID
int IDENTITY(1,1) NOT NULL PRIMARY KEY,
Name
varchar(30) NOT NULL,
ProductNumber nvarchar(25) NOT NULL,
ListPrice
money NOT NULL,
Size
nvarchar(5) NULL,
SizeUnitMeasureCode
nchar(3) NULL
FOREIGN KEY REFERENCES Production.UnitMeasure
(UnitMeasureCode),
247
Datenmanipulation
WeightUnitMeasureCode nchar(3) NULL
FOREIGN KEY REFERENCES Production.UnitMeasure
(UnitMeasureCode),
Weight
decimal(8, 2) NULL,
ProductSubcategoryID
int NULL
FOREIGN KEY REFERENCES Production.ProductSubcategory
(ProductSubcategoryID),
ModifiedDate
datetime NOT NULL
CONSTRAINT DF_Product_ModifiedDate2 DEFAULT (getdate()))
411_04.sql: Anlegen von Fremdschlüsseln
248
Datenmanipulation
Abbildung 4.11: Verknüpfungen der Tabelle
Erstellt man ein Diagramm oder wirft man einen Blick in das Schema der Datenbank
erkennt man deutlich, wie die Tabelle Product und damit auch die hier erstellte Tabelle Product2 mit zwei Tabellen verbunden ist.
Neben der Möglichkeit, die Fremdschlüssel direkt bei der Spaltendefinition anzugeben,
besteht – wie schon erwähnt – ebenfalls die Möglichkeit, sie nach der gesamten Spaltenliste gesammelt anzugeben oder sie über den ALTER-Befehl nachträglich anzuhängen. In
allen drei Varianten ist es möglich, für die Verknüpfung einen Namen zu geben, wie
dies schon bei den vorherigen Beispielen vorgeführt wurde. Daher folgt ein SQL-Skript,
in dem gerade nach der verkürzten Spaltenliste zwei der Verknüpfungen angegeben
werden, während eine benannte Verknüpfung unter Angabe des CONSTRAINTSchlüsselworts weiterhin in der Spaltenliste zu finden ist. In diesem Fall ist die Angabe
allerdings um den Namen der Verknüpfung erweitert.
DROP TABLE Production.Product2
GO
249
Datenmanipulation
CREATE TABLE Production.Product2 (
ProductID
int IDENTITY(1,1) NOT NULL PRIMARY KEY,
...,
ProductSubcategoryID
int NULL
CONSTRAINT FK_Product_ProductSubcategory_
ProductSubcategoryID2
FOREIGN KEY(ProductSubcategoryID)
REFERENCES Production.ProductSubcategory
(ProductSubcategoryID),
...,
CONSTRAINT FK_Product_UnitMeasure_SizeUnitMeasureCode2
FOREIGN KEY(SizeUnitMeasureCode)
REFERENCES Production.UnitMeasure (UnitMeasureCode),
CONSTRAINT FK_Product_UnitMeasure_WeightUnitMeasureCode2
FOREIGN KEY(WeightUnitMeasureCode)
REFERENCES Production.UnitMeasure (UnitMeasureCode))
411_05.sql: Benannte Fremdschlüsselverknüpfungen
250
Datenmanipulation
Abbildung 4.12: Aufbau einer Selbstverknüpfung
Die Selbstverknüpfung ist bereits im Rahmen der verschiedenen Verknüpfungsarten aus
Sicht von Abfragen vorgeführt worden. An dieser Stelle sei auf dieses Thema noch
einmal verwiesen. Eine FOREIGN KEY-Klausel kann selbstverständlich auch auf die
gleiche Tabelle verweisen. Das klassische Schulbeispiel einer Selbstverknüpfung befindet sich in der Employee-Tabelle, in der einige Mitarbeiter als Manager erscheinen, da
ihre EmployeeID in der ManagerID-Spalte genannt werden. Dies führt dazu, dass die
EmployeeID weiterhin der Primärschlüssel ist und dass die ManagerID-Spalte den
Fremdschlüssel enthält. Dies ist dann eine selbstbezügliche Verknüpfung, wo die gleiche Tabelle einmal in der Rolle der Kind- und ein anderes Mal in der Rolle der ElternTabelle auftritt.
Nach der Erstellung einer Tabelle kann es bisweilen schon einmal geschehen, dass die
Strukturen einer Tabelle geändert werden sollen. Dies betrifft die Veränderung der
251
Datenmanipulation
Datentypangabe wie bspw. die Veränderung des Wertebereichs, das Hinzufügen eines
Fremdschlüsselwerts oder einer ganzen Tabelle sowie eines Standardwerts wie auch die
Umbenennung einer Spalte. Während die ersten Beispiele gerade durch den ALTERBefehl durchgeführt werden können, gibt es für die Umbenennung von Spalten keine
Möglichkeit, dies mit SQL direkt durchzuführen.
ALTER TABLE [ database_name . [ schema_name ] . | schema_name . ] table_name
{
ALTER COLUMN column_name
{
[ type_schema_name. ] type_name [ ( { precision [ , scale ]
| max | xml_schema_collection } ) ]
[ NULL | NOT NULL ]
[ COLLATE collation_name ]
| {ADD | DROP } { ROWGUIDCOL | PERSISTED }
}
| [ WITH { CHECK | NOCHECK } ] ADD
DROP
{
{
<column_definition>
[ CONSTRAINT ] constraint_name
| <computed_column_definition>
[ WITH ( <drop_clustered_constraint_option> [ ,...n ] ) ]
| <table_constraint>
| COLUMN column_name
} [ ,...n ]
} [ ,...n ]
| [ WITH { CHECK | NOCHECK } ] { CHECK | NOCHECK } CONSTRAINT
{ ALL | constraint_name [ ,...n ] }
| { ENABLE | DISABLE } TRIGGER
{ ALL | trigger_name [ ,...n ] }
| SWITCH [ PARTITION source_partition_number_expression ]
TO [ schema_name. ] target_table
[ PARTITION target_partition_number_expression ]
}
Abbildung 4.13: Ändern von Eigenschaften
Die vorherige Abbildung zeigt die beiden Teile der allgemeinen Syntax. Innerhalb des
ALTER-Befehls gibt es die Möglichkeit, eine Spalte zu ändern oder sie bzw. ihre Einstellungen zu löschen. Dies geschieht jeweils mit einem weiteren ALTER-Befehl oder dem
auch für die Schema-Objekte verfügbaren DROP-Befehl.
Die nachfolgenden Beispiele zeigen kurz, wie man typische Anwendungsfälle direkt in
SQL umsetzt. Im DBA-Buch werden die einzelnen Befehle sowie natürlich die verschiedenen Spalteneinschränkungen und Schema-Objekte ausführlicher dargestellt. In
252
Datenmanipulation
den meisten Fällen dürfte es daher ausreichen, die Änderungen direkt in der grafischen
Oberfläche auszuführen.
Die Tabelle Address2 soll zunächst ihre Einschränkung, die die Fremdschlüsselverknüpfung zur Tabelle StateProvince enthält, verlieren, wenn diese bereits existiert.
Ansonsten soll später genau diese Einschränkung eingerichtet, eine neue Spalte eingefügt, diese vom Datentyp geändert und schließlich gelöscht werden.
IF OBJECT_ID ('FK_Address_StateProvince_StateProvinceID2',
'C') IS NOT NULL
ALTER TABLE Person.Address2
DROP CONSTRAINT FK_Address_StateProvince_StateProvinceID2
GO
ALTER TABLE Person.Address2 WITH CHECK
ADD CONSTRAINT FK_Address_StateProvince_StateProvinceID2
FOREIGN KEY (StateProvinceID)
REFERENCES Person.StateProvince (StateProvinceID)
GO
ALTER TABLE Person.Address2 ADD AddressLine3 nvarchar(30)
NULL
GO
ALTER TABLE Person.Address2 ALTER COLUMN nvarchar(50)
GO
ALTER TABLE Person.Address2 DROP COLUMN AddressLine3
GO
411_06.sql: Beispiele von Änderungen
253
Datenmanipulation
Wie oben schon erwähnt, ist es nicht möglich, mit einfachen SQL-Anweisungen ein
Objekt umzubenennen. Es gibt zwar grundsätzlich in anderen Datenbanken auch eine
RENAME-Option innerhalb von ALTER, doch natürlich muss man darauf hinweisen, dass
gerade das Umbenennen von Schema-Objekten eine deutlich gefährlichere Aktion darstellt als das einfache Ändern. Hier können abhängige Objekte wie Funktionen, Prozeduren sowie andere Tabellen ungültig werden, was die ganze DB-Struktur zerstören
kann. Daher vermutlich gibt es für die MS SQL Server-DB eine System-Prozedur, mit
deren Hilfe eine solche Umbenennung möglich ist. Sie hat folgende allgemeine Syntax:
sp_rename [ @objname = ] 'object_name' ,
[ @newname = ] 'new_name'
[ , [ @objtype = ] 'object_type' ]
Folgende Werte sind für den Objekttyp möglich:
ƒ
COLUMN für Spalte (benutzerdefiniert)
ƒ
DATABASE für Datenbank (benutzerdefiniert)
ƒ
INDEX für Index
ƒ
OBJECT für Objekt aus sys.objects wie bspw. Einschränkungen (CHECK,
FOREIGN KEY, PRIMARY/UNIQUE KEY), Benutzertabellen und Regeln
ƒ
USERDATATYPE für Benutzerdefinierter Aliasdatentyp (CREATE TYPE) oder CLR-
benutzerdefinierter Typ (sp_addtype) aus .NET.
Im nachfolgenden Beispiel benennt man zunächst eine Tabelle um. Danach ändert man
in der neu benannten Tabelle einen Spaltennamen ab. Im Kommentar steht zusätzlich,
wie die gleiche Operation über Namensnotation (Nennung der Parameternamen) im
Gegensatz zur Positionsnotation erfolgt.
EXEC sp_rename 'Person.Address2', 'Person.AddressNew'
GO
254
Datenmanipulation
EXEC sp_rename 'Person.AddressNew.City',
'Person.AddressNew.CityNew', 'COLUMN'
/*
EXEC sp_rename @objname = 'Person.AddressNew.City',
@newname = 'Person.AddressNew.CityNew',
@objtype = 'COLUMN';
*/
GO
411_07.sql: Umbenennen von Spalten und Tabellen
4.1.2
Sichten
Neben den Tabellen, die für die Erstellung von Abfragen mehr innerhalb von Transact
SQL-Programmen als Array-Struktur von der anvisierten Zielgruppe dieses Buchs genutzt werden dürften, sind zusätzlich noch Sichten eine interessante Lösung für die
Erstellung von Abfragen. Sie stellen gespeicherte Abfrage dar, welche mit einer SELECT-Anweisung gebildet werden wie eine gewöhnliche Abfrage und zusätzlich unter
einem Namen als Schema-Objekt in der Datenbank gespeichert werden. Als besonderer
Clou wirken sie in ihrer Funktionsweise und Benutzung wie gewöhnliche Tabellen, d.h.
als Benutzer muss man nicht unterscheiden, ob man eine Sicht oder eine Tabelle abfragt
bzw. kann es auch nicht in jeder Situation oder mit jeder Berechtigung herausfinden.
Dabei speichert man bei der Sicht tatsächlich nur das Relationenschema, d.h. die Regel,
welche Daten zu beschaffen sind, und gerade nicht die Daten selbst. Diese werden dynamisch und aktuell aus den vorhandenen Tabellen abgerufen.
Für Administratoren ist diese Technik interessant, weil so im tatsächlichen Sinne des
Wortes eine zusätzliche Sicht auf die Datenstrukturen und auf die Daten erstellt werden
kann, welche als Schutzschild für die in Wirklichkeit vorhandenen Strukturen und Daten dienen. Die beiden Aspekte lassen sich getrennt betrachten:
255
Datenmanipulation
ƒ
Strukturen: Durch die Erstellung einer Sicht ist es möglich, bspw. mehrere Tabellen
zu verknüpfen und dadurch die Komplexität von sehr weit und damit sehr gut
normalisierten Datenmodellen wieder zu vereinfachen. Der Benutzer, der SQL für
die Bearbeitung der Daten eingibt, ist nicht gezwungen, selbst mehrere Tabellen
miteinander zu verbinden und damit möglicherweise zusätzlich zu berücksichtigen,
dass mehrere Schlüsselfelder sich zu einem Schlüssel zusammensetzen. Dadurch
können Fehler und falsch erzeugte Daten verhindert werden.
ƒ
Daten: Durch die Erstellung einer Sicht ist es möglich, die Daten bereits soweit zu
filtern, dass ein Benutzer gerade nicht alle Daten sehen kann, sondern nur einen
Teil. Dies betrifft teilweise auch wiederum die Strukturen, da man nicht nur
einzelne Zeilen, sondern ganz einfach auch ganze Spalten verbergen kann, indem
sie in der Sicht nicht erscheinen. Der Fall, an dem man möglicherweise sofort
denkt, wenn man Datensicherheit über eine Sicht herstellen möchte, ist die
Verwendung geeigneter Bedingungen in der WHERE-Klausel, um geheime Daten
auszublenden.
Die allgemeine Syntax zur Erstellung einer Sicht ist relativ einfach, da ein Großteil der
Arbeit natürlich von der SELECT-Anweisung.ausgeführt wird und die restliche Syntax
nur für die Erstellung der Sicht als solche existiert. Wie eine Tabelle kann man sie über
CREATE erstellen, wobei hier anstelle von TABLE das Schlüsselwort VIEW steht. Eine
Sicht kann ebenfalls in einem speziellen Datenbankschema oder im Standardschema
dbo liegen. Danach folgt eine optionale Spaltenliste, mit der direkt die Spaltennamen
angegeben werden können, welche von der Sicht nach außen angeboten werden sollen.
Als Alternative werden einfach die Spalten(alias)namen der Abfrage verwendet. Dies ist
die einfachste Lösung und dürfte auch am häufigsten zum Einsatz kommen. Vor der
eigentlichen Abfrage folgt dann noch die WITH-Klausel, während nach der Abfrage die
Anweisung WITH CHECK OPTION folgen kann.
Folgende allgemeine Syntax ist für die Erstellung einer Sicht verfügbar:
CREATE VIEW [ schema_name . ] view_name
[ (column [ ,...n ] ) ]
256
Datenmanipulation
[ WITH [ ENCRYPTION | SCHEMABINDING
| VIEW_METADATA ]
[ ,...n ] ]
AS select_statement [ ; ]
[ WITH CHECK OPTION ]
Die nachfolgende Abbildung zeigt die Funktionsweise einer Sicht. Sie würde in fast der
gleichen Weise auch die Funktionsweise einer Abfrage darstellen, wenn man die SQLAnweisung im Textkasten um CREATE VIEW kürzt und sich vorstellt, dass die Abfrage
als solche gerade nicht in der Datenbank gespeichert wird.
Aus den drei Tabellen Employee, Contact und EmployeeAddress, wobei die dritte
Tabelle nur eine Beziehungstabelle zwischen Employee und Contact darstellt, um
Mitarbeitern einen Kontakt aus allen möglichen Kontakten zuzuordnen. Aus diesen drei
Tabellen ruft man aus Employee und Contact (die dritte wird nur für die Verknüpfung
verwendet) nicht alle Spalten ab, sondern nur solche wie EmployeeID, die Namensbestandteile sowie die Kontaktdaten für Telefon und E-Mail. Dies wäre dann ein Beispiel
für
die
Struktursicherheit
oder
die
Strukturänderung
und
-vereinfachung, die durch eine Sicht realisiert werden können. Die Vereinfachung liegt
darin, dass man nachher bei der Verwendung der Sicht die Beziehungstabelle nicht
mehr einsetzen muss, um die Daten abzurufen. Die Sicherheit könnte darin liegen, dass
zusätzlich auf Zeilenebene noch ein weiterer Teil der Daten ausgeblendet wird. Dies ist
im aktuellen Beispiel nicht gegeben und wäre mit Blick auf das reine Datenmodell nicht
sichtbar, da hier schließlich nur die Spalten zu erkennen sind. Eine WHERE-Klausel
könnte allerdings die Daten für bestimmte Benutzergruppen filtern.
257
Datenmanipulation
CREATE VIEW HumanResources.vEmployee2
AS
SELECT e.EmployeeID,
c.FirstName,
c.MiddleName,
c.LastName,
c.Phone,
c.EmailAddress
FROM HumanResources.Employee e INNER JOIN Person.Contact c
ON c.ContactID = e.ContactID
INNER JOIN HumanResources.EmployeeAddress ea
ON e.EmployeeID = ea.EmployeeID
Abbildung 4.14: Spaltenzuordnung in Sicht
Die gerade in der Abbildung gezeigte Syntax folgt noch einmal im nachfolgenden Beispiel. Die Spalten, welche durch die Sicht angeboten werden, tragen die Namen der
abgerufenen Spalten, wie man auch später in der der ausgeführten SQL-Anweisung
sehen kann. Diese soll auch verdeutlichen, dass die Sicht wie eine Tabelle innerhalb der
FROM-Klausel verwendet werden kann und alle Abrufoptionen bietet wie eine Tabelle.
IF OBJECT_ID ('HumanResources.vEmployee2', 'V') IS NOT NULL
DROP VIEW HumanResources.vEmployee2
GO
258
Datenmanipulation
CREATE VIEW HumanResources.vEmployee2
AS
SELECT e.EmployeeID,
c.FirstName,
c.MiddleName,
c.LastName,
c.Phone,
c.EmailAddress
FROM HumanResources.Employee e
INNER JOIN Person.Contact c
ON c.ContactID = e.ContactID
INNER JOIN HumanResources.EmployeeAddress ea
ON e.EmployeeID = ea.EmployeeID
GO
-- Sicht abfragen
SELECT Firstname, LastName, EmailAddress
FROM HumanResources.vEmployee2
WHERE EmployeeID < 4
412_01.sql: Sicht erstellen (Spaltennamen der Abfrage)
Man erhält als Ergebnis der Abfrage eine Ergebnismenge, welche die Daten aus der
Abfrage aktuell anzeigt und zusätzlich die Spaltennamen der Abfrage und damit die zu
Grunde liegenden Spaltennamen verwendet.
259
Datenmanipulation
Firstname
LastName
EmailAddress
---------------- --------------- ---------------------------Guy
Gilbert
[email protected]
Kevin
Brown
[email protected]
Roberto
Tamburello
roberto0@adventure-
works.com
Als Alternative für die vorherige Lösung bietet sich an, die Spaltennamen als Aliasnamen anzugeben. Dies zeigt das nachfolgende Beispiel, in dem Spalten wie die EmployeeID aus der Tabelle Employee als ID angegeben wird, wozu lediglich der Aliasname
benötigt wird. In der Abfrage dieser Sicht kann man dann sehen, wie die entsprechenden Spaltennamen der Sicht, welche die Aliasnamen aus der zu Grunde liegenden Abfrage darstellen, für den Datenabruf aus der Sicht genutzt werden.
CREATE VIEW HumanResources.vEmployee2
AS
SELECT e.EmployeeID
AS ID,
c.FirstName + ' ' + c.LastName AS Name,
c.Phone
AS Phone,
c.EmailAddress AS Email
FROM ...
GO
-- Sicht abfragen
SELECT Name, Email
FROM HumanResources.vEmployee2
260
Datenmanipulation
WHERE ID < 4
412_02.sql: Spaltennamen aus Aliasnamen
Schließlich kann man sich auch noch dafür entscheiden, die Spaltennamen der Abfrage
oder sogar die Aliasnamen der Spalten durch einen Klammerausdruck zu überschreiben.
Dieser folgt dem Namen der Sicht und enthält die richtige Anzahl und in der richtigen
Reihenfolge die Spaltennamen, welche von der Sicht zurückgeliefert werden sollen.
Während die korrekte und damit inhaltlich sinnvolle Zuordnung nicht automatisch kontrolliert werden kann, muss die Anzahl der Spaltennamen im Klammerausdruck mit der
Spaltenanzahl
in
der
SELECT-Anweisung
übereinstimmen.
CREATE VIEW HumanResources.vEmployee2
(ID, Name, Phone, Email)
AS
SELECT e.EmployeeID,
c.FirstName + ' ' + c.LastName,
c.Phone,
c.EmailAddress
FROM ...
412_03.sql: Spaltennamen aus Spaltenliste
Sichten sind eine hervorragende Möglichkeit, um komplexe Datenstrukturen für den
Abruf zu vereinfachen, in dem die Sichtdefinition bereits benötigte Umrechnungen,
Funktionsaufrufe und natürlich Verknüpfungen sowie Filter enthält. Insbesondere durch
die Auswahl von Daten durch einen Filter und die Anzeige von Daten, welche durch die
Spaltenauswahl bereits eingeschränkt werden, ist es möglich, verschiedene Vereinfachungen und Sicherheitsstrategien umzusetzen.
261
Datenmanipulation
4.2
Daten bearbeiten
Auch wenn der anvisierte Leser dieses Buch eigentlich nur eine Datenbank bearbeitet,
deren Daten quasi „vom Himmel“ gefallen ist, so wie auch die ganze Datenbank in den
entscheidenden Teilen vom Himmel gefallen ist, gibt es natürlich doch eine große Anzahl an Datenbankbenutzern, die nicht nur Abfragen erstellen, sondern mit Blick auf
Prozeduren/Funktionen auch Daten erfassen, ändern und löschen müssen. Dies dürfte
im Normalfall durch eine Anwendung geschehen, welche entsprechende Formulare
bereitstellt, kann allerdings auch gerade dann notwendig werden, wenn größere Teile
Software direkt in der Datenbank realisiert werden oder wenn für die Bearbeitung/Weiterentwicklung der Datenbank auch solche Arbeiten anfallen.
Die SQL-Anweisungen, mit deren Hilfe Datenmanipulationen durchgeführt werden
können, gehören ebenfalls zu dem Teil von SQL, der mit dem Akronym DML (Data
Manipulation Language) umschlossen wird und zu dem auch der den größten Teil des
gesamten Buchs einnehmende SELECT-Befehl gehört. Die SQL-Sprache zeichnet sich
hier durch eine sehr große Konsequenz und Schönheit aus, weil Bestandteile wie die
FROM- und WHERE-Klausel zur Gänze als auch weitere Konstrukte wie die Nennung von
Spaltennamen in Form von Klammernausdrücken wieder in der Syntax erscheinen und
dadurch INSERT, UPDATE und DELETE von ihrer Grundsyntax her sehr mit SELECT
vergleichbar sind, obwohl sie wirklich völlig unterschiedliche Tätigkeiten ausführen.
4.2.1
Vorbereitung
In den meisten Fällen, in denen die drei genannten Anweisungen auftreten, handelt es
sich um eine Standardanweisung, welche die vielen Möglichkeiten und Alternativen,
welche die jeweilige Syntax bietet, gar nicht nutzt. Dies liegt nicht zuletzt daran, dass
eine entsprechende Schichtung in der Software genutzt wird, welche nicht nur einen
Großteil der auftretenden Abfragen völlig vereinfacht und daher ohne besondere Raffinesse abbildet, sondern die auch das Einfügen, Löschen und Bearbeiten von Datensätzen derart einfach abbildet, dass die gleiche Methode für alle möglichen im Rahmen der
Anwendung auftretenden Situationen die gleiche ist. Arbeit man allerdings direkt mit
der Datenbank, sind oft auch versierte Programmierer überrascht, was SQL zu leisten
262
Datenmanipulation
imstande ist. Dies ist insbesondere bei Abfragen immer wieder zu sehen, da der Abruf
von Daten – und sei es nur aus Kontrollzwecken – doch noch häufiger ab und an in SQL
vorgenommen wird als das Einfügen bzw. Bearbeiten von Daten. Daher versucht dieser
Abschnitt die Komplexität und die verschiedenen Anweisungsmöglichkeiten, welche
sich durch INSERT, UPDATE und DELETE ergeben, an möglichst eingängigen Beispielen
zu zeigen.
Leider setzen die verschiedenen Beispiele auch schon einmal Sprachstrukturen ein, die
erst später erklärt werden. Da hier allerdings das Huhn-und-Ei-Problem mit unterschiedlichen Vor- und Nachteilen nur gelöst werden kann, dürfte es wohl so sein, dass die
entsprechenden Beispiele erst später vollständig verstanden werden und an dieser Stelle
die Beispiele eher der Vollständigkeit halber aufgenommen werden und bei eine ersten
Lektüre des Buchs noch nicht so genutzt werden können wie bei einem wiederholten
Aufruf.
In den drei relevanten Befehlen befinden sich einige Syntaxinseln, die wenigstens zwei
Anweisungen auftreten und daher an dieser Stelle vorweg angegeben werden:
ƒ
Anstelle des referenzierten <object> kann die Nennung eines Schema-Objekts
stehen, wobei dieses auf unterschiedliche Weise adressiert werden kann. Es handelt
sich dabei um die Möglichkeit, eine Tabelle oder ein Schema mit zusäztlicher
Nennung von Schema und/oder Datenbank sowie Servernamen aufzurufen.
<object> ::=
{
[ server_name . database_name . schema_name .
| database_name .[ schema_name ] .
| schema_name .
]
table_or_view_name
263
Datenmanipulation
}
ƒ
Die <output_clause> liefert Daten aus einzelnen Zeilen zurück, die von einer
DML-Anweisung betroffen waren. Diese Daten stehen dann außerhalb der
Anweisung für Kontrollen, Bestätigungen und Validierungen zur Verfügung, ohne
dass eine eigene, das Netzwerk und die DB belastende Abfrage zur Kontrolle
formuliert werden müsste. Insbesondere für berechnete Spalten, Spaltenwerte aus
Triggeraktionen oder natürlich automatischen Zählungen (IDENTITY) von
Primärschlüsselwerten ist dies von Bedeutung.
<OUTPUT_CLAUSE> ::=
{
[ OUTPUT <dml_select_list> INTO {
@table_variable | output_table } [ ( column_list ) ]
]
[ OUTPUT <dml_select_list> ]
}
ƒ
Innerhalb der <output_clause> ruft man Spaltennamen oder Ausdrücke
(Kombination von Spaltennamen, Aufruf von zusätzlichen Funktionen etc.) auf, um
die zurückzuliefernden und interessanten Spalten zu bezeichnen.
<dml_select_list> ::=
{ <column_name> | scalar_expression } [
[AS] column_alias_identifier ]
[ ,...n ]
ƒ
264
Unter einem Spaltennamen in einer <output_clause> versteht man in einem
Trigger dabei nicht nur ihren tatsächlichen Namen, sondern auch die Angabe, ob
man sich auf die gelöschten oder eingefügten Daten bezieht. Dazu stehen die
beiden Tabellenaliasnamen DELETED und INSERTED zur Verfügung, welche solche
Datenmanipulation
Verweise auflösen. Ansonsten lässt sich natürlich in anderen Zusammenhängen
auch der gewöhnliche Tabellenname verwenden.
<column_name> ::=
{ DELETED | INSERTED | from_table_name } . { * | column_name
}
Um die Beispiele möglichst kurz zu halten, sollen zwei neue Tabellen erstellt werden, in
denen zwar wenige Spalten enthalten sind, die allerdings eine Vielzahl von interessanten Effekten zeigen. Es handelt sich in beiden Fällen um eine Tabelle für Produkte,
wobei Product2 allerdings eine berechnete Spalte und verschiedene Standardwerte
besitzt. Eine weitere Spalte mit einem automatischen Wert stellt der Primärschlüssel
dar, welcher die so genannte IDENTITY-Eigenschaft besitzt und daher einen automatischen Zählwert besitzt.
Zusätzlich gibt es für diese Tabelle auch noch einen Trigger. Auch wenn Trigger nicht
in aller Ausführlichkeit in diesem Buch dargestellt werden sollen, da es sich doch um
ein sehr administratives Thema handelt, soll der Einsatz eines Triggers zeigen, wie
neben einer berechneten Spalte auch über einen Trigger automatische Wertaktualisierungen durchgeführt werden können.
Ein Trigger ist darüber hinaus auch in diesem Fall notwendig, weil der Wert der Profit-Spalte sich aus der Differenz der beiden Spalten ListPrice und StandardCost
ermittelt. Es ist nicht möglich, eine berechnete Spalte zu erzeugen, deren Wert sich auf
die beschriebene Art aus zwei anderen Spalten zusammensetzt. Daher ist ein Trigger
notwendig. Der Trigger ist nicht sonderlich lang, sodass er schnell erklärt ist: Ein Trigger ist ein in der Datenbank gespeichertes Modul, welches darauf lauert, dass eine Anwendung/der Benutzer eine bestimmte Operation (AFTER INSERT, UPDATE) an einer
Tabellen (ON Production.Product2) durchführt. Dies löst den Trigger aus, was
wiederum bedeutet, dass sein Inhalt/seine Anweisungen ausgeführt werden. In diesem
Fall setzt man einen UPDATE-Befehl für die Aktualisierung ab. Diese Aktualisierung
trägt in die Spalte Profit genau die Differenz aus ListPrice und StandardCost
ein. Um die geänderten Produkte zu ermitteln, nutzt man die Tabelle inserted, in der
265
Datenmanipulation
alle eingetragenen bzw. geänderten Datensätze enthalten sind. Mit Hilfe einer Unterabfrage kann man die ProductID aus dieser Tabelle abrufen und im IN-Operator mit den
ProductID-Werten der Product2-Tabelle vergleichen, um die richtigen Werte zu
aktualisieren. Der qualifizierte Spaltenname in der Unterabfrage ist für die Funktionsweise wesentlich und führt bei Vergessen dazu, dass gar keine Spalte aktualisiert wird..
-- Erstellen einer Tabelle
IF OBJECT_ID ('Production.Product2', 'T') IS NOT NULL
DROP TABLE Production.Product2
GO
CREATE TABLE Production.Product2 (
ProductID
int IDENTITY(1,1) NOT NULL PRIMARY KEY,
Name
varchar(30)
NOT NULL,
ProductNumber nvarchar(25) NOT NULL,
ListPrice
money
NOT NULL DEFAULT 0,
StandardCost
money
NOT NULL DEFAULT 0,
Profit
money
NULL,
ModifiedDate
datetime
NOT NULL
CONSTRAINT DF_Product_ModifiedDate2 DEFAULT (getdate()))
GO
-- Erstellen eines Triggers
IF OBJECT_ID ('Production.ProfitCalc','TR') IS NOT NULL
DROP TRIGGER Production.ProfitCalc
GO
266
Datenmanipulation
CREATE TRIGGER Production.ProfitCalc ON Production.Product2
AFTER INSERT, UPDATE
AS
UPDATE Production.Product2
SET Profit = ListPrice - StandardCost
WHERE ProductID IN (SELECT i.ProductID FROM inserted AS i)
GO
421_01.sql: Erstellen einer Tabelle und eines Triggers
Dann erstellt man noch eine sehr viel einfachere Tabelle, in der weder eine Spalte mit
IDENTITY-Eigenschaft, noch eine berechnete Spalte und nicht einmal Standardwerte
vorhanden sind. Es gibt darüber hinaus auch keinen Trigger, der auf Operationen an
dieser Tabelle prüft und Korrekturen oder Kontrollen vornimmt.
-- Erstellen einer Tabelle
IF OBJECT_ID ('Production.Product3', 'T') IS NOT NULL
DROP TABLE Production.Product3
GO
CREATE TABLE Production.Product3 (
Name
varchar(30)
NOT NULL,
ProductNumber nvarchar(25) NOT NULL,
ListPrice
money
NOT NULL,
StandardCost
money
NULL)
GO
421_02.sql: Erstellen einer sehr einfache Tabelle
267
Datenmanipulation
4.2.2
Einfügen
Der Befehl für das Einfügen von Datensätzen ist – wie schon an anderer Stelle erwähnt
– sehr komplex und besteht aus verschiedenen Bestandteilen, die auch in ähnlicher oder
sogar gleicher Form in den anderen DML-Befehlen auftauchen. Nicht alle Möglichkeiten sind häufig im Einsatz, sodass zunächst der Standardfall beschrieben wird, gefolgt
von weiteren Optionen.
ƒ
Der Standardfall von INSERT ruft über die INTO-Klausel einen Tabellennamen auf.
Dieser kann von einer Spaltenliste in runden Klammern gefolgt werden, die
allerdings nur dann angegeben sein muss, wenn nicht in alle Spalten der Tabelle
nicht in der richtigen Reihenfolge geschrieben wird. Ansonsten nennt diese
Spaltenliste die verschiedenen Spalten, in die Werte geschrieben werden sollen, in
der Reihenfolge, wie die Werte in der darauf folgenden VALUES-Klausel
erscheinen. Die Zuordnung zwischen Spalten und Werten erfolgt also ganz einfach
und pragmatisch über die Position.
ƒ
Wenn in der VALUES-Klausel die Werte angegeben werden, dann kann man Zahlen
ohne Hochkommata, Zeichenketten und Datumswerte mit Hochkommata, weitere
Ausdrücke wie Funktionen und Berechnungen, Variablen oder auch den Wert NULL
bzw. DEFAULT für den Standardwert der Spalte benutzen. Dies alles kann bunt
gemischt werden.
ƒ
Die Werte können allerdings anstelle einer direkten Wertvorgabe über VALUES,
was sicherlich der häufigste Fall ist, aus einer Abfrage (abgeleitete Tabelle, d.h.
eine einfache SELECT-Anweisung) oder einem Prozeduraufruf stammen, wobei
diese Prozedur dann genauso eine Ergebnismenge zurückliefern muss wie eine
gewöhnliche Abfrage. Dies ist eine sehr schöne Lösung, wenn Daten aus der einen
Tabelle in eine andere übetragen werden sollen. Typische Szenarien sind in diesem
Bereich Datenbereinigung, Normalisierung, Datenstrukturveränderung.
ƒ
Sofern die Anweisung im Rahmen eines T-SQL-Programms genutzt wird und nicht
nur eine einfache Anweisung darstellt, dann können über die OUTPUT-Klausel
Werte, die man gerade in die Tabelle eingetragen hat, zurückgegeben werden. Dies
268
Datenmanipulation
ist insbesondere für berechnete Spalten, IDENTITY-Spalten oder Werte aus
Triggern interessant.
ƒ
Wenn man anstelle von festen Wertvorgaben eine Abfrage oder Prozedur einsetzt,
welche die einzufügenden Werte ermittelt, dann kann man die Werte, die diese
zurückgibt, über eine TOP n-Klausel mit statischen oder dynamischen Werten für n
begrenzen.
Die allgemeine Syntax hat die Form:
[ WITH <common_table_expression> [ ,...n ] ]
INSERT
[ TOP ( expression ) [ PERCENT ] ]
[ INTO]
{ <object> | rowset_function_limited
[ WITH ( <Table_Hint_Limited> [ ...n ] ) ]
}
{
[ ( column_list ) ]
[ <OUTPUT Clause> ]
{ VALUES ( { DEFAULT | NULL | expression } [ ,...n ] )
| derived_table
| execute_statement
}
}
| DEFAULT VALUES
269
Datenmanipulation
[; ]
4.2.2.1
Standardfälle
Die möglichen Formen der INSERT-Anweisung kann man am besten anhand von Beispielen lernen. Im ersten Beispiel folgen zwei typische Standardfälle. In der ersten Anweisung wird zunächst die Tabelle Product3 komplett gelöscht. Dies ist dann notwendig, wenn man das Beispiel mehrfach durchführen möchte und nicht alle Zeilen mehrfach in der Tabelle speichern möchte bzw. wenn der Primärschlüsselwert bereits in der
Tabelle vorhanden ist.
Der erste Standardfall fügt in die Tabelle Product3, die keine berechneten Spalten hat
und die daher sich für diesen Fall besonders eignet, in jeder Zeile einen Wert ein. Da die
Reihenfolge der Werte mit der Reihenfolge der Spalten(datentypen) und insgesamt mit
der Anzahl der benötigten Werte/vorhandenen Spalten übereinstimmt, kann hier die
Auflistung der Spaltennamen entfallen.
Der zweite Standardfall fügt in die Tabelle Product2 nur in eine Auswahl von Spalten
Werte ein. Da hier sowohl die Anzahl der Werte nicht mit der Anzahl der Spalten übereinstimmt und zusätzlich auch die Reihenfolge anders sein kann, ist die der Tabelle
folgende Spaltenliste in Klammern notwendig. Sie ordnet die Werte in der VALUESKlausel den richtigen Spalten zu.
-- Standardfall "alle Spalten"
DELETE FROM Production.Product3
GO
INSERT INTO Production.Product3
VALUES ('LL Mountain Seat Assembly', 'SA-M198', 133.34,
98.77)
GO
INSERT INTO Production.Product3
270
Datenmanipulation
VALUES ('ML Mountain Seat Assembly', 'SA-M237', 147.14,
108.99)
GO
-- Standardfall "Spaltenauswahl"
INSERT INTO Production.Product3
(Name, ProductNumber, ListPrice, StandardCost)
VALUES ('HL Mountain Seat Assembly', 'SA-M687', 196.92,
145.87)
GO
INSERT INTO Production.Product2
(Name, ProductNumber, ListPrice, StandardCost)
VALUES ('LL Road Seat Assembly', 'SA-R127', 133.34, 98.77)
GO
-- Überprüfung
SELECT ProductID, ProductNumber, Profit, ModifiedDate
FROM Production.Product2
422_01.sql: Standardfälle des Einfügens
Die Überprüfung dieser kleinen Tests lässt sich insbesondere einfach durch die vorher
genannte Abfrage durchführen. Dabei sind insbesondere die Spalten interessant, die
berechnet oder deren Wert über einen Trigger automatisch eingefügt werden.
ProductID
ProductNumber
Profit
ModifiedDate
----------- ---------------- ---------- ------------------1
SA-R127
34,57
2005-09-05 22:04:48
271
Datenmanipulation
4.2.2.2
Erfassung aus Abfrage
Eine besonders schöne Variante, um Daten von einer Tabelle zu anderen zu transportieren, stellt die INSERT-Anweisung bereit, wenn sie mit einer Abfrage kombiniert wird.
Nützlich ist dieses Vorgehen, wenn bspw. aus mehreren Tabellen eine Tabelle erzeugt
werden soll, oder wenn gerade andersherum aus einer Tabelle mehrere andere Tabelle
im Rahmen einer Normalisierung über SQL erreicht werden soll. Interessant ist dies
natürlich auch gerade für temporäre Tabellen, die als Wertespeicher ähnlich eines Arrays genutzt werden, und welche Daten aus einer oder mehreren Tabellen enthalten
sollen. Das nächste sehr kurze Beispiel zeigt genau dieses Vorgehen. In die Tabelle
Product2 fügt diese Anweisung Daten aus der Tabelle Product3 ein, wobei eine
solche Abfrage genutzt wird.
INSERT INTO Production.Product2
(Name, ProductNumber, ListPrice, StandardCost)
SELECT * FROM Production.Product3
422_02.sql: Einfügen über eine Abfrage
4.2.2.3
Standard- und NULL-Werte
Als weitere Möglichkeiten, Werte in eine Spalte einzutragen, kann man noch NULL für
einen nicht vorhandenen Wert, DEFAULT für den Standardwert und jeden beliebigen
Ausdruck verwenden, der zu einem geeigneten Wert führt, verwenden. Insbesondere der
beliebige Ausdruck erscheint oftmals in Form einer Rechnung oder eines Funktionsaufrufs, wobei dann die Funktion als Rückgabewert einen geeigneten Ausdruck zurückliefert.
Im nachfolgenden Beispiel ist auch noch angegeben, wie in jede Spalte Standardwerte
eingefügt werden. Dies ist allerdings sehr selten, da nicht viele Tabellen einfach nur aus
Spalten mit Standardwerten bestehen.
INSERT INTO Production.Product2
272
Datenmanipulation
(Name, ProductNumber, ListPrice, StandardCost, Profit)
VALUES ('LL Road ' + 'Seat Assembly', 'SA-R127', DEFAULT,
DEFAULT, NULL)
GO
-- INSERT INTO tabelle DEFAULT VALUES
422_03.sql: Sonderfälle beim Einfügen
4.2.2.4
Rückgabewerte
Interessant ist auch die Möglichkeit, eingetragene Daten unmittelbar nach dem Eintrag
wieder zurückzuholen, ohne dabei ausdrücklich eine neue, die DB belastende Abfrage
zu formulieren. Darüber hinaus gelingt die Rückgabe sehr einfach und kurz. Die Syntax
der OUTPUT-Klausel wird hier noch einmal wiederholt, weil es viele Optionen gibt, sie
einzusetzen, welche allerdings letztendlich alle sehr ähnlich sind und daher nicht unbedingt ein eigenes Beispiel erfordern.
Zunächst ruft man die OUTPUT-Klausel nach der möglichen Spaltenliste, welche die
Zuordnung der Werte vornimmt, auf. Es folgt eine Liste mit Spaltennamen, wobei diese
im Normalfall zur Identifizierung/Qualifizierung der Spalten bei Einfüge/Aktualisierungsvorgängen mit INSERTED und bei Löschvorgängen mit DELETED qualifiziert werden. Bei INSERT ist die Verwendung der mit einem Aliasnamen umbenannten FROM-Tabelle nicht möglich, da es keine FROM-Klausel und damit auch keine FROMTabelle gibt. Dies ist nur bei DELETE der Fall. Es sind auch Spaltenaliase möglich,
welche bei Einsatz von Funktionsaufrufen/Berechnungen zum Einsatz kommen können.
Nach der bereits in der gewöhnlichen INSERT-Klausel bekannten INTO-Klausel, die
allerdings hier nicht optional ist, folgt das Zuweisungsziel. Bei diesem handelt es sich
entweder um eine Tabellenvariable oder eine tatsächliche Ausgabetabelle. Die Ausgabetabelle ist eine in der Datenbank tatsächlich vorhandene oder lediglich eine temporäre
Tabelle. Da diese oftmals gar nicht vorhanden ist und teilweise sogar die Definition
einer temporären Tabelle zu umständlich ist, kann man eine Tabellenvariable verwen-
273
Datenmanipulation
den. Sie nutzt eine bislang noch nicht aufgetretene Technik, ein Array-ähnliches Konstrukt zu erzeugen, das mehrere Spalten und Reihen enthalten kann.
Die allgemeine Syntax hat zur Wiederholung folgenden Aufbau:
<OUTPUT_CLAUSE> ::=
{
[ OUTPUT <dml_select_list> INTO { @table_variable
| output_table }
[ ( column_list ) ] ]
[ OUTPUT <dml_select_list> ]
}
<dml_select_list> ::=
{ <column_name> | scalar_expression }
[ [AS] column_alias_identifier ]
[ ,...n ]
<column_name> ::=
{ DELETED | INSERTED | from_table_name } . { * | column_name
}
Im ersten Beispiel befindet sich die Grundform der zuvor beschriebenen allgemeinen
Technik. Als Ziel der nach einem Einfügevorgang gespeicherten Zeile dient die erwähnte Tabellenvariable, welche mit dem Schlüsselwort table und der üblichen Auflistung
von Spalten und ihren Datentypen erzeugt wird. Den Eintrag der einen Zeile, welcher zu
verschiedenen automatisch erzeugten Werten führt, lenkt man mit OUTPUT in diese
Variable. Dabei ruft man die eingefügten Werte über die INSERTED-Tabelle ab und
weist sie mit INTO tabellenvariablenname den Spalten der Tabellenvariable zu.
274
Datenmanipulation
Wie man sich vorstellen kann, ist dabei jede beliebige Reihenfolge möglich. Schließlich
kontrolliert man sowohl die Tabellenvariable als auch die tatsächliche Tabelle.
-- Variablen erstellen
DECLARE @outputTable table(ProductID
Profit
int,
money,
ModifiedDate datetime)
-- Daten löschen
DELETE FROM Production.Product2
-- Eintragen und abrufen
INSERT INTO Production.Product2
(Name, ProductNumber, ListPrice, StandardCost)
OUTPUT INSERTED.ProductID,
INSERTED.Profit,
INSERTED.ModifiedDate AS Date
INTO @outputTable (ProductID,
Profit,
ModifiedDate)
VALUES ('LL Road Seat Assembly', 'SA-R127', 133.34, 98.77)
-- Überprüfung
SELECT * FROM @outputTable
SELECT ProductID, Profit FROM Production.Product2
422_04.sql: Ausgabewerte
Man erhält als Ergebnis die über die IDENTITY-Eigenschaft und die automatische Spaltenberechnung erzeugten Werte zurück. Der Trigger-Wert wird allerdings nicht ermit-
275
Datenmanipulation
telt, was sehr ärgerlich ist. Nichtsdestoweniger ist er dennoch in der Tabelle eingetragen, wie die unmittelbar im Anschluss ausgeführte Kontrolle zeigt.
ProductID
Profit
ModifiedDate
----------- -------------- -------------10
NULL
2005-09-08
(1 Zeile(n) betroffen)
ProductID
Profit
----------- --------------------10
34,57
(1 Zeile(n) betroffen)
Die durch OUTPUT ermittelten Spalten zeigen also genau die Werte, die vor Ausführung
des Triggers vorhanden sind. Hat man einen so genannten INSTEAD OF-Trigger erstellt,
der anstelle einer anderen Anweisung ausgeführt wird, erhält man die Werte zurück, die
bei einer tatsächlichen Ausführung der ja eigentlich umgangenen Ausführung gespeichert worden wären. Dies gilt sogar dann, wenn gar keine Änderungen vorgenommen
werden.
Der Abruf von eingefügten Daten beschränkt sich durchaus nicht auf eine einzelne
Zeile, sodass der Aufwand, eine Tabellenvariable zu erstellen, die möglicherweise nur
eine einzige Spalte hat, geringer erscheinen lassen sollte. Das nächste Beispiel ist etwas
gekürzt, da es im Wesentlichen die gleichen Inhalte besitzt wie das vorherige. Der einzige Unterschied besteht darin, dass wieder eine SELECT...INTO-Abfrage zum Zug
kommt, die drei Datensätze in die Product2-Tabelle einträgt. Auch hier bleiben die
Trigger-Werte leider außen vor.
INSERT INTO Production.Product2
(Name, ProductNumber, ListPrice, StandardCost)
276
Datenmanipulation
OUTPUT INSERTED.ProductID,
INSERTED.Profit,
INSERTED.ModifiedDate AS Date
INTO @outputTable (ProductID, Profit, ModifiedDate)
SELECT * FROM Production.Product3
422_05.sql: Abruf mehrerer Zeilen
Wie ein allgemeiner Tabellenausdruck im Rahmen von INSERT zu verwenden ist, zeigt
der Abschnitt, in dem die CTE (common table expressions) präsentiert werden. Daher
entfällt hier eine Erklärung zur in eckigen Klammern angegebenen WITH-Klausel vor
dem INSERT-Befehl.
4.2.2.5
Zufällige Werte aus Abfragen
Schließlich besteht noch die Möglichkeit, im Rahmen einer Abfrage, deren Ergebnisse
in eine andere Tabelle eingefügt werden sollen, die ausgewählten und damit eingefügten
Datensätze zu begrenzen. Dabei setzt man die schon bekannte TOP n-Klausel ein. Hierbei bestehen folgende Möglichkeiten: Entweder gibt man einen numerischen Wert an,
der genutzt wird, oder man beschränkt sich auf eine Prozentangabe, indem noch das
Schlüsselwort PERCENT angehängt wird. In beiden Fällen kann man entweder feste und
damit genaue Werte vorgeben oder einen dynamischen Ausdruck verwenden. Im Normalfall dürfte diese einen Variablenaufruf enthalten. Im nächsten Beispiel ist dies allerdings eine Unterabfrage, welche die Anzahl der Produkte in der originalen ProductTabelle ermittelt und diese Zahl halbiert.
Das nächste Beispiel führt zwar nur die gerade beschriebene dynamische Verwendung
ausdrücklich aus, doch zeigt es auch, wie man einen festen absoluten und prozentualen
Wert vorgibt.
-- Einfügen über Abfrage
DELETE FROM Production.Product3
277
Datenmanipulation
GO
-- INSERT TOP (5) INTO Production.Product3
-- INSERT TOP (10) PERCENT INTO Production.Product3
INSERT TOP (
(SELECT COUNT(*)
FROM Production.Product)
/ 2) INTO Production.Product3
(Name, ProductNumber, ListPrice, StandardCost)
SELECT Name, ProductNumber, ListPrice, StandardCost
FROM Production.Product
GO
-- Überprüfung
SELECT COUNT(*) AS Products
FROM Production.Product3
422_06.sql: Zufälliges Einfügen
Man erhält als Information, dass 252 Produkte eingefügt worden sind, wenn man die
Hälfte ermittelt, wie gerade angegeben. Ansonsten werden genau fünf oder fünf Prozent
der Daten aus der originalen Product-Tabelle eingefügt.
4.2.3
Aktualisieren
Sind die Daten erst einmal in der Datenbank gespeichert, dauert es nicht lange, ehe man
sie wieder ändern muss. Dies gelingt über den UPDATE-Befehl. Er besitzt ebenfalls eine
sehr umfangreiche allgemeine Syntax mit folgenden wesentlichen Merkmalen:
278
Datenmanipulation
ƒ
Der Standardfall ruft die UPDATE-Anweisung mit einem Tabellen- oder Sichtnamen
auf, setzt neue Werte über die SET-Klausel, wobei auch mehrere Spalten
aktualisiert werden können. Zusätzlich können die Zeilen, die aktualisiert werden
sollen, über die WHERE-Klausel begrenzt werden.
ƒ
Wenn die Werte, die in eine angegebene Spalte neu gespeichert werden sollen, um
bestehende Werte zu überschreiben, nicht fest als Zeichenketten oder Zahlen
vorgegeben sind, können folgende andere Optionen gewählt werden: das
Schlüsselwort DEFAULT für den Standardwert, das Schlüsselwort NULL für den
NULL-Wert, ein beliebiger Ausdruck wie ein Funktionsaufruf, eine Berechnung
oder eine Variable und schließlich natürlich auch eine Eigenschaft, ein
Methodenrückgabewert eines benutzerdefinierten Typs aus .NET.
ƒ
Um geänderte Werte wieder für Kontrolle oder Ausgabe zurückzugeben, kann man
die OUTPUT-Klausel verwenden. Sie erlaubt die Speicherung einzelner oder aller
Spalten einer bearbeiteten Tabelle in eine Tabellenvariable.
ƒ
Sofern die Aktualisierung und damt die Anwendung von UPDATE in einem Cursor
vorgenommen wird, um den aktuell abgerufenen Datensatz des Cursors zu
bearbeiten, kann man anstelle des Primärschlüssels in der WHERE-Klausel auch
WHERE CURRENT OF mit Namen des Cursors bzw. der Cursovariable verwenden.
Dies vereinfacht die Adressierung des gerade abgerufenen Datensatzes. Ansonsten
ist die WHERE-Klausel wie bei einer SELECT-Anweisung als Filter zu verwenden.
ƒ
Soll nur eine bestimmte Menge an Datensätzen, die absolut oder prozentutal
vorgegeben sein kann, bearbeitet werden, kann man auch noch die TOP n-Klausel
unmittelbar nach UPDATE verwenden. Sie begrenzt die bearbeiteten Zeilen auf die
statisch oder dynamisch angegebene Menge.
ƒ
Die zusätzliche FROM-Klausel, welche der OUTPUT-Klausel folgt, kann genauer
angegeben, in welcher Tabelle die Daten aktualisiert werden sollen. Dies ist
insbesondere interessant, wenn die Daten bspw. aus einer Verknüpfung gelöscht
werden sollen und nicht nur auf Basis einer einzigen Tabelle.
Die vollständige allgemeine Syntax lautet:
279
Datenmanipulation
[ WITH <common_table_expression> [...n] ]
UPDATE
[ TOP ( expression ) [ PERCENT ] ]
{ <object> | rowset_function_limited
[ WITH ( <Table_Hint_Limited> [ ...n ] ) ]
}
SET
{ column_name = { expression | DEFAULT | NULL }
| { udt_column_name.{ { property_name = expression
| field_name = expression }
| method_name ( argument [
,...n ] )
}
}
| column_name { .WRITE ( expression , @Offset ,
@Length ) }
| @variable = expression
| @variable = column = expression [ ,...n ]
} [ ,...n ]
[ <OUTPUT Clause> ]
[ FROM{ <table_source> } [ ,...n ] ]
[ WHERE { <search_condition>
| { [ CURRENT OF
280
Datenmanipulation
{ { [ GLOBAL ] cursor_name }
| cursor_variable_name
}
]
}
}
]
[ OPTION ( <query_hint> [ ,...n ] ) ]
[ ; ]
4.2.3.1
Standardfall
Das nächste Beispiel setzt die von Skript 422_06.sql gefüllte Product3-Tabelle voraus.
Sie enthält 252 Produkte, d.h. die Hälfte der in Product gespeicherten Produkte. Um
die Daten von Prodcut3 nicht dauerhaft zu verändern, sondern um das Beispiel auch
variieren zu können, wird eine Transaktion um die Bearbeitungen gesetzt. Über die
Anweisung BEGIN TRANSACTION kündigt man an, dass die nachfolgenden Anweisungen einen Verbund bilden und nur als Ganzes ausgeführt werden. Die Anweisung
ROLLBACK setzt daher die gesamten Anweisungen wieder zurück.
Das Beispiel enthält drei Standardfälle: eine einfache Aktualisierung, bei der ein Filter
zum Einsatz kommt und ein statischer Wert gespeichert wird; eine dynamische Aktualisierung, bei der neben dem statischen Filter nun eine dynamische Aktualisierung stattfindet und schließlich eine mehrfache Aktualisierung, bei der mehrere Spalten betroffen
sind. Insbesondere der zweite Standardfall ist interessant, denn er zeigt, wie die Adressierung einer Spalte in der Zuweisung durch SET die aktuelle Zeile nutzt. Eine andere
dynamische Aktualisierung lässt sich denken, wenn man eine Funktion oder eine Berechnung verwendet.
-- Standardfälle der Aktualisierung
281
Datenmanipulation
BEGIN TRANSACTION
-- Einfache Aktualisierung mit Filter
UPDATE Production.Product3
SET ListPrice = 1
WHERE ListPrice = 0
-- Dynamische Aktualisierung mit Filter
UPDATE Production.Product3
SET ListPrice = ListPrice * 1.1
WHERE ListPrice != 1
-- Mehrfache Aktualisierung
UPDATE Production.Product3
SET ListPrice = ListPrice * 1.1,
StandardCost = StandardCost * 1.1
GO
ROLLBACK
423_01.sql: Standardfälle der Aktualisierung
Als Ergebnis liefern die drei Aktualisierungen: (200 Zeile(n) betroffen), (52
Zeile(n) betroffen), (252 Zeile(n) betroffen). Insgesamt sind in der
Product3-Tabelle also 200 Produkte mit Preis 0, deren Preis auf einen Euro erhöht
werden. Da dann insgesamt 52 Produkte einen Preis ungleich 1 haben, werden diese in
der nächsten Anweisung bearbeitet. Schließlich gibt es noch eine Preis- und Kostensteigerung von 10%, die alle 252 Produkte betrifft.
282
Datenmanipulation
4.2.3.2
Ausgabe von Änderungen
Um die interessanten Effekte und den Nutzen zu sehen, wenn man die aktualisierten
Werte über die OUTPUT-Klausel wieder abruft, müssen Sie zunächst das Skript
422_07.sql ausführen. Es übertragt die Hälfte aller Werte aus der Product-Tabelle in
die Product2-Tabelle und funktioniert damit ähnlich wie 422_06.sql. So haben Sie
genügend Daten, mit denen Sie spielen können und die Sie zerstören können.
Um die Werte abzurufen, benötigt man wieder eine tatsächlich in der Datenbank permanent oder temporäre vorhandene Tabelle oder eine Tabellenvariable. Da diese
schneller zu erstellen ist und zudem in solchen Fällen üblicher, erstellt man zunächst
eine solche Tabelle, welche insbesondere die durch Wertvorgaben oder Automatismen
(berechnete Spalten, Trigger) geänderten Spalten enthalten soll. Für die Aktualisierungsanweisung enthält dieses Beispiel einen komplexen Ausdruck in Form von CASE
auf der Wertzuweisungsseite von SET. Dadurch ist es möglich, die Preise mit dem Wert
0 auf einen Euro und die Preise ungleich 0 um 10% zu erhöhen. Unabhängig von diesem Schmankerl setzt die Anweisung auch die OUTPUT-Klausel ein, welche die geänderten Werte zusätzlich noch in die Tabellenvariable speichert, die später zu Kontrollzwecken ausgegeben wird.
-- Variablen erstellen
DECLARE @outputTable table(ProductID
int,
ListPrice
money,
Profit
money,
ModifiedDate datetime)
-- Daten ändern und abrufen
BEGIN TRANSACTION
UPDATE Production.Product2
SET ListPrice = (CASE ListPrice
283
Datenmanipulation
WHEN 0 THEN 1
ELSE ListPrice * 1.1
END)
OUTPUT INSERTED.ProductID,
INSERTED.Profit,
INSERTED.ListPrice,
INSERTED.ModifiedDate AS Date
INTO @outputTable (ProductID,
Profit,
ListPrice,
ModifiedDate)
-- Überprüfung
SELECT TOP (3) * FROM @outputTable
SELECT TOP (3) * FROM Production.Product2
ROLLBACK
423_02.sql: Einsatz von OUTPUT
Bei der Überprüfung ergibt sich das gleiche Bild, das man zuvor schon bei der INSERTAnweisung und der Verwendung von OUTPUT gesehen hat. In der Tabellenvariable
befinden sich die durch Wertvorgabe oder Ausdrücke vorgegebenen sowie die durch
berechnete Spalten ermittelten neuen oder alten Werte, aber leider nicht die Werte, die
durch Trigger erzeugt wurden. Diese sind jedoch sehr wohl in der Tabelle enthalten, wie
die Abfrage der bearbeiteten Tabelle zeigt.
(252 Zeile(n) betroffen)
(252 Zeile(n) betroffen)
ProductID
284
ListPrice
Profit
ModifiedDate
Datenmanipulation
----------- ----------- --------- -------------14
1,00
0,00
2005-09-10
15
1,00
0,00
2005-09-10
16
1,00
0,00
2005-09-10
(3 Zeile(n) betroffen)
Name
ListPrice
StandardCost
Profit
ModifiedDate
---------------- ---------- ------------- ------- ----------Adjustable Race
1,00
0,00
1,00
2005-09-10
Bearing Ball
1,00
0,00
1,00
2005-09-10
BB Ball Bearing
1,00
0,00
1,00
2005-09-10
(3 Zeile(n) betroffen)
Ändert man nur eine Zeile wie bspw. im Rahmen einer Cursor-Verarbeitung, dann steht
auch nur eine weitere Möglichkeit bereit, gerade geänderte Werte direkt in Variablen
abzurufen. Die Einschränkung, dass dies nur bei einer einzigen Zeile erlaubt ist, weil
eine Variable nur einen Wert und nicht eine Liste/Spalte von Werten empfangen kann,
grenzt den Nutzen dieser Technik etwas ein, bedeutet allerdings eine deutlich kürzere
Syntax als bei einem Einsatz der OUTPUT-Klausel.
Im folgenden Beispiel führt man die gleiche Aktualisierung wie in einem vorherigen
Beispiel durch, d.h. Preis und Standardkosten erfahren eine zehnprozentige Preiserhöhung, welche vor und nach der Durchführung in Form eine SELECT-Anweisung überprüft wird. Um nur eine einzige Zeile zu ändern, filtert die WHERE-Klausel anhand der
Produktnummer genau einen Datensatz heraus. Den geänderten Wert speichert man
dann in die vorangestellte Variable, die man für Kontrollen etc. weiterhin benutzen
kann.
285
Datenmanipulation
-- Deklaration
DECLARE @listPrice money, @standardCost money
-- Kontrolle vorher
SELECT ListPrice, StandardCost
FROM Production.Product3
WHERE ProductNumber = 'FR-R38B-44'
-- Daten ändern und abrufen
BEGIN TRANSACTION
UPDATE Production.Product3
SET @listPrice = ListPrice = ListPrice * 1.1,
@standardCost = StandardCost = StandardCost * 1.1
WHERE ProductNumber = 'FR-R38B-44'
-- Kontrolle
SELECT @listPrice, @standardCost
ROLLBACK
423_03.sql: Abruf einzelner Spalten in Variablen
Als Ergebnis erhält man zunächst die ursprünglichen und dann die neuen Werte.
ListPrice
StandardCost
--------------------- --------------------337,22
(1 Zeile(n) betroffen)
204,6251
(1 Zeile(n) betroffen)
286
Datenmanipulation
--------------------- --------------------370,942
225,0876
(1 Zeile(n) betroffen)
4.2.3.3
Aktualisierung auf Basis anderer Tabellendaten
In vielen Fällen ist es gar nicht damit getan, einfach nur einen statischen oder einen
dynamischen Wert vorzugeben, der aus einer Berechnung mit der zu verändernden
Spalte oder einer sonstigen Datensituation herrührt. Stattdessen sollen Tabellendaten
angeglichen werden. Insbesondere bei Datenbereinigungsoperationen im Rahmen von
Import-/Export-Schnittstellen ist dies ein wesentliches Unterfangen. Damit ist gemeint,
dass der Wert in einer Spalte von Tabelle A mit dem Wert einer Spalte in Tabelle B
aktualisiert werden soll. Dazu ist zunächst die FROM-Klausel in der UPDATE-Anweisung
nötig, welche zusätzlich um eine korrelierte Unterabfrage in der SET-Klausel ergänzt
wird.
Das nachfolgende Beispiel führt folgende Regel aus: Setze für jedes Produkt die Werte
von ListPrice aus Product in ListPrice von Product2. In der FROM-Klausel ruft
man noch einmal die zu bearbeitende Tabelle auf und gibt ihr einen Aliasnamen, der für
die korrelierte Unterabfrage notwendig ist. Diese Unterabfrage befindet sich in der
Zuweisung der SET-Klausel. Hier ruft man aus der zu vergleichenden Tabelle Product
ebenfalls die ListPrice-Spalte ab, wobei der Filter den Datensatz heraussucht, dessen
Produktnummer der inneren Tabelle mit der Produktnummer aus der äußeren Tabelle
übereinstimmt.
UPDATE Production.Product2
SET ListPrice = (SELECT ListPrice
FROM Production.Product AS p
WHERE p2.ProductNumber =
p.ProductNumber)
FROM Production.Product2 AS p2
287
Datenmanipulation
423_04.sql: Aktualisierung gemäß anderer Tabelle
4.2.3.4
Zufällige Änderungen
Auch bei der Aktualisierung kann man die Änderungen mit Hilfe einer TOP n-Klausel
auf eine zufällige Auswahl an Datensätzen begrenzen. Dabei gibt es wiederum die Möglichkeiten, einen festen oder dynamischen Wert anzugeben, wobei dies zusätzlich absolut oder unter Angabe von PERCENT prozentual verstanden wird. Als dynamische Wertvorgaben kommt jeder Ausdruck in Betracht, sei es eine Variable, eine skalare Unterabfrage oder eine Berechnung. Wichtig ist lediglich, dass sie zu einem numerischen Wert
führen.
Das nächste Beispiel zeigt die verschiedenen Varianten für die TOP n-Klausel in UPDATE. Die erste wählt genau fünf, die zweite fünf Prozent und die dritte die Hälfte aus,
wobei diese Zahl in einer Variable gespeichert ist.
-- Variable erstellen
DECLARE @rowCount int
SET @rowCount = 5
-- Daten ändern
BEGIN TRANSACTION
-- UPDATE TOP (5) Production.Product2
-- UPDATE TOP (5) PERCENT Production.Product2
UPDATE TOP (@rowCount) Production.Product2
SET ListPrice = 10000
ROLLBACK
423_05.sql: Zufällige Änderung
288
Datenmanipulation
4.2.3.5
Bearbeiten großer Datentypen
Spalten, deren Werte durch die Datentypen varchar(max)-, nvarchar(max)- und
varbinary(max) beschrieben werden, haben eine eigene Methode, mit denen Inhalte
in die Spalte geschrieben oder bestehende Inhalte ersetzt werden können. Es handelt
sich um eine Methode und keine (Spalten-)Funktion, was auch daran zu erkennen ist,
dass sie mit dem Punkt-Operator an den Spaltennamen angeschlossen wird und dieser
Spaltennamen nicht als Parameter im Funktionsaufruf übergeben wird. Die allgemeine
Syntax dieser Funktion lautet:
WRITE (expression, @Offset, @Length)
In expression steht im Normalfall eine Zeichenkette, ansonsten kann es ein beliebiger
Ausdruck sein, der schließlich in der bestehenden Zeichenkette gespeichert werden oder
sie ersetzen soll. In @Offset befindet sich die Position, an der die neue Zeichenkette
platziert werden soll, während in @Length die Länge des zu ersetzenden Bereichs enthalten ist. Sofern der neue Text nur eingefügt und ansonsten der bestehende Text nicht
ersetzt werden soll, ist hier der Wert 0 richtig. Ein NULL-Wert kann und braucht auch
als neuer Inhalt nicht vorgegeben werden.
Das nachfolgende Beispiel bearbeitet in den Produktkommentaren einen Kommentar so,
dass die verfügbaren Größen des Fahrrades, das hier diskutiert wird, in Zukunft in Parenthese ebenfalls angegeben ist. Dazu setzt man Comments.WRITE (N'(Sizes:
38-44, 48) ',15, 0) ein, da der neue Text an Stelle 15 eingefügt werden und nicht
ersetzen soll. Das vorgeschaltete N kennzeichnet die Zeichenkette noch einmal deutlich
als solche, wobei dies nicht unbedingt erforderlich ist. Da man die ursprüngliche ProductReview-Tabelle unangetastet lassen möchte und zudem hier auch in der Comments-Spalte der falsche Datentyp enthalten ist, müssen die Daten zunächst in eine
Spiel-Tabelle übertragen werden, deren Comments-Spalte den nvarchar(max)Datentyp aufweist.
-- Tabelle mit nvarchchar(max) anlegen
CREATE TABLE Production.ProductReview2 (
289
Datenmanipulation
ProductReviewID int NOT NULL PRIMARY KEY,
Comments nvarchar(max) NULL
)
-- Daten übernehmen
INSERT INTO Production.ProductReview2
SELECT ProductReviewID, Comments
FROM Production.ProductReview
GO
-- Tabellenvariable anlegen
DECLARE @review table (
ProductReviewID int NOT NULL,
CommentsBefore nvarchar(max),
CommentsAfter
nvarchar(max))
-- Daten aktualisieren
BEGIN TRANSACTION
UPDATE Production.ProductReview2
SET Comments.WRITE (N'(Sizes: 38-44, 48) ',15, 0)
OUTPUT INSERTED.ProductReviewID,
DELETED.Comments,
INSERTED.Comments
INTO @review
WHERE ProductReviewID = 4
290
Datenmanipulation
ROLLBACK
-- Kontrolle
SELECT SUBSTRING(CommentsBefore, 1, 20),
SUBSTRING(CommentsAfter, 1, 30)
FROM @review
423_06.sql: Ändern großer Datentypen
Als Ergebnis erhält man die Bestätigung der Änderung, da sich nun der korrigierte
Kommentar in der Tabelle befindet.
(1 Zeile(n) betroffen)
----------------------- ------------------------------The Road-550-W from
The Road-550-W (Sizes: 38-44,
(1 Zeile(n) betroffen)
Sofern keine Spalte, die durch einen der Datentypen von varchar(max)-, nvarchar(max)- und varbinary(max) beschrieben wird, bearbeitet werden soll, muss
man dann die verschiedenen Zeichenkettenfunktionen verwenden. Für die gleiche Ersetzung wie zuvor könnte dies mit der STUFF()-Funktion geschehen.
UPDATE Production.ProductReview2
SET Comments = STUFF (Comments,16,0,N'(Sizes: 38-44, 48)
')
WHERE ProductReviewID = 4
423_07.sql: Ändern großer Datentypen
Man erhält als Ergebnis auch in diesem Fall die Bestätigung der Änderung.
-------------------- ---------------------------------------
291
Datenmanipulation
The Road-550-W from
The Road-550-W (Sizes: 38-44, 48) from
A
(1 Zeile(n) betroffen)
4.2.3.6
Zusätzliche FROM-Klausel
Es ist möglich, neben der Angabe einer Tabelle direkt nach der UPDATE-Klausel noch
eine weitere Tabelle bzw. vielmehr eine Tabellenverknüpfung in einer FROM-Klausel
zwischen der OUTPUT-Klausel und der WHERE-Klausel zu verwenden. Diese wirkt im
Grunde genommen wie ein weiterer Filter, da hier festgelegt wird, auf Basis welcher
Daten die Daten in der nach UPDATE angegebenen Tabelle aktualisiert werden sollen.
Im Falle einer inneren Verknüpfung würden also daher nur solche Daten der UPDATETabelle aktualisiert werden, die genau an dieser Verknüpfung teilnehmen. Weitere Angaben in der WHERE-Klausel schränken natürlich die möglicherweise zu aktualisierende
Datenmenge weiter ein. Ein typischer Einsatz für diese zusätzliche FROM-Klausel kann
ein Datenabgleich zwischen Tabellen sein, d.h. wenn Daten in eine Tabelle eingetragen/aktualisiert werden, wobei die Werte aus einer Tabelle übernommen werden sollen.
Dies ist ein häufiges Szenario im Fall von SQL-basierten Datenbereinigungen und korrekturen.
Das nachfolgende Beispiel zeigt die Technik der zusätzlichen FROM-Klausel, wobei
darüber hinaus noch die erweiterte Variante vorgeführt wird, denn es nutzt ebenfalls die
OUTPUT-Klausel. Dies ist insoweit interessant, als dass man die geänderten Daten der
Tabelle, die direkt nach UPDATE benannt wird, aus der INSERTED-Tabelle abruft, während andere Daten aus verknüpften Tabellen gerade aus den Aliasnamen dieser verknüpften Tabellen stammen, da sie ja nicht verändert wurden.
Die Preise der Tabelle Product2 sollen erneut um 10% erhöht werden, sofern die Preise ungleich 0 sind. Dies ist allerdings nicht die einzige Einschränkung. Stattdessen verknüpft die FROM-Klausel auch noch die Daten der Product2-Tabelle mit den Produkten, welche in der ProductInventory-Tabelle referenziert werden. Das sind also
gerade die Produkte, die sich auf Lager befinden, was bedeutet, dass innerhalb dieser
Menge die Produkte mit einem Preis ungleich 0 eine 10-prozentige Preiserhöhung er-
292
Datenmanipulation
fahren. Zur Kontrolle überträgt man die aktualisierten Produkte und ihre LocationID
(Lagerplatz) in die vorher erzeugte Tabellenvariable, wobei die Felder aus der aktualisierten Product2-Tabelle aus INSERTED und die Spalte LocationID aus der ProductInventory-Tabelle über den Tabellenalias aufgerufen werden.
-- Variable erstellen
DECLARE @outputTable table(ProductID
ListPrice
int,
money,
ModifiedDate datetime,
LocationID
int)
-- Daten ändern und abrufen
UPDATE Production.Product2
SET ListPrice = ListPrice * 1.1
OUTPUT INSERTED.ProductID,
INSERTED.ListPrice,
INSERTED.ModifiedDate,
pinv.LocationID
INTO @outputTable (ProductID,
ListPrice,
ModifiedDate,
LocationID)
FROM Production.Product2 AS p2
INNER JOIN Production.ProductInventory AS pinv
ON p2.ProductID = pinv.ProductID
WHERE ListPrice > 0
-- Überprüfung
293
Datenmanipulation
SELECT TOP (3) * FROM @outputTable
423_08.sql: Zusätzliche FROM-Klausel
In der Kontrolle ergibt sich, dass nur insgesamt 46 Produkte die Bedingung erfüllen, auf
Lager zu sein und einen Preis ungleich 0 zu besitzen.
(46 Zeile(n) betroffen)
ProductID
ListPrice
ModifiedDate
LocationID
----------- --------------------- ------------------ -------452
146,674
2005-09-11
1
453
161,854
2005-09-11
1
454
216,612
2005-09-11
1
(3 Zeile(n) betroffen)
4.2.4
Löschen
Schließlich gibt es auch noch eine weitere Situation eines Datensatzes, die unter den
Begriff der Datenmanipulation fällt. Nachdem er eingefügt, abgefragt und ggf. sogar
geändert wurde, kann es ihm passieren, dass er aus der Datenbank gelöscht wird. Dies
gelingt über die DELETE-Anweisung. Sie hat in vielfältiger Weise eine sehr ähnliche
Syntax wie die anderen beiden vorgestellten Anweisungen:
ƒ
Es gibt zwei Standardfälle, von denen der eine nur aus DELETE
FROM
tabellenname besteht und den gesamten Datenbestand der Tabelle löscht, ohne
allerdings die Tabelle selbst zu löschen. Der zweite ergänzt diese Anweisung noch
um eine WHERE-Klausel, sodass auch hier ein Filter zum Einsatz kommen kann,
welcher die zu löschenden Daten weiter einschränkt.
294
Datenmanipulation
ƒ
Es können wiederum zufällige Datensätze gelöscht werden, in dem die TOP nKlausel benutzt wird. Dabei kann die Menge der zu löschenden Datensätze absolut
oder bei zusätzlicher Verwendung von PERCENT auch relativ angegeben werden.
Die Mengenvorgabe ist im Normalfall eine direkt vorgegebene Zahl; man kann
allerdings auch einen beliebigen Ausdruck verwenden, der sich schließlich zu
einem Zahlwert auflöst.
ƒ
Im Rahmen der Cursor-Verarbeitung (nicht in diesem Kapitel) kann man innerhalb
von WHERE nicht nur einen üblichen Filter angeben, sondern über die Klausel
WHERE CURRENT OF cursor den aktuell abgerufenen Datensatz eines Cursors
löschen.
ƒ
Die gelöschten Werte können über die OUTPUT-Klausel aus der DELETED-Tabelle
nach außen zurückgegeben werden.
ƒ
Die zusätzliche FROM-Klausel, welche der OUTPUT-Klausel folgt, kann genauer
angeben, aus welcher Tabelle die Daten gelöscht werden sollen. Dies ist
insbesondere interessant, wenn die Daten bspw. aus einer Verknüpfung gelöscht
werden sollen und nicht nur auf Basis einer einzigen Tabelle.
Die allgemeine Syntax hat die Form:
[ WITH <common_table_expression> [ ,...n ] ]
DELETE
[ TOP ( expression ) [ PERCENT ] ]
[ FROM ]
{ <object> | rowset_function_limited
[ WITH ( <table_hint_limited> [ ...n ] ) ]
}
[ <OUTPUT Clause> ]
[ FROM <table_source> [ ,...n ] ]
295
Datenmanipulation
[ WHERE { <search_condition>
| { [ CURRENT OF
{ { [ GLOBAL ] cursor_name }
| cursor_variable_name
}
]
}
}
]
[ OPTION ( <Query Hint> [ ,...n ] ) ]
[; ]
4.2.4.1
Standardfälle
Um die Funktionsweise von DELETE zu sehen, müssen Sie zunächst das Skript
422_07.sql ausführen. Es überträgt auch für diesen Abschnitt die Hälfte der Produkte
aus Product in Product2, um die Datensätze problemlos löschen zu können.
Das nachfolgende Beispiel zeigt die Standardfälle, welche im Rahmen von Löschvorgängen auftreten und daher auch die häufigsten Anweisungen bilden. Der erste Standardfall wählt wie eine SELECT-Anweisung über einen in WHERE angegebenen Filter
Datensätze aus einer Tabelle aus und löscht diese. Dies ist sicherlich die häufigste Form
des DELETE-Befehls. Die zweite Anweisung löscht ganz einfach den gesamten Datenbestand, wobei die Tabelle an sich allerdings bestehen bleibt. Dies ist in jedem SQLBuch immer ein wichtiges Beispiel (und kommt nun in unserem dritten Buch in dieser
Form vor), da ja dafür der DROP TABLE tabelle-Befehl existiert und man als Anfänger Gefahr läuft, dies zu verwechseln.
-- Löschen mit Filter
296
Datenmanipulation
DELETE FROM Production.Product2
WHERE ListPrice = 0
-- Vollständiges Löschen
DELETE FROM Production.Product2
424_01.sql: Standardfälle des Löschens
Als Ergebnis erhält man Zunächst (200 Zeile(n) betroffen), da 200 Produkte
einen Preis von 0 haben, und dann (52 Zeile(n) betroffen), da nur noch 52 Produkte in der Tabelle übrig sind und – wie schon bekannt sein sollte – insgesamt 252
Produkte in der Tabelle enthalten waren.
4.2.4.2
Zufällige Löschung
Über die bekannten Formen der TOP n-Klausel, die schon in den anderen Anweisungen
der Datenmanipulationssprache von SQL vorgestellt wurden, lässt sich eine zufällig
ausgewählte Menge an Datensätzen löschen. Dabei zeigen die drei Zeilen, von denen
zwei als Kommentar im Quelltext stehen, wie die Menge der zu löschenden Zeilen
absolut oder prozentual mit PERCENT angegeben werden kann. Neben einem direkten
numerischen Wert kann man auch jeden beliebigen Ausdruck einsetzen, der zu einem
Zahlwert führt. In diesem Fall handelt es sich mal nicht um die schon bekannte Abfrage
zur halben Zeilenzahl, sondern um eine Variable, deren Wert aus einer Abfrage stammt.
-- Variable erstellen
DECLARE @rowCount int
SET @rowCount = 5
-- Daten löschen
-- DELETE TOP (5)FROM Production.Product2
-- DELETE TOP (5) PERCENT Production.Product2
DELETE TOP (@rowCount) FROM Production.Product2
297
Datenmanipulation
WHERE ListPrice = 0
424_02.sql: Zufällige Löschung
4.2.4.3
Löschen mit Rückgabe
Auch DELETE bietet über die OUTPUT-Klausel an, die gerade gelöschten Werte abzurufen und für Kontrollen und Validierungen in eine Tabellenvariable oder eine (temporäre) Tabelle zu speichern. Dabei kann es keine Überlegungen hinsichtlich berechneter
Spalten oder Trigger-Werte geben, da exakt die gelöschten Werte, die ja bereits durch
Berechnungen oder Trigger in die Tabelle gebracht worden sind, auch wieder abgespeichert werden.
Im nächsten Beispiel erstellt man zunächst die benötigte Tabellenvariable mit vier
Spalten für Produktnummern, Listenpreis, Profit und Änderungsdatum. In der LöschAnweisung ruft man genau die Werte für diese Spalten aus der DELETED-Tabelle ab,
welche die gelöschten Werte speichert (im Gegensatz zur INSERTED-Tabelle, welche
die gerade eingefügten/geänderten Werte enthält).
-- Variablen erstellen
DECLARE @outputTable table(ProductID
int,
ListPrice
money,
Profit
money,
ModifiedDate datetime)
-- Daten löschen und abrufen
DELETE FROM Production.Product2
OUTPUT DELETED.ProductID,
DELETED.Profit,
DELETED.ListPrice,
DELETED.ModifiedDate AS Date
298
Datenmanipulation
INTO @outputTable (ProductID,
Profit,
ListPrice,
ModifiedDate)
WHERE ListPrice = 0
424_03.sql:Löschen mit Rückgabe
Das Ergebnis muss nicht ausgegeben werden, da in diesem Fall keine Besonderheiten
hinsichtlich Trigger oder berechneten Spalten und damit automatisch generierten Werten bestehen. In der ursprünglichen Tabelle fehlen genau die Datensätze, die in der
Tabellenvariable noch abgerufen und genutzt werden können.
4.2.4.4
Löschen mit zusätzlicher FROM-Anweisung
Es besteht, wie die allgemeine Syntax auch für die DELETE-Anweisung gezeigt hat,
auch hier die Option, noch eine weitere FROM-Klausel einzufügen. Dabei gibt die erste
FROM-Klausel die Tabelle an, aus welcher man tatsächlich löschen will, während die
zweite FROM-Klausel den Datenbestand angibt, der für diesen Löschvorgang zu Rat
herangezogen werden soll. Dies muss man sich so vorstellen, dass auf Basis der über
diese zweite FROM-Klausel abgerufenen und durch die optionale WHERE-Klausel gefilterten Daten wie bei einer Abfrage diejenigen identifiziert werden, die zu löschen sind.
Das folgende Beispiel zeigt bereits die fortgeschrittene Verwendung dieser Technik.
Auf der einen Seite sollen aus der Production.Product-Tabelle Produkte entfernt
werden. Auf der anderen Seite sollen dies nur Produkte sein, die keinen Lagerplatz
besitzen. Diesen erkennt man daran, dass sie an der Verknüpfung mit der Tabelle Production.ProductInventory nicht teilnehmen, also in der äußeren Abrage einen
NULL-Wert in der Spalte LocationID aufweisen. Dieser NULL-Wert dient als Filter,
während die Verknüpfung in der zweiten FROM-Klausel überhaupt den Datenbestand
beschreibt, auf den dieser Filter Anwendung finden soll.
Neben der Tatsache, dass man überhaupt auf diese Art und Weise einen Datenbestand
angeben kann, der für den Löschvorgang als Vergleich und damit auch als zusätzlicher
299
Datenmanipulation
Filter genutzt werden kann, wobei überhaupt eine WHERE-Klausel hinzutritt, zeigt das
Beispiel auch, wie die OUTPUT-Klausel hier zu nutzen ist. In der Tabellenvariable ist in
diesem Beispiel aus Kontrollgründen auch noch eine LocationID-Spalte, in der natürlich aufgrund der Löschanweisung nur NULL-Werte zu finden sein sollten. Da allerdings
diese LocationID gar nicht in der Product2-Tabelle enthalten ist, können diese Werte
auch nicht aus der der DELETED-Tabelel stammen, da diese Werte nicht aus der bearbeiteten Tabelle stammen. Es handelt sich um eine zusätzliche Spalte aus einer nicht durch
den Löschvorgang betroffenen Tabelle. Für die Referenzierung dieser Spalte ist in diesem Fall der Aliasname aus der FROM-Klausel zu benutzen.
-- Variable erstellen
DECLARE @outputTable table(ProductID
int,
ListPrice
money,
LocationID
int)
-- Daten löschen und abrufen
DELETE FROM Production.Product2
OUTPUT DELETED.ProductID,
DELETED.ListPrice,
pinv.LocationID
INTO @outputTable (ProductID,
ListPrice,
LocationID)
FROM Production.Product2 AS p2 LEFT OUTER JOIN
Production.ProductInventory AS pinv
ON p2.ProductID = pinv.ProductID
WHERE pinv.LocationID IS NULL
-- Überprüfung
300
Datenmanipulation
SELECT COUNT(*) AS Deleted FROM @outputTable
424_04.sql: Zusätzliche FROM-Klausel
Das Beispiel liefert den Wert von 66. Führt man eine einfache Abfrage mit der äußeren
Verknüpfung und dem Filter auf den Datensätze aus, die gerade an der Verknüpfung
nicht teilnehmen, dann findet man als Antwort die gleichen 66 Datensätze.
4.2.4.5
Vereinfachtes Löschen
Als grundsätzliche Alternative zu DELETE gibt es noch die Anweisung TRUNCATE. Sie
funktioniert grundsätzlich wie DELETE FROM tabelle, d.h. wie eine DELETEAnweisung ohne WHERE-Klausel und löscht daher ebenfalls alle Zeilen. Die Anweisung
löscht die Zeilen einer Tabelle, wobei sie allerdings die verschiedenen Löschoperationen nicht protokolliert. Sie ist schneller als DELETE und verbraucht weniger (System)Ressourcen für die Transaktionsprotokollierung. Während IDENTITY-Spaltenwerte bei
DELETE ihren Zählerstand behalten, wird er bei TRUNCATE wieder auf den Standardwert
oder 1 zurückgesetzt. Darüber hinaus aktiviert TRUNCATE auch keinen Trigger, da ja
gerade die Löschoperationen für die einzelnen Zeilen nicht protokolliert/bemerkt werden. Allerdings kann TRUNCATE nicht für alle Tabellen eingesetzt werden. Die folgenden Einschränkungen sind zu beachten: Tabellen, die Ziel einer FOREIGN KEYReferenz sind, können nicht geleert werden (aufgrund der IDENTITY-Rücksetzung). Zu
leerende Tabelle dürfen an keiner indizierten Sicht teilnehmen. Die Tabelle wird durch
Transaktionsreplikation oder Mergereplikation veröffentlicht.
Die Anweisung hat die allgemeine Syntax:
TRUNCATE TABLE
[ { database_name.[ schema_name ]. | schema_name . } ]
table_name
[ ; ]
301
Datenmanipulation
302
Grundlagen T-SQL
5 Grundlagen T-SQL
303
Grundlagen T-SQL
304
Grundlagen T-SQL
5 Grundlagen T-SQL
Die in den Kapitel 2 und 3 dargestellten Techniken für die Datenabfrage sind bis auf
wenige Ausnahmen auch in anderen Datenbanken gültig und entsprechen in vielfältiger
Weise dem Umfang von Standard-SQL. Dies trifft gleichfalls für die Anweisungen der
Datenmanipulation für Einfüge-, Lösch- und Aktualisierungsanweisungen zu. Viele
Datenbanken bieten allerdings zusätzlich auch umfassende Erweiterungen von SQL
oder sogar eigene Programmiersprachen an bzw. erlauben, eine oder mehrere gängige
Programmiersprachen innerhalb der Datenbank für die Anwendungsentwicklung zu
nutzen. Dies betrifft nicht nur solche Großdatenbanken wie den MS SQL Server oder
Oracle (PL/SQL, Java, C++), sondern auch kleinere Lösungen wie MySQL oder MS
Access. In MS Access lässt sich seit vielen Jahren mit VBA arbeiten, während der MS
SQL Server die SQL-Erweiterung Transact SQL (T-SQL) und seit Version 2005 ebenfalls auch .NET anbietet. Bisweilen wird auch die gesamte SQL-Variante vom MS SQL
Server als T-SQL bezeichnet. Wir wollen es in diesem Buch so handhaben, dass alle
Strukturen, die weitestgehend mit dem Standard übereinstimmen und so oder sehr ähnlich in anderen Datenbanken funktionieren würden, als SQL bezeichnet werden, und
lediglich die Anweisungen und Schlüsselwörter für die Anwendungsentwicklung
(Funktionen, Prozeduren, Trigger) als T-SQL.
5.1
T-SQL Blöcke
Der Einsatzbereich von T-SQL betrifft den gesamten Bereich der Anwendungsentwicklung mit der Zielsetzung, die Datenbank aus Administratorensicht zu automatisieren,
Sicherheits- und Integritätsmerkmale der Datenbank zu realisieren, di e mit einfachem
SQL bzw. einfachen Datenstrukturanweisungen nicht möglich wären, strukturierten und
vereinfachten Zugriff auf die Datenbank zu gewähren, wobei gleichzeitig auch Sicherheits- und Filteraspekte im Vordergrund stehen. In diesem Sinne ist T-SQL also
nicht nur ein Werkzeug für Programmierer oder Anwender, welche die Datenbank lediglich für Datenabruf und –bearbeitung nutzen, sondern gerade auch für Administratoren. Sie benötigen dieses Buch, um die Syntax von SQL zu lernen, wie sie für die Da-
305
Grundlagen T-SQL
tenbearbeitung notwendig ist, sowie für die Erfassung der allgemeinen Sprachstruktur.
Ihre Aufgabe können Sie dann allerdings nur mit dem zusätzlichen Wissen um die
technischen Möglichkeiten der Datenbank selbst erfüllen, die Inhalt des zweien Bandes
dieser Reihe sind.
T-SQL enthält zwar eine Vielzahl an gängigen Programmierkonstrukten von einfachen
Programmiersprachen, ist jedoch in keiner Weise so umfangreich und vielschichtig wie
bspw. PL/SQL, die Variante von SQL von Oracle. In Oracle kann man dann u.a. auch
noch mit Java programmieren, was teilweise das gesonderte Erlernen von PL/SQL überflüssig machte, Da allerdings PL/SQL tatsächlich einen umfangreichen Sprachschatz
bietet, lassen sich die meisten Aufgaben auch vollständig mit PL/SQL erledigen.
Im Vergleich zum MS SQL Server 2000 musste man schlichtweg feststellen, dass TSQL nur eine sehr einfache Erweiterung von Standard-SQL war und nicht wirklich eine
eigene Programmiersprache darstellte. In der Version 2005 sind auch nur sehr wenige
neue Konstrukte hinzugekommen, sodass man feststellen muss, dass Microsoft diese
Erweiterung offensichtlich nicht weiter verfolgt und nur in dem Umfang, wie er bereits
in der Vergangenheit bekannt war, anbieten möchte. Stattdessen – um den MS SQL
Server 2005 fortzuentwickeln und gegenüber der Konkurrenz sehr viel besser zu positionieren als früher – besteht nun die Möglichkeit, in vielfältiger Weise mit .NET zu
arbeiten, was sowohl Thema in diesem Buch als auch in weiteren Bänden dieser Reihe
sein wird.
5.1.1
Variablen und Anweisungen
Nach der kurzen Einführung in die Datenbankprogrammierung innerhalb der Datenbank
sollen nun die Grundzüge von T-SQL dargestellt werden. Anwendungen können in
Form von Prozeduren, Funktionen oder Trigger auftreten, wenn sie in der Datenbank
gespeichert sind. In Form von Textdateien lassen sich allerdings T-SQL-Programme
ebenfalls speichern und dann unmittelbar ausführen, wie dies auch für gewöhnliches
SQL der Fall ist. Diese einfachen Blöcke sollen zunächst als Bespiel dienen. In den
Beispielen in diesem Kapitel sind die Schlüsselwörter groß geschrieben. Dies entspricht
der Syntax in den offiziellen Microsoft-Dokumenten und verbessert sehr die Lesbarkeit,
306
Grundlagen T-SQL
um zwischen Schlüsselwörtern und den Datenstrukturen wie Schema-, Tabellen- und
Spaltennamen zu unterscheiden. Allerdings ist die Unterscheidung von Groß- und
Kleinschreibung irrelevant für die Funktionstüchtigkeit eines Programms. Ob man nun
also für eine Fallunterscheidung IF oder if verwendet, ist letztendlich Geschmackssache.
Eine Variable lässt sich mit Hilfe der DECLARE-Anweisung. Sie erlaubt die Deklaration
von mehreren Variablen direkt nacheinander, welche durch ein Komma getrennt sind.
Vor den Variablennamen setzt man ein @-Zeichen, das auch zur Unterscheidung zwischen Spalten- und Variablennamen nützlich ist. Dem Variablennamen folgt dann einer
der Datenbank-Datentypen mit optionaler Längenangabe.
Die allgemeine Syntax hat die Form:
DECLARE
{{ @variable [AS] datentyp }
| { @cursor_variable CURSOR }
| { @tabellen_variable_name < tabellen_definition > }
} [ ,...n]
Die allgemeine Syntax zeigt auch, dass neben der Variablendeklaration auch Cursor für
den Abruf und die zeilenweise Verarbeitung von mehreren Datenreihen sowie die Erstellung von lokalen Tabellen zur Erzeugung von Array-Strukturen deklariert werden
können. Beiden Themen ist ein eigener Abschnitt gewidmet, sodass diese zwei Wege
hier nur erwähnt, aber nicht weiter ausgeführt werden.
Die Initialisierung einer Variablen, d.h. die Speicherung eines Wertes, erfolgt dann
entweder über das Schlüsselwort SET oder ganz einfach mit Hilfe einer SELECTAnweisung. Sobald eine Variable deklariert wird, ist ihr anfänglicher Wert zunächst
NULL. Dabei ist die SET-Anweisung durchaus umfangreicher als in der nachfolgenden
allgemeinen Darstellung angegeben. Es fehlt in dieser Darstellung die Möglichkeit,
einen Cursor zu verwenden, was im Abschnitt zu den Cursorn erläutert wird. Angege-
307
Grundlagen T-SQL
ben ist stattdessen die Möglichkeit, auch die Eigenschaft oder ein Feld eines benutzerdefinierten Datentyps zu setzen. Dabei kann auch eine Instanzmethode (ein Punkt zum
Aufruf) oder eine statische Methode (einen Doppelpunkt zum Aufruf) verwendet werden, um einen Wert zu ändern. Dies sind Techniken, welche mit CLR (Common Language Runtime) in einem gesonderten Kapitel beschrieben werden und entscheidend sind
für die Weiterentwicklung des MS SQL Servers 2005 und die Integration des .NETFrameworks in der Datenbank aus Sicht des Programmierers und Entwicklers.
SET
{ @lokale_variable
[:: eigenschaft
| feld ] = ausdruck
| benutzerdefiniert_datentyp {
. | :: } methode (argument [ ,...n ] )
}
Eine weitere Möglichkeit, um eine Variable zu instanziieren, besteht darin, sie in einer
SELECT-Anweisung aufzurufen. Im typischen Fall wird dies genutzt, um innerhalb einer
Abfrage einen Spaltenwert bzw. unter Einsatz einer Funktion einen berechneten oder
umgewandelten Spaltenwert abzurufen. Es ist auch möglich, mehrere Werte abzurufen,
wobei allerdings nur der letzte Wert tatsächlich zurückgegeben wird. Sofern keine Abfrage verwendet wird, lässt sich jeder beliebige andere Ausdruck verwenden und damit
auch eine SET-Anweisung ersetzen.
SELECT { @local_variable = expression } [ ,...n ] [ ; ]
5.1.2
Datentypen
Spalten, Ausdrücke in Abfragen und Anweisungen in T-SQL sowie natürlich Variablen
lassen sich mit Hilfe eines Datentyps beschreiben. Dabei gibt ein Datentypen an, welche
Werte in einer Variable oder Spalte gespeichert werden können. Diese allgemeinen
308
Grundlagen T-SQL
Wertebereiche lassen sich noch mit weiteren Bedingungen beschränken. Dazu gibt es
bspw. die Möglichkeit, in SQL bei der Tabellendefinition Einschränkungen anzugeben,
welche den Wertebereich noch weiter eingrenzen. Neben dieser Aufgabe ermöglicht es
ein Datentyp auch, zwischen verschiedenen semantischen Bedeutung zu unterscheiden
und bspw. berechnende Funktionen oder vergleichende/berechnende Operatoren auf die
Werte, die von einem Datentyp beschrieben werden, anzuwenden. Der Plus-Operator
kann bspw. bei zwei Werten, die als Zahl angegeben sind, eine Addition durchführen,
während für Zeichenfolgen stattdessen eine Verknüpfung stattfindet und die beiden
Zeichenketten miteinander kombiniert werden. Ein anderes Beispiel ist die Verarbeitung
von Datumswerten, wobei zwei Datumswerte voneinander abgezogen oder ein Datum
um eine bestimmte Dauer erhöht wird und entweder Dauern oder weitere Datumswerte
liefern. Zusätzlich können Optimierungsverfahren implementiert werden, welche die
Sortierung, den Zugriff oder ganz allgemein die Verarbeitung beschleunigen. Dies sind
allerdings Optimierungen, welche direkt in der Datenbank oder in der ausführenden
Umgebung der Programmiersprache stattfinden und die nicht in allen Programmiersprachen vorhanden sind. Für Datenbanken dagegen sind Datentypen jedoch gang und gäbe;
man könnte sich gar keine DB ohne Datentypen vorstellen.
Die verschiedenen Datentypen lassen sich in unterschiedliche Gruppen einordnen, wobei einige Datentypen je nach Gruppenordnung auch in unterschiedliche Gruppen gehören könnten. Die Dokumentation vom MS SQL Server findet die folgenden Gruppierungen für die vorhandenen Datentypen:
ƒ
Genaue numerische Werte mit einer festen Vorgabe für Stellen vor und nach dem
Komma
ƒ
Ungefähre numerische Werte mit einer flexiblen Anzahl an Stellen vor und nach
dem Komma
ƒ
Unicode-Zeichenfolgen für Zeichenketten in Unicocde
ƒ
Zeichenfolgen für Zeichenketten, die nicht inUnicocde angegeben sind
ƒ
Binärzeichenfolgen für die Abbildung und Verarbeitung von Binärdaten, welche in
ihrer Zeichenkettenrepräsentation vorliegen (Bilder, große Objekte)
309
Grundlagen T-SQL
ƒ
Datum und Zeit für die Abbildung von Werten, die aus Zeiteinheiten wie Tagen,
Monaten, Jahren oder Stunden, Minuten und Sekunden bestehen.
ƒ
Andere Datentypen für T-SQL-Programme wie Cursor oder spezielle Datentypen
mit besonderen Fähigkeiten wie der XML-Datentyp
Die nachfolgende Aufstellung liefert einen Überblick über die vorhandenen Datentypen.
Typ
Bereich
Größe
bigint
-2^63
(-9.223.372.036.854.775.808)
(9.223.372.036.854.775.807)
int
-2^31 (-2.147.483.648) bis 2^31-1 (2.147.483.647)
4 Byte
smallint
-2^15 (-32.768) bis 2^15-1 (32.767)
2 Byte
tinyint
0 bis 255
1 Byte
bis
2^63-1
8 Byte
Genaue numerische Werte
Für die Speicherung von Zahlen mit fester Genauigkeit und mit fester Anzahl von Dezimalstellen eignen sich die beiden Datentypen decimal und numeric mit dem folgenden allgemeinen Aufbau:
decimal[ (p[ , s] )]
numeric[ (p[ , s] )]
Sofern man die Genauigkeit maximiert, liegt der Wertebereich im Intervall - 10^38 +1
bis 10^38 - 1. In der allgemeinen Syntax steht p für Genauigkeit (engl. precision) und
damit für die maximal speicherbare Gesamtzahl an Dezimalstellen links und rechts vom
Dezimalkomma. Der Wertebereich für diese Zahl ist das Intervall 1 bis 38 mit einem
Standardwert 18. Der Buchstabe s dagegen steht für die Anzahl der Dezimalstellen
(engl. scale). Die zulässigen Werte liegen zwischen 0 und p, da ja die Genauigkeit sowohl die Zahlen links als auch rechts vom Dezimalkomma angibt. Ein Wert für s ist
310
Grundlagen T-SQL
optional und kann nur angegeben werden, wenn auch eine Genauigkeit angegeben ist,
wobei ein Standardwert von 0 gilt. Insgesamt gilt die Beziehung: 0 <= s <= p.
Genauigkeit
Bytes
Genauigkeit
Bytes
1-9
5
20-28
13
10-19
9
29-38
17
Speicherplatz in Bytes
Spezielle für Währungen gibt es zwei unterschiedlich große Datentypen, die bis zu
einem Zehntausendstel genau einen Wert erfassen.
Typ
Bereich
Größe
money
922.337.203.685.477.5808 bis 922.337.203.685.477.5807
8 Byte
smallmoney
-214.748.3648 bis 214.748.3647
4 Byte
Datentypen für Währungen
Bit-Werte können mit dem Datentyp bit als ganzzahliger Datentyp, der den Wert 1, 0
oder NULL annehmen kann, gespeichert werden.
Neben den Zahlen mit fester Genauigkeit, gibt es zwei weitere für die Speicherung von
ungefähren Werten mit Gleitkomma. Dabei hat float die allgemeine Syntax float [
( n ) ] und erwartet eine Ganzzahl zwischen 1 und 53 (Standardwert) für die Angabe, wie viele Bits in der wissenschaftlichen Schreibweise zum Speichern der Mantisse
verwendet werden sollen. Dies definiert auch die Genauigkeit und Speichergröße.
Typ
Bereich
Größe
float
- 1.79E+308 bis -2.23E-308. 0 und 2.23E-308 bis 1.79E+308
Abhängig
von n
real
- 3.40E + 38 bis -1.18E - 38. 0 und 1.18E - 38 bis 3.40E + 38
4 Bytes
311
Grundlagen T-SQL
Ungefähre numerische Werte
Für float gelten die in nachfolgender Tabelle angegebenen Speicherplätze.
n
Genauigkeit
Bytes
1-24
7 Stellen
4 Bytes
25-53
15 Stellen
8 Bytes
Speicherplatz in Bytes
Für die Abbildung von Datums- und Tageszeitangaben stehen die beiden Datentypen
datetime und smalldatetime zur Verfügung.
Typ
Bereich
Genauigkeit
datetime
1. Januar 1753 bis 31. Dezember 9999.
3,33
Millisekunden
smalldatetime
1. Januar 1900 bis 6. Juni 2079
1 Minute
Zeit- und Datumstypen
Für Zeichenfolgen sind folgende Datentypen vorhanden:
ƒ
Für die Abbildung Nicht-Unicode-Zeichendaten mit fester Länge steht char [ (
n ) ] mit n Byte Länge zur Verfügung. Dabei ist n ein Wert zwischen 1 und 8.000
und bestimmt auch gleichzeitig die Speichergröße in Byte.
ƒ
Für die Abbildung Nicht-Unicode-Zeichendaten mit variabler Länge steht varchar
[ ( n | max ) ] zur Verfügung. Dabei ist n ein Wert zwischen 1 bis 8.000,
während die Angabe von max dazu führt, dass die maximale Speichergröße 2^31-1
Byte verwendet wird. Tatsächlich ermittelt sich die Speichergröße aus der
tatsächlich genutzten Länge + 2 Byte.
312
Grundlagen T-SQL
ƒ
Für die Abbildung Unicode-Zeichendaten mit fester Länge steht nchar [ ( n )
] mit n Byte Länge zur Verfügung. Dabei ist n ein Wert zwischen 1 und 4.000,
wobei die Speichergröße der doppelte Wert von n in Bytes ist.
ƒ
Für die Abbildung Unicode-Zeichendaten mit variabler Länge steht nvarchar [
( n | max ) ] zur Verfügung. Dabei ist n ein Wert zwischen 1 bis 4.000,
während die Angabe von max dazu führt, dass die maximale Speichergröße 2^31-1
Byte verwendet wird. Tatsächlich ermittelt sich die Speichergröße aus der
doppelten tatsächlich genutzten Länge + 2 Byte.
ƒ
Für die Abbildung sehr großer Unicode-Zeichendaten variabler Länge steht ntext
mit einer maximalen Länge von 2^30 - 1 (1.073.741.823) Zeichen zur Verfügung.
Dabei entsteht eine Speichergröße von der doppelten Anzahl gespeicherter Zeichen.
ƒ
Für die Abbildung von sehr umfangreichen Nicht-Unicode-Daten variabler Länge
steht text zur Verfügung. Die maximale Länge beträgt 2^31-1 (2.147.483.647)
Zeichen.
Schließlich gibt es noch den Datentyp image für die Speicherung von Binärdaten bis zu
einer Länge von 0 bis 2^31-1 (2.147.483.647) Bytes.
In Ergänzung zu den gerade beschriebenen Datentypen, die sowohl für Tabellenspalten
als auch für T-SQL-Variablen verwendet werden können, gibt es noch eine kleine
Sammlung an weiteren Datentypen. Sie bilden Datenstrukturen ab, die in Sonderfällen
genutzt werden können. Teilweise können dies auch Spaltendatentypen sein, teilweise
jedoch lassen sie sich nur in T-SQL verwenden. Nicht alle diese Datentypen sind auch
in dieser Form in anderen Datenbanken verfügbar, so wie auch die beiden Datentypen
für Währungen typisch für den MS SQL Server sind. Diese besonderen Datentypen
sind:
ƒ
Der Datentyp cursor bietet die Möglichkeiten, Variablen oder OUTPUT-Parameter
gespeicherter Prozeduren mit einem Verweis auf einen Cursor abzubilden. Der
NULL-Wert ist auch möglich.
313
Grundlagen T-SQL
ƒ
Der Datentyp timestamp kann einmal in einer Tabelle verwendet werden. Diese
Spalte enthält dann einen Zähler, der beim Einfügen und Aktualisieren von
Datensätzen automatisch erhöht wird. Es handelt sich dabei nicht um eine
tatsächliche Zeit, sondern vielmehr um das Fortschreiben eines relativen Zeitpunkts
der Datenbank. Der Einsatzbereich dieses Zählers ist die Versionierung von
Tabellenzeilen.
ƒ
Der Datentyp sql_variant bietet die Möglichkeit, Spalten, Parametern,
Variablen und Rückgabewerten von benutzerdefinierten Funktionen mit der
Fähigkeit auszustatten, verschiedene Datentypen auf einmal zu unterstützen. Dies
gilt nicht für die Datentypen varchar(max), varbinary(max),
nvarchar(max), xml, text, ntext, image, timestamp, benutzerdefinierte
Typen und sql_variant selbst.
ƒ
Der Datentyp uniqueidentifier stellt einen 16-Byte Schlüssel dar (GUID).
Dieser Datentyp bietet sich für Primärschlüsselspalten an, wobei der neue Wert
besonders einfach mti Hilfe der newid()-Funktion eingefügt werden kann.
ƒ
Der Datentyp table stellt einen Datentyp dar, welcher eine Ergebnismenge in
Form einer Tabelle abbildet. Dies kann zum Speichern von Datenmengen in einer
Variablen oder als Rückgabewert für eine Tabellenfunktion genutzt werden.
ƒ
Der Datentyp xml bildet XML-Instanzen ab, die sogar mit Hilfe eines XML
Schemas validiert werden können. XML-Strukturen können sowohl in einer Spalte
als auch in einer Tabelle gespeichert werden.
Mit einem Beispiel sollen die verschiedenen Konzepte und Basis-Techniken der zurückliegenden Abschnitte nun abgeschlossen werden. Zunächst werden drei Variablen für
den Namen, die Nummer und die Farbe eines Produkts erstellt. Die Datentypen dieser
drei Variablen entsprechen genau den Datentypen der Tabellenspalten, welche die Variablen beschreiben. Dies gewährleistet, dass die Inhalte bei einer Datenabfrage in diese
Variablen abgerufen werden können bzw. dass Vergleiche erfolgreich durchgeführt
werden können, d.h. ohne Konvertierungen zu benötigen.
314
Grundlagen T-SQL
Die Variable vProductNumber wird direkt über eine Initialisierung mit einem Wert
gefüllt, der im nächsten Schritt dazu führt, dass diese Variable als Bindevariable für die
Abfrage von Farbe und Name des entsprechenden Produkts genutzt wird. Diese Werte
werden schließlich über die PRINT-Anweisung in der Standardausgabe ausgegeben. Die
allgemeine Syntax lautet PRINT zeichenkette | @lokale_variable | zeichenketten_ausdruck und erwartet entweder eine Zeichenkette, eine Variable oder
einen beliebigen Ausdruck, welcher eine Zeichenkette zurückgibt. Dies kann eine Verknüpfung von Zeichenketten oder Variablen sowie natürlich Funktionen mit passendem
Rückgabewert sein.
-- Deklaration
DECLARE @vName nvarchar(50),
@vProductNumber nvarchar(25),
@vColor nvarchar(15)
-- Initialisierung durch direkte Wertvorgabe
SET @vProductNumber = 'SO-B909-L'
-- Initialisierung per Abfrage
SELECT @vName = Name,
@vColor = Color
FROM Production.Product
WHERE ProductNumber = @vProductNumber
-- Ausgabe
PRINT @vName + ' Nr.:' + @vProductNumber +
', Farbe: ' + @vColor
512_01.sql: Variablen und einfache Anweisungen
315
Grundlagen T-SQL
Man erhält als Ergebnis im Nachrichtenfenster, in dem normalerweise die Abfrageergebnisse ausgegeben werden, eine Nachricht mit den Produktinformationen.
Mountain Bike Socks, L Nr.:SO-B909-L, Farbe: White
5.2
Kontrollanweisungen
Für die Erstellung von Programmen mit bedingten Abläufen bietet T-SQL einfache
Konstrukte, um Fallunterscheidung sowie Schleifen zu formulieren. Sie sollen in diesem Abschnitt vorgestellt werden.
5.2.1
Fallunterscheidungen
Fallunterscheidungen lassen sich mit if-else-Strukturen abbilden, wobei leider keine
weiteren Fälle angegeben werden können. Sofern der Testausdruck im IF-Zweig den
Wahrheitswert TRUE liefert, werden die Anweisungen in seinem Anweisungsblock
ausgeführt, ansonsten diejenigen des ELSE-Blocks. Bei einer einzigen Anweisung ist es
nicht notwendig, den Anweisungsblock mit den beiden Schlüsselwörtern BEGIN und
END zu umschließen. Bei mehreren Anweisungen in einem Block ist dies dagegen verpflichtend.
Die allgemeine Syntax lautet:
IF Testausdruck
{ SQL-Anweisung | Anweisungsblock }
[ ELSE
{ SQL-Anweisung | Anweisungsblock } ]
Im folgenden Beispiel erstellt man im Deklarationsabschnitt verschiedene Variablen für
die Kundennummer, die Gebietsnummer sowie Adresszeile und –nummer sowie die
Stadt. Die Kundennummer gibt man direkt über eine SET-Anweisung vor. Die Gebietssowie die Adressnummer erfolgt über eine Abfrage, welche die beiden Tabellen
Customer und CustomerAddress verknüpft.
316
Grundlagen T-SQL
Die Fallunterscheidung prüft nun darauf, ob (ganz rein zufällig natürlich) die Gebietsnummer 4 lautet. In diesem Fall soll die Adresse ausgegeben werden. In diesem Zweig
befinden sich zunächst eine Abfrage und die entsprechende Ausgabeanweisung. Da sich
in diesem Zweig mehr als eine Anweisung befinden, müssen die beiden Schlüsselwörter
BEGIN und END die Anweisungen umschließen. Im ELSE-Zweig ist dies dagegen unnötig, weil sich hier nur eine einzige Anweisung befindet.
-- Deklaration
DECLARE @vCustomerID int,
@vTerritoryID int,
@vAddressID int,
@vAddressLine nvarchar(200),
@vCity nvarchar(30)
-- Initialisierung durch direkte Wertvorgabe
SET @vCustomerID = 6
-- Initialisierung per Abfrage
SELECT @vTerritoryID = TerritoryID,
@vAddressID = AddressID
FROM Sales.Customer AS c
INNER JOIN Sales.CustomerAddress AS ca
ON c.CustomerID = ca.CustomerID
WHERE c.CustomerID = @vCustomerID
PRINT 'Gebiet: ' + CAST(@vTerritoryID AS nvarchar(1))
PRINT 'Adresse: ' + CAST(@vAddressID AS nvarchar(3))
317
Grundlagen T-SQL
-- Fallunterscheidung
IF @vTerritoryID = 4
BEGIN
SELECT @vAddressLine = AddressLine1,
@vCity = City
FROM Person.Address
WHERE AddressID = @vAddressID
PRINT @vAddressLine + ' ' + @vCity
END
ELSE
PRINT 'Gebiet ungleich 4'
521_01.sql: Fallunterscheidung
Da tatsächlich die Gebietsnummer 4 lautet, wird in der Standardausgabe die Adresszeile
ausgegeben.
Gebiet: 4
Adresse: 989
39933 Mission Oaks Blvd Camarill
5.2.2
Schleifen
Um Anweisungen mehrfach aufgrund einer Bedingung auszuführen, gibt es auch ein
Schleifenkonstrukt. Wie in anderen Programmiersprachen auch lautet sie WHILE und
enthält die beiden optionalen Schlüsselwörter BREAK zur vorzeitigen und bedingten
Unterbrechung der Schleife sowie CONTINUE für die bedingte Fortsetzung der Schleife
vor der Ausführung der nachfolgenden Anweisungen. Die allgemeine Syntax lautet:
318
Grundlagen T-SQL
WHILE Testausdruck
{ SQL-Anweisung | Anweisungsblock }
[ BREAK ]
{ SQL-Anweisung | Anweisungsblock }
[ CONTINUE ]
{ SQL-Anweisung | Anweisungsblock }
Als Beispiel setzt man eine Zählervariable sowie zwei Variablen für die Speicherung
von Vor- und Nachnamen von Angestellten ein. Innerhalb der Schleife erhöht man den
Zählerwert und ruft mit diesem Wert als Primärschlüssel über eine Abfrage einen neuen
Employee-Datensatz ab. Wenn der Zähler den Wert 4 erreicht, soll die Schleife unterbrochen werden. Dies geschieht mit Hilfe der BREAK-Anweisung. Dies führt dazu, dass
nur vier Angestellten ausgegeben werden.
-- Deklaration
DECLARE @vZaehler int,
@vFirstName nvarchar(50),
@vLastName nvarchar(50)
-- Initialisierung
SET @vZaehler = 1
-- Initialisierung per Abfrage
WHILE @vZaehler <= 10
BEGIN
SELECT @vFirstName = FirstName,
@vLastName = LastName
319
Grundlagen T-SQL
FROM HumanResources.Employee AS emp
INNER JOIN Person.Contact AS c
ON emp.ContactID = c.ContactID
WHERE emp.EmployeeID = @vZaehler
-- Inkrementation
SET @vZaehler = @vZaehler + 1
-- Ausgabe
PRINT CAST(@vZaehler AS nvarchar(2)) + ' ' +
@vFirstName + ' ' +
@vLastName
-- Fallunterscheidung
IF @vZaehler = 4
BREAK
ELSE
CONTINUE
END
522_01.sql: Schleife
5.3
Dynamische Anweisungen
T-SQL bietet die sehr interessante Möglichkeit, SQL-Anweisungen dynamisch zusammenzusetzen, indem Zeichenketten aus Variablen oder aus direkten Vorgaben mit Hilfe
von Fallunterscheidungen kombiniert werden. Dies erlaubt es, sehr dynamische Anweisungen auszuführen, welche wiederum zu besonders interessanten Funktionen und Prozeduren umgesetzt werden können.
320
Grundlagen T-SQL
5.3.1
Einsatz von EXEC
Es gibt zwei Wege, um T-SQL-Anweisungen dynamisch auszuführen. Sie unterscheiden sich in der Komplexität der möglichen Syntax und damit auch im Umfang der Möglichkeiten. Die allgemeine Syntax der EXEC-Anweisung, welche die einfachere und
damit etwas begrenzte Variante darstellt, lautet:
{ EXEC | EXECUTE }
( { @string_variable | [ N ]'tsql_string' } [ + ...n
] )
[ AS { LOGIN | USER } = ' name ' ]
Es ist auch möglich, eine solche Anweisungen gegen einen verlinkten Server auszuführen. Dann verändert sich die allgemeine Syntax zu:
{ EXEC | EXECUTE }
( { @string_variable | [ N ] 'command_string' }
[ + ...n ]
[ {, { value | @variable [ OUTPUT ] } } [...n] ]
)
[ AS { LOGIN | USER } = ' name ' ]
[ AT linked_server_name ]
Die Parameter beider Varianten sind die folgenden:
ƒ
@string_value enthält für die dynamische Ausführung den Namen einer lokalen
Variable in den Datentypen char, varchar, nchar oder nvarchar, in welcher die
Anweisungen gespeichert sind.
ƒ
[N] 'tsql_string' erwartet eine auszuführende Anweisung als Zeichenkette.
321
Grundlagen T-SQL
ƒ
AS { LOGIN | USER } = ' name ' legt den so genannten Ausführungskontext
fest. Hier kann man angeben, unter welchem Benutzernamen man die Anweisung
ausführen möchte.
ƒ
@variable enthält eine Bindevariable, die in die Anweisung Werte übergibt.
value enthält dagegen direkt diesen gebundenen Wert.
ƒ
OUTPUT legt fest, dass ein Ausgabeparameter der ausgeführten Anweisung per
Referenz in diese Variable übergeben werden soll.
ƒ
AT linked_server_name enthält den Namen eines verlinkten Servers, auf dem
die Anweisung ausgeführt werden soll. Über die Prozedur sp_addlinkedserver
können solche Verbindungen eingerichtet werden.
Zur Illustration erstellt man zwei Variablen für die Speicherung von Fragmenten für
SQL-Anweisungen. Diese beiden Variablen unterscheiden sich in der Spaltenauswahl,
d.h. in sql1 werden drei und in sql2 vier Spalten aus der SalesOrderHeaderTabelle genannt. Dabei verknüpft die Variable sql2 zusätzlich die Variable sql1 mit
einer zusätzlichen Spalte. Die Ausführung erfolgt dann ganz einfach mit Hilfe der EXECUTE-Anweisung, in der sowohl die Variable sql2 als auch weitere notwendige Zeichenkettenfragmente zur Bildung einer vollständigen SQL-Anweisung aufgerufen werden. Dies könnte auch noch mit einer Fallunterscheidung oder entsprechenden Parametern einer Funktion/Prozedur verbunden werden.
-- Deklaration von SQL-Variablen
DECLARE @sql1 nvarchar(100),
@sql2 nvarchar(100)
-- Initialisierung
SET @sql1 = 'CustomerID, OrderDate, ShipDate'
SET @sql2 = @sql1 + ', DueDate'
-- Dynamische Ausführung
322
Grundlagen T-SQL
EXECUTE ('SELECT ' + @sql2 + ' FROM Sales.SalesOrderHeader')
531_01.sql: Dynamische Ausführung von SQL
Im Ergebnis zu den vorherigen Beispielen erhält man nun im Ergebnisbereich gerade
keine Textausgabe über eine PRINT-Anweisung, sondern stattdessen eine ganz gewöhnliche Ergebnismenge. Dies ist eine besonders attraktive Fähigkeit von T-SQL, welche in
anderen Datenbanken nicht möglich ist. Man ist nicht gezwungen, ein T-SQLProgramm so zu beenden, dass ein Ergebnistext ausgegeben wird, der in einer anderen
Programmiersprache nicht sinnvoll genutzt werden kann. Stattdessen kann man über
überaus variantenreiche Techniken Ergebnismengen erzeugen, die in einer äußeren
Programmiersprache aus dem .NET-Umfeld genau wie eine Ergebnismenge auf Basis
einer SQL-Anweisung verarbeitet werden kann.
CustomerID
OrderDate
ShipDate
DueDate
----------- ---------- ---------- ----------676
2001-07-01 2001-07-08 2001-07-13
117
2001-07-01 2001-07-08 2001-07-13
442
2001-07-01 2001-07-08 2001-07-13
Das nächste Beispiel zeigt, wie die Verbindung zu einer MS Access-Datenbank eingerichtet, auf verschiedene Arten genutzt und schließlich gelöscht wird. Mit den gleichen
Prozeduren, die hier aus Platzgründen nicht alle dargestellt werden sollen, lassen sich
Verbindungen auch zu anderen Datenbanksystemen oder sogar MS Excel einrichten.
Dabei befindet sich in der Access-Datenbank die exportierte Product-Tabelle.
-- Verlinkten Server einrichten
EXEC sp_addlinkedserver
@server = 'AWAccess',
@provider = 'Microsoft.Jet.OLEDB.4.0',
323
Grundlagen T-SQL
@srvproduct = 'OLE DB Provider for Jet',
@datasrc = 'C:\Product.mdb'
-- Ausgabemöglichkeit einrichten
EXEC sp_serveroption 'AWAccess', 'rpc out', true;
-- Kontrolle
EXEC sp_helpserver;
-- Einfache Abfrage
EXEC ( 'SELECT * FROM Product;') AT AWAccess;
-- Übergabe eines einfachen Paramters
EXEC ( 'SELECT * FROM Product WHERE Color = ?;', 'Black') AT
AWAccess;
-- Übergabe eines Parameters als Variable
DECLARE @vColor varchar(10);
SET @vColor = 'Blue';
EXEC ( 'SELECT * FROM Product WHERE Color = ?;', @vColor) AT
AWAccess;
-- Verlinkten Server löschen
EXEC sp_dropserver @server = 'AWAccess';
531_02.sql: Ausführen von Anweisungen an verlinktem Server
5.3.2
Einsatz von sp_executesql
Eine technisch anspruchsvollere Lösung, um dynamische T-SQL-Anweisungen auszuführen, bietet die Systemprozedur sp_executeql. Ihre allgemeine Syntax lautet:
324
Grundlagen T-SQL
sp_executesql [ @stmt = ] stmt
[
{, [@params=] N'@parameter_name data_type [
[ OUT [ PUT ][,...n]' }
{, [ @param1 = ] 'value1' [ ,...n ] }
]
Folgende Parameter kommen zum Einsatz:
ƒ
[ @stmt = ] stmt enthält die Anweisung als Zeichenkette oder als Variable in
ntext-Form sein bzw. sich in diesen Datentyp konvertieren lassen. Der
Verkettungsoperator + darf zwar nicht zum Einsatz kommen, aber die Verkettung
kann in einer Variablen zum Einsatz kommen, die dann verwendet wird. Diese
Zeichenkette darf Variablen enthalten, welche allerdings nicht durch den
Verkettungsoperator hinzugefügt werden müssen, sondern die anhand des @Zeichens erkannt werden.
ƒ
[ @params = ] N'@parameter_name data_type [ ,... n ] ' enthält die
verschiedenen Parameter aus der auszuführenden T-SQL-Zeichenkette mit Namen
und Datentyp. Dies ähnelt der Spaltenliste einer Tabelle oder von
Modulparametern. Der Standardwert für diesen Parameter ist NULL.
ƒ
[ @param1 = ] 'value1' enthält jeden definierten Parameter mit seinem Wert
als Zeichenkette oder Variable. Jeder Parameter muss angegeben werden.
ƒ
OUTPUT gibt an, dass der jeweilige Parameter ein Rückgabeparameter ist, wie dies
bei Prozeduren möglich ist. Die dynamische Rückgabe eines Cursors ist ebenfalls
möglich.
Die
nachfolgenden Beispiele zeigen
unterschiedliche Standardfälle von
sp_executesql. Im ersten Beispiel führt man eine einfache Abfrage aus. Hier ist es
nicht notwendig, Parameter zu verwenden, sodass hier auch keine besondere Berück-
325
Grundlagen T-SQL
sichtigung von Parametern für die Abfrage notwendig ist. Man verwendet einfach nur
den @stmt-Parameter, um die Anweisung anzugeben.
Das zweite Beispiel stellt eine Erweiterung des einfachen Standardfalls dar und verwendet direkt innerhalb der nicht zusammen gesetzten T-SQL-Zeichenkette zwei Parameter. Sie müssen innerhalb vom Parameter @params angekündigt werden und werden
schließlich im dritten Parameter der Reihe nach mit Werten gefüllt. Dabei ist dieser Teil
der Parameterliste dynamisch. Auf der einen Seite erwartet die Prozedur so viele Parameter wie angekündigt, auf der anderen Seite gibt man die Werte über ihre tatsächlichen
Namen an.
Das dritte Beispiel schließlich fügt noch einen Ausgabeparameter hinzu. Dies kann
sowohl ein Rückgabewert aus einer Abfrage oder der Verwendung einer Prozedur mit
Ausgabeparameter sein. In diesem Fall beschränkt man sich darauf, den ermittelten
Aggregatwert einer Zählung in einen solchen Parameter zu speichern und diesen Wert
dann zurückzuliefern.
-- Einfache Abfrage
EXEC sp_executesql
@stmt = N'SELECT * FROM Production.Product';
-- Übergabe zweier Parameter
EXEC sp_executesql
@stmt = N'SELECT * FROM Production.Product WHERE Color =
@Color AND Size = @Size',
@params = N'@Color varchar(10), @Size varchar(2)',
@Color = 'Black', @Size = '40';
-- Angabe eines Ausgabeparameters
326
Grundlagen T-SQL
DECLARE @vProducts int
EXEC sp_executesql
@stmt = N'SELECT @vCount = COUNT(*) FROM
Production.Product',
@params = N'@vCount INT OUTPUT',
@vCount = @vProducts OUTPUT;
PRINT STR(@vProducts)
532_01.sql: Einsatz von sp_executesql
5.4
Fehlerbehandlung
Selbstverständlich ist davon auszugehen, dass nach der Lektüre des Buchs die Behandlung von Fehlern zu den Aufgaben gehören, die am ehesten vernachlässigt werden können, doch gerade wenn man die Aufgabe erhält, von Kollegen Quelltext zu überarbeiten, ist es sicherlich nicht verkehrt, sich doch schon einmal mit diesem Thema zu beschäftigen. Daher kommt diesem Abschnitt auch die Aufgabe zu, einen kleinen Teil für
die Gemeinschaft zu tun („Jeden Tag eine gute Tat.“).
Dieser Abschnitt stellt daher die neue Ausnahmebehandlung und die klassische Fehlerbehandlung vor.
5.4.1
Ausnahmen
In der Version 2005 ist ein neues Programmierkonstrukt für die Fehlerbehandlung hinzugekommen, welches bereits in anderen Sprachen wie C# oder Java bekannt ist. Es
bietet eine Basislösung für die dort vorhandene Ausnahmebehandlung über die Syntax
von TRY und CATCH an, d.h. nicht die gesamten Fähigkeiten der Ausnahmebehandlung
der beispielhaft erwähnten Sprachen sind auch in T-SQL vorhanden.
Die Funktionsweise ist einfach und entspricht denen anderer Sprachen: Ein Abschnitt,
der möglicherweise einen Fehler liefern könnte, kann mit Hilfe einer IF-Konstruktion
327
Grundlagen T-SQL
auf diesen Fehler abgeprüft werden. Dies führt allerdings dazu, dass im Quelltext sowohl Fallunterscheidungen, welche die Logik des Programms abbilden, als auch solche
Fallunterscheidungen auftreten, welche für die Fehlerbehandlung eingefügt wurden.
Dies erschwert häufig das Verständnis des Algorithmus und führt immer zu sehr tief
verschachtelten Fallunterscheidungen und dadurch zu schwer lesbarem Quelltext. Ein
solcher Block wird von der neuen Syntax BEGIN TRY und END TRY umschlossen. Jeder
Fehler, der die Datenbankverbindung nicht schließt und der einen Schweregrad größer
als 10 besitzt, führt nicht zu einer Fehlermeldung, sondern zu einem Sprung in den auf
den TRY-Block folgenden CATCH-Block. Dieser Block wird von der neuen Syntax BEGIN CATCH und END CATCH umschlossen und muss unmittelbar nach END TRY folgen.
Es darf also ausdrücklich kein anderer Quelltext zwischen den beiden Blöcken stehen.
Die gesamte Konstruktion muss innerhalb eines einzigen Blocks Platz finden und kann
nicht mehrere Batches, mehrere Blöcke, die durch BEGIN und END umschlossen werden,
sowie keine Fallunterscheidungen überspannen. In den Fällen, in denen der versuchte
Quelltext aus beliebigen Gründen keinen Fehler auslöst, werden die Anweisungen im
CATCH-Block übersprungen und die Ausführung mit dem auf ihn unmittelbar folgenden
Anweisungen fortgesetzt.
Die allgemeine Syntax hat die Form:
BEGIN TRY
{ SQL-Anweisung | Anweisungsblock }
END TRY
BEGIN CATCH
{ SQL-Anweisung | Anweisungsblock }
END CATCH
[ ; ]
Zu Anfang des Abschnitts wurde diese neue Technik als Basislösung von ähnlichen
Möglichkeiten anderer Programmiersprachen bezeichnet. Dies liegt daran, dass durch
328
Grundlagen T-SQL
die fehlende Objektorientierung keine typisierte Untersuchung der möglichen Fehler
angegeben werden kann. So ist es also nicht möglich und wäre auch unsinnig, mehrere
CATCH-Blöcke anzugeben. In objektorientierten Programmiersprachen bietet sich hierüber die Möglichkeit, verschiedene Fehlertypen abzufangen und sogar über das Substitutionsprinzip Eltern-Fehlerarten anstelle ihrer Kinder anzugeben, um mehrere abgeleitete Fehler in einem CATCH-Block zu sammeln. Aufgrund der fehlenden Objektorientierung in T-SQL ist dies nicht möglich. Da zudem auch keine Fehler aus Prozeduren oder
Funktionen aufgefangen werden können auch keine Ausnahmen geworfen werden können, muss der Nutze der neuen Syntax mit Vergleich auf ähnliche Implementierungen
etwas eingeschränkt werden.
Die wesentliche Zielsetzung, eine strukturierte Fehlerbehandlungstechnik anzugeben,
welche sich syntaktisch von Fallunterscheidungen für die Formulierung von Algorithmen unterscheidet, ist allerdings gelungen. Darüber hinaus ist es zulässig, die Ausnahmebehandlungen zu verschachteln, sodass nicht nur eine einzige Ebene von Fehlern
solchermaßen behandelt werden kann.
5.4.1.1
Fehlermeldungen
Wie schon gerade in den allgemeinen Erläuterungen dargestellt, können nicht alle Fehler durch eine Ausnahmebehandlung tatsächlich abgerufen werden. Dies sind nur die
Fehler mit einem Schweregrad zwischen 11 und 16. Diese und andere Schweregrade
werden in der nachfolgenden Tabelle kurz vorgestellt. Die Fehlermeldungen mit einem
Schweregrad zwischen 19 und 25 erfasst das Fehlerprotokoll und sollten dem Administrator gemeldet werden (sofern man dies nicht selbst ist).
Schweregrad Beschreibung
0-9
Nicht schwerwiegende Fehler oder Statusinformationen zurückgeben,
die vom Datenbankmodul (Database Engine) nicht ausgelöst werden.
10
Statusinformationen oder nicht schwer wiegende Fehler, die vor der
Rückgabe vom Datenbankmodul aus Kompatibilitätsgründen in den
Schweregrad 0 konvertiert werden.
11
Das Objekt oder die Entität ist nicht vorhanden.
329
Grundlagen T-SQL
12
Spezielle Abfragehinweise verhindern Sperren in Abfragen, die im
Rahmen von Lesevorgängen zu inkonsistenten Daten führen können.
13
Deadlockfehler der Transaktion.
14
Sicherheitsbezogene Fehler wie fehlende Berechtigungen.
15
Syntaxfehler in Transact-SQL.
16
Allgemeine Fehler, die vom Benutzer behoben werden können.
17-19
Softwarefehler, die vom Benutzer nicht behoben werden können.
17
Anweisung führte zu fehlenden Ressourcen wie Arbeitsspeicher,
Sperren, Speicherplatz sowie Grenzwertverletzungen.
18
Softwarefehler des Datenbankmoduls, die weder die Ausführung
beendet noch die Verbindung schließt.
19
Datenbankmodul überschreitet einen nicht konfigurierbaren Grenzwert,
was den aktuellen Batchprozess beendet.
20-25
Systemprobleme (schwerwiegende Fehler), welche den Task des
Datenbankmoduls mehr ausgeführt und normalerweise auch die
Anwendungsverbindung beenden und protokolliert werden.
20
Eine Anweisung löst in einem Task ein Problem aus..
21
Eine Anweisng löst sich auf alle Tasks in der aktuellen Datenbank
auswirkt.
22
Eine Tabelle oder ein Index, wurden durch ein Software- oder
Hardwareproblem beschädigt. Weitere Hilfe mit DBCC CHECKDB und
Neustart der Instanz oder Neuerstellung des Objekts
23
DB- Integrität der Datenbank ist durch ein Hardware- oder
Softwareproblem gestört. Weitere Hilfe mit DBCC CHECKDB und
Neustart der Instanz oder Neuerstellung des Objekts
24
Medien- und Hardwarefehler.
Fehlerarten
330
Grundlagen T-SQL
5.4.1.2
Standardfall
Um die Ausnahmebehandlungstechnik von T-SQL gut zu nutzen, ist es vor allen Dingen wichtig zu wissen, welche Fehler überhaupt erkannt und daher entsprechend behandelt werden können. Die verschiedenen Beispiele in diesem Kapitel zeigen diverse
Einsatzbereiche bzw. behandelbare Fehler, die auch die Übersichtstabelle des letzten
Abschnitts im Mittelteil mit dem Schweregrad 11 bis 16 zeigte.
Das erste Beispiel zeigt den einfachsten Standardfall, in dem ein kritischer Bereich
lediglich in einem TRY-Block platziert wird und die selbst erstellte Fehlerbehandlung
(Ausgabe einer Fehlermeldung) darauf folgt. Im TRY-Block versucht man, einen durch
eine Primärschlüssel-Fremdschlüssel-Beziehung referenzierten Datensatz zu löschen.
Dies gelingt nicht, sodass die Anweisungen im CATCH-Block ausgeführt werden.
BEGIN TRY
DELETE FROM Production.UnitMeasure
WHERE UnitMeasureCode = 'CM'
END TRY
BEGIN CATCH
PRINT 'Löschen nicht möglich'
END CATCH
541_01.sql: Ausnahme bei referenzieller Integrität
Einige Fehler können unmittelbar im gleichen Block, d.h. in der gleichen Ebene behandelt werden. Andere erfordern dagegen, dass Fehler und ihre Behandlung in einer anderen Ausführungsebene behandelt werden. Dazu zählen Kompilierungsfehler, welche die
Ausführung eines Batch verhindern, und auch Fehler bei der Auflösung von Objektnamens auftreten. Ein nicht vorhandenes Objekt kann nur in einem kompilierten Block
wie bspw. einer Prozedur oder Funktion erkannt werden. Daher setzt das nächste Beispiel nicht nur einen einfachen Block ein, sondern besteht aus einer (wenig sinnvollen)
331
Grundlagen T-SQL
Prozedur. Diese ist allerdings in der Lage, folgende drei Überlegungen abzubilden: 1.
Ein erwarteter Parameter, der beim Aufruf der Prozedur nicht gesetzt wird, führt zu
einer Ausnahme. 2. Ein syntaktisch korrekter und keine referenzielle Integrität verletzende SQL-Anweisung für die Aktualisierung oder Löschung führt zu keiner Ausnahme. Dies ist eigentliche trivial, aber mit Blick auf das vorherige Beispiel und vielleicht
wenig Erfahrung mit Programmiersprachen oder Datenbanken scheint das Beispiel für
Anfänger gut geeignet, falsche Erwartungen zu beheben. 3. Der Aufruf eines nicht vorhandenen Objekts führt zu einer Ausnahme.
CREATE PROCEDURE uspWrongObject (
@test int) AS
DECLARE @empNr int
SELECT @empNr = MAX(EmployeeID)
FROM HumanResources.Employee
IF @test = 1 BEGIN
-- Löschen eines nicht vorhandenen Datensatzes
DELETE FROM HumanResources.Employee
WHERE EmployeeID = @empNr + 1
END ELSE
-- Oder: Löschen aus nicht vorhandenem Objekt
DELETE FROM HumanResources.Employees
WHERE EmployeeID = @empNr + 1
541_01.sql: Prozedur für Ausnahmetest
Der erste Test ruft die Prozedur ohne Parameter auf, obwohl diese Aufrufmöglichkeit
nicht vorgesehen ist, und daher eine Ausnahme auslöst. Im Gegensatz zum ersten Bei-
332
Grundlagen T-SQL
spiel, in dem die PRINT-Anweisung eine eigene Fehlermeldung ausgab, setzen diese
Beispiele jeweils die beiden Funktionen ERROR_NUMBER() und ERROR_MESSAGE()
ein. Diese und weitere Funktionen werden später noch einmal als Gruppe vorgestellt.
Sie erlauben es, weitere Informationen über Ausnahmen zu ermitteln.
-- Fehlender Parameter
BEGIN TRY
EXEC uspWrongObject
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER()
AS ErrorNumber,
ERROR_MESSAGE() AS ErrorMessage
END CATCH
Da eine Ausnahme ausgelöst wird, lautet das Ergebnis:
ErrorNumber ErrorMessage
----------- ------------------------------------------------201
Die Prozedur oder Funktion 'uspWrongObject'
erwartet den '@test'-Parameter, der nicht
bereitgestellt wurde.
Im Gegensatz zum vorherigen Beispiel mit einer Lösch-Aktion löst der nachfolgende
Test gerade keine Ausnahme aus, da ein nicht gefundener Datensatz kein Problem im
Sinne der Ausnahmebehandlung darstellt.
-- Kein Datensatz gelöscht
BEGIN TRY
333
Grundlagen T-SQL
EXEC uspWrongObject 1
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER()
AS ErrorNumber,
ERROR_MESSAGE() AS ErrorMessage
END CATCH
541_03.sql: Fehlender Datensatz ohne Ausnahme
Als Ergebnis erhält man daher nur die sicherlich schon bekannte Information:
(0 Zeile(n) betroffen)
Da Fehler, die im Bereich der Namensauflösung entstehen, nicht in der gleichen Ausführungsebene als Ausnahme erkannt werden, ist der nachfolgende Aufruf der Prozedur
und die Auslösung dieses Falls die einzige Situation, in dem dies überhaupt erkannt
wird. Die Prozedur ruft man mit einer beliebigen Zahl ungleich 1 auf, um den Standardfall der Fallunterscheidung auszulösen.
-- Falscher Objektname
BEGIN TRY
EXEC uspWrongObject 2
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER()
AS ErrorNumber,
ERROR_MESSAGE() AS ErrorMessage
END CATCH
541_02.sql: Test der Ausnahmebehandlung
334
Grundlagen T-SQL
Als Ergebnis erhält man in diesem Fall:
ErrorNumber ErrorMessage
----------- ------------------------------------------------208
Ungültiger Objektname
'HumanResources.Employees'.
5.4.1.3
Funktionen zur Fehleruntersuchung
Zur Untersuchung des aufgetreten Fehlers kann man folgende Funktionen im CATCHBlock verwenden. Zwei wichtige Funktionen, welche die Fehlernummer und den zusammen fassenden Fehlertext ausgeben, wurden bereits in den vorherigen Beispielen
genutzt. Folgende Funktionen sind verfügbar:
ƒ
ERROR_NUMBER() liefert die Fehlernummer.
ƒ
ERROR_SEVERITY() liefert den Schweregrad.
ƒ
ERROR_STATE() liefert die Fehlerstatusnummer.
ƒ
ERROR_PROCEDURE() liefert den Namen der gespeicherten Prozedur oder des
Triggers, welche den Fehler verursachten.
ƒ
ERROR_LINE() liefert die Zeilennummer in dem Modul, in dem der Fehler
aufgetreten ist.
ƒ
ERROR_MESSAGE() liefert den Text der Fehlermeldung.
Das nächste Beispiel greift noch einmal die die referenzielle Integrität verletzende Anweisung eines vorherigen Beispiels auf und ersetzt die dort erfasste eigene Fehlermeldung durch sämtliche Funktionen, die für die Ausnahmebehandlung möglich sind.
BEGIN TRY
DELETE FROM Production.UnitMeasure
335
Grundlagen T-SQL
WHERE UnitMeasureCode = 'CM'
END TRY
BEGIN CATCH
SELECT
ERROR_NUMBER()
AS ErrorNumber,
ERROR_SEVERITY()
AS ErrorSeverity,
ERROR_STATE()
AS ErrorState,
ERROR_PROCEDURE() AS ErrorProcedure,
ERROR_LINE()
AS ErrorLine,
ERROR_MESSAGE()
AS ErrorMessage
END CATCH
541_04.sql: Verwendung der Ausnahmefunktionen
Das Ergebnis und die Nützlichkeit der Informationen spricht eigentlich für sich. Insbesondere solche Informationen wie der Prozedurname, in der der Fehler aufgetreten ist
(hier NULL, da ein einfaches Skript erstellt wurde), als auch die Zeilennummer, die den
Fehler ausgelöst hat, sind neben der sehr ausführlichen Fehlermeldung sehr interessante
Daten, die auch gut protokolliert werden können, wenn dies notwendig sein sollte.
ErrorNumber ErrorSeverity ErrorState
ErrorProcedure
----------- ------------- ----------- ----------------547
16
ErrorLine
ErrorMessage
0
NULL
----------- -------------------------------------------------
336
Grundlagen T-SQL
3
Die DELETE-Anweisung steht in Konflikt mit der
REFERENCE-Einschränkung "FK_Product_UnitMeasure
_SizeUnitMeasureCode". Der Konflikt trat in der
"AdventureWorks"-Datenbank, Tabelle "Production.
Product", column 'SizeUnitMeasureCode' auf.
(1 Zeile(n) betroffen
5.4.2
Traditionelle Fehlerbehandlung
Dieser Abschnitt stellt die traditionelle, d.h. im MS SQL Server 2000 eingesetzte
Fehlerbehandlung dar, die natürlich weiterhin genutzt werden kann. Auf der einen Seite
zeichnet sie sich durch eine kurze Syntax aus, die durch die Funktion @@error und
ihren alleinigen Aufruf bedingt ist. Auf der anderen Seite sieht man bereits im ersten
Beispiel dieses Abschnitts, wie die verschachtelte Fallunterscheidung zu schwer lesbaren Quelltext führen kann.
5.4.2.1
Fehler abrufen
Es gibt in der T-SQL-Syntax verschiedene Strukturen, die mit zwei @-Zeichen begonnen
werden und daher wie globale Variablen wirken. Offiziell nennt man diese Strukturen
allerdings Funktionen, was allerdings mehr historisch ist, weil sie aus einer Zeit stammen, in der im MS SQL Server noch keine Funktionen im eigentliche Sinne erstellt
werden konnten. Diese Funktion liefert den Wert 0, wenn die vorherige SQLAnweisung keine Fehler hervorbracht, und ansonsten die Fehlernummer der vorherigen
Anweisung. Die möglichen Fehler, die über diese Funktion abgerufen werden können,
stehen mit mehrsprachigen Fehlermeldungen in der sys.messages-Katalogsicht. Diese können wiederum über eine SQL-Anweisung in folgender Form abgerufen werden:
SELECT * FROM sys.messages
WHERE language_id = 1031
337
Grundlagen T-SQL
AND severity > 0
ORDER BY severity
542_02.sql: Abruf der Fehlerarten
Da die Fehlermeldungen sehr umfangreich sind und vermutlich ohnehin schon bei
verschiedenen selbstständigen Versuchen mit T-SQL gelesen wurden, soll nur kurz die
Liste der Spaltennamen zeigen, welche Informationen in dieser Katalogsicht enthalten
sind: message_id (Nachrichtennummer), language_id (Sprachnummer), severity
(Schweregrad), is_event_logged (protokolliert oder nicht), text (Fehlermeldungstext).
Da die Funktion immer nur Informationen über den letzten Fehler liefert, sollte man
diese Informationen unmittelbar nach der SQL-Anweisung, die einen Fehler auslöst,
auch abrufen und in einer lokalen Variable speichern. Dies zeigt auch das nachfolgende
Beispiel. Zunächst erstellt man zwei Variablen, welche die Werte für die Fehlernummer
und die Anzahl der betroffenen Reihen speichern sollen. Diese Information stammt aus
@@rowcount, einer weiteren Funktion, die nach diesem Beispiel kurz erläutert wird,
und welche in diesem Fall die Anzahl betroffener Reihen enthält. Die Ausführung der
fehlerhaften Anweisung, welche einen verwendeten Datensatz zu löschen versucht und
daher gegen die Regeln der referenziellen Integrität verstößt, löst einen Fehler aus, der
mit @@error abgerufen werden kann.
Um herauszufinden, ob überhaupt ein Fehler aufgetreten ist, kann man @@error auf
den Wert 0 prüfen. Ist ein Fehler aufgetreten, ist ein Wert ungleich 0 vorhanden, der
dann bei Bedarf wieder untersucht werden kann. Im aktuellen Beispiel löst man einen
Fehler mit der Nummer 547 aus, der daher auch direkt geprüft und mit einer passenden
eigenen Fehlerbehandlung bzw. einfach Fehlermeldung versehen werden kann.
-- Variablen zur Fehleranalyse
DECLARE @vError int, @vRowCount int
-- Fehlerhafte Anweisung
338
Grundlagen T-SQL
DELETE FROM Production.UnitMeasure
WHERE UnitMeasureCode = 'CM'
-- Speichern von @@ERROR und @@ROWCOUNT
SELECT @vError = @@ERROR, @vRowCount = @@ROWCOUNT
-- Untersuchung und Ausgabe
IF @vError <> 0 BEGIN
IF @vError = 547 BEGIN
PRINT 'Fehler bei referenzieller Integrität'
END
ELSE BEGIN
PRINT 'Fehler ' + RTRIM(CAST(@vError AS NVARCHAR(10)))
END
END
542_02.sql: Traditionelle Fehlerbehandlung
Man erhält als Ergebnis sowohl die eigentliche Fehlermeldung als auch die eigene Fehlermeldung.
Meldung 547, Ebene 16, Status 0, Zeile 5
Die DELETE-Anweisung steht in Konflikt ...
Die Anweisung wurde beendet.
Fehler bei referenzieller Integrität
Das vorherige Beispiel setzte neben der Funktion @@error auch die Funktion
@@rowcount ein, um die Anzahl betroffener Reihen zu ermitteln, die aufgrund des
Fehlers gleich null war. Dies ist einer der wesentlichen Einsatzbereiche von
339
Grundlagen T-SQL
@@rowcount. Im Zusammenhang mit Cursorn, die es ermöglichen, durch eine mehrzei-
lige Ergebnismenge zu navigieren, liefert diese Funktion die aktuelle Zeilennummer.
Die Funktion liefert bei Anweisungen, die tatsächlich keine Zeilen abrufen, wie USE,
SET <Option>, DEALLOCATE CURSOR, CLOSE CURSOR, BEGIN TRANSACTION oder
COMMIT TRANSACTION den Wert 0. Die EXEC(UTE)-Anweiung behält darüber hinaus
den vorherigen Status bei.
5.5
Cursor
Bislang wurden ausschließlich Anweisungen ausgeführt, die jeweils eine einzige Datenzeile abriefen. Dies betrifft auch das Beispiel, in welchen mehrere Datensätze über eine
Schleife abgerufen wurden. Gerade diese Lösung zum Abruf mehrerer Zeilen ist eigentlich überhaupt gar keine, weil sie eine geringe Leistungsfähigkeit besitzt. Stattdessen
gibt es die so genannte Cursor-Technik, welche eine Abfrage unter einem (Variablen)Namen speichert und so für die Nutzung verfügbar macht.
5.5.1
Cursor-Varianten
Es gibt zwei Varianten für die Deklaration eines Cursors, die sich in ihrem Verhalten
unterscheiden. Dies betrifft das Scrollverhalten und damit die Möglichkeiten, wie der
Cursor letztendlich in T-SQL genutzt werden kann.
5.5.1.1
SQL92-Syntax
Die folgenden Bestandteile erscheinen in der allgemeinen Syntax für einen Cursor,
welcher der Syntax von SQL-92 folgt:
DECLARE name [ INSENSITIVE ] [ SCROLL ]
CURSOR FOR abfrage
[ FOR { READ ONLY | UPDATE [ OF column_name [ ,...n ] ] } ]
[;]
Folgende Attribute können diesem Cursor mitgegeben werden:
340
Grundlagen T-SQL
ƒ
name: Der Name eines Cursors muss den allgemeinen Regeln für einen Bezeichner
bzw. für eine Variable erfüllen. Unter diesem Namen ist der Cursor später zu
verwenden.
ƒ
INSENSITIVE: Sinn und Zweck eines Cursors ist es, eine Abfrage abzubilden, die
mehrere Zeien abrufen kann. Bei der Angabe dieses Schlüsselworts erzeugt der
Cursor beim Abruf der Daten eine temporäre Kopie der Daten in der tempdbDatenbank. Dies bedeutet, dass Änderungen, die an den Daten erfolgen, die durch
die Abfrage abgerufen werden, bei einem Abruf vom Cursor nicht erkannt werden.
Darüber hinaus sind im Umkehrschluss auch keine Datenänderungen an den so
genannten Basistabellen möglich und werden Änderungen, die gerade an den
Basistabellen bspw. von einem anderen Benutzer vorgenommen werden, auch im
Cursor so zurückgegeben.
ƒ
SCROLL: Diese Angabe richtet den Cursor so ein, dass sämtliche Abrufoptionen
(FIRST, LAST, PRIOR, NEXT, RELATIVE, ABSOLUTE) genutzt werden können.
Sofern auf SCROLL verzichtet wird, kann ansonsten nur immer die nächste Zeile
abgerufen und damit nur NEXT eingesetzt werden.
ƒ
abfrage: Der Cursor repräsentiert eine Abfrage, die auch mehrere Basistabellen
umfassen kann und an sich auch komplexer Natur sein kann. Allerdings ist es nicht,
die Schlüsselwörter COMPUTE, COMPUTE BY, FOR BROWSE und INTO zu
verwenden.
ƒ
READ ONLY: Im Normalfall ist es möglich, die Daten, die ein Cursor abruft, auch
zu bearbeiten, d.h. mit einer UPDATE-Anweisung zu aktualisieren und mit einer
DELETE-Anweisung zu löschen. Dies geschieht am einfachsten mit Hilfe der WHERE
CURRENT OF-Klausel, welche den aktuell abgerufenen Datensatz kennzeichnet.
Durch die READ ONLY-Einstellung allerdings ist der Cursor tatsächlich nur lesbar.
ƒ
UPDATE [OF column_name [,...n]]: Wenn ein Cursor aktualisierbar sein soll,
dann kann man entscheiden, ob man alle Spalten aktualisierbar machen möchte
oder nicht. Sofern man die UPDATE OF-Klausel nicht verwendet, können
Änderungen an allen Spalten vorgenommen werden. UPDATE OF dagegen erlaubt
341
Grundlagen T-SQL
es, ausdrücklich die Spalten anzugeben, welche im Rahmen der CursorVerwendung geändert werden sollen.
Wie man sehen kann, gibt es verschiedene Möglichkeiten, änderbare und nichtänderbare Cursor oder auch nur Cursor-Teile anzugeben. Es ist unbedingt notwendig,
nur die Cursor überhaupt änderbar zu machen, die tatsächlich geändert werden sollen,
und auch dann nur die Spalten änderbar zu machen, welche im Laufe eines T-SQLProgramms geändert werden sollen. So kann die Datenbank wesentlich besser, Transaktionen und Zeilensperren schneller durchführen
5.5.1.2
Transact-SQL Extended Syntax
Neben der Syntax für den SQL-92-Cursor stellt der MS SQL Server noch eine erweiterte Syntax für Cursor zur Verfügung. Ihre allgemeine Form lautet:
DECLARE name CURSOR
[ LOCAL | GLOBAL ][ FORWARD_ONLY | SCROLL ]
[ STATIC | KEYSET | DYNAMIC | FAST_FORWARD ]
[ READ_ONLY | SCROLL_LOCKS | OPTIMISTIC ]
[ TYPE_WARNING ]FOR abfrage
[ FOR UPDATE [ OF spalte [ ,...n ] ] ]
[;]
Folgende Attribute sind für einen T-SQL-Cursor verfügbar:
ƒ
name: Der Name des Cursors, unter dem er später angesprochen werden kann.
ƒ
LOCAL: Dieses Schlüsselwort bestimmt den Gültigkeitsbereich des Cursors. Er ist
lokal zu der ihn erstellenden Folge von Anweisungen (Batch), der gespeicherten
Prozedur oder dem Trigger. Sein Bezeichner ist daher nur innerhalb von den
genannten Bereichen gültig, sodass der Cursor nur dort aufgerufen werden kann.
Die aufrufende Umgebung kann auf ihn nur zugreifen, wenn er an einen OUTPUT-
342
Grundlagen T-SQL
Paramter gebunden und dementsprechend zurückgegeben wird. Sobald der
genannte Bereich beendet wird, weil bspw. die letzte Anweisung abgelaufen ist,
dann wird der Cursor auch wieder aufgelöst, d.h. seine Zuordnung wird aufgelöst.
Die Standardeinstellung für den Gültigkeitsbereich ist LOCAL.
ƒ
GLOBAL: Wie bereits LOCAL gibt auch GLOBAL den Gültigkeitsbereich eines
Cursors an. Dabei bestimmt ihn GLOBAL als von jeder Prozedur oder jedem Batch
abrufbar.
ƒ
FORWARD_ONLY: Ein mit diesem Attribut ausgestatteter Cursor liefert die Zeilen
nur in aufsteigender Richtung und erlaubt damit als einzige Abrufoption NEXT.
Dies ist der Standardfall, wenn weder FORWARD_ONLY noch SCROLL angegeben ist.
Zusätzlich können noch drei weitere Schlüsselwörter (STATIC, KEYSET und
DYNAMIC) angegeben werden, von denen DYNAMIC der Standardfall ist, wenn
FORWARD_ONLY nicht im Zusammenhang mit einem dieser Schlüsselwörter benutzt
wird. Sofern dagegen FORWARD_ONLY fehlt, und eines dieser drei Schlüsselwörter
erscheinen, wird SCROLL für die Richtungsoptin angenommen.
ƒ
STATIC gibt an, dass der Cursor innerhalb der tempdb-Daten die abgerufenen
Daten zwischenspeichern soll. Dieser Cursor lässt keine Änderungen zu, da sich die
Änderungen nur temporär auswirken würden.
ƒ
KEYSET legt fest, dass die Reihenfolge und die Existenz von Zeilen innerhalb der
abgerufenen Daten fest ist. Die abgerufenen Schlüssel werden dabei in der keysetTabelle der tempdb gespeichert. Ändert der Besitzer des Cursors oder ein anderer
Benutzer durch COMMIT Nichtschlüsselwerte in den Tabellen, die dem Cursor zu
Grunde liegen, machen sich diese durch Scroll-Vorgänge im Cursor bemerkbar.
Einfügungen werden dagegen nicht wahrgenommen. Löschungen liefert einen
@@FETCH_STATUS-Wert von -2. Neue Werte sind dann sichtbar, wenn die WHERE
CURRENT OF-Klausel verwendet wird.
ƒ
DYNAMIC gibt an, dass der Cursor Datenänderungen unmittelbar bei ScrollVorgängen anzeigt. Dadurch können sich die Werte, die Reihenfolge von
Datensätzen sowie ihre Existenz innerhalb der abgerufenen Menge jeweils ändern.
343
Grundlagen T-SQL
ƒ
FAST_FORWARD erzeugt einen Cursor mit FORWARD_ONLY
READ_ONLY mit
erhöhter Leistung. Dabei dürfen SCROLL und FOR UPDATE nicht verwendet
werden.
ƒ
READ_ONLY erzeugt einen Cursor, der nur für Lesevorgänge genutzt werden kann.
Die WHERE CURRENT OF-Klausel in UPDATE oder DELETE kann nicht genutzt
werden.
ƒ
SCROLL_LOCKS legt fest, dass positionierte Aktualisierungen oder Löschungen
erfolgreich geschehen, da die abgerufenen Zeilen gesperrt werden. Dies schließt die
Verwendung von FAST_FORWARD aus.
ƒ
TYPE_WARNING legt fest, dass eine Warnmeldung an den Client geschickt wird,
sobald der Cursortyp sich implizit ändern sollte.
ƒ
select_statement enthält die Abfrage, welche die Cursor-Daten beschafft. Die
Klauseln COMPUTE, COMPUTE BY, FOR BROWSE und INTO sind unzulässig. Sofern
die verwendete SQL-Anweisung für die zuvor dargestellten Anweisungen
ungeeignet ist, wird der Typ implizit konvertiert.
ƒ
FOR UPDATE [OF column_name [,...n]] legt die Spalten an, die im Rahmen
der Bearbeitung aktualisiert werden sollen. Sofern Spaltennamen angegeben sind,
können Änderungen dann nur in diesen Spalten ausgeführt werden. Ansonsten
stehen alle Spalten bereit.
5.5.2
Verwendung
Für die tatsächliche Benutzung eines Cursors sind eine Reihe von speziellen Anweisungen, Funktionen und Prozeduren verfügbar, die hier dargestellt werden sollen.
5.5.2.1
Anweisungen
Neben der umfangreichen Anzahl von Einstellungen ist die eigentliche Verwendung
eines Cursors grundsätzlich einfach, weil ein sehr schematisches Vorgehen notwendig
ist, um sie zu verwenden. Dabei müssen nach der eigentlichen Deklaration noch folgende Anweisungen zum Einsatz kommen. In der allgemeinen Syntax ist dabei immer von
344
Grundlagen T-SQL
zwei möglichen Ausprägungen eines Cursors zu lesen. Zunächst gibt es den klassischen
über DECLARE erzeugten Cursor, der unter einem eigenen Namen (cursor_name) verfügbar ist. Dann gibt es allerdings auch noch eine Cursorvariable, die über den Datentyp
CURSOR angegeben und unter diesem Variablennamen (cursor_variable_name)
verfügbar ist. Es ist möglich, im Rahmen von Prozeduren auch Cursor zurückzugeben.
Um sie zu verwenden, muss im aufrufenden Programm eine entsprechende Variable
bereitstehen. Das Schlüsselwort GLOBAL in den folgenden Erläuterungen gibt immer an,
dass sich diese Anweisung auf eine globalen Cursor bezieht.
Der erste Schritt besteht darin, den Cursor überhaupt zu öffnen. Die Daten werden dabei
abgerufen und stehen bereit.
OPEN { { [ GLOBAL ] cursor_name } | cursor_variable_name }
Der zweite Schritt besteht darin, die Daten aus dem Cursor zeilenweise in lokale Variablen zu übertragen. Dabei besteht die Möglichkeit, bei einem scrollbaren Cursor absolute und relative Positionierungen anzugeben.
FETCH
[ [ NEXT | PRIOR | FIRST | LAST
| ABSOLUTE { n | @nvar }
| RELATIVE { n | @nvar }
]
FROM
]
{ { [ GLOBAL ] cursor_name } | @cursor_variable_name }
[ INTO @variable_name [ ,...n ] ]
Der dritte Schritt besteht darin, den Cursor wieder zu schließen. Er ist dadurch nicht
zerstört, sondern kann wieder geöffnet werden. Die Zuordnungen zwischen der aktuellen Ergebnismenge und dem Cursor werden genauso wie die Zeilensperren aufgehoben.
345
Grundlagen T-SQL
CLOSE { { [ GLOBAL ] cursor_name } | cursor_variable_name }
Der vierte Schritt besteht darin, die Referenz zwischen Cursornamen/-variablen und
dem Cursor aufzuheben. Dies sorgt dafür, dass alle dem Cursor zugeordneten Ressourcen freigegeben werden.
DEALLOCATE { { [ GLOBAL ] cursor_name }
| @cursor_variable_name }
5.5.2.2
Cursor-Funktionen
Bei der Ermittlung von Cursor-Zuständen gibt es eine Reihe von speziellen Funktionen
(zwei der mysteriösen @@-Funktionen und eine ()-Funktion), die Informationen zu einem Cursor zurückliefern.
ƒ
@@FETCH_STATUS ermittelt den Status der letzten FETCH-Anweisung und liefert
die Werte
ƒ
346
–
0 für einen erfolgreichen Abruf
–
-1 für einen fehlgeschlagenen Abruf
–
-2 für eine Zeile, die nicht mehr in der Ergebnismenge vorhanden ist
@@CURSOR_ROWS ruft die Anzahl der Zeilen ab, die im Cursor abrufbar sind. Die
Funktion liefert folgende Werte:
–
-m liefert bei einem asynchron aufgefüllten Cursor die Werte, die aktuell verfügbar sind (keyset-Tabelle).
–
-1 zeigt an, dass es sich um einen dynamischen Cursor handelt und daher die
Anzahl der abgerufenen Zeilen nicht definitiv ermittelt werden kann.
–
0 zeigt an, dass kein Cursor offen ist oder der zuletzt geöffnete Cursor nun geschlossen ist und die Information nicht abgerufen werden kann.
–
n liefert die Anzahl der verfügbaren Zeilen.
Grundlagen T-SQL
ƒ
Die Skalarfunktion CURSOR_STATUS liefert die Information zurück, ob eine
Prozedur einen Cursor und eine relationale Ergebnismenge zurückgeliefert hat. Die
allgemeine Syntax lautet:
CURSOR_STATUS
(
{ 'local' , 'cursor_name' }
| { 'global' , 'cursor_name' }
| { 'variable' , 'cursor_variable' }
)
Die drei Angaben local, global und variable stehen für die verschiedenen
Cursor-Arten und werden als konstante Zeichenfolgen übergeben. Die
anderen beiden verschiedenen Parameter enthalten den Namen eines
DECLARE-Cursors oder einer Cursor-Variable. Die Funktion hat folgende
Rückgabewerte (EM = Ergebnismenge):
Wert
Cursorname
Cursorvariable
EM (auch INSENSITIVE- und
Zugeordnete Cursor ist offen.
KEYSET-Cursor) hat mind. eine
Zeile.
EM (auch INSENSITIVE- und KEYSETCursor) hat mind. eine Zeile.
EM eines dynamischen Cursors
hat (k)eine oder mehrere Zeilen.
EM eines dynamischen Cursors
(k)eine oder mehrere Zeilen.
0
Leere EM (nicht bei dynamischen
Cursorn).
Zugeordneter Cursor offen, EM aber leer.
-1
Cursor geschlossen.
Zugeordneter Cursor geschlossen.
-2
Nicht verfügbar.
Prozedur wies OUTPUT-Variable keinen
Cursor zu.
1
hat
Durch Prozedur zugewiesener Cursor
347
Grundlagen T-SQL
wurde nach Prozedurabschluss geschloss.
Die Zuordnung zwischen OUTPUTVariable und Cursor ist aufgehoben.
Cursorvariable
zugeordnet.
-3
Angegebener
vorhanden.
Cursor
nicht
erhielt
keinen
Cursor
Cursorvariable nicht vorhanden oder kein
zugewiesener Cursor vorhanden.
Rückgabewerte
5.5.3
Beispiele
Um die wesentlichen Schritte bei der Erstellung eines Cursors zu zeigen, ruft man im
folgenden Beispiel nur einige Daten zu Abteilungen ab und gibt sie über die PRINTAnweisung aus. Der Cursor wird über die DECLARE-Anweisung erstellt und als scrollbar definiert. Im Wesentlichen befindet sich tatsächlich eine ganz gewöhnliche SELECTAnweisung in dieser Definition, die bis auf die erwähnten Ausnahmen mit allem aufwarten kann, was eine SELECT-Anweisung bieten kann. Danach deklartiert man eine
Reihe von Die Zuordnungen zwischen der aktuellen Ergebnismenge und dem Cursor
werden genauso wie die Zeilensperren aufgehoben. Danach deklariert man eine Reihe
von Variablen, deren Datentypen zu dem Cursor abgerufenen Spalten passen. Im einfachsten Fall sind dies die Datentypen der Spalten, wenn im Cursor keine Berechnungen, Funktionsaufrufe oder Aggregate vorhanden sind. Um den Cursor tatsächlich zu
benutzen, folgen die vier Schritte:
1. Man öffnet den Cursor über die OPEN-Anweisung.
2. Im zweiten Schritt ruft man die Daten zunächst einmal ab, um zu prüfen, ob
überhaupt Daten vorhanden sind und bereits Daten zu besitzen. Dies ist ein
Schönheitsfehler der T-SQL-Syntax, die es in anderen Datenbanken nicht
gibt. Hier muss man leider einmal vorher abrufen, um dann im Rahmen
einer Schleife weitere Abrufe durchzuführen. Sofern also sehr viele Spalten
abgerufen werden, ist eine große Menge Quelltext zu kopieren, was
348
Grundlagen T-SQL
sicherlich nicht von besonders gutem Quelltext zeugt. Leider ist allerdings
keine andere Möglichkeit verfügbar. Die Schleife wird über den mit
@@FETCH_STATUS ermittelten Status des Cursors durchgeführt. Er liefert drei
Werte: 0 (erfolgreicher Abruf), -1 (fehlgeschlagener Abruf) oder -2 (Zeile
nicht mehr in der Ergebnismenge vorhanden). Innerhalb der Schleife muss
man dann zunächst die vom ersten Abruf empfagenen Zeilen verarbeiten,
bevor man wieder neue Daten abruft, da man ansonsten die erste Zeile
verliert.
3. Im dritten Schritt schließt man den Cursor und könnte ihn noch einmal
öffnen.
4. Im vierten Schritt löscht man die Referenzen auf den Cursor und stellt den
Ursprungszustand wieder her.
-- Cursor
DECLARE cDepartment CURSOR SCROLL
FOR SELECT TOP 5 DepartmentID, Name, GroupName
FROM HumanResources.Department
-- Variablen
DECLARE @vDepID smallint,
@vName nvarchar(50),
@vGroupName nvarchar(50)
-- 1. Schritt: Öffnen
OPEN cDepartment
-- 2. Schritt: Abrufen
FETCH NEXT FROM cDepartment
INTO @vDepID, @vName, @vGroupName
349
Grundlagen T-SQL
WHILE @@FETCH_STATUS = 0
BEGIN
PRINT CONVERT(nvarchar(10), @vDepID) + ' '
+ @vName + ' ' + ' ' + @vGroupName
FETCH NEXT FROM cDepartment
INTO @vDepID, @vName, @vGroupName
END
-- 3. Schritt: Schließen
CLOSE cDepartment
-- 4. Schritt: Löschen
DEALLOCATE cDepartment
553_01.sql: Verwenden eines einfachen Cursors
Man erhält eine Ausgabe der Ergebnisse in gedruckter Form:
1 Engineering
Research and Development
2 Tool Design
Research and Development
Wie oben erwähnt, gibt es eine Vielzahl an Möglichkeiten, die Daten aus dem Cursor
abzurufen. Im nächsten Skript werden die verschiedenen Varianten einmal im Zusammenhang gezeigt. Es handelt sich dabei um den Cursor, der bereits im vorherigen Beispiel erzeugt wurde. Daher wird hier nur noch der zweite Schritt vorgeführt. Es lassen
sich direkt die erste und letzte Zeile abrufen. Man kann die vorherigen und die folgenden Datenreihen abrufen, sodass man nach vor und zurück scrollen kann. Darüber hinaus kann man auch direkt absolut oder relativ Zeilen angeben, zu denen man springen
möchte.
350
Grundlagen T-SQL
-- 2. Schritt: Abrufen
-- Letzte Reihe
FETCH LAST FROM cDepartment
INTO @vDepID, @vName, @vGroupName
PRINT CONVERT(nvarchar(10), @vDepID) + ' ' + @vName
-- Vorherige Reihe, relativ zur aktuellen
FETCH PRIOR FROM cDepartment
INTO @vDepID, @vName, @vGroupName
PRINT CONVERT(nvarchar(10), @vDepID) + ' ' + @vName
-- Absolut zweite
FETCH ABSOLUTE 2 FROM cDepartment
INTO @vDepID, @vName, @vGroupName
PRINT CONVERT(nvarchar(10), @vDepID) + ' ' + @vName
-- Relativ dritte Reihe zur aktuellen
FETCH RELATIVE 3 FROM cDepartment
INTO @vDepID, @vName, @vGroupName
PRINT CONVERT(nvarchar(10), @vDepID) + ' ' + @vName
-- Relativ zweite Reihe vor der aktuellen
FETCH RELATIVE -2 FROM cDepartment
INTO @vDepID, @vName, @vGroupName
PRINT CONVERT(nvarchar(10), @vDepID) + ' ' + @vName
553_02.sql : Abrufoptionen
351
Grundlagen T-SQL
Man erhält als Ergebnis die folgenden Zeilen:
5 Purchasing
4 Marketing
2 Tool Design
5 Purchasing
3 Sales
Mit den drei Cursor-Funktionen lassen sich die verschiedenen Zustände von Cursorn
abrufen. Dazu folgen zwei gleichartige aufgebaute Programme, welche für die verschiedenen Ereignisse im Lebens eines Cursors seine Zustände abrufen. Es handelt sich um
den Cursor für die Abteilungsdaten, der bereits zuvor auch eingesetzt wurde. Das erste
Programm konzentriert sich auf einen mit der bisher schon genutzten Syntax angelegten
Cursor über DECLARE. Der im Quelltext gefettete Teil stellt die Abfrage der verschiedenen Funktionen dar, die in den späteren Ereignissen nicht mehr vollständig abgedruckt
wird.
-- Variablen
DECLARE @vDepID smallint,
@vName nvarchar(50),
@vGroupName nvarchar(50)
-- Cursor deklarieren
DECLARE cDepartment CURSOR SCROLL
FOR SELECT TOP 5 DepartmentID, Name, GroupName
FROM HumanResources.Department
SELECT @@CURSOR_ROWS AS Reihen,
@@FETCH_STATUS AS [F-Status],
352
Grundlagen T-SQL
CURSOR_STATUS('global', 'cDepartment') AS [C-Status]
-- 1. Schritt: Öffnen
OPEN cDepartment
SELECT ...
-- 2. Schritt: Abrufen
FETCH FIRST FROM cDepartment
INTO @vDepID, @vName, @vGroupName
SELECT ...
-- 3. Schritt: Schließen und löschen
CLOSE cDepartment
SELECT ...
-- 4. Schritt: Löschen
DEALLOCATE cDepartment
SELECT ...
553_03.sql: Testen der Cursorfunktionen
Man erhält als Ergebnis mehrere Ergebnismengen, die als Zusammenfassung folgendes
Aussehen haben. Deutlich ist zu sehen, dass der FETCH-Status jeweils 0 ist, da innerhalb
dieser Verbindung ein vorheriger Cursor eine Zeile erfolgreich zurückgeliefert hatte.
Die Anzahl der Reihen verrät allerdings sehr deutlich, dass erst, nachdem man den Cursor auch tatsächlich geöffnet hat, fünf Zeilen abgerufen wurden. Nachdem der Cursor
geschlossen ist, sind keine Reihe mehr verfügbar. Der Cursor-Status, der über die ()Funktion abgerufen wird, gibt an, dass der Cursor bei der Deklaration und nach dem
Schließen geschlossen und nach dem Öffnen und während des Abrufens geöffnet ist.
Reihen
F-Status
C-Status
353
Grundlagen T-SQL
+----------- ----------- -------Deklaration
| 0
0
-1
1. Schritt: Öffnen
| 5
0
1
2. Schritt: Abrufen
| 5
0
1
3. Schritt: Schließen | 0
0
1
4. Schritt: Löschen
0
-1
| 0
Das Programm wird noch einmal umgewandelt, um zu zeigen, wie es bei Cursorvariablen funktioniert. Es wird nur der veränderte Teil im nachfolgenden Quelltext angegeben.
Zunächst deklariert man die Cursorvariable, dann weist man ihr eine Abfrage zu, um
dann die verschiedenen weiteren Lebenszustände zu testen.
...
-- Cursorvariable deklarieren
DECLARE @cDepartment cursor
-- Cursorvariable mit Abfrage verbinden
SET @cDepartment = CURSOR SCROLL FOR
SELECT TOP 5 DepartmentID, Name, GroupName
FROM HumanResources.Department
SELECT @@CURSOR_ROWS AS Reihen,
@@FETCH_STATUS AS [F-Status],
CURSOR_STATUS('variable', '@cDepartment') AS [CStatus]
...
553_04.sql: Testen der Cursorfunktionen
354
Grundlagen T-SQL
Die Anzahl der Reihen wird genauso ermittelt wie zuvor. Der FETCH-Status ist jeweils
0, da im Laufe der Verbindung ein Cursor (nach dem Aufrufen dieses Cursors dann der
aktuelle) Daten zurückgeliefert hat. Insbesondere der Cursor-Status, der über die ()Funktion abgerufen wird, liefert nun andere Werte, da es sich um eine Cursorvariable
handelt, die sie untersucht. Zunächst ist der zugeordnete Cursor geschlossen, da ja tatsächlich nur die Variable an sich deklariert wurde. Dann ist der zugewiesene Cursor
geöffnet, und man erhält den Wert 1 zurück. Nachdem der Cursor geschlossen ist,
springt der Wert wieder auf -1 um, bis dann zum Schluss mit dem Wert -2 gemeldet
wird, dass kein Cursor mehr zugewiesen wird.
Reihen
F-Status
C-Status
+----------- ----------- -------Deklaration
| 0
0
-1
1. Schritt: Öffnen
| 5
0
1
2. Schritt: Abrufen
| 5
0
1
3. Schritt: Schließen | 0
0
-1
4. Schritt: Löschen
0
-2
| 0
Auch wenn zuvor bereits ein Beispiel für Cursorvariablen angegeben wurde, soll dieses
Thema noch einmal gesondert behandelt werden. Ein gewöhnlicher Cursor ist mit seiner
SQL-Anweisung direkt verknüpft. Sie kann später nicht noch einmal geändert werden.
Stattdessen müsste man einen neuen Cursor deklarieren und diesem die geänderte Fassung zuweisen. Eine Cursorvariable hingegen steht als Patzhalter für unterschiedliche
SQL-Befehle bereit. Dabei kann man sowohl eine neue SQL-Anweisung über die SETAnweisung, als auch einen gewöhnlichen Cursor zuweisen. Im letzten Fall ist es darüber
hinaus auch noch möglich, aus einer Prozedur einen Cursor zu empfangen. Dies wird
später im Rahmen der Vorstellung von Prozeduren gezeigt.
Die SET-Anweisung spielt hierbei eine wichtige Rolle. Sie hat folgende allgemeine
Syntax. Insbesondere die direkte Angabe einer SQL-Anweisung zeigt, dass hier jeder
355
Grundlagen T-SQL
beliebige Cursor auch dynamisch erstellt werden kann. Die einzelnen Attribute, mit
denen der Cursor beschrieben wird, sind dieselben wie bei einem gewöhnlichen Cursor.
SET
{ @local_variable = expression }
|
{ @cursor_variable =
{ @cursor_variable | cursor_name
| { CURSOR [ FORWARD_ONLY | SCROLL ]
[ STATIC | KEYSET | DYNAMIC | FAST_FORWARD ]
[ READ_ONLY | SCROLL_LOCKS | OPTIMISTIC ]
[ TYPE_WARNING ]
FOR select_statement
[ FOR { READ ONLY | UPDATE [ OF column_name [ ,...n
] ] } ]
}
}
}
Im nächsten Beispiel sieht man, wie ein globaler Cursor erstellt und einer Cursorvariable zugewiesen wird. Dies ist eine der drei erwähnten Möglichkeiten - Zuweisung, SQLAnweisung vorgeben oder Prozedur-Ausgabeparameter. Die Cursorvariable erstellt man
nur durch Angabe des Datentyps cursor. Die Referenz zum Cursor kann man über die
SET-Anweisung einrichten. Die Verarbeitung dieser Cursorvariable ist dann grundsätzlich genauso wie bei einem gewöhnlichen Cursor. Der wesentliche Unterschied in der
Syntax ist nur die Verwendung des Variablen- und nicht des Cursornamens, d.h. hier ist
356
Grundlagen T-SQL
in den verschiedenen Anweisungen wie OPEN, FETCH etc. überall ein @-Zeichen zu
verwenden.
-- Variablen
DECLARE @vDepID smallint,
@vName nvarchar(50),
@vGroupName nvarchar(50)
-- Cursor deklarieren
DECLARE cDepartment CURSOR SCROLL GLOBAL FOR
SELECT TOP 5 DepartmentID, Name, GroupName
FROM HumanResources.Department
-- Cursorvariable deklarieren
DECLARE @cvDepartment CURSOR
-- Cursorvariable einem Cursor zuordnen
SET @cvDepartment = cDepartment
-- Test
SELECT CURSOR_STATUS('global', 'cDepartment') AS [C-Status],
CURSOR_STATUS('variable',
'@cvDepartment') AS [CV-Status]
-- 1. Schritt: Öffnen
OPEN @cvDepartment
-- 2. Schritt: Abrufen
FETCH LAST FROM @cvDepartment
INTO @vDepID, @vName, @vGroupName
357
Grundlagen T-SQL
PRINT CONVERT(nvarchar(10), @vDepID) + ' ' + @vName + ' '
+ ' ' + @vGroupName
-- Test
SELECT CURSOR_STATUS('global', 'cDepartment') AS [C-Status],
CURSOR_STATUS('variable',
'@cvDepartment') AS [CV-Status]
-- 3. Schritt: Schließen
CLOSE @cvDepartment
-- 4. Schritt: Löschen
SELECT CURSOR_STATUS('global', 'cDepartment') AS [C-Status],
CURSOR_STATUS('variable',
'@cvDepartment') AS [CV-Status]
DEALLOCATE @cvDepartment
DEALLOCATE cDepartment
553_05.sql: Cursor und Cursorvariable
Als Ergebnis erhält man den abgerufenen Daten als viel interessantere Information, die
verschiedenen gleichartigen Statusmeldungen der beiden verschiedenen Einheiten.
Wenn der Cursor geschlossen ist und daher den Wert -1 zurückliefert, dann hat auch die
Cursorvariable diesen Wert, wobei hier dieser Wert sich auf den eigentlichen Cursor
bezieht, der auch aus Sicht der Cursorvariable noch geschlossen ist. Der Wert 1 nach
dem Öffnen zeigt beim Cursor an, dass mindestens ein Wert in der Abfrage enthalten
ist, während der gleiche Wert für die Cursorvariable bedeutet, dass der referenzierte
Cursor geöffnet ist.
C-Status CV-Status
-------- ---------
358
Grundlagen T-SQL
-1
-1
5 Purchasing
1
1
-1
-1
Inventory Management
Die zweite Variante besteht daraus, die Cursorvariable zu erstellen, die eigentliche Abfrage allerdings in der SET-Anweisung vorzugeben. Dies ermöglicht es bspw., in Abhängigkeit von äußeren Werten und daher im Rahmen einer Fallunterscheidung verschiedene SET-Anweisungen und damit dynamisch die eigentliche Abfrage zu verändern. Im Normalfall belässt man hier natürlich die Spaltenstruktur der Rückgabe, verändert aber Filter oder Berechnungen. Das nächste Beispiel zeigt, wie eine solche Abfrage direkt vorgegeben wird und wie man mit einem solchen Cursor umgeht.
-- Variablen
DECLARE @vDepID smallint,
@vName nvarchar(50),
@vGroupName nvarchar(50)
-- Cursorvariable deklarieren
DECLARE @cvDepartment CURSOR
-- Cursorvariable eine Abfrage zuordnen
SET @cvDepartment = CURSOR SCROLL GLOBAL FOR
SELECT TOP 5 DepartmentID, Name, GroupName
FROM HumanResources.Department
-- 1. Schritt: Öffnen
OPEN @cvDepartment
-- 2. Schritt: Abrufen
359
Grundlagen T-SQL
FETCH ABSOLUTE 2 FROM @cvDepartment
INTO @vDepID, @vName, @vGroupName
PRINT CONVERT(nvarchar(10), @vDepID) + ' ' + @vName + ' '
+ ' ' + @vGroupName
-- 3. Schritt: Schließen
CLOSE @cvDepartment
-- 4. Schritt: Löschen
DEALLOCATE @cvDepartment
553_06.sql: Cursorvariable und SQL-Anweisung
5.6
Transaktionen
Im Rahmen von Datenbearbeitungsvorgängen ist es wichtig, mit Transaktionen zu arbeiten, wenn mehrere Anweisungen in einer Serie zusammen gehören und sie nur als
Ganzes durchgeführt werden sollen. Ein Schulbeispiel, um Transaktionen zu erklären,
stellt eine Überweisung von einem Bankkonto zum nächsten dar. Wenn das Geld gerade
von Konto A abgehoben ist, aber noch nicht auf Konto B gutgeschrieben ist, passiert in
diesem Szenario ein elektronisches Unglück, und das Geld landet im finanziellen Nirvana. Bei einem elektronischen Unglück, in dem die Transaktionssteuerung der Datenbank oder der Anwendung noch eingreifen kann, würde bereits die Abbuchung wieder
rückgängig gemach und damit der ganze Prozess zurückgedreht.
Transaktionsverwaltung ist bei Mehrbenutzerbetrieb ein sehr wichtiges Thema und
taucht als Unterthema in verschiedener Weise auf. Es schimmerte beispielsweise gerade
auch durch die Darstellung der verschiedenen Cursor-Arten. Hier bestand die Möglichkeit, einem Cursor mitzugeben, welche Spalten aktualisierbar sein sollen, oder dass er
die Änderungen an den abgerufenen Daten auch im Cursor widerspiegeln soll oder
nicht. Dies sind Entscheidungspunkte und Einstellungen, die aus den Überlegungen
zum Mehrbenutzerbetrieb gehören.
360
Grundlagen T-SQL
Dieser Abschnitt möchte die verschiedenen T-SQL-Anweisungen, mit denen die Transaktionssteuerung eingerichtet werden kann, darstellen. Der MS SQL Server ist selbstverständlich eine transaktionsfähige Datenbank und führt auch automatisch Transaktionen für einzelne Anweisungen, während denen ein Fehler auftritt, durch, aber für den
Programmierer ist es wichtig, mehrere Anweisungen zusammenzustellen. Wenn viele
Datensätze in eine Tabelle eingefügt werden, und ein Fehler tritt auf, würde die Datenbank automatisch die gesamten Einzelaktion zurücksetzen, sobald der Fehler auftritt,
doch wenn diese Aktion in Verbund mit anderen auftritt und bei einem Fehler alle Aktionen gemeinsam zurückgesetzt werden sollen, dann muss dies im T-SQL-Skript auch
genauso eingetragen sein.
5.6.1
Einfache Transaktionen
Einfache Transaktionssteuerung meint, dass Anfang und Ende einer Reihe von zusammenhängenden Anweisungen angegeben werden und dass ggf. die kompletten Anweisungen bestätigt oder zurückgesetzt werden. Dazu ist nicht viel mehr zu tun, als die
nachfolgenden, sehr einfachen Anweisungen für Transaktionen zu verwenden. Sie sind
in der Grundform in jeder Datenbank vorhanden. Aus Programmiersprachen, welche
außerhalb der Datenbank Software erstellen können, bieten ebenfalls ähnliche Anweisungen über die Datenbankschnittstelle an, sodass die Transaktionssteuerung sowohl
außen als auch innen durchgeführt werden kann.
5.6.1.1
Anweisungen
Um eine Transaktion zu beginnen, verwendet man BEGIN TRANSACTION. Dadurch
legt man auch einen Zeitpunkt oder einen Zustand fest, an dem die Daten logisch und
physisch konsistent sind. Dies betrifft insbesondere die referenzielle Integrität sowie
den Datenbestand. Sobald ein Fehler durch die Datenbank oder durch ein T-SQLProgramm identifiziert wird, können Anweisungen nach dieser Angabe zurückgesetzt
werden.
Man startet über diese Anweisung eine so genannte lokale Transaktion, die dann auch
zu einer Aufzeichnung führt, sobald Anweisungen aus dem DML-Bereich durchgeführt
361
Grundlagen T-SQL
werden. Andere Anweisungen, die keine Datenänderungen bewirken, können ohne
Aufzeichnung ausgeführt. Verschachtelte Transaktionen mit einem eigenen Namen
bedeuten keine Auswirkungen auf die äußere Transaktion und ermöglichen es auch
nicht, zu einem solchen Namen zurückzuspringen. Dies ist nur mit Sicherungspunkten
möglich. Ein Zurücksetzen der äußeren Transaktion setzt die gesamte Transaktion zurück. Eine lokale Transaktion wandelt die Datenbank in eine verteilte um, wenn vor
einer Bestätigung oder einem Zurücksetzen eine DML-Anweisung auf einer Remotetabellen (OLE DB-Anbieter unterstützt die ITransactionJoin-Schnittstelle nicht)
ausgeführt
wird
oder
eine
remote
gespeicherte
Prozedur
(REMOTE_PROC_TRANSACTIONS auf ON) ausgeführt wird.
Die allgemeine Syntax lautet:
BEGIN { TRAN | TRANSACTION }
[ { transaction_name | @tran_name_variable }
[ WITH MARK [ 'description' ] ]
]
Folgende Parameter kann man verwenden:
ƒ
transaction_name enthält den Namen der Transaktion. Es wird empfohlen,
Namen nur beim äußersten Paar von geschachtelten Transaktionen zu verwenden.
ƒ
@tran_name_variable enthält als Variablen einen Transaktionsnamen. Sie hat
einen der Datentypen char, varchar, nchar oder nvarchar.
ƒ
WITH MARK [ 'description' ] legt fest, dass die Transaktion im Protokoll
markiert wird,wobei description eine beschreibende Unicode-Zeichenkette mit 255
Zeichen und eine Nicht-Unicode-Zeichenkette mit 510 Zeichen ist. Die
Speicherung erfolgt in der msdb.dbo.logmarkhistory-Tabelle. Sobald diese
Einstellung verwendet wird, muss auch ein Transaktionsname angegeben werden,
wobei man hier zu dieser Markierung zurücksetzen kann.
362
Grundlagen T-SQL
Um eine Transaktion zu beenden, verwenden man den COMMIT-Befehl, der in einer
Kurzfassung (SQL 92) und einer Langfassung möglich ist. Die Langfassung ermöglicht
es dann auch, den Namen einer Transaktion wieder aufzugreifen, der wie zuvor als
Zeichenkette oder als Variable angegeben werden kann. Die allgemeine Syntax beider
Befehle lautet:
COMMIT [ WORK ]
COMMIT { TRAN | TRANSACTION }
[ transaction_name | @tran_name_variable ] ]
Um eine Transaktion und damit sämtliche Aktivitäten, die seit dem letzten BEGIN ausgeführt wurden, wieder zurückzusetzen verwendet man ROLLBACK, welches ebenfalls in
einer Kurzform (SQL 92) und einer Langform existiert. Sie unterscheiden sich wieder
darin, dass man bei der Langform noch den Namen der Transaktion oder einen Sicherungspunkt (Zwischenstand) angeben kann. Sie kann kein COMMIT überspringen, sondern setzt nur bis zum letzten COMMIT zurück.
ƒ
Setzt man Anweisungen in einer Prozedur zurück, wirkt sich dies auf alle
Anweisungen bis zum äußersten Transaktionsbeginn aus. Die im Batch vor dem
Prozeduraufruf angegebenen Anweisungen werden also ebenfalls zurückgesetzt.
Die Batch-Anweisungen nach dem Aufruf werden weiterhin ausgeführt.
ƒ
Wird in einem Trigger eine Kette von Anweisungen zurückgsetzt, werden neben
allen Datenänderungen, die bis zum automatischen Trigger-Aufruf durchgeführt
wurden, auch die Änderungen vom Trigger zurückgesetzt. Sofern Anweisungen
nach der ROLLBACK-Anweisung im Trigger verbleiben, werden diese weiterhin
ausgeführt. Lediglich die Anweisungen nach der Trigger-Ausführung, d.h. die im
äußeren Programm, werden nicht ausgeführt.
ƒ
Auf einen Cursor wirkt sich ein Zurücksetzen folgendermaßen aus, wobei hier drei
Regeln angegeben werden können.
–
Sofern CURSOR_CLOSE_ON_COMMIT den Wert ON hat, schließt man alle offenen Cursor, belässt allerdings die Referenzen.
363
Grundlagen T-SQL
–
Sofern CURSOR_CLOSE_ON_COMMIT den Wert OFF hat, wirkt sich das Zurücksetzen nicht auf geöffnete synchrone STATIC- oder INSENSITIVE-Cursor oder
vollständig gefüllte asynchrone STATIC-Cursor aus. Andere offene Cursor
werden wie zuvor behandelt.
–
Sobald ein Fehler einen Batch beendet und ein Rollback auslöst, werden die
Referenzen gelöscht. Dies ist Typ-unabhängig und berücksichtigt nicht CURSOR_CLOSE_ON_COMMIT. Dies reicht auch auf Cursor von Prozeduren. Die
gleichen Regeln gelten für in Triggern angegebene ROLLBACK-Anweisungen.
ROLLBACK [ WORK ]
ROLLBACK { TRAN | TRANSACTION }
[ transaction_name | @tran_name_variable
| savepoint_name | @savepoint_variable ]
5.6.1.2
Funktionen
Schließlich gibt es noch zwei Funktionen, die im Rahmen der Transaktionsverwaltung
nützlich sind:
ƒ
364
XACT_STATE() liefert den aktuellen Transaktionsstatus einer Sitzung zurück.
Folgende Werte sind möglich:
–
1 gibt an, dass die Sitzung eine aktive Transaktion aufweist. In diesem Fall
können Änderungen bestätigt und zurückgesetzt werden.
–
0 gibt an, dass in der Sitzung keine Transaktion aktiv ist.
–
-1 gibt an, dass die Sitzung eine aktive, fehlerhafte Transaktion aufweist.
Durch einen Fehler können Anweisungen weder bestätigt noch zu einem Sicherungspunkt zurückgesetzt werden. Allerdings kann die gesamte Transaktion
zurückgesetzt werden, was auch dazu führt, dass man wieder Schreibvorgänge
durchführen kann, denn in diesem Zustand sind nur Lesevorgänge zulässig.
Grundlagen T-SQL
Solche Situation können im Rahmen der Ausnahmebehandlung auftreten und
gelöst werden.
ƒ
@@TRANCOUNT liefert einen Statuswert zurück, ob eine Transaktion geöffnet ist
oder nicht. Eine BEGIN TRANSACTION-Anweisung erhöht den Zähler um 1,
während ROLLBACK ihn wieder auf 0 zurücksetzt. Der Rücksprung zu einem
Sicherungspunkt führt keine Änderung am Zahlwert aus. Die Anweisung COMMIT
reduziert den Wert um 1.
5.6.1.3
Beispiele
Für die verschiedenen Beispiele setzt man noch einmal die schon mehrfach verwendete
Product3-Tabelle mit folgender Struktur ein. Sie wird Skript 561_01.sql ebenfalls erstellt.
CREATE TABLE Production.Product3 (
Name
varchar(30)
NOT NULL,
ProductNumber nvarchar(25) NOT NULL,
ListPrice
money
NOT NULL,
StandardCost
money
NULL)
Das erste Beispiel zeigt eine einfache Transaktion, die als Grundlage für weitere Beispiele benutzt werden kann. Das einfachste denkbare Beispiel besteht nur aus BEGIN
TRAN und COMMIT oder ROLLBACK, was allerdings schon bei einigen Beispielen zu
DML-Befehlen zum Einsatz gekommen ist. In diesem Fall ist die Transaktion auch
benannt und kann über ihren Namen später zurückgesetzt werden. Dabei mischt man die
Möglichkeiten, den Namen als Zeichenkette vorzugeben oder aus einer Variable abzurufen.
-- Transaktionsnamen als Variable erstellen
DECLARE @TranName varchar(20)
SET @TranName = 'Produkterfassung'
365
Grundlagen T-SQL
-- Transaktion starten
BEGIN TRANSACTION @TranName
-- DML-Anweisungen ausführen
DELETE FROM Production.Product3
INSERT INTO Production.Product3
VALUES ('LL Mountain Seat Assembly', 'SA-M198', 133.34,
98.77)
INSERT INTO Production.Product3
VALUES ('ML Mountain Seat Assembly', 'SA-M237', 147.14,
108.99)
-- Test: Eingetragene Datensätze und Status
SELECT COUNT(*) AS Eingetragen,
@@TRANCOUNT AS [T-Count],
XACT_STATE() AS Status
FROM Production.Product3
-- Bestätigen
COMMIT TRANSACTION Produkterfassung
-- Test: Eingetragene Datensätze und Status
SELECT COUNT(*) AS Eingetragen,
@@TRANCOUNT AS [T-Count],
XACT_STATE() AS Status
FROM Production.Product3
366
Grundlagen T-SQL
561_01.sql: Erstellung und Bestätigung einer Transaktion
Als Ergebnis der beiden Testabfragen erhält man folgende Ergebnisse. Die eingetragenen Datensätze bleiben auf den Wert von 2 stehen, da die Transaktion ja bestätigt wird.
Führt man das gleiche Beispiel mit ROLLBACK aus, so stünde hier in der zweiten Ergebnismenge der Wert 0. In der Funktion @@TRANCOUNT befindet sich zunächst der Wert 1,
da eine Transaktion geöffnet wurde, und dann der Wert 0, weil sie wieder beendet wurde. Ähnliches bedeuten auch die Werte, die von XACT_STATE zurück geliefert wurden.
Beim ersten Schritt ist eine Transaktion aktiv, beim zweiten Schritt nicht mehr.
Eingetragen T-Count
Status
----------- ----------- -----2
1
1
2
0
0
Im nächsten Beispiel sieht man, wie zwei Transaktionen ineinander verschachtelt werden. Dabei lohnt es sich nur, für die äußere auch einen Namen zu vergeben, da ein Rollback zu einer inneren nicht möglich ist und daher der Name nicht für diesen Zweck
genutzt werden kann. Dies lässt sich mit den später darzustellenden Sicherungspunkten
realisieren. Dennoch lässt sich eine innere Transaktion bestätigen und dann die äußere
Transaktion zurücksetzen, sodass auch die Anweisungen der inneren zurückgesetzt
werden. Genau dies zeigt das Beispiel. Zunächst löscht man die zwei Datensätze aus
dem vorherigen Beispiel in einer eigenen Transaktion, trägt dann in einer eigenen
Transaktion einen neuen Datensatz ein, was auch bestätigt wird, um dann schließlich
wieder die äußere Transaktion zurückzusetzen, was dazu führt, dass die ursprünglichen
zwei Datensätze wieder in der Tabelle gespeichert sind.
-- Äußere Transaktion
BEGIN TRANSACTION Produktbearbeitung
DELETE FROM Production.Product3
-- Test: 0 Datensätze (zwei gelöscht)
367
Grundlagen T-SQL
SELECT COUNT(*) AS Eingetragen FROM Production.Product3
-- Innere Transaktion (ROLLBACK nicht möglich)
BEGIN TRANSACTION
INSERT INTO Production.Product3
VALUES ('LL Mountain Seat Assembly', 'SA-M198', 133.34,
98.77)
-- Test: 1 Datensatz
SELECT COUNT(*) AS Eingetragen FROM Production.Product3
-- Bestätigung des einen Datensatzes
COMMIT TRANSACTION
-- Zurücksetzen der äußeren Transaktion
ROLLBACK TRANSACTION Produktbearbeitung
-- Test: 2 Datensätze (aus vorherigem Beispiel)
SELECT COUNT(*) AS Eingetragen FROM Production.Product3
561_02.sql: Verschachtelte Transaktionen
Das nächste Beispiel zeigt den Standardfall, wie die Transaktionsverwaltung im Rahmen der Ausnahmebehandlung verwendet wird. Außerhalb eines BEGIN..CATCHBereichs beginnt man eine Transaktion, um dann bei einem Fehler die Transaktion
zurückzusetzen. Dabei testet man mit Hilfe der @@TRANCOUNT-Funktion, ob überhaupt
eine Transaktion existiert, um einen Fehler zu vermeiden. Sollte kein Fehler aufgetreten
sein, folgt nach dem BEGIN..CATCH-Bereich die gewöhnliche Bestätigung, da ja offensichtlich die versuchte Aktion erfolgreich war.
368
Grundlagen T-SQL
BEGIN TRANSACTION
BEGIN TRY
DELETE FROM Production.UnitMeasure
WHERE UnitMeasureCode = 'CM'
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
PRINT 'Transaktion zurückgesetzt'
ROLLBACK TRANSACTION
END CATCH
IF @@TRANCOUNT > 0 BEGIN
PRINT 'Transaktion bestätigt'
COMMIT TRANSACTION
END
561_03.sql: Transaktion in Ausnahmebehandlung
Mit der Anweisung SET XACT_ABORT { ON | OFF } legt man fest, ob eine Transaktion automatisch zurückgesetzt werden soll, wenn ein Laufzeitfehler entsteht. Dies betrifft genau die Fehler, welche auch im Rahmen einer Ausnahmebehandlung erkannt
und daher auch innerhalb des CATCH-Blocks behandelt werden können. Das nächste
Beispiel zeigt, wie zunächst für die Sitzung die beschriebene Einstellung aktiviert wird
und wie dann innerhalb des CATCH-Blocks über die Funktion XACT_STATE() abgerufen wird, ob die Anweisungen bestätigt werden können oder nicht. Durch die vorherige
Einstellung sind sie allerdings nicht commitfähig, sodass in der Fallunterscheidung ein
ROLLBACK stattfindet.
369
Grundlagen T-SQL
SET XACT_ABORT ON
BEGIN TRANSACTION
BEGIN TRY
DELETE FROM Production.UnitMeasure
WHERE UnitMeasureCode = 'CM'
END TRY
BEGIN CATCH
IF XACT_STATE() = -1 BEGIN
PRINT N'T nicht zu bestätigen. ROLLBACK notwendig.'
ROLLBACK TRANSACTION
END
IF XACT_STATE() = 1 BEGIN
PRINT N'T zu bestätigen. COMMIT möglich.'
COMMIT TRANSACTION;
END
END CATCH
561_03.sql: Transaktion in Ausnahmebehandlung
5.6.2
Sicherungspunkte
Wie schon gerade erwähnt, kann man innere Transaktionen nicht zurücksetzen. Um
allerdings doch eine Möglichkeit zu besitzen, verschiedene einzelne Zwischenstationen
zu speichern und auch zu ihnen zurückzukehren, gibt es die SAVE-Anweisung, mit der
so genannte Sicherpunkte erstellt werden können. Sie erhalten einen Namen, der wie bei
Transaktionsnamen aus einer Variable oder einer Zeichenkette stammen kann.
370
Grundlagen T-SQL
SAVE { TRAN | TRANSACTION }
{ savepoint_name | @savepoint_variable }
Um eine Transaktion schließlich zu einem Sicherungspunkt zurückzusetzen, ist die
erweiterte Form der ROLLBACK-Anweisung zu verwenden. Sie erwartet zusätzlich den
Namen eines Sicherungspunktes.
ROLLBACK { TRAN | TRANSACTION }
[ transaction_name | @tran_name_variable
| savepoint_name | @savepoint_variable ]
Das nächste Beispiel zeigt die Sicherungspunkte in Aktion und stellt damit auch eine
Variation des vorherigen Beispiels dar. Es gibt dieses Mal nur eine einzige Transaktion.
Die vorherige innere Transaktion wurde ungefähr durch die Sicherungspunkte ersetzt,
sodass nun nach dem Lösch- und dem Eintragungsvorgang jeweils ein solcher Sicherungspunkt angelegt wird. Dadurch kann man später entweder den Zustand wiederherstellen, in dem die Tabelle leer war oder in dem ein Datensatz eingetragen war. Um
auch zu zeigen, dass man Sicherungspunkte überspringen kann, entscheidet man sich
bei der ROLLBACK-Anweisung für den ersten und damit vorletzten Sicherungspunkt.
Dies führt dazu, dass eine leere Tabelle zurückbleibt.
-- Transaktion starten
BEGIN TRANSACTION Produktbearbeitung
DELETE FROM Production.Product3
-- Test: 0 Datensätze
SELECT COUNT(*) AS Eingetragen FROM Production.Product3
-- Zustand speichern
SAVE TRANSACTION Geloescht
INSERT INTO Production.Product3
371
Grundlagen T-SQL
VALUES ('LL Mountain Seat Assembly', 'SA-M198', 133.34,
98.77)
-- Test: 1 Datensatz
SELECT COUNT(*) AS Eingetragen FROM Production.Product3
-- Zustand speichern
SAVE TRANSACTION Eingetragen
-- Zurücksetzen der Transaktion zum vorherigen Zustand
ROLLBACK TRANSACTION Geloescht
-- Test: 0
SELECT COUNT(*) AS Eingetragen FROM Production.Product3
562_01.sql: Sicherungspunkte einsetzen
5.6.3
Erweiterte Transaktionssteuerung
Unabhängig von den Anweisungen, die bislang diskutiert wurden, besteht noch die
Möglichkeit, innerhalb einer Verbindung erweiterten Einfluss auf das Verhalten zu
nehmen, wie Zeilensperren und Zeilen versioniert werden, oder um verteilte Transaktionen einzurichten.
5.6.3.1
Isolationsstufen
Dazu gibt es die Anweisung SET TRANSACTION ISOLATION LEVEL, welche eine von
fünf verschiedenen Arten anbietet, dieses Verhalten zu steuern. Es ist also nicht möglich, mehrere Isolationsstufenoptionen auf einmal festzulegen; sie gelten alternativ. Man
kann innerhalb einer Verbindung diese Stufe wechseln, ansonsten ist sie für die gesamte
Sitzung gültig.
Die allgemeine Syntax der Anweisung lautet:
SET TRANSACTION ISOLATION LEVEL
372
Grundlagen T-SQL
{ READ UNCOMMITTED
| READ COMMITTED
| REPEATABLE READ
| SNAPSHOT
| SERIALIZABLE
}
Sofern man eine Isolationsstufe in einer Prozedur/Funktion oder einem Trigger/benutzerdefinierten Typ vorgibt, gilt innerhalb dieses Aufrufs die angegebene Stufe.
Sie wird dann wieder zurückgesetzt, wenn das aufgerufene Objekt die Steuerung zurückgibt.
Die Bedeutung und Funktionsweise der verschiedenen Angaben ist nachfolgende erläutert:
ƒ
READ UNCOMMITTED legt fest, dass Zeilen, die gerade von anderen Transaktionen
bearbeitet werden und noch nicht bestätigt sind, ebenfalls gelesen werden können
(genaues Gegenteil von READ COMMITTED).
ƒ
READ COMMITTED legt fest, dass Zeilen, die gerade von anderen Transaktionen
bearbeitet werden und noch nicht bestätigt sind, nicht gelesen werden können
(genaues Gegenteil von READ UNCOMMITTED). Diese Stufe ist von den
Einstellungen in der Datenbankoption READ_COMMITTED_SNAPSHOT abhängig:
Der Wert OFF (Standard) sorgt dafür, werden automatisch gemeinsame Sperren
verwendet, um die Änderung durch andere Transaktionen von gerade gelesenen
Datensätzen
zu verhindern. Der Wert ON sorgt dafür, dass die so genannte
Zeilenversionsverwaltung eingesetzt wird. Hier wird der aktuellen Transaktion ein
konsistenter Datenzustand vor Anweisungsbeginn gezeigt. Für Aktualisierungen
werden keine Sperren verwendet.
ƒ
REPEATABLE READ legt fest, dass dass Zeilen, die gerade von anderen
Transaktionen bearbeitet werden und noch nicht bestätigt sind, nicht gelesen
373
Grundlagen T-SQL
werden können. Zusätzlich sind auch die Datensätze, die von der aktuellen
Transaktion bearbeitet werden, für andere Transaktionen so lange unsichtbar, bis
sie bestätigt werden.
ƒ
SNAPSHOT legt fest, dass die Daten, die in einer Transaktion gelesen werden, einem
konsistenten Zustand der Datenbank entsprechen, welcher zu Beginn der
Transaktion gegeben war. Nur bestätigte Daten können so gelesen werden. Dies
sorgt dafür, dass andere Transaktionen unbeeinflusst Daten schreiben können und
dass die SNAPSHOT-Transaktion unbeeinflusst lesen kann. Die Datenbankoption
ALLOW_SNAPSHOT_ISOLATION muss auf ON gestellt sein.
ƒ
SERIALIZABLE legt fest, dass Anweisungen keine von anderen Transaktionen
geänderte Daten lesen können, die noch nicht bestätigt sind, dass andere
Transaktionen auch erst nach eigener Bestätigung geänderte Daten lesen können
und dass andere Transaktionen erst nach der aktuellen Transaktion Daten mit neuen
Schlüsselwerten einfügen können, die für die aktuelle Transaktion reserviert waren.
Die nachfolgende Tabelle gibt eine Übersicht über die verschiedenen Einstellungen und
die zulässigen Parallelitätsnebeneffekte wider. Dabei sind in der Kopfzeile drei Begriffe
eingetragen, die genau solche Parallelitätsnebeneffekte bezeichnen. Die drei Phänomene
beschreiben zwar durchaus unterschiedliche Situationen, sind aber natürlich aufgrund
der Voraussetzung, dass sie Lese- und Schreibvorgänge beschreiben, sehr vergleichbar.
ƒ
Dirty Read oder verlorene Aktualisierung: Transaktion A liest Daten, die von
Transaktion B geändert und nicht bestätigt wurden. Dadurch kann es geschehen,
dass die Daten nachträglich wieder geändert werden.
ƒ
Nicht wiederholbarer Lesevorgang: Wiederholte Lesevorgänge liefern verschiedene
Ergebnisse.
ƒ
Phantom: Suchergebnisse ändern sich bei wiederholter Ausführung, weil
Änderungen am Datenbestand vorgenommen werden. Transaktion A liest
Datensätze, die von Transaktion B geändert werden. Beim nächsten Abruf von
Transaktion A befinden sich bei gleichen Filtern andere Datensätze in der
Ergebnismenge.
374
Grundlagen T-SQL
Allgemeine gilt immer, dass der Wunsch nach wenigen Nebeneffekten mit höherem
Systemressourcenverbrauch einher geht, sodass hier bei intensiver Nutzung von Transaktionsvorgaben von Fall zu Fall das notwendige Maß verwendet werden muss.
Isolationsstufe
Dirty Read Nicht wiederholbarer Lesevorgang Phantom
Read Uncommitted Ja
Ja
Ja
Read Committed
Nein
Ja
Ja
Repeatable Read
Nein
Nein
Ja
Snapshot
Nein
Nein
Nein
Serializable
Nein
Nein
Nein
Parallelitätsnebeneffekte
5.6.3.2
Verteilte Transaktionen
Neben den Transaktionen, die nur auf einem Server ausgeführt werden, gibt es auch so
genannte verteilte Transaktionen, die auf mehreren Servern ausgeführt werden können.
Die Server-Instanz, welche die BEGIN DISTRIBUTED TRANSACTION-Anweisung
abschickt, wird dabei als Transaktionsurheber bezeichnet. Sie ist für die Ausführung
und den Abschluss dieser Transaktion verantwortlich. Durch diese Anweisung werden
alle Anweisungen, die auf anderen Servern ausgeführt werden - darunter auch gespeicherte Prozeduren etc. - innerhalb der gleichen verteilten Prozedur ausgeführt.
BEGIN DISTRIBUTED { TRAN | TRANSACTION }
[ transaction_name | @tran_name_variable ]
[ ; ]
375
Grundlagen T-SQL
376
Analysen
6 Analysen
377
Analysen
378
Analysen
6 Analysen
Um umfangreiche Analysen auf Datenbeständen auszuführen, genügt oft das gewöhnliche SQL nicht mehr, was in den zurückliegenden Kapiteln dargestellt wurde und das in
ähnlicher Form in den meisten Datenbanken in dieser Form eingesetzt werden kann. Mit
einigen weiteren Besonderheiten und natürlich auch mit der Syntax, die im T-SQLKapitel vorgestellt wurde, kann man Ergebnisse produzieren, die ansonsten nur mit
umfangreichen Programmen, Einsatz von Arrays, Vergleichen und Schleifen erzeugt
werden könnten. Dieses Kapitel soll nun für eine Reihe von typischen Anwendungsfällen Lösungen bieten. Einige dieser Lösungen sind bereits in anderen Büchern beschrieben worden, da sie nicht nur für MS SQL Server genutzt werden können, sondern von
ihren SQL-Fundamenten her auch in anderen Datenbanksystemen genutzt werden können. Allerdings ist die genaue Syntax doch in jedem Datenbanksystem anders bzw.
teilweise lassen sich für die gleiche Problemstellung einfachere Wege beschreiten, weil
diverse Zusatzhilfsmittel bereit stehen. Viele Lösungen können auch schon in der Vorgängerversion eingesetzt werden, wobei allerdings der Fokus der gesamten Beispiele
durchaus auf der Version 2005 liegt.
6.1
Tabellenausdrücke
Die abgeleiteten Tabellen, also die Unterabfragen in der FROM-Klausel,, sind in den
meisten Datenbanken, welche Unterabfragen nicht erlauben, nicht nutzbar. Diese Abfragen wären dagegen auch in Oracle nutzbar. In diesem Kapitel soll aber der Fokus auf
speziellen Techniken liegen, die gerade nur im MS SQL Server so genutzt werden können, weswegen eine sehr ähnliche Lösung erst hier erwähnt wird, obwohl sie zu den
interessantesten Neuerungen im MS SQL Server 2005 gehört, was T-SQL anbetrifft.
Mit den Tabellenausrücken oder Common Table Expressions, welche die Dokumentation und die Microsoft-Werbung mit dem Akronym CTE abkürzt, steht eine Alternative
zur Verfügung, welche vermutlich relativ schnell die abgeleiteten Tabellen ersetzen
dürfte, da sie zu besser lesbarem Quelltext führt und einige Schwierigkeiten, die mit
379
Analysen
abgeleiteten Tabellen bestehen, löst. Grundsätzlich sind CTEs Fall deckungsgleich mit
abgeleiteten Tabellen, bieten jedoch einen darüber hinaus gehenden größeren Nutzen.
6.1.1
Grundprinzip
Ein allgemeiner Tabellenausdruck erfüllt die gleiche Funktionalität wie eine abgeleitete
Tabelle und kann in vielen Fällen den Einsatz von temporären Tabellen zur Speicherung
von Zwischenergebnissen ersetzen. Er erlaubt es, eine temporäre Ergebnismenge zu
erzeugen, die mit Hilfe einer beliebigen Abfrage definiert wird, die nur wenige, unwesentliche Einschränkungen besitzt. Im Gegensatz zu einer abgeleiteten Tabelle ist es
möglich, mehrfache Referenzen auf eine solche CTE zu formulieren, sie rekursiv aufzurufen und sie darüber hinaus in solchen Anweisungen wie INSERT, UPDATE und DELETE zu verwenden. Wie eine abgeleitete Tabelle kann sie auch innerhalb von CREATE
VIEW genutzt werden, um die Ergebnismenge einer Sicht festzulegen.
Das neu eingeführte Schlüsselwort ist WITH, welches erfordert, dass vorherige Anweisungen mit einem Semiklon abgeschlossen werden. Dies kennzeichnet auch eine deutliche Fehlermeldung. Diesem Schlüsselwort folgt der Name, unter dem die temporär
erzeugte Ergebnismenge wie eine abgeleitete Tabelle oder eine temporäre Tabelle angesprochen werden kann. Die Spaltennamen kann man innerhalb runder Klammern direkt
angeben oder aus den Spalten(alias)namen erzeugen lassen. Die allgemeine Syntax hat
die folgenden Form:
[ WITH <common_table_expression> [ ,...n ] ]
<common_table_expression>::=
expression_name [ ( column_name [ ,...n ] ) ]
AS
( CTE_query_definition )
Ein einfaches Beispiel soll das Grundprinzip der CTE illustrieren. Die nachfolgende
CTE erstellt unter dem Namen Sales eine Ergebnismenge mit den Spaltennamen
Customer, Turnover, Region und Year. Sie werden aus den Spaltenaliasnamen ab-
380
Analysen
geleitet. Würden diese fehlen, so bedeutete dies lediglich für die letzte Spalte ein Problem, da sie unter Verwendung einer Funktion beschrieben wird und daher einen Aliasnamen benötigt. Auch wenn die Abfrage, welche Sales erstellt, sehr einfach ist, so
kann man sich sicherlich komplizierte Berechnungen, beliebige Unterabfragen und
natürlich eine WHERE-Klausel denken.
Die so erstellte CTE lässt sich dann wie eine gewöhnliche Tabelle innerhalb der FROMKlausel der direkt folgenden Abfrage nutzen. Man kann gut erkennen, wie die einzelnen
Spaltennamen der CTE in SELECT aufgerufen, für Aggregate genutzt und umbenannt
bzw. auch in GROUP BY verwendet werden. Sie lassen sie sich genauso einsetzen wie
sonstige Spaltennamen.
WITH Sales AS (
SELECT CustomerID
AS Customer,
TotalDue
AS Turnover,
TerritoryID
AS Region,
YEAR(OrderDate) AS Year
FROM Sales.SalesOrderHeader
)
SELECT Year,
COUNT(Customer) AS Customers,
SUM(Turnover) AS Turnover
FROM Sales
GROUP BY Year
611_01.sql: Allgemeine Funktionsweise von CTEs
381
Analysen
Man erhält zunächst als Ergebnis durch die CTE eine Aufstellung der Kundenanzahl
und des Gesamtumsatzes pro Region und Jahr.
Customer
Turnover
Region
Year
----------- --------------------- ----------- ----------676
27231,5495
5
2001
117
1716,1794
5
2001
209,9169
6
2004
...
18759
(31465 Zeile(n) betroffen)
Das endgültige Ergebnis ist dann eine Darstellung der Jahre, der Kundenanzahl und des
Gesamtumsatzes – ein Ergebnis, das zwar auch mit anderen Mitteln erstellt werden
kann, welches aber nicht besonders tief verschachtelt war und letztendlich einfacher
formuliert und verstanden werden konnte.
Year
Customers
Turnover
----------- ----------- --------------------2004
13951
32196912,4165
2001
1379
14327552,2263
2002
3692
39875505,095
2003
12443
54307615,0868
(4 Zeile(n) betroffen)
Folgende allgemeine Regeln gelten für den Einsatz von CTEs:
ƒ
382
Nach dem allgemeinen Tabellenausdruck muss eine DML-Anweisung folgen,
welche sich auf eine Unterauswahl der durch die CTE definierten Spalten bezieht.
Analysen
ƒ
Verweise auf andere CTE oder sich selbst sind möglich, wenn die referenzierten
CTEs zuvor definiert wurden.
ƒ
CTEs dürfen nicht geschachtelt erscheinen, d.h. innerhalb von WITH kann nicht ein
weiteres WITH verwendert werden.
ƒ
Wie auch bei Sichten ist die Verwendung folgender Schlüsselwörter verboten:
COMPUTE oder COMPUTE BY, ORDER BY ohne vorherige TOP-Klausel, INTO,
OPTION-Klausel mit Abfragehinweisen, FOR XML und FOR BROWSE.
ƒ
Eine CTE kann für eine Cursor-Definition zum Einsatz kommen.
ƒ
Tabellen auf Remoteservern können aufgerufen werden.
Im nächsten Beispiel definiert man die Spaltennamen der CTE gerade nicht mit Hilfe
der Aliasnamen oder ermittelt sie direkt aus den abgerufenen Spaltennamen, sondern
setzt sie runden Klammern direkt nach dem Namen der CTE. Dabei müssen die Spaltenreihenfolge und -anzahl mit den abgerufenen Spalten übereinstimmen, um eine sinnvolle und korrekte Datenstruktur zu erzeugen.
WITH Sales (Customer, Turnover, Region, Year) AS (
SELECT CustomerID, TotalDue, TerritoryID, YEAR(OrderDate)
FROM Sales.SalesOrderHeader
)
SELECT Year,
COUNT(Customer) AS Customers,
SUM(Turnover)
AS Turnover
FROM Sales
GROUP BY Year
611_02.sql: Explizite Angabe von Spaltennamen
383
Analysen
Es ist auch möglich, dynamisch die tatsächliche Ergebnismenge einer CTE zu beeinflussen. Dazu setzt man im nächsten Beispiel ganz einfach eine Variable ein, welche
einen festen Wert hat. Dies ist zwar nicht sonderlich dynamisch, doch man kann sich
vorstellen, dass dieser Wert auch ein Funktion-/Prozedurparameter sein könnte. Diese
Variable enthält nun den Wert einer Jahreszahl, welches innerhalb der WHERE-Klausel
als Filter zum Einsatz kommt. An diesem Beispiel kann man auch sehr schön die Notwendigkeit sehen, eine vorherige Anweisung mit einem Semikolon abzuschließen, um
keinen Fehler auszulösen.
DECLARE @year int
SET @year = 2002;
WITH Sales (Customer, Turnover, Region, Year) AS (
SELECT CustomerID, TotalDue, TerritoryID, YEAR(OrderDate)
FROM Sales.SalesOrderHeader
WHERE YEAR(OrderDate) >= @year
)
SELECT Year,
COUNT(Customer) AS Customers,
SUM(Turnover) AS Turnover
FROM Sales
GROUP BY Year
611_03.sql: Dynamische CTE
6.1.2
Erweiterte Tabellenausdrücke
Nach den einleitenden Beispielen, welche vor allen Dingen die neue und in anderen
Datenbanksystemen unbekannte Syntax einführten, folgen nun einige anspruchsvollere
384
Analysen
Beispiele, welche die Flexibilität und den Nutzen von CTEs besser herausstellen können.
Das erste Beispiel erstellt nicht nur eine einzige CTE, sondern stattdessen folgt dem
ersten allgemeinen Tabellenausdruck nach einem Komma ein weitere, wobei hier einfach nur der Name und das AS-Schlüsselwort angeschlossen werden. Interessant ist hier,
dass diese beiden CTEs später sogar miteinander verknüpft werden. Es wäre genauso
möglich gewesen, dass die zweite CTE SalesPerson die erste CTE Sales aufruft.
Die Verknüpfung macht deutlich, wie solche allgemeinen Tabellenausdrücke sehr komplexe SQL-Anweisungen zu vereinfachen helfen, da man zunächst die Rohdaten bzw. in
diesem Fall sogar zwei verschiedene Menge an Rohdaten besorgt, welche dann später
genutzt werden können. Die einzelnen Abfragen lassen sich dann sogar getrennt testen
(markieren, Anweisung ausführen), sofern keine Verweise auf Variablen oder andere
CTEs etc. existieren.
WITH Sales (Customer, Turnover, Region, Year) AS (
SELECT CustomerID, TotalDue, TerritoryID, YEAR(OrderDate)
FROM Sales.SalesOrderHeader
),
SalesPerson AS (
SELECT LastName AS Name, TerritoryID AS Region
FROM Person.Contact AS c
INNER JOIN HumanResources.Employee AS emp
ON c.ContactID = emp.ContactID
INNER JOIN Sales.SalesPerson AS sp
ON emp.EmployeeID = sp.SalesPersonID
)
SELECT Name, COUNT(Customer) AS Customers
385
Analysen
FROM Sales AS s INNER JOIN SalesPerson AS sp
ON s.Region = sp.Region
GROUP BY Name
612_01.sql: Mehrfache CTEs
In diesem Fall ermittelte die erste CTE die Kundennummer, den zugehörigen Gesamtumsatz, Region und Jahr aus der Tabelle SalesOrderHeader, während die zweite
CTE den Nachnamen und die Region aus der Tabelle SalesPerson abrief. Diese beiden Ergebnismengen lassen sich dann anhand der TerritoryID, welche in der neu
erzeugten Region-Spalte enthalten ist, verknüpfen, um ganz einfach die Namen der
Verkäufer und die Anzahl der Kunden auszugeben.
Name
Customers
-------------------------------------------------- ---------Mensa-Annan
4594
Campbell
4594
Reiter
486
Während zwar die letzte Formulierung mit abgeleiteten Tabellen noch möglich gewesen
wäre, aber schon sehr exotisch wirkt und mehr verstört als zur gemütlichen Lektüre
einlädt, lässt sich die Formulierung aus dem nachfolgenden Beispiel nur mit einer CTE
so einfach formulieren. Wiederum erzeugt man die schon bekannte CTE Sales, wobei
allerdings dieses Mal eine Aggregation stattfindet und nur noch die Spalten Turnover
für den Gesamtumsatz, Region und Year übrig bleiben. Diese Werte werden nun benutzt, um die Umsatzzahlen des aktuellen Jahres mit den Werten des vorherigen Jahres
zu vergleichen. Eine Lösung für die Version 2000 bestand daraus, anstelle eine CTE
eine temporäre Tabelle zu erstellen, um die Beziehung cur.Year = prv.Year + 1
einzurichten, d.h. um die temporäre Ergebnismenge mehrfach zu referenzieren. Eine
abgeleitete Tabelle ist hier leider gar nicht nützlich, sodass tatsächlich ohne CTE nur die
386
Analysen
Erstellung eines ganzen T-SQL-Skripts übrig bleibt. Setzt man allerdings allgemeine
Tabellenausdrücke, vereinfacht sich die gesamte Arbeit auf eine Abfrage, in der man
ganz einfach die Rohdaten mehrfach referenziert und so das Jahr 2002 mit dem Jahr
2000+1 in Beziehung setzt, um für die gleiche Region den Vorjahresumsatz zu ermitteln. Darüber hinaus ist hier auch noch ein OUTER JOIN erforderlich, wenn Werte für
Regionen aus dem aktuellen Jahr auch dann in die Ergebnismenge übernommen werden
sollen, die noch gar nicht im vorherigen Jahr bestanden oder abgerufen werden können,
was in jedem Fall für das erste Jahr der Fall ist.
WITH Sales (Turnover, Region, Year) AS (
SELECT SUM(TotalDue), TerritoryID, YEAR(OrderDate)
FROM Sales.SalesOrderHeader
GROUP BY YEAR(OrderDate), TerritoryID
)
SELECT cur.Year, cur.Region, cur.Turnover, prv.Turnover
FROM Sales AS cur LEFT OUTER JOIN Sales AS prv
ON cur.Year = prv.Year + 1
AND cur.Region = prv.Region
WHERE cur.Region BETWEEN 5 AND 7
ORDER BY cur.Year
612_02.sql: Mehrfache Referenzen
Man erhält die gerade beschriebene Ausgabe der Umsatzzahlen in einer Kombination
aus Jahr und Region mit den Vorjahreswerten. Für das erste Jahr entstehen dabei NULLWerte in der Vorjahresumsatzspalte, weil es sich um das erste Jahr handelt. Ohne die
äußere Abfrage würden diese Werte unterdrückt werden. Die Werte für diese Jahre
387
Analysen
erscheinen dann allerdings als Vorjahreswerte für die gleiche Region in den folgenden
Zeilen, welche spätere Jahre abbilden.
Year
Region
Turnover
Turnover
----------- ----------- --------------------- --------------2001
6
2173647,1453
NULL
2001
7
199531,723
NULL
2001
5
1928148,6139
NULL
2002
5
3814944,144
1928148,6139
2002
6
7215430,5017
2173647,1453
2002
7
1717145,7439
199531,723
In vielen Datenmodellen werden Bäume innerhalb einer einzigen Tabelle abgebildet.
Das Schulbeispiel in diesem Zusammenhang ist immer ein Blick in die Personaltabelle,
welche möglicherweise eine der ManagerID-Spalte entsprechenden Spalte enthalten
könnte. Dies ist im Fall der AdventureWorks ebenfalls gegeben. Auch für solche Fälle, die Bäume oder Hierarchien abbilden sollen, lassen sich CTEs hervorragend einsetzen, wobei es sich dann um so genannte rekursive allgemeine Tabellenausdrücke handelt. Dabei gelten die folgenden Regelungen:
ƒ
Auch wenn die zu Grunde liegenden Spalten NOT NULL sind, so erlaubt die CTE
dennoch NULL-Werte.
ƒ
Es ist möglich, eine Endlos-Schleife einzurichten, wenn ein logischer Fehler
besteht. Dies geschieht, wenn gleiche Werte für über- oder untergeordnete Objekte
miteinander verglichen werden. Eine solche Rekursion lässt sich über die Anzahl
der Rekursionsebenen (ähnlich Schleifeniterationen) beschreiben. Mit Hilfe des
Abfragehinweises MAXRECURSION und einem Wert zwischen 0 und 32767 lässt
sich eine definitive Grenze angeben.
388
Analysen
ƒ
Eine rekursive CTE lässt sich nicht für Datenaktualisierungen (UPDATE)
verwenden, da eine völlig neue Ergebnismenge entsteht, in welcher der gleiche
Datensatz mehrfach referenziert werden kann, wenn dies in der zu Grunde
liegenden Hierarchie so eingerichtet ist.
ƒ
Remotetabellen werden lokal zwischengespeichert (Spool), um den Netzwerkverkehr zu minimieren.
ƒ
Als Cursorarten sind nur schnelle Vorwärtscursor und statische (Snapshot) Cursor
möglich. Andere Cursor werden zu statischen Cursorn umgewandelt.
Das erwähnte Schulbeispiel lässt sich nun tatsächlich mit Hilfe der AdventureWorksDB nachvollziehen, denn die Angestellten, denen andere Angestellte untergeordnet sind
oder denen sie berichten müssen, sind in der ManagerID-Spalte gespeichert. Diese
enthält im Grunde genommen einfach nur einen Wert aus der EmployeeID-Spalte, der
an einer anderen Stelle zu den entsprechenden Daten des Mitarbeiters führt, welcher für
andere ein Vorgesetzter ist. Es handelt sich um eine umfangreiche Hierarchie, d.h. es
kann sein, dass auch dieser Angestellte wiederum einen Vorgesetzten hat und dass sein
Attribut ManagerID die EmployeeID eines anderen Angestellten aufweist. Dieser
gesamte Baum soll durch eine Abfrage unter Einsatz eines rekursiven allgemeinen Tabellenausdrucks ermittelt werden.
Zunächst erstellt man eine Unterauswahl aus der Employee-Tabelle, welche mit der
Contact-Tabelle verknüpft wird, um für eine leichtere Lektüre der Ergebnismenge
auch den Nachnamen des Angestellten zur Verfügung zu haben. Diese Menge für die
Angestellten mit laufenden Nummern 3 und 7 wird dann um die Menge mit gleichem
Aufbau (EmployeeID, ManagerID, LastName) ergänzt, wobei aufgrund des UNION
ALL-Operators auch Duplikate übernommen werden. Diese zweite Abfrage in der gleichen CTE steht mit der ersten Abfrage in einem Korrelationszusammenhang, weil hier
die ManagerID (Eltern-Knoten) aus der neu abgerufenen Employee-Tabelle mit der
EmployeeID (Kind-Knoten) aus der ersten Abfrage in einem INNER JOIN verknüpft
wird. Dies ist dann nicht nur eine Korrelation, sondern eine Rekursion, weil die Abfrage
sich selbst mit Hilfe der CTE-Technik referenzieren lässt. Dadurch befinden sich in der
389
Analysen
endgültigen Ergebnismenge die Datensätze der Angestellten mit den Angestelltennummern 3 und 7 (erster Teil der Abfrage) sowie die Datensätze der eigenen Untergebenen
(zweite Abfrage mit B) wie auch deren Untergebenen. Der Baum beginnt also bei 3 und
7 und klappt sich dann vollständig auf.
Der letzte Schritt ist dann sehr einfach und verblüfft jedoch möglicherweise ein wenig,
denn für die Darstellung des gesamten Baumes kann man ganz einfach die gesamte
zweite CTE abrufen. Eine Lösung ohne den Einsatz von allgemeinen Tabellenausdrücken lässt sich auch sehr leicht denken. In diesem Fall sieht die herkömmliche T-SQLLösung vor, zwei temporäre Tabellen zu erstellen, die den gleichen Aufbau haben und
welche über die auch für die CTE verwendeten Abfragen in einem INSERT…SELECTBefehl gefüllt werden. Diese zweite erstellte Tabelle enthält dann die Daten, wie sie nun
in der CTE insgesamt nach der Rekursion vorhanden sind. Das Skript ist um Einiges
länger und umständlicher als die neue Lösung, weil die CREATE-, INSERT- und SELECT-Befehle zu erstellen sind.
WITH EmpHierarchy AS (
SELECT EmployeeID, ManagerID, LastName
FROM HumanResources.Employee AS emp
INNER JOIN Person.Contact AS C
ON emp.ContactID = c.ContactID
WHERE EmployeeID IN (3,7)
UNION ALL
SELECT emp.EmployeeID, emp.ManagerID, c.LastName
FROM HumanResources.Employee AS emp
INNER JOIN Person.Contact AS C
ON emp.ContactID = c.ContactID
INNER JOIN EmpHierarchy AS emph
390
Analysen
ON emp.ManagerID = emph.EmployeeID
)
SELECT * FROM EmpHierarchy
612_03.sql: Rekursion
In diesem Fall muss einmal die gesamte Ergebnismenge abgedruckt werden, da die
Funktionsweise der Abfrage und die Nützlichkeit der Lösung sich erst dann wirklich
erschließt. Um die Menge einfacher lesbar zu machen, wurden von Anfang an nur die
beiden Angestellten mit EmployeeID 3 und 7 ausgewählt. Sie bilden auch die ersten
beiden Datensätze, wobei sie als Vorgesetzte die Angestellten mit den Nummern 12 und
21 besitzen, deren Datensätze später folgen.
EmployeeID
ManagerID
LastName
----------- ----------- --------------3
12
Tamburello
7
21
Dobney
37
7
Rapier
76
7
Kramer
84
7
Anderson
122
7
Baker
156
7
Kogan
194
7
Michaels
4
3
Walters
9
3
Erickson
11
3
Goldberg
391
Analysen
158
3
Miller
263
3
Cracium
267
3
Sullivan
270
3
Salavaria
5
263
D'Hers
265
263
Galvin
79
158
Margheim
114
158
Matthew
217
158
Raheem
(20 Zeile(n) betroffen)
Eine zweite Form des gerade vorgeführten Beispiels liefert der nächste Quelltext. Er
wird um eine weitere CTE ergänzt, welche die Employee-Tabelle mit der ContactTabelle verknüpft. Dadurch ist es nachher möglich, die doppelte Verknüpfung, welche
in der oberen und unteren Abfrage im vorherigen Beispiel notwendig war, um den jeweiligen Nachnamen des Angestellten herauszufinden, zu löschen. Stattdessen greift
man in beiden Abfragen der zweiten CTE auf die Abfrage der ersten CTE zurück, welche sowohl die beiden Zahlen für Angestellten- und Vorgesetztennummer als auch den
Nachnamen liefert.
WITH EmpCon AS (
SELECT EmployeeID, ManagerID, LastName
FROM HumanResources.Employee AS emp
INNER JOIN Person.Contact AS C
ON emp.ContactID = c.ContactID
),
392
Analysen
EmpHierarchy AS (
SELECT EmployeeID, ManagerID, LastName
FROM EmpCon
WHERE EmployeeID IN (3,7)
UNION ALL
SELECT EmpCon.EmployeeID, EmpCon.ManagerID, EmpCon.LastName
FROM EmpCon INNER JOIN EmpHierarchy AS emph
ON EmpCon.ManagerID = emph.EmployeeID
)
SELECT * FROM EmpHierarchy
612_04.sql: Rekursion und mehrfache CTE
6.1.3
Datenmanipulation und CTEs
Schließlich ist es auch noch möglich, eine CTE im Rahmen von DML-Anweisungen als
Datenquelle für Vergleiche oder Datenabrufe zu verwenden. Dies hat zwar nichts mit
Analysen und Abfragen zu tun, kann allerdings bei der in diesem Kapitel vorgenommenen Vorstellung von CTEs besonders gut untergebracht werden. Als Beispiel wird wiederum die schon zuvor verwendete Tabelle Product3 mit allen Produktdaten verwendet.
CREATE TABLE Production.Product3 (
Name
varchar(50)
NOT NULL,
ProductNumber nvarchar(25) NOT NULL,
ListPrice
money
NOT NULL,
StandardCost
money
NULL)
393
Analysen
613_01.sql: Beispieltabelle für CTE und DML
6.1.3.1
INSERT
Beim INSERT-Befehl kann man die Daten nicht nur aus einer Tabelle, Sicht, Prozedur
oder Tabellenwertfunktion abrufen, sondern auch aus einer direkt zuvor und damit innerhalb der INSERT-Anweisung erstellten CTE. So wird die Tabelle Product3 mit
Daten aus der Product-Tabelle gefüllt, wobei diese zunächst in der CTE ProductTemp abgerufen werden.
WITH ProductTemp AS (
SELECT Name, ProductNumber, ListPrice, StandardCost
FROM Production.Product
)
INSERT INTO Production.Product3
(Name, ProductNumber, ListPrice, StandardCost)
SELECT * FROM ProductTemp
613_01.sql: Einfügen aus CTE
6.1.3.2
UPDATE
Im Rahmen einer Aktualisierung lässt sich eine CTE als Vergleichsziel und damit ebenfalls als Datenquelle nutzen. Im nächsten Beispiel ruft man zunächst in der ProductTemp-CTE die Produktdaten mit erhöhten Preisen und Standardkosten ab, um sie dann
im Rahmen der Aktualisierung als neue Daten zu verwenden.
WITH ProductTemp (Name, ProductNumber,
ListPrice, StandardCost) AS (
SELECT Name, ProductNumber,
394
Analysen
ListPrice * 1.1, StandardCost * 1.05
FROM Production.Product
)
UPDATE Production.Product3
SET ListPrice = pt.ListPrice,
StandardCost = pt.StandardCost
FROM Production.Product3 AS p INNER JOIN ProductTemp AS pt
ON p.ProductNumber = pt.ProductNumber
613_02.sql: Aktualisieren mit CTE
6.1.3.3
DELETE
Analog zu Update kann man schließlich auch den DELETE-Befehl mit Hilfe einer CTE
erweitern. Auch hier dienen diese Daten dem Vergleich, um die Datensätze zu ermitteln, die tatsächlich zu löschen sind. Hierbei kann man eine Unterabfrage genauso verwenden wie eine Tabellenverknüpfung. Die CTE ruft alle Produkte mit einem Preis von
0 ab, welche dann durch die Verknüpfung in der FROM-Klausel der DELETE-Anweisung
verwendet werden, um die zu löschenden Datensätze zu ermitteln.
WITH ProductTemp AS (
SELECT ProductNumber
FROM Production.Product
WHERE ListPrice = 0
)
DELETE Production.Product3
FROM Production.Product3 AS p INNER JOIN ProductTemp AS pt
395
Analysen
ON p.ProductNumber = pt.ProductNumber
613_03.sql: Löschen mit CTE
6.2
Aggregate und Rangfolgen
Eine häufige Anforderung ist es, Aggregate und Rangfolgen zu ermitteln, die teilweise
auch miteinander verbunden sind, wenn bspw. die Rangfolgen anhand von Aggregaten
aufgebaut sein sollen und dafür nicht die Rohwerte verwendet werden sollen. In beiden
Fällen gibt es eine Reihe von alten Techniken, die auch schon im MS SQL Server 2000
funktioniert haben. Das neue T-SQL liefert allerdings auch eine Reihe an neuen Techniken, mit denen diese Fragestellungen einfacher beantwortet werden können.
6.2.1
Aggregate mit OVER
Die Darstellung der neuen OVER-Klausel soll zunächst auf Basis einer herkömmlichen
Lösung erfolgen, mit denen früher Aggregate berechnet werden konnten. Dazu ist vor
allen Dingen die SalesOrderHeader-Tabelle sehr nützlich, weil man mit ihr all die
schönen Umsatzabfragen durchführen kann, die Managerherzen höher schlagen lassen
und daher für Programmierer auch besonders interessant sein sollten.
In der nächsten Abfrage werden neben dem Jahr die Aggregate für den gesamten Umatz, den prozentualen Anteil vom Jahresumsatz zum Gesamtumsatz und die Differenz
vom Jahresumsatz zum durchschnittlichen Umsatz berechnet. Die Ermittlung für den
Jahresumsatz ist besonders einfach, da dies ja genau das Standardbeispiel für die Verwendung von GROUP BY in Zusammenhang mit Aggregatfunktionen wie bspw. SUM
darstellt. Für die beiden anderen Aggregate aber benötigt man den Gesamtumsatz der
Tabelle, der nicht zunächst in einer Variable ermittelt werden soll, um ihn dann wieder
später zu verwenden, sondern der ebenfalls in der gleichen SQL-Anweisung berechnet
werden soll. Die Unterscheidung zwischen dem Jahresumsatz und dem gesamten Umsatz aller Datensätze in der SalesOrderHeader-Tabelle ist sehr wichtig, da dies zwei
völlig getrennte Informationen sind. Während die eine Information über Gruppierung
und Aggregierung ermittelt wird, stellt die andere ein Gesamttabellenaggregat dar, das
396
Analysen
eigentlich mit der Gruppierung kollidiert, die syntaktisch notwendig ist, um die Aggregate pro Jahresszahl auszugeben. Eine erste spontane Lösung muss hier meistens verworfen werden.
Hierzu gibt es zwei Lösungen, von denen möglicherweise die zweite häufiger anzutreffen ist als die erste, da Lösungen mit Kreuzverknüpfungen grundsätzlich eher vermieden werden. Diese ist allerdings übersichtlicher formuliert. Sie setzt eine abgeleitete
Tabelle – oder ab 2005 natürlich auch eine CTE ein –, um die Gesamttabellenaggregate
zu ermitteln. In diesem Fall sind das der Gesamtumsatz und der Gesamttabellendurchschnitt. Diese Abfrage befindet sich in der Beispiellösung nun als zweite Tabelle innerhalb der FROM-Klausel, damit man auf diese Werte zugreifen kann. Es ist allerdings
keine Verknüpfung notwendig, da nach den beiden uns interessierenden Spalten Gesamtumsatz und Umsatzdurchschnitt genauso gruppiert wird wie nach dem Jahr. Diese
beiden Spalten können dann für die Berechnung der Umsatzdifferenz und des Verhältnisse des Jahresumsatzes zum Gesamtumsatz verwendet werden.
SELECT YEAR(OrderDate)
SUM(TotalDue)
AS Year,
AS Turnover,
(SUM(TotalDue) / agg.Turnover)*100 AS "%",
AVG(TotalDue) - agg.AvgTurnover
AS "/"
FROM Sales.SalesOrderHeader,
(SELECT SUM(TotalDue)
AVG(TotalDue)
AS Turnover,
AS AvgTurnover
FROM Sales.SalesOrderHeader) AS agg
GROUP BY YEAR(OrderDate), agg.Turnover, agg.AvgTurnover
621_01.sql: Standardlösung für Aggregate
Man erhält als Ergebnis eine Aufstellung der Jahre, in denen überhaupt Umsatz erwirtschaftet worden ist, mit den angefragten Werten.
397
Analysen
Year
Turnover
%
/
------ ---------------- -------- ------------2001
14327552,2263
10,18
5917,9368
2002
39875505,095
28,33
6328,6398
2003
54307615,0868
38,59
-107,3649
2004
32196912,4165
22,88
-2164,0193
Mit einer CTE sieht das Ergebnis ungleich verständlicher aus, weil hier ja zunächst die
Aggregate innerhalb der WITH-Klausel ermittelt werden, auf die dann nachher zugegriffen werden kann. Die Kreuzverknüpfung bleibt zwar erhalten, aber die Abfrage sieht
insgesamt übersichtlicher aus.
WITH Agg AS
(SELECT SUM(TotalDue)
AVG(TotalDue)
AS Turnover,
AS AvgTurnover
FROM Sales.SalesOrderHeader)
SELECT YEAR(OrderDate)
SUM(TotalDue)
AS Year,
AS Turnover,
(SUM(TotalDue) / Agg.Turnover)*100 AS "%",
AVG(TotalDue) - Agg.AvgTurnover
AS "/"
FROM Sales.SalesOrderHeader, Agg
GROUP BY YEAR(OrderDate), Agg.Turnover, Agg.AvgTurnover
621_01.sql: Lösung mit CTE
Wenn die Kreuzverknüpfung als Lösung nicht gefällt, bietet sich als zweite Lösung an,
auf gewöhnliche Spaltenunterabfragen zu setzen. Dies ist dann eine gute Idee, wenn
398
Analysen
bspw. nur eine Spalte ein Gesamtaggregat für eine Berechnung benötigt. Im nächsten
Beispiel kann man sehen, wie innerhalb der verschiedenen Spalten, welche unterschiedliche Gesamtaggregate abrufen, die gleiche Bedingung vorhanden sein muss, um die
Abfrage inhaltlich richtig zu formulieren. Auf diese Weise kann man auch die Berechnungen aus dem vorherigen Beispiel durchführen, indem die Werte aus den Unterabfragen mit Rechenoperatoren mit einer weiteren Spalte verknüpft werden. Man erkennt
allerdings auch, dass bspw. bei einem Filter innerhalb der WHERE-Klausel dieser Filter
in jeder Spaltenunterabfrage zum Einsatz kommen muss, wenn die Werte miteinander
vergleichbar sein müssen. Für die Differenz zum Durchschnitt einmal die des Jahres bis
2003 und für die Berechnung des prozentualen Anteils die des Jahres bis 2004 zu verwenden, erzeugt zwar interessante, aber in keinem Fall logisch richtige Ergebnisse.
Daher ist diese Technik nur für einmalig auftretende Aggregate sinnvoll und nicht für
mehrfache Berechnungen. Allerdings kann man an diesem Beispiel auch sehen, dass ja
dies letztendlich die gleiche Technik ist, mit der die in einem anderen Kapitel als Übersichtsabfragen bezeichneten Ergebnismengen erzeugt werden können; denn die Filter
können natürlich auch unterschiedlich sein, wenn dies gewollt ist und an den Spaltennamen zu erkennen ist.
SELECT SalesOrderID,
(SELECT SUM(TotalDue)
FROM Sales.SalesOrderHeader
WHERE YEAR(OrderDate) < 2002
AND TerritoryID < 3) AS Turnover,
(SELECT MIN(TotalDue)
FROM Sales.SalesOrderHeader
WHERE YEAR(OrderDate) < 2002
AND TerritoryID < 3) AS Min,
(SELECT AVG(TotalDue)
399
Analysen
FROM Sales.SalesOrderHeader
WHERE YEAR(OrderDate) < 2002
AND TerritoryID < 3) AS Avg
FROM Sales.SalesOrderHeader
WHERE YEAR(OrderDate) < 2002
AND TerritoryID < 3
621_02.sql: Gesamtaggregate mit Spaltenunterabfrage
Die vorherige Lösung lässt sich nun ab Version 2005 auch wieder mit einer CTE viel
einfacher formulieren. Es bleibt zwar weiterhin die Notwendigkeit bestehen, einzelne
Spaltenunterabfragen auf Basis der in der CTE ermittelten Rohdaten zu formulieren,
doch sind diese viel einfacher. Dies liegt daran, dass die benötigten Rohdaten mit Filter
und Funktionseinsatz sowie weiteren möglichen Berechnungen und Verknüpfungen
zentral in de CTE erstellt werden, sodass dann in der eigentlichen Abfrage nur noch die
verschiedenen Aggregate aufgerufen werden müssen.
WITH Agg AS (
SELECT *
FROM Sales.SalesOrderHeader
WHERE YEAR(OrderDate) < 2002
AND TerritoryID < 3
)
SELECT SalesOrderID,
(SELECT SUM(TotalDue) FROM agg) AS Turnover,
(SELECT MIN(TotalDue) FROM agg) AS Min,
(SELECT AVG(TotalDue) FROM agg) AS Avg
400
Analysen
FROM Agg
GROUP BY SalesOrderID
621_02.sql: Aggregate mit CTE
Weitere Möglichkeiten in T-SQL bestehen dann darin, eine temporäre Tabelle mit den
Rohdaten zu erstellen (ähnlich einer CTE), um auf dieser dann die einzelnen Aggregate
auszuführen, und natürlich für häufig wiederkehrende Aggregate wie den Gesamtumsatz den Wert über eine Abfrage in einer Variablen zu speichern. Diese sollen nicht
noch vorgestellt werden, da sie weder kürzere Programme liefern noch moderne Techniken wie CTE einsetzen.
Nach diesem längeren Ausflug in alte Techniken und auch ihre Übertragung auf neue
Techniken wie CTE folgt nun eine überaus elegante und kurze Lösung, die allerdings
eine neue Klausel einsetzt. In ihrer einfachsten Form ist sie tatsächlich sehr einfach
verständlich und wird vermutlich unmittelbar Einlass in den Syntaxsprachschatz von
SQL-Benutzern finden. Sie löst sämtliche Entscheidungsprobleme der vorherigen Lösungen in Nichts auf und bietet schlichtweg die kürzeste und am leichtesten zu verstehende Syntax. Nach der Verwendung einer Aggregatfunktion setzt man OVER() mit
leerer Parameterliste in die Spalte. Dadurch ermittelt man genau die Gesamtaggregate
für die in der FROM-Klausel aufgerufenen Tabellen. Im aktuellen Beispiel verkürzt sich
daher eine Übersichtsabfrage auf folgende Weise.
SELECT SalesOrderID,
SUM(TotalDue) OVER() AS Turnover,
MIN(TotalDue) OVER() AS Min,
AVG(TotalDue) OVER() AS Avg,
(TotalDue / SUM(TotalDue) OVER()) AS Schnitt
FROM Sales.SalesOrderHeader
WHERE YEAR(OrderDate) < 2002
401
Analysen
AND TerritoryID < 3
621_02.sql: Aggregate mit OVER()
Man erhält als Ergebnis die Auflistung der einzelnen Bestellnummern, die Aggregate
für Gesamt-, Minimal- und Durchschnittsumsatz sowie den prozentualen Anteil der
einzelnen Bestellung am gesamten Umsatz. Dieses Ergebnis ist natürlich durch die
Wiederholung der Gesamtaggregate nicht sonderlich sinnvoll oder lobenswert, aber es
zeigt sehr schön, wie einfach man die Gesamtaggregate ermittelt und dass man sie auch
für weitere Berechnungen wie den prozentualen Anteil etc. verwenden kann.
SalesOrderID Turnover
Min
Avg
Schnitt
------------ -------------- -------- ----------- --------43664
3458314,9992
6,8234
15438,9062
0,0093
43665
3458314,9992
6,8234
15438,9062
0,0054
Die OVER()-Klausel ist in der vorher gezeigten Variante sehr einfach anzuwenden. Sie
bietet allerdings noch die Möglichkeit, innerhalb der runden Klammern einen Bereich
anzugeben. Dies gelingt über PARTITION BY, gefolgt von einem Ausdruck, der für eine
Gruppierung innerhalb von GROUP BY verwendet werden darf. Es handelt sich dabei
also um eine referenzierte Spalte aus den Tabellen, die in der FROM-Klausel aufgerufen
werden, ergänzt um eine Funktion. Spaltenaliasnamen sind wie bei einer normalen
Gruppierung nicht erlaubt. Wie bei GROUP BY sind hier auch mehrere Ausdrücke möglich, sodass sich ursprüngliche Lösungen sehr vereinfachen lassen. Die allgemeine Syntax für die gesamte Klausel lautet:
Fensterrangfunktion
< OVER_CLAUSE > :: =
OVER ( [ PARTITION BY value_expression , ... [ n ] ]
<ORDER BY_Clause> )
Fensteraggregatfunktion
402
Analysen
< OVER_CLAUSE > :: =
OVER ( [ PARTITION BY value_expression , ... [ n ] ] )
Die beiden Darstellungen sind sehr ähnlich. Sie unterscheiden sich zunächst in den
unterschiedlichen Funktionen, die einer OVER()-Klausel vorangehen. Ein weiterer Unterschied besteht darin, dass bei den Fensterrangfunktionen auch noch eine Sortierung
über ORDER BY folgen kann. Folgende beiden Funktionstypen können also verwendet
werden:
ƒ
Aggregatfunktionen: AVG, MIN, CHECKSUM, SUM, CHECKSUM_AGG, STDEV, COUNT,
STDEVP, COUNT_BIG, VAR, GROUPING, VARP, MAX
ƒ
Rangfolgefunktionen: RANK, NTILE, DENSE_RANK, ROW_NUMBER
Während die gewöhnlichen Aggregatfunktionen durch die Ausführungen und Beispiele
mittlerweile hinlänglich bekannt sein sollten, sind die Rangfolgefunktionen noch unbekannt. Sie werden später noch in einigen speziellen Beispielen erläutert. Im Wesentlichen dienen sie dazu, Rangfolgen wie bspw. Hitparaden zu erzeugen.
Im nächsten Beispiel werden die Aggregate für Gesamtsumme, Minimum und Durchschnitt nicht für die ganze Tabelle ermittelt, sondern vielmehr für die Jahreszahl. Hier
hätte man entweder die gewöhnliche OrderDate-Spalte mit der YEAR-Funktion bearbeiten können oder man verwendet einfach den Spaltenaliasnamen aus der Unterabfrage. Es soll also ein Bereich (Partition) gebildet werden, was durch die zusätzliche Angabe PARTITION BY ausdruck ermöglicht wird. Wären wie bei einer gewöhnlichen
Gruppierung weitere Spaltenausdrücke notwendig, dann könnten sie diesem ersten
Ausdruck folgen.
SELECT YEAR, TerritoryID,
SUM(TotalDue) OVER(PARTITION BY YEAR)
AS
Turnover,
MIN(TotalDue) OVER(PARTITION BY YEAR)
AS Min,
AVG(TotalDue) OVER(PARTITION BY YEAR)
AS Avg,
403
Analysen
(TotalDue / SUM(TotalDue)
OVER(PARTITION BY YEAR) * 100)
AS "%"
FROM (SELECT YEAR(OrderDate) AS Year,
SUM(TotalDue) AS TotalDue,
TerritoryID
FROM Sales.SalesOrderHeader
GROUP BY YEAR(OrderDate), TerritoryID) AS d
WHERE TerritoryID <= 3
ORDER BY 1,2
621_03.sql: Aggregate mit OVER() und Partitionen
Man erhält ein sehr schönes Ergebnis, in dem die prozentualen Werte nun für die einzelnen Jahre ermittelt werden, wobei in einem Jahr in mehreren Gebieten Umsatz erwirtschaftet worden ist. Der Gesamtumsatz ist hier also für die einzelnen Jahre der gleiche, so wie auch die anderen beiden Aggregate natürlich immer gleich sind. Lediglich
beim Verhältnis der einzelnen Umsätze pro Region sieht man sehr schön, wie ein Jahr
insgesamt immer 100 % ergeben.
YEAR
Terr-ID Turnover
Min
Avg
%
------- ------- -------------- ------------- ------------ --2001
1
4722199,10
754833,2045
1574066,3672
2
4722199,10
754833,2045
1574066,3672
3
4722199,10
754833,2045
1574066,3672
57,25
2001
15,98
2001
26,76
404
Analysen
2002
1
12445196,31
3275322,1694
4148398,7711
2
12445196,31
3275322,1694
4148398,7711
3
12445196,31
3275322,1694
4148398,7711
45,41
2002
26,31
2002
28,26
6.2.2
Akkumulationen und Durchschnitte
Neben einfachen Aggregaten, die als fester Wert für eine ganze Tabelle oder natürlich
für einen bestimmten Bereich ermittelt werden, gibt es auch die Anforderung, Akkumulationen und verschiedene Arten von Durchschnitten zu errechnen. Für die verschiedenen Beispiele könnte man natürlich auch wieder eine CTE verwenden. Es soll allerdings
doch wieder eine ältere Lösung in Form von einer temporären Tabelle eingesetzt werden, die mit Hilfe der beiden Rautenzeichen als globale temporäre Tabelle angelegt
wird und daher von verschiedenen Sitzungen aufgerufen werden kann. Diese erstellt das
nachfolgende Beispiel, sodass man für die späteren Beispiele eine Tabelle mit Rohdaten
besitzt. Sie besteht aus der Kundennummer, dem Bestelldatum und dem Gesamtumsatz.
Das Bestelldatum ist dabei die Kombination aus Jahr und Monat.
IF OBJECT_ID('#Sales') IS NOT NULL
DROP TABLE ##Sales
GO
CREATE TABLE ##Sales (
CustomerID int,
OrderDate
varchar(7),
TotalDue
money
)
405
Analysen
GO
INSERT INTO ##Sales
SELECT CustomerID,
LTRIM(STR(YEAR(OrderDate)))
+'-'
+LTRIM(STR(MONTH(OrderDate))) AS OrderDate,
SUM(TotalDue)
FROM Sales.SalesOrderHeader
GROUP BY CustomerID, LTRIM(STR(YEAR(OrderDate)))
+'-'
+LTRIM(STR(MONTH(OrderDate)))
GO
SELECT * FROM ##Sales
622_01.sql: Erstellung der Beispieldaten
Mit der nachfolgenden Ausgabe soll noch einmal gezeigt werden, welche Daten für die
Beispiele relevant sind. Die verschiedenen Kundennummern führen in einer Jahr/Monat-Kombination zu einem mehr oder minder großen Umsatz.
CustomerID
OrderDate TotalDue
----------- --------- -------------16308
2002-8
2410,6266
306
2002-9
250,6771
12602
2003-8
9,934
406
Analysen
15866
6.2.2.1
2003-9
42,9624
Akkumulation
Im ersten Beispiel, welche die gerade abgerufenen und zwischengespeicherten Daten
benutzt, erstellt man eine einfache Akkumulation. Für dieses und für alle folgenden
Beispiele gilt, dass man im Alltag vermutlich die exakte Syntax oder den Lösungsweg
nicht auswendig weiß und für eine beliebige Abfrage niederschreiben kann. Die Aufgabe besteht dann im praktischen Leben mehr darin, den vorgezeichneten Lösungsweg auf
die gerade zu bearbeitenden Tabellen anzuwenden.
Bei einer Akkumulation soll eine Spalte geführt werden, welche die Zahlwerte aus einer
anderen Spalte addiert, d.h. in der letzten Reihe befindet sich dann die Gesamtsumme,
welche sich auch durch eine entsprechende Aggregierung für die gesamte Tabelle ergeben hätte. Zur Lösung dieser Anforderung kann man bei einer einzigen Spalte, in der
eine solche Berechnung genutzt bzw. ausgegeben wird, eine Spaltenunterabfrage verwenden. Sie enthält eine Verknüpfung mit der äußeren Datentabelle anhand des Primärschlüssels CustomerID. Hier handelt es sich also um eine korrelierte Spaltenunterabfrage, deren zusätzliche Bedingung für die erfolgreiche Teilsummenbildung bis zum
aktuellen Datum ist, dass nur die Datensätze mit einem Datum, welches kleiner gleich
dem aktuellen Datensatz der äußeren Abfrage ist.
SELECT s1.CustomerID,
s1.OrderDate,
s1.TotalDue AS Month,
(SELECT SUM(s2.TotalDue)
FROM dbo.##Sales AS s2
WHERE s2.CustomerID = s1.CustomerID
AND s2.OrderDate <= s1.OrderDate)AS Total
FROM dbo.##Sales AS s1
407
Analysen
GROUP BY s1.CustomerID, s1.OrderDate, s1.TotalDue
ORDER BY s1.CustomerID, s1.OrderDate
622_02.sql: Akkumulation
Man erhält die angekündigte Ausgabe von Datensätzen, wobei in der äußeren Spalte die
Akkumulation enthalten ist. Rechnet man einmal selbst die ersten drei Werte für die
Monatsumsätze zusammen, dann erhält man genau die Summe, welche auch als akkumulierter Wert ermittelt wird.
CustomerID
OrderDate Month
Total
----------- --------- --------------- -------------1
2001-11
26128,8674
26128,8674
1
2001-8
14603,7393
40732,6067
1
2002-2
37643,1378
78375,7445
1
2002-5
34722,9906
113098,7351
2
2002-11
5469,5941
5469,5941
2
2002-8
10184,0774
15653,6715
6.2.2.2
Gleitender Durchschnitt
Sofern ohnehin nur eine einzige Spalte den akkumulierten Wert nutzt, kann man eine
korrelierte Spaltenunterabfrage verwenden. Im nächsten Beispiel gibt es einmal einen
gleitenden Durchschnitt als neues Thema und zusätzlich auch eine Akkumulation. Hier
kann man zum einen sehen, wie man die gleiche Abfrage von oben mit Hilfe einer
Selbstverknüpfung innerhalb der FROM-Klausel löst, und wie man gleichzeitig einen
Durchschnitt ermittelt, der gleitend ist. Ein solcher gleitender Durchschnitt zeichnet sich
dadurch aus, dass er auf der Basis aller vorherigen Datensätze ermittelt wird. Im Gegensatz dazu wird ein Gesamtdurchschnitt auf Basis aller Datensätze ermittelt. Ein solcher
gleitender Durchschnitt stellt natürlich nur dann einen sinnvollen Wert dar, wenn die
408
Analysen
Werte, auf deren Basis er ermittelt wird, in einer sinnvollen Reihenfolge aufeinander
folgen. Dies ist typischerweise durch eine zeitliche Abfolge gegeben. Als Anweisung ist
nichts anderes zu tun, als die AVG-Funktion zu verwenden. Die Daten werden wiederum
durch eine Selbstverknüpfung in der FROM-Klausel oder eine korrelierte Spaltenunterabfrage erzeugt.
Die beiden Beispiele zeigen darüber hinaus auch, dass sich natürlich sämtliche anderen
Aggregate ebenfalls über diese Techniken ermitteln lassen. Nur in wenigen Fällen dürfte es zwar gewünscht sein, das Minimum oder Maximum der vorherigen Werte zu ermitteln, doch stellt dies kein syntaktisches Problem dar.
SELECT s1.CustomerID,
s1.OrderDate,
s1.TotalDue AS Month,
SUM(s2.TotalDue) AS Total,
AVG(s2.TotalDue) AS Avg
FROM dbo.##Sales AS s1 INNER JOIN dbo.##Sales AS s2
ON s1.CustomerID = s2.CustomerID
AND s2.OrderDate <= s1.OrderDate
GROUP BY s1.CustomerID, s1.OrderDate, s1.TotalDue
ORDER BY s1.CustomerID, s1.OrderDate
622_02.sql: Gleitender Durchschnitt und Akkumulation
Das Ergebnis zeigt vermutlich besser als die obige Erklärung, wie beide Berechnungen
und die gesamte Technik mit der abgeleiteten Tabelle funktionieren. Eine CTE wäre
natürlich auch möglich gewesen, wenngleich die angegebene Lösung die traditionelle
Variante ist und in den meisten Fällen völlig ausreichend sein dürfte, da keine mehrfachen Referenzen oder ähnliche Vorteile von CTEs notwendig sein dürften. In der Spalte
409
Analysen
für den gleitenden Durchschnitt wird immer für den aktuellen Wert und die zurückliegenden Werte der Durchschnitt ermittelt.
CustomerID
OrderDate Month
Total
Avg
----------- --------- ------------- ------------ ----------1
2001-11
26128,8674
26128,8674
26128,8674
1
2001-8
14603,7393
40732,6067
20366,3033
1
2002-2
37643,1378
78375,7445
26125,2481
1
2002-5
34722,9906
113098,7351
28274,6837
2
2002-11
5469,5941
5469,5941
5469,5941
2
2002-8
10184,0774
15653,6715
7826,8357
6.2.2.3
Fenster-Aggregate
Für die tägliche Arbeit bei der Erstellung von statistischen Auswertungen gelten noch
die Fenster-Aggregate, welche nicht einfach nur einen gleitenden Durchschnitt ermitteln
und damit sehr stark auf Schwankungen reagieren, sondern mehrere Datensätze aus der
Vergangenheit und/oder der Zukunft in die Berechnung einfließen lassen. Diese Bereichsaggregate sind bspw. als Zwei-Monats-durchschnitt bekannt und begrenzen dadurch die Auswahl der Datensätze sehr stark.
Das Prinzip, wie überhaupt die Aggregatbildung durchgeführt wird, bleibt unverändert.
Man setzt eine Selbstverknüpfung ein, verknüpft dabei anhand der Kundennummer
bzw. des Primärschlüssel und wählt die Datensätze für die Berechnung aus, indem nicht
einfach nur eine Größer-/Kleiner-Beziehung zum Einsatz kommt, sondern stattdessen
eine Berechnung.
SELECT s1.CustomerID,
s1.OrderDate,
410
Analysen
s1.TotalDue AS Month,
SUM(s2.TotalDue) AS Total,
AVG(s2.TotalDue) AS Avg
FROM dbo.##Sales AS s1 INNER JOIN dbo.##Sales AS s2
ON s1.CustomerID = s2.CustomerID
AND (CAST(s2.OrderDate+'-1' AS DATETIME) >
DATEADD(MONTH, -3,CAST(s1.OrderDate+'-1' AS
DATETIME))
AND CAST(s2.OrderDate+'-1' AS DATETIME) <=
CAST(s1.OrderDate+'-1' AS DATETIME))
GROUP BY s1.CustomerID, s1.OrderDate, s1.TotalDue
ORDER BY
s1.CustomerID, CAST(s1.OrderDate+'-1' AS
DATETIME)
622_03.sql: Fenster-Aggregate
In der Ergebnismenge kann man viel besser erkennen, wie die Fenster bzw. Bereiche
gebildet werden. Immer innerhalb von drei Monaten (Achtung: nicht Datensätzen) wird
ein neuer Durchschnitt gebildet. Über die oben angegebene Berechnung kann man auch
erkennen, dass man tatsächlich beliebige Fensterbereiche selbst vorgeben kann, darunter
auch solche, die weniger gebräuchlich sind und in die Vergangenheit und in die Zukunft
unterschiedliche Anzahl von Zeiteinheiten blicken.
CustomerID
OrderDate Month
Total
Avg
----------- --------- ------------- ----------- -----------1
2001-8
14603,7393
14603,7393
14603,7393
1
2001-11
26128,8674
40732,6067
20366,3033
411
Analysen
1
2002-2
37643,1378
37643,1378
37643,1378
1
2002-5
34722,9906
72366,1284
36183,0642
2
2002-8
10184,0774
10184,0774
10184,0774
2
2002-11
5469,5941
15653,6715
7826,8357
2
2003-2
1739,4078
1739,4078
1739,4078
6.2.3
Hitparaden
Eine häufige Anforderung ist es, die x größten, erfolgreichsten, kleinsten und sonstigen
–sten Datensätze zu beschaffen. Für die Erfüllung dieser Aufgabe kann man auf der
einen Seiten ORDER BY, einen Cursor oder natürlich die schon bekannte TOP n-Abfrage
einsetzen. Die eine oder andere Lösung ist je nach genauem Anwendungszusammenhang die empfehlenswerte, wobei aller Wahrscheinlichkeit nach insbesondere TOP n in
vielen Fällen die beste Wahl darstellen dürfte. Zusätzlich zu diesen Möglichkeiten gibt
es noch eine Reihe von neuen Funktionen, mit deren Hilfe ähnliche Ergebnisse erzeugt
werden können, wobei hier der Fokus mehr auf die Erstellung einer Hitparade bzw. die
Ausgabe einer Reihenfolge liegt. Dies ist zwar mit einer Standardsortierung ebenfalls
möglich, aber in diesem Abschnitt sollen Lösungen vorgestellt werden, mit deren Hilfe
nicht nur die Sortierung überhaupt durchgeführt werden kann, sondern mit denen auch
die Rangnummern und Plätze ausgegeben werden, die ansonsten bei Einsatz eine einfachen Sortierung nicht in der Ergebnismenge erscheinen.
6.2.3.1
Zeilennummern erstellen
Bisweilen möchte man in einer Abfrage Zeilennummern haben, um seitenweise (immer
die nächsten 50 Datensätze) oder in Form einer Hitparade (die ersten 10) die Ergebnismenge zu lesen bzw. auch weiterzuverarbeiten. Bisweilen kann man hier schon einfach
den Primärschlüssel verwenden, wenn die Ergebnismenge direkt mit dem Schlüsselwert
1 beginnt und sich dann aufsteigend fortsetzt. In den seltensten Fällen dürfte dies allerdings gegeben sein. Daher wird eine Möglichkeit gesucht, solche Zeilennummern extra
412
Analysen
auszugeben. Eine neue und besonders schnelle und damit auch für umfangreiche Ergebnismengen interessante Lösung stellt die ROW_NUMBER()-Funktion dar.
Ihre allgemeine Syntax lautet:
ROW_NUMBER ( ) OVER ( [ <partition_by_clause> ]
<order_by_clause> )
Sie erwartet keine Parameter, wird aber um die OVER()-Klausel ergänzt. Diese ermöglicht es dann in einer ORDER BY-Klausel anzugeben, nach welcher Spalte die Datensätze sortiert werden sollen und kann um eine PARTITION BY-Klausel ergänzt werden,
welche die Bereiche angibt, in denen neue Reihenfolgen gebildet werden sollen. Im
nachfolgenden Beispiel wird anstelle einer möglichen CTE einmal wieder eine herkömmliche temporäre Tabelle erstellt, in der sowohl Werte aus einer Abfrage als auch
die möglichen Zeilennummern gespeichert werden sollen. Dies ist natürlich nicht zwingend notwendig, weil die ROW_NUMBER()-Funktion gerade auch sehr gut in einer einfachen Abfrage eingesetzt werden kann, doch soll der Einsatz einer temporären Tabelle
noch einmal als allgemeines Beispiel zu diesem Thema zusätzlich genutzt werden.
Die Tabelle #Sales enthält die drei Spalten TerritoryID, Year und Turnover in
passenden Datentypen, um für Jahre und Regionen Umsatzzahlen zu speichern. Die
Tabelle füllt man dann durch eine SELECT…INTO-Abfrage, sodass man für beliebige
Abfragen Rohdaten besitzt, die sich auf die originale Sales.SalesOrderHeaderTabelle beziehen. Basierend auf dieser Tabelle erstellt man dann eine Abfrage, welche
drei verschiedene Aufrufe der ROW_NUMBER()-Funktion besitzt.
ROW_NUMBER() OVER (ORDER BY Year, TerritoryID) ist bereits die erweiterte
Form des Funktionsaufrufs und gibt zwei Spaltennamen in der ORDER BY-Klausel an.
Da dies auch die Sortierung der gesamten Abfrage ist, verlaufen diese Nummern kontinuierlich ansteigend. Dies lässt sich an den Werten der beiden anderen Spalten nicht
nachvollziehen. ROW_NUMBER() OVER (ORDER BY Year) erzeugt eine Nummerierung der Ergebnisse anhand des Jahres. Dadurch ist bspw. der zweite Datensatz die
Kombination aus dem ersten Jahr und dem zweiten Gebiet. Dies ist allerdings in der
Geschwisterspalte mit der Sortierung nach Jahr und Gebiet die Nummer 16. Schaut
413
Analysen
man die Ergebnisse genauer an, erkennt man auch, dass zwar auf der einen Seite durch
die unterschiedliche Sortierung in der gesamten Abfrage und in den einzelnen
ROW_NUMBER()-Funktionen verschiedene Zeilennummern entstehen, dass aber auf der
anderen Seite viele Reihen doppelt ausgegeben werden, um den unterschiedlichen Sortierungen Genüge zu tun, welche in den ROW_NUMBER()-Funktionen gefordert werden.
Damit zeigt dieses Beispiel sowohl, wie einfach Zeilennummern ausgegeben werden
können, aber auch, dass es in den meisten Fällen am sinnvollsten ist, die Sortierung der
gesamten Abfrage mit der ROW_NUMBER()-Funktion abzustimmen. Durch die Kombination und Ausgabe von mehreren Sortierungen mit widersprüchlichen Angaben zwingt
man die Ergebnismenge dazu, doppelte Werte zu erzeugen.
IF OBJECT_ID('#Sales') IS NOT NULL
DROP TABLE #Sales
GO
CREATE TABLE #Sales (
TerritoryID int NULL,
Year int NULL,
Turnover money NULL
)
GO
INSERT INTO #Sales
SELECT TerritoryID,
YEAR(OrderDate) AS Year,
SUM(TotalDue)
AS Turnover
FROM Sales.SalesOrderHeader
GROUP BY YEAR(OrderDate), TerritoryID
414
Analysen
ORDER BY 1,2
GO
SELECT TerritoryID,
Year,
Turnover,
ROW_NUMBER() OVER (ORDER BY Year) AS [RN-Y],
ROW_NUMBER() OVER (ORDER BY Year,
TerritoryID) AS [RN-YT],
ROW_NUMBER() OVER (ORDER BY TerritoryID) AS [RN-T]
FROM #Sales
ORDER BY Year, TerritoryID
623_01.sql: Einsatz von ROW_NUMBER()
Die Ergebnismenge zeigt, wie einfach und schön man Zeilennummern ausgeben kann.
Sie zeigt allerdings auch, dass man durch Kombination von mehreren Spalten mit Sortierungen doppelte Werte ausgeben kann. Im Normalfall sollte dies zu keinem Problem
führen, da man wohl mit einer einzigen Spalte Zeilennummern auskommen dürfte.
Sollte dies nicht der Fall sein, ist darauf zu achten, dass eine zusätzliche Nummerierung
nicht zu dem beschriebenen Phänomen von doppelten Zeilen führt. Wenigstens die
Angabe ROW_NUMBER() OVER (ORDER BY TerritoryID) sollte entfallen, da die
anderen Zeilennummernangaben wenigstens nach der Jahreszahl sortieren.
TerritoryID Year
Turnover
RN-Y
RN-YT
RN-T
----------- ----- ---------------- ------ --------- -----1
2001
2703481,7947
1
1
1
1
2001
2703481,7947
11
2
2
415
Analysen
2
2001
754833,2045
12
3
15
2
2001
754833,2045
2
4
16
9
2004
3823410,2386
62
78
72
10
2004
3039555,8775
71
79
73
10
2004
3039555,8775
61
80
74
...
(80 Zeile(n) betroffen)
Im nächsten Beispiel verzichtet man auf die Verwendung einer temporären Tabelle,
sondern erstellt die Zeilennummern unmittelbar in einer Abfrage, die eine vorhandene
Tabelle der Datenbank abfragt. In diesem Fall erzeugt man auch nur eine einzige Spalte
mit Zeilennummern, was auch dem Standardfall entspricht. In diesem Beispiel ist neben
diesen Überlegungen auch noch ein Syntax-Aspekt neu hinzugetreten. Die Zeilennummer erstellt man nicht nur anhand der aufsteigend sortierten Umsätze, sondern fordert
darüber hinaus auch noch, dass die Jahreszahlen einen Bereich bilden sollen. Das bedeutet, dass immer dann, wenn ein neues Jahr abgearbeitet wird, die Sortierung der
Umsätze für dieses Jahr neu beginnen soll und dadurch erneut mit der Zeilennummer 1
begonnen werden soll. Damit ist die Syntax von ROW_NUMBER() ausgeschöpft: Man
eine Nummerierung anhand einer Gruppierung.
SELECT TerritoryID,
YEAR(OrderDate) AS [Year],
SUM(TotalDue)
AS Turnover,
ROW_NUMBER() OVER(PARTITION BY YEAR(OrderDate)
ORDER BY SUM(TotalDue))
FROM Sales.SalesOrderHeader
WHERE TerritoryID BETWEEN 5 AND 7
416
AS RN
Analysen
GROUP BY TerritoryID, YEAR(OrderDate)
623_02.sql: Verwendung einer Partition
Die Funktionsweise und Nützlichkeit von ROW_NUBMER() zeigt sich sehr schön im
nachfolgend abgedruckten Ergebnis. Die einzelnen Jahre folgen in der Year-Spalte als
Gruppen untereinander. Für jedes neue Jahre startet die Zeilennummerierung neu, sodass man für drei abgerufene Gebiete jeweils die Zahlen von 1 bis 3 in der Spalte findet,
wenn pro Jahr in allen drei Gebieten Umsatz erwirtschaftet wurde. Wie man auch
schon bei der ORDER BY-Klausel mehrere Spalten angegeben werden können, so ist dies
auch bei PARTITION BY-möglich. In komplexen Abfragen kann man die Partition
sowohl nach dem Jahr als auch dem Kontinent durchführen. Für die Beispielabfrage ist
hier keine sinnvolle weitere Partitionierung denkbar, da die Werte bereits weit genug
aggregiert sind.
TerritoryID Year
Turnover
RN
----------- ----------- --------------------- --7
2001
199531,723
1
5
2001
1928148,6139
2
6
2001
2173647,1453
3
7
2002
1717145,7439
1
5
2002
3814944,144
2
6
2002
7215430,5017
3
Auch wenn es in der Version 2005 keinen Grund mehr geben kann, bei neu erstellten
Abfragen auf die alten Techniken zur Erzeugung von Reihennummern zurückzugreifen,
so kann es natürlich geschehen, dass man in bereits bestehenden Anwendungen genau
diese alten Techniken für die Version 2000 findet. Daher sollen kurz einige Möglichkeiten beschrieben oder auch mit einem Beispiel belegt werden.
417
Analysen
Wenn man eine mengenbasierte Lösung sucht, dann stößt man auf die COUNT-Funktion,
welche man in Kombination mit einer korrelierten Spaltenunterabfrage einsetzen kann.
Diese zählt dann die Anzahl der Zeilen, welche bereits zuvor verarbeitet worden sind.
Die Lösung ist grundsätzlich nicht schwierig zu verstehen, dauert allerdings immer
deutlich länger als der Einsatz von ROW_NUMBER(). Bedingungen, die in der äußeren
Abfrage geprüft werden, muss man auch in diesem Fall wieder in der WHERE-Klausel
der korrelierten Spaltenunterabfrage prüfen. Hier ist in Version 2000 auch keine abgeleitete Tabelle möglich, welche den Filter enthält, da ja keine Mehrfachverweise zulässig sind. Der Einsatz einer CTE ist zwar in Version 2005 möglich, doch hier sollte man
die nachfolgend angegebene Technik aus Leistungsgründen gar nicht einsetzen.
SELECT SalesOrderID,
TerritoryID,
TotalDue,
(SELECT COUNT(s2.SalesOrderID)
FROM Sales.SalesOrderHeader AS s2
WHERE s2.SalesOrderID <= s1.SalesOrderID) AS RN
FROM Sales.SalesOrderHeader AS s1
WHERE s1.SalesOrderID < 43662
623_03.sql: Traditionelle Rangfolgen (einfach)
Auch wenn über die vorherige Lösung allerhand Schmähungen aufgrund der geringen
Geschwindigkeit ausgestoßen wurden, so liefert sie doch zuverlässig das gewünschte
Ergebnis.
SalesOrderID TerritoryID TotalDue
RN
------------ ----------- --------------------- ----------43659
418
5
27231,5495
1
Analysen
43660
5
1716,1794
2
43661
6
43561,4424
3
(3 Zeile(n) betroffen)
Auch eine komplexe Variante der mengenbasierten Ausgabe von Zeilennummern ist
möglich. Dabei kann man durch eine Gruppierung dafür sorgen, dass Partitionen gebildet werden. So wird auch noch einmal deutlich, dass die Partitionen der
ROW_NUMBER()-Funktion tatsächlich nichts Anderes als Gruppen darstellen. Zusätzlich
setzt das nächste Beispiel auch noch einen Filter auf die Regionen 5 bis 7 ein. Auf der
einen Seite soll dadurch das Ergebnis für den Nachweis, dass Bereiche entstehen, deutlicher hervorgehoben werden, auf der anderen Seite sieht man hier, dass – wie oben
angekündigt – dieser Filter sowohl in der äußeren als auch in der inneren Abfrage
gleichlautend angegeben werden muss.
SELECT TerritoryID,
YEAR(OrderDate) AS [Year],
SUM(TotalDue)
AS Turnover,
(SELECT COUNT(DISTINCT s2.TerritoryID)
FROM Sales.SalesOrderHeader AS s2
WHERE s2.TerritoryID <= s1.TerritoryID
AND s2.TerritoryID BETWEEN 5 AND 7) AS RN
FROM Sales.SalesOrderHeader AS s1
WHERE TerritoryID BETWEEN 5 AND 7
GROUP BY TerritoryID, YEAR(OrderDate)
ORDER BY YEAR(OrderDate), s1.TerritoryID
623_03.sql: Traditionelle Rangfolgen (komplex)
419
Analysen
Im Ergebnis erhält man die angekündigten Bereiche und ihre jeweiligen neu beginnenden Nummerierungen.
TerritoryID Year
Turnover
RN
----------- ----------- --------------------- --7
2001
199531,723
1
5
2001
1928148,6139
2
6
2001
2173647,1453
3
7
2002
1717145,7439
1
5
2002
3814944,144
2
6
2002
7215430,5017
3
Diese beiden Techniken sollten sowohl für die Version 2005 als auch für die Version
2000 genügen, um Reihennummern in jeder beliebigen Abfrage zu erstellen. Sofern die
Version 2005 genutzt wird, lohnt sich nur der Einsatz der neuen ROW_NUMBER()Funktion, da dieser von beiden vorgestellten Varianten die Ergebnisse am schnellsten
produziert. Für die Version 2000 ist die so genannte IDENTITY-Variante auf Platz 1,
obwohl sie leider nicht so schnell geschrieben werden kann wie die mengenbasierte,
ausformulierte Variante des letzten Beispiels. Sofern allerdings die Geschwindigkeit
aufgrund der abgerufenen Zeilen vernachlässigt werden kann, ist die sicherlich kein so
wichtiges Kriterium.
Bei der IDENTTIY- und de Cursor-Variante erstellen jeweils eine temporäre Tabelle, in
welcher über einen SELECT…INTO-Ausdruck oder über eine WHILE-Schleife und einen
aufsteigenden Zähler die Werte eingetragen werden, ergänzt um einen aufsteigenden
Zählerwert. Während dieser beim Cursor tatsächlich über einen Zähler generiert wird,
ermöglicht die schnellere Variante den Vorteil, dass eine Spalte direkt in der SELECT…INTO-Anweisung mit der Funktion IDENTITY(int, 1,1) gefüllt wird und
daher automatisch aufsteigend gefüllt wird. Man erhält dann eine Syntax wie SELECT
420
Analysen
spalte1, spalte2, IDENTITY(int, 1,1) AS RN INTO #tempTab FROM
tabelle.
6.2.3.2
Rangfolgefunktionen
In der Version 2005 gibt es einige neue so genannte Rangfolgefunktionen, die in folgender Liste kurz vorgestellt werden. Sie besitzen alle eine zusätzliche PARTITION BYKlausel, welche in der Lage ist, die Funktion auf einzelne Bereiche anzuwenden und
dadurch Gruppierungen und eine sehr flexible Nutzung ermöglicht. Zusätzlich greifen
sie optional auf eine Sortieranweisung zurück, um die Reihenfolge zu bestimmen.
ƒ
RANK ( )
OVER (
[ < partition_by_clause > ]
< order_by_clause > ) erzeugt für jede Zeile innerhalb einer
Ergebnismenge oder eines Bereichs (Partition) eine Rang in Form einer
Ganzzahl an. Sollten mehrere Reihen gleichwertig sein, dann erhalten sie
alle denselben Rang. Hier können Lücken auftreten: Wenn zwei Datensätze
gleichwertig sind, erhalten sie die gleiche Nummer. Der nächste Datensatz
überspringt einen Zählerwert und lautet dann bspw. auf 3, wenn die ersten
beiden Datensätze den Wert 1 erhalten haben, da insgesamt drei Datensätze
einzuordnen sind.
ƒ
DENSE_RANK ( ) OVER (
[ < partition_by_clause > ]
erzeugt für jede Zeile innerhalb einer
Ergebnismenge oder eines Bereichs (Partition) eine Rang in Form einer
Ganzzahl an. Sollten mehrere Reihen gleichwertig sein, dann erhalten sie
alle denselben Rang. Hier können keine Lücken auftreten.
<
ƒ
order_by_clause
>
)
ROW_NUMBER ( ) OVER (
[ <partition_by_clause> ]
421
Analysen
liefert von 1 beginnend aufsteigende Ganzzahlen in
einer eigenen Spalte. Dadurch ersetzt man ältere Vorgehensweisen, die
temporäre Tabellen mit IDENTITY-Spalten eingesetzt haben.
<order_by_clause> )
Das nächste Beispiel summiert die Umsätze pro Gebiet und erzeugt dafür Spalten mit
den Werten aller drei Funktionen.
SELECT TerritoryID,
SUM(TotalDue) AS Turnover,
RANK() OVER (ORDER BY SUM(TotalDue))
AS R,
DENSE_RANK() OVER (ORDER BY SUM(TotalDue)) AS DR,
ROW_NUMBER() OVER (ORDER BY SUM(TotalDue)) AS RN
FROM Sales.SalesOrderHeader
GROUP BY TerritoryID
ORDER BY SUM(TotalDue)
623_04.sql: Einsatz und Vergleich von Rangfolgefunktionen
Man erhält als Ergebnis durch die ROW_NUMBER()-Funktion die aufsteigenden Zeilennummern, die einfach nur eine Art Schlüsselwert für jede einzelne Zeile bieten. Die
DENSE_RANK()-Funktion ermittelt die Plätze dagegen in diesem Fall genauso wie die
RANK()-Funktion, da keine doppelten Werte auftreten (bei Umsätzen auch unwahrscheinlich).
TerritoryID Turnover
R
DR
RN
----------- --------------------- -------- -------- ---8
5939763,4963
1
1
1
7
9136704,474
2
2
2
...
422
Analysen
6
21501812,4574
9
9
9
4
31213459,5756
10
10
10
(10 Zeile(n) betroffen)
Die nächste Abfrage verwendet darüber hinaus noch eine Sortierung, wobei hier
RANK() und DENSE_RANK() entgegen gesetzte Sortierrichtungen besitzen.
SELECT TerritoryID,
SUM(TotalDue) AS Turnover,
RANK() OVER (ORDER BY SUM(TotalDue))
AS R,
DENSE_RANK() OVER (ORDER BY SUM(TotalDue) DESC) AS
DR,
ROW_NUMBER() OVER (ORDER BY SUM(TotalDue)) AS RN
FROM Sales.SalesOrderHeader
GROUP BY TerritoryID
ORDER BY SUM(TotalDue) DESC
623_05.sql: Sortierreihenfolge
Die ausgegebenen Werte stimmen mit denen zuvor ermittelten Werten überein, doch die
Sortierreihenfolge ist jeweils umgekehrt. Da DENSE_RANK() und ROW_NUMBER() die
gleiche absteigende Sortierung aufweisen, sind diese Spalten gleich.
TerritoryID Turnover
R
DR
RN
----------- --------------------- ------- ------- ---4
31213459,5756
10
1
10
6
21501812,4574
9
2
9
423
Analysen
Nun benötigt man noch eine Abfrage, welche gleiche Werte hervorbringt und daher
auch Lücken aufweist, wenn man mit der RANK()-Funktion die Ränge vergibt. Für die
Kombination Land/Region sollen die verschiedenen Verkaufsgebiete gezählt werden,
wobei in einigen Ländern die gleiche Anzahl an Verkaufsgebieten vorhanden ist.
SELECT d.*,
RANK () OVER (ORDER BY Provinces DESC) AS R,
DENSE_RANK () OVER (ORDER BY Provinces DESC) AS DR
FROM (SELECT CountryRegionCode,
COUNT(StateProvinceCode) AS Provinces,
TerritoryID
FROM Person.StateProvince
GROUP BY CountryRegionCode, TerritoryID
HAVING TerritoryID < 5) AS d
ORDER BY Provinces DESC
623_06.sql: Vergleich von RANK() und DENSE_RANK()
Wie man in der Ergebnisliste sehen kann, werden von RANK() tatsächlich gleiche Werte
mit gleichen Rangplätzen versehen, was zu Lücken führt, um die nicht besetzten Plätze
zu überspringen und beim ersten unterschiedlichen Datensatz mit der Reihennummer
fortzufahren.
CountryRegionCode Provinces
TerritoryID R
DR
----------------- ----------- ----------- -------- -----US
14
2
1
1
US
14
3
1
1
US
9
1
3
2
424
Analysen
US
5
4
4
3
AS
1
1
5
4
(5 Zeile(n) betroffen)
Sofern diese schönen Funktionen nicht bereit stehen, gibt es eine andere Lösung, die –
allerdings ohne CTE – auch in Version 2000 lauffähig ist und mengenbasiert arbeitet.
Im nächsten Beispiel ermittelt man mit korrelierten Spaltenunterabfragen die benötigten
Werte für die dichte und nicht-besetzte Rangfolgeneinordnung. Die dichte Besetzung
ermittelt man dagegen, indem man die Duplikate durch DISTINCT ausblendet und daher
nicht zählt. Möchte man die Lösung für die Version 2000 lauffähig erstellen, muss man
die CTE-Daten in eine temporäre Tabelle speichern oder eine abgeleitete Tabelle in
jeder Spaltenunterabfrage verwenden.
WITH Sales AS (
SELECT CountryRegionCode,
COUNT(StateProvinceCode) AS Provinces,
TerritoryID
FROM Person.StateProvince
GROUP BY CountryRegionCode, TerritoryID
HAVING TerritoryID < 6
)
SELECT s1.*,
(SELECT COUNT(*)
FROM Sales AS s2
WHERE s2.Provinces < s1.Provinces) + 1 AS R,
(SELECT COUNT(DISTINCT Provinces)
425
Analysen
FROM Sales AS s2
WHERE s2.Provinces < s1.Provinces) + 1 AS DR
FROM Sales AS s1
ORDER BY Provinces
623_07.sql: Lösung für Version 2000
Man erhält ebenfalls eine Aufstellung der Länder mit den Anzahlen der Verkaufsgebiete.
CountryRegionCode Provinces
TerritoryID R
DR
----------------- ----------- ----------- ------ ---AS
1
1
1
1
VI
1
5
1
1
US
5
4
3
2
US
9
1
4
3
US
11
5
5
4
US
14
2
6
5
US
14
3
6
5
(7 Zeile(n) betroffen)
6.2.4
Bereiche/Quantile
Schließlich ist auch noch die NTILE()-Funktion vorhanden, die in der Lage ist, die
Daten in einem sortierten Bereich in Gruppen zu verteilen. Dabei ist die Gruppenanzahl
vorher ebenfalls angegeben. Als Rückgabewert liefert diese Funktion die Gruppennummer, zu welcher der Datensatz gehört. Im Parameter der Funktion steht die Anzahl
426
Analysen
der Gruppen. Innerhalb der OVER-Klausel folgen die Spalten, anhand derer die Einteilung in Gruppen durchgeführt wird.
NTILE (integer_expression)
OVER ( [ <partition_by_clause> ] < order_by_clause > )
SELECT ProductID,
ListPrice,
ProductSubcategoryID,
NTILE(3) OVER (ORDER BY ListPrice,
ProductSubcategoryID) Quantil
FROM Production.Product
WHERE ProductID BETWEEN 800 AND 831
AND ProductSubcategoryID BETWEEN 10 AND 14
ORDER BY ListPrice, ProductSubcategoryID
624_01.sql: Dreiteilung einer Ergebnismenge
Man erhält eine Einteilung in die einzelnen Gruppen sowie die eigentlichen Daten. Auf
Basis einer solchen Ergebnismenge kann man dann wiederum gruppengesteuerte Ausgaben machen.
ProductID
ListPrice
ProductSubcategoryID Quantil
----------- ------------ -------------------- --------805
34,20
11
1
806
102,29
11
1
807
124,73
11
1
802
148,22
10
1
427
Analysen
803
175,49
10
2
804
229,49
10
2
830
348,76
12
2
831
348,76
12
3
814
348,76
12
3
822
594,83
14
3
(10 Zeile(n) betroffen)
Sofern keine Zahlen ausgegeben werden sollen, kann man sich mit Hilfe der CASEAnweisung auch Zeichenketten erzeugen. Man muss lediglich die zurück gelieferten
Zahlen abprüfen und übersetzen.
SELECT ProductID,
ListPrice,
ProductSubcategoryID,
CASE NTILE(3) OVER (ORDER BY ListPrice,
ProductSubcategoryID)
WHEN 1 THEN 'niedrig'
WHEN 2 THEN 'mittel'
WHEN 3 THEN 'hoch'
END AS Quantil
FROM Production.Product
624_02.sql: Übertragung der Zuordnung in Texte
In diesem Fall erhält man eine Einordnung in Gruppen per Text.
428
Analysen
ProductID
ListPrice
ProductSubcategoryID
Quantil
----------- --------------------- -------------------- -----805
34,20
11
175,49
10
594,83
14
niedrig
...
803
mittel
...
822
hoch
Neben dieser Spielerei kann man unter Verwendung einer abgeleiteten/temporären
Tabelle und natürlich eines allgemeinen Tabellenausdrucks Aggregate pro Bereich
ermitteln, indem man ganz einfach eine Gruppierung pro Bereich durchführt und für die
einzelnen Bereiche das Aggregat anwendet.
WITH Production AS (
SELECT ProductID,
ListPrice,
ProductSubcategoryID,
NTILE(3) OVER (ORDER BY ListPrice) AS Quantil
FROM Production.Product
)
SELECT Quantil,
COUNT(ProductID) AS Number,
429
Analysen
MIN(ListPrice)
AS MinP,
MAX(ListPrice)
AS MaxP,
AVG(ListPrice)
AS AvgP
FROM Production
GROUP BY Quantil
624_03.sql: Aggregate bei Quantilen
Man erhält pro Quantil die entsprechenden Aggregatwerte als Ergebnis.
Quantil
Number
MinP
MaxP
AvgP
--------- ----------- -------- ------- ----------1
168
0,00
0,00
0,00
2
168
0,00
330,06
76,687
3
168
333,42
3578,27 1239,3116
(3 Zeile(n) betroffen)
6.3
Pivot
Ein besonders schwieriges Problem ist die Erstellung einer Pivot-Tabelle. Dies mag
zwar für eine gewöhnliche Abfrage weniger häufig notwendig sein, aber wenn ein Bericht erstellt werden soll oder eine einfache äußere Anwendung die Daten in pivotierter
Form erhalten soll, dann kann man sich verschiedene Strategien überlegen, wie und wo
die Tabellendrehung am einfachsten auszuführen ist. Letztendlich ist es vermutlich auch
in diesem Fall so, dass eine Lösung in SQL aufgrund der Sprachstruktur einfacher zu
bewerkstelligen ist als in einer äußeren Anwendung unter dem Einsatz von Schleifen,
Fallunterscheidungen und einem dadurch komplizierten Algorithmus. Die Lösung ist
leider in T-SQL auch nicht besonders einfach, insbesondere dann nicht, wenn nicht die
Version 2005 bereit steht und genutzt wird. Im direkten Vergleich zwischen alter und
430
Analysen
neuer Fassung ist der Einsatz von 2005 besonders lohnenswert, wobei auch die neue
Lösung leider nicht nur mit einer einzigen Anweisung auskommt.
6.3.1
Klassisches Pivotieren
Die Funktionsweise einer Pivot-Abfrage sowie die Implikationen, die sich auf dem Weg
zum endgültigen Ziel ergeben, soll langsam aufgebaut werden, wobei gleichzeitig auch
die herkömmliche Technik, die nicht nur im MS SQL Server, sondern in allen Datenbanken auf ähnliche Weise funktioniert, vorgeführt werden. Zunächst benötigt man die
Rohdaten für eine zu drehende Abfrage. Dies sollen die Jahreszahlen und ihre entsprechenden Umsätze sein.
SELECT YEAR(OrderDate) AS [Year],
SUM(TotalDue)
AS Total
FROM Sales.SalesOrderHeader
GROUP BY YEAR(OrderDate)
631_01.sql: Rohdaten
Man erhält eine einfache Ausgabe aus zwei Spalten mit den vier Jahreszahlen, in denen
Umsatz erwirtschaftet wurde, und natürlich den zugehörigen Umsätzen. Eine PivotTabelle enthält nun gerade nicht vier Zeilen, sondern nur eine, wobei als Spaltenköpfe
gerade die Jahreszahlen und als Werte die Umsatzzahlen auftreten.
Year
Total
----------- --------------------2004
32196912,4165
2001
14327552,2263
2002
39875505,095
2003
54307615,0868
431
Analysen
(4 Zeile(n) betroffen)
Die einfachste Möglichkeit, eine Pivot-Darstellung zu erreichen, gelingt über eine Folge
von Spaltenunterabfragen. In jeder einzelnen dieser Unterabfragen wird genau ein Wert
ermittelt. Dies ist leider nicht dynamisch, da ja die Anzahl der Jahre bereits von vornherein feststehen muss. Dieses Problem wird sich in allen nachfolgenden Beispielen
nicht beheben lassen.
SELECT DISTINCT
(SELECT SUM(TotalDue)
FROM Sales.SalesOrderHeader
WHERE Year(OrderDate) = 2001) AS [2001],
(SELECT SUM(TotalDue)
FROM Sales.SalesOrderHeader
WHERE Year(OrderDate) = 2002) AS [2002]
FROM Sales.SalesOrderHeader
631_01.sql: Spaltenweises Pivotieren mit Spaltenunterabfragen
Da die obige Abfrage nicht alle Jahreszahlen umfasste und sie ansonsten auch viel zu
umfangreich geworden wäre, besteht die Ergebnismenge aus zwei Spalten und einer
Zeile mit den Werten für zwei Jahre.
2001
2002
--------------------- --------------------14327552,2263
39875505,095
(1 Zeile(n) betroffen)
Die vorherige Lösung gelingt, erfordert aber die mehrfache Kopie der gleichen Abfrage
mit unterschiedlichen Filterwerten. Um dies zu verhindern, lässt sich eine Lösung mit
432
Analysen
Fallunterscheidung denken, die rund um den Globus mit Erfolg eingesetzt wird. In ihrer
Urform erzeugt sie zunächst noch nicht die benötigen Tabelle, sondern die Rohdaten,
mit deren Hilfe man das Prinzip verstehen kann. In der angekündigten Fallunterscheidung, die man wiederum wie eine Spaltenunterabfrage für jede Spalte und damit für
jeden Wert in der x-Achse durchführt, untersucht man die Jahreszahl. Sofern die richtige Jahreszahl (2001, 2002, etc.) im Datensatz vorliegt, soll die Summe berechnet werden, während bei falschem Wert für die Jahreszahl NULL ausgegeben werden soll.
SELECT CASE
WHEN YEAR(OrderDate) = 2001
THEN SUM(TotalDue) ELSE NULL
END AS [2001],
CASE
WHEN YEAR(OrderDate) = 2002
THEN SUM(TotalDue) ELSE NULL
END AS [2002]
FROM Sales.SalesOrderHeader
GROUP BY YEAR(OrderDate)
631_01.sql: Einsatz von CASE
Man erhält die erwarteten Spalten, erzeugt jedoch auch die angekündigten NULL-Werte,
sodass insgesamt wiederum vier Zeilen entstehen.
2001
2002
--------------------- --------------------NULL
NULL
14327552,2263
NULL
433
Analysen
NULL
39875505,095
NULL
NULL
(4 Zeile(n) betroffen)
Um dieses Problem zu beheben, platziert man die Summierung für jede Spalte nicht
innerhalb der CASE-Anweisung, sondern legt diese vielmehr in die SUM-Funktion hinein. Weil NULL-Werte nicht summiert werden können, blendet man automatisch diese
Werte aus.
SELECT SUM(CASE YEAR(OrderDate)
WHEN
2001 THEN TotalDue ELSE NULL
END) AS [2001],
SUM(CASE YEAR(OrderDate)
WHEN 2002 THEN TotalDue ELSE NULL
END) AS [2002]
FROM Sales.SalesOrderHeader
631_01.sql: Pivotieren mit CASE
Als Ergebnis erhält man eine tatsächlich gedrehte Tabelle. Diese wird um eine Warnung
ergänzt. Diese gibt an, dass durch die Summierung/Aggregierung NULL-Werte, die
solchermaßen bearbeitet werden sollten, gelöscht wurden.
2001
2002
--------------------- --------------------14327552,2263
39875505,095
Warnung: Ein NULL-Wert wird durch einen Aggregat- oder
sonstigen SET-Vorgang gelöscht.
(1 Zeile(n) betroffen
434
Analysen
6.3.2
Einsatz von (UN)PIVOT
Da die zurückliegenden Beispiele doch eine gewisse Mühseligkeit bedeuten und man
bei einer einfachen Abfrage von vorneherein wissen muss, wie viele Werte nachher in
der Spaltenliste zu einer eigenen Spalten führen müssen, gibt es in Version 2005 einen
neuen Operator mit dem verständlichen Namen PIVOT. Er ermöglicht die Erzeugung
von Pivottabellen innerhalb einer einzigen Abfrage ohne die berühmten CASEAnweisungen.
Seine allgemeine Syntax hat die Form:
<pivoted_table> ::=
table_source PIVOT <pivot_clause> table_alias
<pivot_clause> ::=
( aggregate_function ( value_column )
FOR pivot_column
IN ( <column_list> )
)
<column_list> ::=
column_name [ , ... ]
Die gesamte Syntax besteht aus verschiedenen Teilen, die ineinander verzahnt sind und
welche bei den ersten eigenen Beispielen zunächst verschiedene Hürden bilden. Daher
bauen die nachfolgenden Beispiele aufeinander auf und führen das vorherige Beispiel
weiter fort, um die alternative, moderne Lösung direkt mit der herkömmlichen vergleichen zu können. Folgende Bestandteile lassen sich identifizieren:
Die eingehende Tabelle, welche vor dem PIVOT-Operator genannt wird bzw. in Form
einer CTE vorab oder in Form einer abgeleiteten Tabelle direkt in der FROM-Klausel
erstellt wird, erfährt eine Gruppierung wie durch GROUP BY. Dies erfolgt anhand er
Gruppierungsspalten, die optional angegeben sein können. Sie bilden bei einer Kreuzta-
435
Analysen
belle die verschiedenen Gruppen ab, für die Werte für die Gruppe der Ausgabespalten
ermittelt werden. Ausgabespalten wiederum sind die Spalten, welche früher individuell
durch die CASE-Anweisungen erstellt wurden. Diese direkte und individuelle Erstellung
ist auch weiterhin notwendig. Um die endgültige Ausgabe zu ermitteln, generiert die
Abfrage die verschiedenen Werte auf die folgende Weise:
1. Weitere Gruppierung der Zeilen für die Pivotspalte, welche bereits durch
die Gruppierung mit GROUP BY im vorherigen Schritt erstellt wurde. Wahl
einer Untergruppe für die einzelnen Ausgabespalten anhand der Bedingung
pivot_column
=
CONVERT(<data
type
of
pivot_column>,
'output_column').
5. Auswertung der möglicherweise angegebenen Aggregatfunktion für die
einzelnen Wertespalten dieser Untergruppe. Die Wertespalte bildet bei einer
Kreuztabelle (mehrere Zeilen) typischerweise die Werte der meist einzelnen
linken Spalte, während die zugehörigen Werte in die meist mehrfach
auftretenden output_columns eingetragen werden. Bei einer leeren
Untergruppe entsteht eine NULL, sofern keine Zählung mit COUNT
durchgeführt wird, die automatisch den Wert 0 zurückgibt.
Die gesamte Konstruktion führt dazu, dass ein so genannter Tabellenwertausdruck (also
eine Abfrage, die zu einer Ergebnismenge führt) pivotiert wird. Dabei ordnet der PIVOT-Operator eindeutige Werte aus der Ergebnismenge Spalten der geplanten und durch
den Operator sowie die Spaltenliste definierten Ausgabetabelle zu. Andere Werte aggregiert er dabei nach der vorgeschriebenen Weise, sofern dies notwendig ist und die
Aggregation nicht ohnehin schon an anderer Stelle (abgeleitete Tabelle, CTE) vorgenommen wurde. Der UNPIVOT-Operator kehrt den gesamten Prozess um und kann auf
Basis einer pivotierten Tabelle wieder die Ausgangswerte erzeugen.
6.3.2.1
Pivottabellen einrichten
Die Funktionsweise der neuen Operatoren ist beeindruckend, doch leider nicht wirklich
einfach zu verstehen. Daher dürfte das nachfolgende Beispiel die allgemeinen Erläuterungen der Einführung gut untermauern. Zunächst erstellt man wiederum die Rohdaten,
436
Analysen
wie dies schon zuvor geschehen ist, um für die gleiche Aufgabestellung, die zur Darstellung des traditionellen Lösungswegs genutzt wurde, die moderne Lösung anzugeben. In
der CTE Turnover befindet sich nun der Abruf einer zweispaltigen Ergebnismenge
OrderDate und TotalDue, welche man im nächsten Schritt pivotieren möchte. In
dieser Ergebnismenge sind allerdings noch keine aggregierten Werte, sondern nur ein
kompletter Zeilenabruf der SalesOrderHeader-Tabelle. Die Aggregierung könnte
man hier bereits vornehmen; sie lässt sich jedoch auch direkt im PIVOT-Operator durchführen.
Da im Vorfeld bereits klar ist, dass die Jahreszahlen zwischen 2001 und 2004 in der
CTE abgerufen werden, erstellt man nun für diese Jahreszahlen Spalten, in die nachher
die entsprechenden Umsätze für diese Jahre übernommen werden sollen. Auch bei dieser Lösung muss das eigentliche Ergebnis also von vorneherein klar sein. Lediglich die
lange CASE-Anweisung und damit ein Großteil der unleserlichen Syntax der traditionellen Lösung verschwinden.
Nachdem man die CTE in der FROM-Klausel referenziert hat, folgt nun endlich der lange
angekündigte PIVOT-Operator. Zunächst gibt man an, was mit der Spalte TotalDue
geschehen soll, deren Werte zu den einzelnen Ausgabespalten für die Jahreszahlen
zugeordnet werden soll. Diese Zuordnung gelingt über die Gruppierung, wie sie auch
durch GROUP BY OrderDate hätte eingerichtet werden können. In diesem Fall allerdings folgt in der FOR-Klausel der Spaltennamen, für die gruppiert werden soll, und in
der IN-Klausel folgenden Werte, welche für die Zuordnung der Werte zu den Ausgabespalten genutzt werden. Dies stellt nun die verkürzte CASE-Anweisung dar. Sobald ein
Wert von 2001 erscheint, wird der zugehörige Umsatz zur akkumulierten Umsatzzahl
von allen Werten aus 2001 hinzugefügt. Schließlich hat man für alle angegebenen und
schon im Vorfeld bekannten Jahreszahlen die nötigen Gesamtumsatzzahlen zusammen,
welche in der pivotierten Ergebnismenge ausgegeben werden können. Die Zuordnung
zu den Ausgabespalten erfolgt dabei über die Spaltenliste und die IN-Klausel, da hier
die gleichen Werte erscheinen.
WITH Turnover AS (
437
Analysen
SELECT YEAR(OrderDate) AS OrderDate,
TotalDue
FROM Sales.SalesOrderHeader
)
SELECT [2001], [2002], [2003], [2004]
FROM Turnover
PIVOT (SUM(TotalDue)
FOR OrderDate
IN ([2001], [2002], [2003], [2004])) AS p
632_01.sql: Standardfall von PIVOT
Man erhält wie zuvor folgende Ergebnismenge:
2001
2002
2003
2004
-------------- ------------ ------------- ------------14327552,2263
39875505,095 54307615,0868 32196912,4165
Die erzeugten Spaltennamen in der endgültigen Ausgabetabelle enthalten nun die Werte, welche in der Spaltenliste angegeben sind bzw. in der IN-Klausel aufgerufen werden.
Dies ist nicht wirklich verwunderlich, da in der Spaltenliste immer auch die Spaltennamen angegeben sind, welche nachher in der Ausgabe erscheinen. In diesem Fall dienen
sie allerdings auch dazu, die Wertzuordnungen für die Pivotierung einzurichten. Daher
sei an dieser Stelle noch einmal darauf hingewiesen, dass ein Aliasname dafür sorgt,
dass die Werte zugewiesen werden, in der Ausgabe allerdings ganz andere Spaltennamen entstehen.
SELECT [2001] AS Year1, [2002] AS Year2
FROM Turnover
438
Analysen
PIVOT (SUM(TotalDue)
FOR OrderDate
IN ([2001], [2002])) AS p
632_01.sql: Erzeugen von anderen Spaltennamen
In diesem Fall erhält man als Ergebnis gerade nicht die vorgegebenen Jahreszahlen als
Spaltennamen, sondern die Aliasnamen.
Year1
Year2
--------------------- --------------------14327552,2263
39875505,095
Neben dieser einfachen Pivotierung benötigt man beizeiten auch Kreuztabellen. Dies
sind Tabellen, welche sich gerade nicht dadurch auszeichnen, nur eine einzige Zeile zu
besitzen, sondern eine Gruppierung anhand von erweiterten Gruppenmerkmalen, die
sich nicht nur an der Spaltenliste (Jahreszahlen) orientieren. Im nächsten Beispiel ruft
man daher in der CTE noch die TerritoryID mit dem Aliasnamen Region ab. Diese
Spalte gelangt dann über die eigentliche Abfrage auf Basis der CTE in die endgültige,
pivotierte Ergebnismenge. Nur durch ihre Nennung, werden die Werte in den Ausgabespalten noch einmal gruppiert, sodass für jede Region in den angegebenen Jahren die
entsprechenden Werte ermittelt werden. Dies entspricht einer Anweisung wie GROUP
BY Region, Year. Mehr als eine Spalte ist selbstverständlich möglich, sodass weitere
Gruppierungen möglich sind.
WITH Turnover AS (
SELECT TerritoryID
AS Region,
YEAR(OrderDate) AS OrderDate,
TotalDue
FROM Sales.SalesOrderHeader
439
Analysen
)
SELECT Region, [2001], [2002]
FROM Turnover
PIVOT (SUM(TotalDue) FOR OrderDate IN ([2001], [2002])) AS p
632_02.sql: Kreuztabelle
Man erhält eine als Kreuztabelle bekannte Ausgabe, in der man sich vorstellen kann, in
der ersten Zeile und in der ersten Spalte jeweils Spaltenköpfe zu besitzen.
Region
2001
2002
----------- --------------------- --------------------1
2703481,7947
5651688,6685
2
754833,2045
3275322,1694
322207,5294
1778804,7452
...
10
(10 Zeile(n) betroffen)
Wie man nun diese Tabelle tatsächlich ausgibt, ist völlig beliebig. Auf Basis der gerade
angegebenen Lösung könnte man sich genauso gut vorstellen, dass die Regionen in der
ersten Zeile und die Jahreszahlen in der ersten Spalte erscheinen. Dies erfordert nur, die
Regionsnummern in der Spaltenliste und in der IN-Klausel zu erwähnen, wobei in der
IN-Klausel nun die Region-Spalte und nicht mehr die OrderDate-Spalte aufgerufen
wird.
SELECT OrderDate, [1], [2], [3]
FROM (SELECT TerritoryID
AS Region,
YEAR(OrderDate) AS OrderDate,
TotalDue
440
Analysen
FROM Sales.SalesOrderHeader) AS Turnover
PIVOT (SUM(TotalDue) FOR Region IN ([1], [2], [3])) AS p
632_03.sql: Variante der Kreuztabelle
Man erhält die angekündigte Variante der vorherigen Lösung, die nur noch vier Zeilen
enthält, da ja als Zeilen nur noch vier Jahre abgerufen werden.
OrderDate
1
2
3
----------- ------------- -------------- --------------2001
2703481,7947
754833,2045
1263884,1024
4952772,2793
1406555,6861
1771532,7396
...
2004
(4 Zeile(n) betroffen)
Grundsätzlich stellt der (UN)PIVOT-Operator eine große Vereinfachung dar, welche
auch nach einigen Übungen gut und schnell ins Repertoire aufgenommen werden kann.
Allerdings ist das Problem noch nicht gelöst, eine dynamische Ermittlung der möglichen Ausgabespalten einzurichten. Auch bei diesem neuen Operator muss man vor der
Ausführung der Abfrage wissen, dass im aktuellen Beispiel genau die Jahre zwischen
2001 und 2004 abgerufen werden. Sollte man doch einmal Werte für 2005 in der Datenbank besitzen, würde man dies nicht bemerken, weil die Abfrage nur auf diese Werte
prüft.
Mit einem kleinen T-SQL-Programm ist es allerdings grundsätzlich möglich, wenigstens mit ein wenig Arbeit eine dynamische Abfrage einzurichten. Dabei setzt man die
benötigte SQL-Anweisung mit drei, vier oder fünf Jahren dynamisch zusammen, sodass
bei jedem neuen Aufruf die aktuellen Jahre zum Einsatz kommen.
Zunächst benötigt man einen Cursor für die wechselnden und eigentlich statisch anzugebenden Ausgabespalten; in diesem Fall also für die Jahre. Er dürfte in den meisten
Fällen über eine DISTINCT-Abfrage erstellt werden können. Um die aktuelle Spalte, die
441
Analysen
gewünschte Zeichenkette in der Form [2001], [2002] zu erstellen und auch noch die
Tatsache zu berücksichtigen, dass nach dem letzten Wert des Cursors kein Komma zu
Trennung der Werte benötigt wird, erstellt man hiernach eine Variable für die gerade
abgerufene Spalte, eine Variable für die gesamte, sukzessiv abzubauende Zeichenkette
und eine Zählervariable. Insbesondere die Zählervariable ist von einiger Bedeutung, um
zu unterscheiden, ob man sich beim Abruf der Zeilen beim ersten Cursorwert, in der
Cursormitte oder gar an seinem Ende befindet. Die Überprüfung kann auf unterschiedliche Weise durchgeführt werden. Im aktuellen Beispiel prüft man auf den ersten Datensatz. Es lässt sich auch eine Vorgehensweise finden, in der man den ersten Wert über
eine einzelne FETCH-Anweisung abruft und danach erst die Schleife einrichtet. In jedem
Fall muss man die Ausgabespalten korrekt zusammensetzen und schließlich zweimal in
die auszuführende SQL-Anweisung kopieren, ehe man sie dynamisch mit EXEC ausführt. Über diesen sehr einfachen Trick gelingt es schließlich, dynamisch Pivotierungen
einzurichten.
-- Cursor für die Abfrage der Regionen
DECLARE c_region CURSOR SCROLL
FOR SELECT DISTINCT TerritoryID
FROM Sales.SalesOrderHeader
ORDER BY TerritoryID
-- Zeichenketten für Zähler, Ausgabespalte(n) erstellen
DECLARE @spalte int, @spalten varchar(255), @i int
-- Vorgabewerte
SET @spalten = ''
SET @i = 1
-- Verarbeitung Cursor c_region
OPEN c_region
442
Analysen
FETCH c_region INTO @spalte
WHILE (@@FETCH_STATUS = 0) BEGIN
-- Zusammensetzen der Zeichenkette
IF @i = 1 BEGIN
SET @spalten = @spalten + '[' + LTRIM(STR(@spalte)) + ']'
END ELSE BEGIN
SET @spalten = @spalten + ', [' + LTRIM(STR(@spalte)) +
']'
END
-- Inkrementation
SET @i = @i + 1
-- Nächster Abruf
FETCH c_region INTO @spalte
END
CLOSE c_region
DEALLOCATE c_region
-- SQL zusammensetzen und ausführen
EXEC ('SELECT OrderDate, ' + @spalten +
'
FROM (SELECT TerritoryID
AS Region,
YEAR(OrderDate) AS OrderDate,
TotalDue
FROM Sales.SalesOrderHeader) AS Turnover
443
Analysen
PIVOT (SUM(TotalDue)
FOR Region IN (' +
@spalten + ')) AS p')
632_04.sql: Dynamisches PIVOT mit T-SQL
6.3.2.2
Pivottabellen zurückwandeln
In einer korrekt normalisierten und überhaupt ordentlich geplanten Datenbank sollten
eigentlich keine schon vorab pivotierten Daten enthalten sein. Die einzig denkbaren
Situationen, in denen es zu solchermaßen aufbereiteten Daten kommen kann, die man
abrufen, verarbeiten und vor allen Dingen wieder umwandeln soll, wären Sichten und
Import-Tabellen, die nur als Zwischenspeicher für bspw. pivotierte Date aus MS Excel
verfügbar sind. Solche Daten lassen sich allerdings einfach mit dem UNPIVOT-Operator
wieder in nicht-pivotierte Daten umwandeln.
Im nächsten Beispiel erzeugt man zunächst wieder die pivotierten Daten für die Umsätze und die Jahre, in denen sie erbracht worden sind. Auf Basis dieser zwei CTEs erstellt
man dann eine Abfrage, in der die beiden Spalten OrderDate und TotalDue erscheinen. Dabei soll die Spalte TotalDue wieder aus den einzelnen Spalten für die Jahre
gelöscht und in einzelne Zeilen kopiert werden. Daher ruft man innerhalb des UNPIVOTOperators diese umzuwandelnde Spalte auf, nennt in FOR die Spalte, welche für die
Zuordnung zu den Ausgabespalten (Jahreszahlen) genutzt wurde, und gibt in der INKlausel wieder die Werte an, die in diesen Ausgabespalten erscheinen.
WITH Turnover AS (
SELECT YEAR(OrderDate) AS OrderDate,
TotalDue
FROM Sales.SalesOrderHeader
),
[Pivot] AS (
444
Analysen
SELECT [2001], [2002], [2003], [2004]
FROM Turnover
PIVOT (SUM(TotalDue)
FOR OrderDate
IN ([2001], [2002], [2003], [2004])) AS p
)
SELECT OrderDate, TotalDue
FROM [Pivot]
UNPIVOT (TotalDue
FOR OrderDate
IN ([2001], [2002], [2003], [2004])) AS p
632_05.sql: Standardfall von UNPIVOT
Wie schon zuvor muss man auch bei der Umkehrung der Pivotierung unterscheiden, ob
man nur eine einzige Zeile und damit eine einfach gekippte Tabelle erstellt und wieder
umwandelt, oder ob man eine Kreuztabelle bearbeitet. Im nachfolgenden Beispiel erstellt man daher noch einmal eine Pivot-Tabelle, welche für die einzelnen Regionen die
Umsatzzahlen für jedes Geschäftsjahr ermittelt. Diese soll dann wiederum in ihre ursprüngliche Form umgekehrt werden.
Dazu gibt man in der Spaltenliste die benötigten drei Spalten der originären Tabelle
Region, OrderDate und TotalDue an. Im UNPIVOT-Operator nennt man zunächst die
TotalDue-Spalte, da sie die gruppierten und berechneten Werte enthält. Die Ausgabespalten werden aus der OrderDate-Spalte ermittelt, sodass diese in FOR und die erzeugten Werte in der IN-Klausel aufgezählt werden müssen. Die Region-Spalte muss
dagegen gar nicht extra berücksichtigt werden, denn durch ihre Nennung in der Spalten-
445
Analysen
liste und ihre Existenz in der pivotierten Tabelle berücksichtigt sie der Operator automatisch, sodass die ursprünglichen Werte wieder erscheinen.
WITH Turnover AS (
SELECT TerritoryID
AS Region,
YEAR(OrderDate) AS OrderDate,
TotalDue
FROM Sales.SalesOrderHeader
),
[Pivot] AS (
SELECT Region, [2001], [2002]
FROM Turnover
PIVOT (SUM(TotalDue)
FOR OrderDate
IN ([2001], [2002])) AS p
)
SELECT Region, OrderDate, TotalDue
FROM [Pivot]
UNPIVOT (TotalDue
FOR OrderDate
IN ([2001], [2002])) AS p
632_06.sql: Kreuztabelle auflösen
Dadurch erstellt man aus einer pivotierten Ergebnismenge wie
446
Analysen
Region
2001
2002
----------- --------------------- --------------------1
2703481,7947
5651688,6685
2
754833,2045
3275322,1694
3
1263884,1024
3518185,4756
...
(10 Zeile(n) betroffen)
die ursprüngliche Darstellung, die nun bspw. aus der Zwischen-/Übernahme-/ImportTabelle in eine „korrekte“ Datenbanktabelle übernommen werden könnte.
Region
OrderDate
TotalDue
----------- -------------- --------------1
2001
2703481,7947
1
2002
5651688,6685
2
2001
754833,2045
10
2001
322207,5294
10
2002
1778804,7452
...
(20 Zeile(n) betroffen)
Schließlich ist es auch noch möglich, nicht nur die ursprünglichen Daten und ihre Struktur zu erzeugen, sondern vielmehr auch weitere Berechnungen und Umwandlungen
durchzuführen, die direkt auf den pivotierten Daten basieren und im Rahmen der Umkehrung vorgenommen werden. Dies versteht sich eigentlich von selbst, soll allerdings
als Anregung für eigene Arbeiten auf Basis der vom vorherigen Beispiel übernommen,
aber nicht noch einmal in den beiden abgedruckten CTEs gezeigt werden. Anstatt die
447
Analysen
drei Spalten Region, OrderDate und TotalDue und damit die vormalige Struktur
auszugeben, beschränkt man sich in der nächsten Abfrage direkt nur auf die Regionund TotalDue-Spalte, wobei die letzte zusätzlich noch mit SUM aggregiert wird.
SELECT Region, SUM(TotalDue) AS Turnover
FROM [Pivot]
UNPIVOT (TotalDue
FOR OrderDate
IN ([2001], [2002])) AS p
GROUP BY Region
632_07.sql: Weitere Verarbeitung
Man erhält in diesem Fall eine Ergebnismenge, die sofort einen Verarbeitungsschritt
nach dem eigentlichen Unpivotieren darstellt und eine um die Jahreszahlen befreite
Aggregation der Umsatzzahlen pro Region darstellt.
Region
Turnover
----------- --------------------1
8355170,4632
2
4030155,3739
...
10
2101012,2746
(10 Zeile(n) betroffen)
448
Programmierbarkeit
7 Programmierbarkeit
449
Programmierbarkeit
450
Programmierbarkeit
7 Programmierbarkeit
Unter dem schönen, neuen Wort ‚Programmierbarkeit’ verbirgt sich im MS SQL Server
2005 die Möglichkeit, Prozeduren, Funktionen und Trigger als T-SQL- und .NETModule zu hinterzulegen, welcher der Datenbank eine zusätzliche Schicht hinzufügen.
Diese enthält in Form der genannten Module bereits einen Teil der Software, welcher
die Datenbank erneut von der eigentlichen Software in bspw. .NET verbirgt. Neben dem
Effekt des Verbergens und Schützens ist diese Technik auch deswegen so interessant,
weil durch sie eine vereinfachte Benutzung der Datenbank gelingt. Ein typisches Beispiel ist in diesem Zusammenhang die Entwicklung einer Prozedur, welche für eine
ganze Reihe an Parametern einen Einfügevorgang in eine Tabelle vereinfacht. Die äußere Software und damit auch der Programmierer ist nicht gezwungen, entsprechende
INSERT-Anweisungen zu formulieren oder daran zu denken, welche Parameter für
einen sinnvollen Datensatz notwendig sind. Stattdessen ruft er nur noch diese Prozedur
auf, was darüber hinaus auch noch in verschiedenen Programmiersprachen gelingt.
Dieses Kapitel stellt die beiden Themen Funktionen und Prozeduren vor, während Trigger dem Buch zur MS SQL Server-Administration und die Programmierbarkeit von
.NET einem entsprechenden .NET-Buch vorbehalten sind. Insbesondere die Verwendung von .NET ist zwar sicherlich als Fähigkeit extrem interessant, allerdings nützt ein
solches Kapitel für Leser nichts, die zunächst noch .NET an sich lernen müssten.
7.1
Prozeduren
Die Erstellung von Prozeduren und Funktionen ist grundsätzlich in allen kommerziellen
Großdatenbanken wie MS SQL Server oder Oracle und mittlerweile auch in Open Source-Produkten wie MySQL möglich. Während Oracle hier entweder die eigene Programmiersprache PL/SQL (eine sehr stark erweiterte Form von T-SQL) sowie auch die
Verwendung von Java erlaubt, bietet hier der MS SQL Server entweder T-SQL oder
.NET an. Dabei entspricht eine Prozedur einem Konstrukt, das unter vielen verschiedenen Namen wie Modul, DB-Routine, Unterprogramm oder - wie es Microsoft selbst
451
Programmierbarkeit
vorschlägt - als Auflistung von T-SQL-Anweisungen, die unter einem Namen in der DB
gespeichert ist. Dabei ermöglicht es eine Prozedur, nicht nur wie ein Makro mehrere
Anweisungen der Reihe nach auszuführen und für einen späteren wiederholten Aufruf
verfügbar zu machen, sondern bietet darüber hinaus auch die Fähigkeit, Parameter einer
äußeren Anwendung entgegen zu nehmen und diese zu verarbeiten. Prozeduren erscheinen im Normalfall als permanent in der Datenbank gespeicherte Objekte, können allerdings auch wie Tabellen als temporär für eine Sitzung und global temporär für alle
Sitzungen verfügbar gemacht werden.
7.1.1
Einführung
Die Anweisung in T-SQL, um eine Prozedur zu erstellen, ist sehr ähnlich zur Funktionserstellung. Die Unterschiede ergeben sich in den Details und in den Einsatzbereichen.
Die nachfolgende Abbildung zeigt die verschieden Arten von Prozeduren, die im MS
SQL Server erstellt werden können. Auf der unteren Hälfte der Abbildung befinden sich
sechs beispielhaft angegebene Tabellen, die zu zwei verschiedenen Bereichen zusammen gefasst werden. Zwischen einer Anwendung wie das schon leidlich bekannte Management Studio und einer anderen, individuell erstellten Anwendung liegt eine Schicht
von Prozeduren. Sie sollen bspw. den Abruf und die Manipulation von Daten beeinflussen. Gründe können hier erhöhte Sicherheit, erweiterte Möglichkeit zur Validierung
oder auch vereinfachte Benutzbarkeit von außen sein.
Auf der linken Seite der drei unterschiedlichen Prozedurarten werden die drei DMLBefehle INSERT, UPDATE und DELETE aufgelistet. Sie sollen zeigen, dass Prozeduren
zum Einsatz kommen können, wenn Datenmanipulation bspw. für komplexe Datenstrukturen durch das Angebot einer zentralen Zugriffsschicht von Prozeduren vereinfacht werden sollen.
452
Programmierbarkeit
Individuelle
Software
T-SQL
(Management Studio)
Relationale
Ergebnis
Menge &
Cursor
Rückgabewerte
Prozedur
INSERT
UPDATE
DELETE
Prozedur
SELECT
Prozedur
DBAAufgaben
Datenbank
Tab 1
Tab 2
Tab 1
Tab 2
Tab 2
...
Bereich 1
Tab 3
...
Bereich 2
Abbildung 7.1: Typologie von Prozeduren
Direkt rechts neben diesen Prozeduren gibt es eine weitere Gruppe. Sie wirkt sich notwendigerweise auf Tabellen aus, sofern diese Tabellen nicht Administrationsdaten speichern. Stattdessen führen sie wie Skripte in einem Server häufig wiederkehrende Aufgaben aus, die von einem Administrator ansonsten durch im Dateisystem gespeicherte
Skripte oder Eingaben in der grafischen Benutzerschnittstellen durchgeführt werden
müssten. Solche Prozeduren bzw. das entsprechende SQL wie bspw. MassendatenImport/-Export oder allgemeine Verwaltungsaufgaben werden im Buch zur Administration beschrieben. Diese und die DML-Prozeduren haben die Möglichkeit, Rückgabewerte zurückzuliefern. Diese können per Referenz aus der Parameterliste abgerufen
werden. Dies bedeutet, dass Prozeduren nicht wie Funktionen auf der rechten Seite einer
Zuweisung stehen können, sondern als eigenständige Anweisung. Dies bedeutet allerdings auch, dass Prozeduren mehrere Rückgabewerte zurückliefern können.
453
Programmierbarkeit
Auf der rechten Seite schließlich wird eine Besonderheit im MS SQL Server dargestellt,
welche insbesondere für Umsteiger von Oracle überraschend sein kann. Eine Prozedur
bietet die Möglichkeit, relationale Ergebnismengen und Cursor zurückzuliefern. Insbesondere die erste Technik ist besonders interessant, da im Gegensatz zu Sichten hier die
Möglichkeit besteht, vorgefertigte Filter über die Prozedurparameter anzubieten.
Es ließe sich auch noch eine andere Ordnung für Prozeduren finden: Man kann zwischen temporären und tatsächlich gespeicherten („normalen“) Prozeduren unterscheiden, wobei innerhalb der temporären wiederum zwischen globalen und nur auf eine
Sitzung bezogenen Prozeduren unterschieden werden kann. Man kann allerdings auch
noch die Gruppe der so genannten erweiterten Prozeduren zählen, die früher mit C und
der ODS API (ähnlich wie in C-geschriebene MySQL-Prozeduren) erstellt werden
konnten, und zu denen man heute die .NET-Prozeduren zählen müsste, wenn der Begriff in der 2005-Literatur nicht weitestgehend verschwunden wäre. Man kann dann auch
noch zwischen benutzerdefinierten Prozeduren und Systemprozeduren unterscheiden,
wobei die eine Gruppe in diesem Kapitel besonders interessiert, weil sie über den
CREATE-Befehl vom Benutzer erstellt werden können, während die anderen Prozeduren
bereits wie SQL-Funktionen in der Datenbank vorhanden sind. In diesem Zusammenhang muss erwähnt werden, dass dies Prozeduren sind, deren Namen mit sp_ beginnt
und die in der Master-Datenbank gespeichert werden. Diese Prozeduren können (sollten aber nicht) auch selbst erstellt werden, wobei diese dann wie ein Chamäleon arbeiten und dafür sorgen, dass auf den aktuellen DB-Kontext bezogene Anweisungen (und
sei es nur der Abruf des DB-Namens wie DB_NAME()) auf die Datenbank bezogen
werden, in der man sich gerade befindet, obwohl die Prozedur aus der Master-DB
abgerufen wird. Schließlich gelangt man innerhalb von diesem sich abzeichnendem
Baum zu der Unterscheidung, die gerade getroffen wurde, die für die Zwecke dieses
Kapitels eigentlich am besten geeignet ist.
Eine Prozedur wird im Standardschema des Benutzers gespeichert, sofern nicht ausdrücklich ein anderes Schema angegeben ist. Sie wird nur in der aktuellen Datenbank
gespeichert. Lediglich temporäre Prozeduren speichert man automatisch in der tempdb.
Um den Quelltext einer Prozedur zu sichern, kann man ebenfalls ein .sql-Skritp erstel-
454
Programmierbarkeit
len, wobei man allerdings darauf achten muss, dass vorherige Anweisungen mit GO von
der Anweisung CREATE PROCEDURE getrennt sind.
Nachfolgend ist die allgemeine Syntax angegeben:
CREATE { PROC | PROCEDURE }
[schema_name.] procedure_name [ ; number ]
[ { @parameter [ type_schema_name. ] data_type }
[ VARYING ] [ = default ] [ [ OUT [ PUT ]
] [ ,...n ]
[ WITH <procedure_option> [ ,...n ]
[ FOR REPLICATION ]
AS { <sql_statement> [;][ ...n ] | <method_specifier> }
[;]
<procedure_option> ::=
[ ENCRYPTION ]
[ RECOMPILE ]
[ EXECUTE_AS_Clause ]
<sql_statement> ::=
{ [ BEGIN ] statements [ END ] }
<method_specifier> ::=
EXTERNAL NAME assembly_name.class_name.method_name
455
Programmierbarkeit
Die verschiedenen Argumente, von denen sich verschiedene bereits anhand ihres Namens verstehen lassen, sind nachfolgend aufgelistet:
ƒ
schema_name enthält den Namen des Schemas, in dem die Prozedur gespeichert
wird.
ƒ
procedure_name enthält den Namen der Prozedur, der den allgemeinen
Benennungsregeln entspricht und im Schema eindeutig sein muss. Das Präfix sp_
ist dabei am besten zu vermeiden, da es für gespeicherten Systemprozeduren bereits
verwendet wird. Ob eine Prozedur lokal oder global temporär erstellt wird, kann
man wie bei Tabellen über ein (temporär nur für die aktuelle Sitzung) oder zwei
Rautenzeichen (##, global temporär für alle Sitzungen) zu Beginn des Namens
festlegen. Insgesamt ist der Name auf 128 Zeichen und der Name einer lokalen
temporären Prozedur inkl. dem Rautenzeichen auf 118 Zeichen begrenzt.
ƒ
; number enthält eine optionale Zahl. Sie erlaubt es, mehrere Prozeduren unter
dem gleichen Namen in einem Schema anzugeben. Diese unterscheiden sich in der
Nummer, werden allerdings durch den gemeinsamen Namen gruppiert. Die Zahl
kann auch für den Löschvorgang aller Prozeduren der Gruppe genutzt werden.
Bsp.: meineProzedur;1, meineProzedur;2. Diese Eigenschaft ist auf das
Abstellgleis geschoben und wird irgendwann aus der Datenbank entfernt.
ƒ
@parameter enthält einen Parameter der Prozedur, von dem es maximal 2100
geben darf. Sofern kein Standardwert oder ein berechneter Bezug auf einen
anderen Parameter angegeben ist, ist es notwendig, beim Aufruf der Prozedur einen
Wert für diesen Parameter bereitzustellen. Hier muss der Namen auch den
allgemeinen Bezeichnungsregeln entsprechen und wird - ähnlich einer Variable mit Datentyp und Standardwert angegeben.
ƒ
[ type_schema_name. ] data_type enthält den Datentyp eines Parameters
sowie sein optionales Schema. Alle Datentypen außer table können hier
eingesetzt werden. Der Datentyp cursor kann nur für einen Ausgabeparameter
eingesetzt werden. Die Prozedur liefert so einen Cursor zurück.
456
Programmierbarkeit
ƒ
VARYING kann nur für den Datentyp cursor verwendet werden und gibt an, dass
die Struktur der relationalen Ergebnismenge, die zurückgeliefert wird, variieren
kann.
ƒ
default enthält einen möglichen Standardwert für den Parameter. Dies ist eine
Konstante oder auch NULL.
ƒ
OUTPUT legt fest, dass dieser Parameter einen Rückgabewert an die aufrufende
Anweisung liefert, welcher per Referenz übertragen wird. Auch text-, ntext- und
image-Parameter können verwendet werden, solange es keine .NET-Prozedur ist.
ƒ
RECOMPILE legt für reine T-SQL-Prozeduren fest, dass das Datenbankmodul den
Ausführungsplan dieser Prozedur nicht zwischenspeichert, sondern zur Laufzeit
kompiliert.
ƒ
ENCRYPTION legt reine T-SQL-Prozeduren fest, sodass der Quelltext, in der die
Prozedur erstellt wurde, chiffriert wird und damit nur für Benutzer mit sehr weiten
Rechten den Text abrufen können.
ƒ
AS legt fest, unter welchem Berechtigungskontext die Prozedur
ausgeführt werden darf. Dabei gilt die allgemeine Syntax { EXEC | EXECUTE }
AS { CALLER | SELF | OWNER | 'user_name' }. Dieses Thema wird später
noch ausführlich behandelt.
ƒ
FOR
EXECUTE
REPLICATION legt fest, dass diese Prozedur nur im Rahmen von
Replikationen und damit DB-Sicherungsvorgängen ausgeführt wird. Sie darf keine
Parameter enthalten, ist nicht für .NET-Prozeduren zulässig, die RECOMPILEOption ist untersagt, und sie wird nicht auf dem Abonennten einer Replikation
durchgeführt.
ƒ
<sql_statement> enthält die T-SQL-Anweisungen und damit den eigentlichen
Programmtext der Prozedur.
ƒ
NAME , assembly_name.class_name.method_name gibt die
statische Methode einer .NET-Assembly (dll) an, die als Prozedur verfügbar
gemacht werden soll.
EXTERNAL
457
Programmierbarkeit
Um eine Prozedur zu ändern, ist anstelle der CREATE-Anweisung nur das Schlüsselwort
ALTER anzugeben. Dieses sorgt dafür, dass die vorhandene Prozedur mit dem nun angegebenen Quelltext überschrieben wird.
ALTER { PROC | PROCEDURE }
[schema_name.] procedure_name [ ; number ]
[ { @parameter [ type_schema_name. ] data_type }
[ VARYING ] [ = default ] [ [ OUT [ PUT ]
] [ ,...n ]
[ WITH <procedure_option> [ ,...n ] ]
[ FOR REPLICATION ]
AS
{ <sql_statement> [ ...n ] | <method_specifier> }
Aus der Datenbank kann der Quelltext zur Überarbeitung aus dem Kontextmenü einer
gespeicherten Prozedur abgerufen werden. Dazu wählt man SKRIPT FÜR GESPEICHERTEN
PROZEDUREN ALS / ALTER IN und dann eine der drei Optionen, um den Quelltext in die
Zwischenablage zu kopieren, einer Datei zu speichern oder sofort in einem neuen Abfragefenster zu öffnen.
458
Programmierbarkeit
Abbildung 7.2: Abrufen eines Quelltexts zur Überarbeitung
Um eine Prozedur zu löschen, ist wie sonst auch bei Schema-Objekten die DROPAnweisung einzusetzen.
DROP { PROC | PROCEDURE }
{ [ schema_name. ] procedure } [ ,...n ]
Unter dem Stichwort „verzögerte Namensauflösung“ ist das Phänomen bekannt, dass
innerhalb einer Prozedur auf Tabellen oder Sichten zugegriffen werden kann, die noch
gar nicht vorhanden sind. Bei der Erstellung einer Prozedur wird zwar die Syntax auf
Fehler geprüft, nicht allerdings die Existenz von Schema-Objekten. Dies ist erst der
Fall, wenn die kompilierte Prozedur zur ersten Ausführung kommt, was bei dann weiterhin bestehender fehlerhafter Referenzierung auch zu einem Fehler führt.
459
Programmierbarkeit
7.1.2
Prozedurarten
Die verschiedenen Arten von Prozeduren sind bereits zuvor kurz in einer Abbildung
vorgestellt worden. Sie sollen nun noch einmal jeweils mit einem typischen Beispiel
unterlegt werden.
7.1.2.1
Rückgabe einer Ergebnismenge
Eine wesentliche Eigenschaft des MS SQL Servers ist es, Prozeduren erstellen zu können, die eine relationale Ergebnismenge zurückliefern können. Dies bietet eine Sicht
selbstverständlich ebenfalls an. Hier allerdings ist es notwendig, eine WHERE-Klausel
selbst zu schreiben, die einen zusätzlichen Filter auf die Daten anwendet. Bei der Verwendung einer Prozedur dagegen kann man eine Reihe von Parametern vorgeben, welche typische Filter bzw. gewünschte Filterangaben (aus Sicherheits-, Validierungs- oder
Bequemlichkeitsgründen) bereits vorgeben. Dies wird im nächsten Beispiel gezeigt.
Die Prozedur liefert Produktinformationen anhand der Produktnummer. Kein anderer
Filter ist notwendig, um an Produktdaten zu gelangen. Auch muss man die Ergebnismenge nicht so gut kennen, um einen eigenen Filter auf eine bestimmte Untermenge an
Spalten anzuwenden. Lediglich die spätere Übergabe eines geeigneten Wertes ist notwendig. Die Prozedur legt man mit der CREATE-Anweisung an und hängt dann die Parameterliste an ihren Namen an. Als Parameter übergibt man eine Produktnummer, die
verpflichtend ist und die auch keinen Standardwert besitzt. Mit Hilfe dieser Produktnummer sucht man in einer einfachen Abfrage verschiedene Spaltenwerte aus der Product-Tabelle heraus. Die Prozedur endet auch mit dieser Abrage, sodass sie insgesamt
genau dieses relationale Ergebnis zurückliefert.
-- Existenzprüfung und Erstellung einer Pozedur
IF OBJECT_ID ( 'Production.usp_GetProductByNumber', 'P' ) IS
NOT NULL
DROP PROCEDURE Production.usp_GetProductByNumber;
GO
460
Programmierbarkeit
CREATE PROCEDURE Production.usp_GetProductByNumber
(@vProductNumber nvarchar(25))
AS BEGIN
SELECT Name,Color, ListPrice, Size
FROM Production.Product
WHERE ProductNumber = @vProductNumber
END
GO
EXEC Production.usp_GetProductByNumber 'SO-B909-L'
712_01.sql: Standardprozedur
Der vorherige Quelltext zeigte neben der einfachen Prozedur auch noch, wie man sie
innerhalb von T-SQL aufrufen kann. Unter Angabe des Parameterwerts für ein beliebiges Paar Socken erhält man das Ergebnis genau so zurück, als hätte man eine Tabelle
oder Sicht befragt.
Name
Color
ListPrice
Size
------------------------ ------- ---------- ----Mountain Bike Socks, L
White
9,50
L
(1 Zeile(n) betroffen)
Dies war ein Beispiel zu den Prozeduren, die mit einer SELECT-Anweisung enden und
daher insgesamt nicht einen Rückgabewert zurückliefern, sondern eine Abfrage ausführen. Hierbei gibt es grundsätzlich keine Beschränkungen, was den Quelltext anbetrifft,
der vor der letzten Abfrage steht und welcher damit quasi das Ergebnis einleitet. Man
könnte sich vorstellen, dass man eine Art Produktsuchmaschine mit Hilfe einer Prozedur realisiert, die ansonsten nur in einer äußeren Anwendung denkbar wäre. Unter
Angabe eines variierenden Datentyps oder ganz einfacher Zeichenkette kann man ver-
461
Programmierbarkeit
schiedene Spalten in der Product-Tabelle untersuchen und die passenden Ergebnisse
in eine table-Variable speichern, welche schließlich abgefragt wird. In der ersten Abfrage könnte man die Ergebnisse beschaffen, indem der Suchbegriff in der Produktnummer gesucht wird. Die zweite Abfrage, welche die table-Variable auffüllt, könnte
im Namen suchen. Wenn schließlich alle relevanten Spalten untersucht sind, die man in
diese Suchmaschine aufnehmen möchte, fragt man die table-Variable ab und liefert so
das gesamte Ergebnis.
In der nachfolgenden Abbildung ist dieses Prinzip noch einmal dargestellt. In der unteren Hälfte befindet sich die zu Grunde liegende Tabelle in Form ihrer Daten, deren
Struktur sehr viel umfassender ist, als es die Rückgabedaten der Prozedur vermuten
lassen. Es ist also möglicherweise für die Anwendung, welche diese Daten über die
Prozedur anspricht, gar nicht notwendig bzw. für den Benutzer möglicherweise sogar
untersagt, mehr über die Daten und damit sowohl Struktur als auch Inhalte zu erfahren.
Die Prozedur fragt in ihrer Eigenschaft als eigenes Schema-Objekt anstelle einer direkten Abfrage, die in der äußeren Anwendung abgesetzt wird, die Daten ab und verhindert
so einen allzu großzügigen Blick auf diese Daten.
462
Programmierbarkeit
Prozedur
Software
Grundlegende Datenstruktur
Abbildung 7.3: Funktionsweise einer Prozedur
7.1.2.2
Sonstige Prozeduren
Neben den zuvor erläuterten Prozeduren gibt es noch diejenigen, welche nicht die Abfragen, sondern die Datenmanipulation steuern und vereinfachen sollen. Die nachfolgende Prozedur erleichtert es bspw., ein neues Produkt einzufügen. Man könnte sich
hier noch vorstellen, dass der erzeugte Primärschlüsselwert zurückgeliefert wird. Dazu
setzt man die OUTPUT-Klausel ein, was in einem späteren Beispiel vorgeführt wird. Die
Prozedur erwartet diejenigen Werte für ein Produkt, die man in jedem Fall benötigt und
könnte auch noch mit NULL ausgestattete weitere Parameter erwarten, die in der Tabelle
nicht verpflichtend sind.
Solche Prozeduren sind überaus interessant, um eine vereinfachte Datenschnittstelle für
Aktualisierungs- und Einfügevorgänge anzubieten. Es könnte sein, dass für eine An-
463
Programmierbarkeit
wendung nicht alle Spalten relevant sind und daher nach außen nur eine begrenzte Spaltenauswahl angeboten werden soll. Es könnte genauso gut sein, dass besondere Berechnungen oder Validierungen stattfinden sollen, die nicht mit Hilfe einer CHECKBedingung realisiert werden können oder sollen. Sofern keine Trigger als automatisch
ausgeführte Prozedur, die an eine Tabelle gebunden ist, zum Einsatz kommt, bietet sich
eine solche Prozedur besonders an, da hier der Aspekt des vereinfachten Zugriffs zusätzlich realisiert werden kann.
Das nachfolgende Beispiel zeigt nur, wie überhaupt die schon in Kapitel 4 erstellte
Product3-Tabelle, die im Download-Listing ebenfalls enthalten ist und zuvor angelegt
wird, gefüllt wird. Sämtliche andere T-SQL-Anweisungen sind nicht spezifisch für
Prozeduren, sondern können mit der bisher gezeigten Syntax umgesetzt werden.
CREATE PROCEDURE Production.usp_InsertProduct (
@vName
varchar(30),
@vProductNumber nvarchar(25),
@vListPrice
money,
@vStandardCost
money
)
AS BEGIN
INSERT INTO Production.Product3
(Name, ProductNumber, ListPrice, StandardCost)
VALUES (@vName, @vProductNumber, @vListPrice,
@vStandardCost)
END
712_02.sql: DML-Prozedur
464
Programmierbarkeit
Diese Prozedur kann man in T-SQL ebenfalls über die EXEC-Anweisung aufrufen. In
diesem Fall setzt man aus Gründen der besseren Lesbarkeit allerdings ein anderes Format ein, in welchem die einzelnen Parameterwerte nicht einfach in der richtigen Reihenfolge nach dem Prozedurnamen aufgelistet werden, sondern in dem in einer Parametername-Werte-Struktur ausdrücklich auch die Parameternamen genannt werden. So kann
man gut nachvollziehen, welcher Parameter welchen Wert erhalten hat.
EXEC Production.usp_InsertProduct
@vName = 'HL Mountain Seat Assembly',
@vProductNumber = 'SA-M687',
@vListPrice = 196.92,
@vStandardCost = 145.87
712_02.sql: DML-Prozedur
7.1.3
Parameter und Aufruf
Wie schon zuvor gezeigt, gibt es wenigstens zwei verschiedene Varianten, eine Prozedur aufzurufen und ihr Parameterwerte zu übergeben. Die eine Variante bezeichnet man
als Positionsnotation, da hier die Position/Reihenfolge der Parameterwerte über ihre
Zuordnung zu Prozedurparametern entscheidet. Die andere Variante bezeichnet man
dagegen als Namensnotation, da in diesem Fall nur der Name über die Zuordnung entscheidet und hier also auch eine andere Reihenfolge genutzt werden kann. Insbesondere
diese Namensnotation ermöglicht es auch, einfach Standardwerte aufzurufen, ohne die
Parameter eigens anzusprechen.
Der Aufruf einer Funktion oder Prozedur erfolgt mit der EXEC-Anweisung, welche
folgende allgemeine Syntax besitzt:
[{ EXEC | EXECUTE } ]
{
465
Programmierbarkeit
[ @return_status = ]
{ module_name [ ;number ] | @module_name_var }
[ [ @parameter = ] { value
| @variable [ OUTPUT ]
| [ DEFAULT ]
}
]
[ ,...n ]
[ WITH RECOMPILE ]
}
Folgende Parameter kommen zum Einsatz:
ƒ
@return_status erwartet eine Ganzzahl mit dem so genannten Rückgabestatus
eines Moduls. Bei einer Skalarfunktion ist dies der gewöhnliche Rückgabewert,
welcher in jedem beliebigen Datentyp abgerufen werden kann.
ƒ
module_name erwartet den (voll qualifizierten) Namen von Prozedur oder
Skalarwertfunktion.
ƒ
; number erwartet die nicht mehr empfohlene Nummer eines Moduls, das in einer
Gruppe von gleich benannten, sich aber durch die Nummer unterscheidenden
Prozeduren erstellt wurde.
ƒ
@module_name_var erwartet eine Variable, welche den Namen des
auszuführenden Moduls für sehr dynamische Modulauswahl und -angabe enthält.
ƒ
@parameter erwartet den Namen eines Parameters mit voran gestelltem
@-
Zeichen. Sofern der Namen ausdrücklich angegeben wird, muss die Reihenfolge
der Parameter von Aufruf und Deklaration nicht übereinstimmen
(Namensnotation).
466
Programmierbarkeit
ƒ
value erwartet den zugewiesenen Wert des Parameters. Sofern nur der Wert
angegeben wird, ist die Reihenfolge der Parameter in Aufruf und Deklaration
einzuhalten (Positionsnotation).
ƒ
@variable enthält eine Variable, die den Wert eines Parameters oder
Ausgabeparameters speichert.
ƒ
OUTPUT gibt an, dass dieser Parameter ein Ausgabeparameter ist und Werte per
Referenz zurückgibt. Innerhalb des Moduls ist derselbe Parameter ebenfalls mit
OUTPUT angegeben.
ƒ
DEFAULT gibt an, dass der Standardwert verwendet werden soll.
ƒ
WITH RECOMPILE lässt einen neuen Ausführungsplan kompilieren, der nach der
Ausführung wieder gelöscht wird. Dies ist zu verwenden, wenn die Parameterwerte
sehr unterschiedlich sind und die Daten in großem Maße geändert werden.
7.1.3.1
Standardwerte
Wie auch bei Tabellen und ihren Spalten ist es bei Prozeduren ebenfalls erlaubt, Standardwerte anzugeben. Sie werden dann verwendet, wenn sie keine tatsächlichen Parameterwerte beim Aufruf überschreiben. Das nachfolgende Beispiel zeigt, wie ein solcher Standardwerte angegeben wird. Die Syntax erinnert sehr an eine Variablendeklaration, bei der die SET-Anweisung ausgelassen wurde. Inhaltlich sucht diese Prozedur
wiederum Produkte heraus, wobei allerdings nicht die Produktnummer anzugeben ist,
sondern stattdessen der minimale Listenpreis, die Farbe und die Produktkategorie. Diese
Kategorie wird nun auf den Standardwert 1 gesetzt, so wie man sich auch noch vorstellen könnte, den Minimalpreis auf 0 zu setzen.
CREATE PROCEDURE Production.usp_GetProduct (
@vListPrice money,
@vColor
nvarchar(15),
@vCategory
int = 1)
467
Programmierbarkeit
AS BEGIN
SELECT Name, Color, ListPrice, Size, ProductSubcategoryID
FROM Production.Product
WHERE Color = @vColor
AND ListPrice > @vListPrice
AND ProductSubcategoryID = @vCategory
END
713_01.sql: Erstellung einer Prozedur mit Standardwert
Das nachfolgende Skript listet dann die verschiedenen Varianten auf, in denen diese
Prozedur aufgerufen werden kann. Bei der Positionsnation (erster Fall) übergibt man die
Parameterwerte in der erwarteten Reihenfolge. Dies ist bei der Verwendung von Methoden in den meisten Programmiersprachen ebenfalls so geregelt. Sofern der Standardwert als letzter erscheint, kann darauf verzichtet werden. Steht er dagegen innerhalb
von anderen Parametern oder sind mehrere solcher Parameter nacheinander, gibt man
den Standardwert mit dem Schlüsselwort DEFAULT an (zweiter Fall). Möchte man diesen Standardwert überschreiben (dritter Fall), setzt man an seiner Stelle einfach den
gewünschten Wert ein. Bei der Namensnotation (vierter Fall) schließlich muss man sich
gar keine Gedanken um die Berücksichtigung von Standardwerten machen, da man hier
ohnehin nur die Parameter nennt, die man auch tatsächlich mit einem Wert belegen will.
-- Aufruf mit Positionsnotation und Standardwert
EXEC Production.usp_GetProduct 250.00, 'Black'
EXEC Production.usp_GetProduct 250.00, 'Black', default
-- Aufruf mit Positionsnotation und Nicht-Standardwert
EXEC Production.usp_GetProduct 250.00, 'Black', 14
-- Aufruf mit Namensnotation
468
Programmierbarkeit
EXEC Production.usp_GetProduct @vColor = 'Black',
@vListPrice = 250.00
713_01.sql: Aufruf eines Standardwerts
7.1.3.2
Rückgabewert
Im Normalfall kommen Funktionen zum Einsatz, wenn Rückgabewerte benötigt werden. Dabei steht der Funktionsaufruf auf der rechten Seite einer Zuweisung bzw. erscheint als Ausdruck und liefert so diesen Rückgabewert direkt an eine Variable oder
das diesen Ausdruck verarbeitende Ziel zurück. Eine Prozedur hingegen kann als eigenständige Anweisung fungieren, sodass es zunächst kein Ziel von Rückgabewerten gibt.
Dies ist allerdings durch die Wertübergabe per Referenz und einen Parameter, der zusätzlich mit dem Schlüsselwort OUTPUT ausgezeichnet wurde, möglich. Bei einer Prozedur gibt es in diesem Zusammenhang auch keine Einschränkung bei der Anzahl an
Rückgabewerten.
Das nachfolgende Beispiel zeigt zum einen, wie ein solcher Rückgabewerte eingerichtet, mit einem Wert gefüllt auch in der äußeren Anwendung (in diesem Fall T-SQL)
abgerufen wird. Die Prozedur soll wieder Produktinformationen liefern, wobei diese
allerdings in Form einer zusammenfassenden Zeichenkette zurückgeliefert werden sollen. Dazu gibt es den dritten Parameter, der mit OUTPUT ausgezeichnet wurde. Die anderen beiden Parameter bilden wie zuvor den Minimalpreis und die Farbe ab. Eigentlich
müsste man einen Cursor erstellen, um alle Produktinformationen zusammen abzurufen
und bspw. als HTML-Text aufzubereiten, doch um das Beispiel zu verkürzen werden
nur die Werte des ersten abgerufenen Datensatzes abgerufen und im Rückgabeparameter zusammen gesetzt.
CREATE PROCEDURE Production.usp_GetProduct (
@vListPrice money,
@vColor
nvarchar(15),
@vSummary
nvarchar(255) OUTPUT)
469
Programmierbarkeit
AS BEGIN
-- Variablen deklarieren
DECLARE @vName nvarchar(50),
@vDetails nvarchar(505)
-- Daten abrufen
SELECT @vName = Name,
@vDetails = Color + ' ' + STR(ListPrice)
FROM Production.Product
WHERE Color = @vColor
-- Output-Parameter setzen
SET @vSummary = @vName + ' ' + @vDetails
END
713_02.sql: Erstellung eines Rückgabewerts
Um einen solchen Rückgabewert abzurufen, benötigt man außen zunächst eine entsprechende Variable, die einen für den Rückgabewert geeigneten Datentyp besitzt. Diese
Variable wird bei der Positionsnotation einfach ebenfalls mit dem Schlüsselwort OUTPUT an die Stelle in der Parameterliste gesetzt, an der ein Rückgabeparameter erwartet
wird. Bei der Namensnotation dagegen ist die Position grundsätzlich egal, weil vor
Nennung der Variablen auch noch der Parametername angegeben werden muss. Das
Schlüsselwort OUTPUT bleibt bestehen. Nach dem Aufruf ist die zuvor deklarierte und
mit keinem Wert versehene Variable gefüllt. Ein möglicher vorhandener Wert wäre
überschrieben worden.
-- Aufruf mit Output-Parametern
DECLARE @vProductText nvarchar(255)
470
Programmierbarkeit
EXEC Production.usp_GetProduct 300.00, 'Black',
@vProductText OUTPUT
PRINT @vProductText
-- Aufruf mit Namensnotation
EXEC Production.usp_GetProduct
@vColor = 'Blue',
@vListPrice = 220.00,
@vSummary = @vProductText
OUTPUT
PRINT @vProductText
713_02.sql: Abruf eines Rückgabewerts
7.1.3.3
Hinweise für ADO.NET
Auch wenn dieses Buch einen Bogen um .NET schlägt, um Nicht-.NET-Benutzer nicht
mit zuviel Papier über für sie uninteressante Programmiersprachen zu ärgern, sollen
doch einige kurze Hinweise folgen, wie denn diese Prozeduren in einer .NETAnwendung mit ADO abgerufen werden können.
Vorausgesetzt wird also, dass innerhalb einer MS SQL Server-Datenbank eine ganze
Schicht an unterschiedlichen Prozeduren gibt, die (laut offizieller Lesrichtung) das Leben der Daten sicherer und das Leben des Programmierers einfacher gestalten. Mit den
folgenden Techniken kann man die beschriebenen Ergebnisse einer Prozedur abrufen.
Dabei müsste noch einmal angemerkt werden, dass sie natürlich grundsätzlich keinen
Rückgabewert erzeugen muss, dass dieser aber allein aus Gründen der Kontrolle sehr
nützlich ist.
ƒ
Einfache Ergebnismenge: Eine Prozedur erzeugt eine einfache Ergebnismenge,
wenn sie nach einigen T-SQL-Anweisungen schließlich mit einer SELECTAnweisung endet. Ein Objekt der Klasse SqlDataReader sowie ein Objekt der
Klasse SqlDataAdapter kann bei bestehender und permanenter DB-Verbindung
eine solche Ergebnismenge abrufen. Von DataSet abgeleitet, gibt es noch die
471
Programmierbarkeit
DataTable-Klasse, deren Objekte auch in einem Kontext ohne permanente DB-
Verbindung mit den zwischengespeicherten Daten arbeiten können. Auch sie
können diese Daten empfangen.
ƒ
Mehrere Ergebnismengen: Eine Prozedur kann mehrere Ergebnismengen
zurückliefern, wenn innerhalb der Anweisungen tatsächlich mehrere SELECTAnweisungen durchgeführt werden, die zur Rückgabe von Ergebnismengen führen.
Diese können abgerufen werden, wenn man von der SqlDataReader-Klasse die
NextResult()-Methode verwendet.
ƒ
Ausgabeparameter: Ein Objekt der Klasse SqlCommand besitzt eine Collection
namens Parameters, in denen sich die veschiedenen Parameterarten befinden. Sie
haben die Typen bzw. Richtungen Input, Output, InputOutput oder
ReturnValue. Die Ausgabeparameter, die von einer MS SQL Server-Prozedur
zurückgeliefert werden, benötigen die Richtung InputOutput, da sie auch Werte in
die Prozedur bringen können, die vor einem Überschreibevorgang auch zunächst
gelesen werden können. Die anderen Parameter werden mit Input übergeben. Ein
Rückgabewert wird mit der ReturnValue-Richtung eines SqlParameterObjekts abgerufen.
ƒ
Anzahl betroffener Reihen: Sofern die Option SET NOCOUNT ON vom Benutzer
eingesetzt wird, ist es nicht möglich, die Anzahl betroffener Reihen direkt von der
Datenbank zu erhalten, weil die Ausgabe unterdrückt wird. Die Eigenschaft
RecordsAffected eines SqlDataReader-Objekts lässt sich zwar in DMLAnweisungen verwenden, allerdings nicht bei SELECT. Darüber hinaus liefert es die
Menge aller betroffenen Zeilen von allen DML-Anweisungen einer Prozedur,
sodass hier keine Differenzierung möglich ist. Die beste Möglichkeit besteht darin,
für alle DML-Anweisungen, deren Anzahl betroffener Reihen nach außen gemeldet
werden sollen, einen eigenen Ausgabeparameter anzugeben. Den Wert ermittelt
man dann unmittelbar nach der Anweisung über den Aufruf der Systemfunktion
(globale Variable) @@rowcount.
472
Programmierbarkeit
ƒ
Fehler: Um Fehler abzurufen, muss man lediglich mit try und catch arbeiten,
sodass der Prozeduraufruf innerhalb von try liegt. Sollte hier ein Fehler
(Schweregrad größer als 10) zurückgeliefert werden, verlagert dies die Ausführung
in den catch-Block. Hier kann man dann das SqlException-Objekt untersuchen,
welches eine Errors-Collection mit SqlError-Objekten besitzt. Dieses besitzt
wiederum die beiden Eigenschaften Number und Message.
ƒ
Warnungen: Eine Warnung ist eine Fehlermeldung mit einem Schweregrad kleiner
gleich 10. Es wird ein InfoMessage-Ereignis vom SqlConnection-Objekt
ausgelöst, wenn eine Warnung ausgegeben wird, der wiederum ein
SqlInfoMessageEventArgs-Objekt übergeben wird. Dieser Parameter besitzt
eine Errors-Collection, die analog zu derjenigen von SqlException verarbeitet
werden kann.
ƒ
Ausgabe mit PRINT: Dies gelingt über die gleiche Technik wie bei Warnungen.
ƒ
XML-Ausgabe: ADO.NET 2.0 empfängt zurückgegebene XML-Daten einer Spalte
im SqlDataReader-Objekt. Wird nur XML zurückgegeben, kann man auch ein
XmlReader-Objekt verwenden, wobei zuvor vom SqlCommand-Objekt die
besondere Methode ExecuteXmlReader() verwendet werden muss.
ƒ
Benutzerdefinierte Datentypen (UDT): Im Rahmen der .NET-Unterstützung bietet
der MS SQL Server 2005 nun für die Erstellung von objektrelationalen Strukturen
auch an, eigene Datentypen mit .NET-Klassen zu erstellen. Diese können ebenfalls
wie alle anderen Spalten- oder Parametertypen abgerufen werden, solange der
Quelltext der Klasse beim Klienten ebenfalls verfügbar ist. Dies ist in Form einer
DLL der Fall.
ƒ
Struktur der Ergebnismenge abrufen: Der SqlDataReader in ADO.NET 2.0
erlaubt es, die Metadaten der Ergebnismenge mit der Methode
GetSchemaTable() abzurufen. Sie liefert ein DataTable-Objekt mit diesen
Metadaten.
473
Programmierbarkeit
7.1.4
Sonderfälle
Als die allgemeine Syntax vorgestellt wurde, gab es noch verschiedene zusätzliche
Schlüsselwörter und Anweisungen, welche eine Prozedur betreffen können. Sie sollen
in diesem Abschnitt vorgestellt werden.
7.1.4.1
Neukompilierung
Wie schon in der allgemeinen Syntax erwähnt, bestimmt die Option WITH RECOMPILE,
dass das Datenbankmodul den erzeugten Abfrageplan, d.h. die vordefinierte und normalerweise die Geschwindigkeit positiv beeinflussende Art und Weise der (wiederholten)
Ausführung zu verwerfen. Dies zwingt den Abfrageoptimierer, bei jeder Abfrage einen
neuen Plan zu kompilieren, da dieser nicht zwischengespeichert wird.
Sofern gar nicht ein neuer Abfrageplan für die gesamte Prozedur erstellt werden soll,
sondern nur für eine einzelne Anweisung innerhalb derselben, können sie außer bei
INSERT auch direkt in einer einzelnen Abfrage auf der obersten Ebene angegeben werden. Dies gelingt über die Anweisung OPTION ( <query_hint> [ ,...n ] ),
welche der gesamten Abfrage folgt und in diesem Fall das einfache Schlüsselwort RECOMPILE enthält.
CREATE PROCEDURE Production.usp_GetProductByNumber
(@vProductNumber nvarchar(25))
WITH RECOMPILE
AS BEGIN ...
714_01.sql: Neukompilierung bei Ausführung
7.1.4.2
Verschlüsselung
Der Quelltext der Prozedur kann normalerweise über die Prozedur sp_helptext abgerufen werden. Es besteht allerdings die Möglichkeit, diesen Quelltext zu verschlüsseln,
was über die Klausel ENCRYPTION gelingt. Benutzer ohne Zugriff auf die Systemtabellen können dann den Prozedurquelltext nicht mehr abrufen.
474
Programmierbarkeit
CREATE PROCEDURE Production.usp_GetProductByNumber
(@vProductNumber nvarchar(25))
WITH ENCRYPTION
AS BEGIN ...
GO
EXEC sp_helptext 'Production.usp_GetProductByNumber'
714_02.sql: Verschlüsselung bei Erstellung
Man erhält bei einem Aufruf der sp_helptext-Prozedur den mysteriösen Hinweis:
Der Text für das 'Production.usp_GetProductByNumber'-Objekt
ist verschlüsselt.
7.1.4.3
Cursor-Rückgabe
Eine besonders schöne Technik neben der Rückgabe von relationalen Ergebnismengen
ist die Rückgabe eines Cursors, der dann in der äußeren Anwendung (hauptsächlich TSQL) verarbeitet werden kann. Dies soll am nachfolgenden Beispiel demonstriert werden. Zunächst muss ein Parameter wiederum mit dem Schlüsselwort OUTPUT versehen
werden, weil es sich ja tatsächlich um einen Ausgabeparameter handelt. Als Datentyp
verwendet man CURSOR und VARYING, wenn die Struktur Ergebnismenge wechselnd
ist. In diesem Zusammenhang wird also die schon verwendete Prozedur
usp_GetProduct noch einmal abgewandelt. Weder eine Ergebnismenge noch eine
zusammenfassender Text wird ausgegeben, sondern stattdessen ein Cursor, der die
Tabelle Product anbietet und sie vorab mit Hilfe der anderen Parameter für Listenpreis, Farbe und Kategorie gefiltert hat.
Innerhalb der Prozedur öffnet man den als Cursor schon bekannt gemachten Ausgabeparameter über die SET-Anweisung und fügt die CURSOR-Klausel sowie natürlich die
ihn konstituierende Abfrage hinzu. Vielmehr ist nicht mehr zu tun als den Cursor
475
Programmierbarkeit
schließlich als letzte Anweisung der Prozedur über OPEN zu öffnen. Dies sorgt dafür,
dass man in der äußeren Anwendung Zugriff auf diese Daten erhält.
CREATE PROCEDURE Production.usp_GetProduct (
@vListPrice money,
@vColor
nvarchar(15),
@vCategory
int = 1,
@cProducts
CURSOR VARYING OUTPUT)
AS BEGIN
SET @cProducts = CURSOR FORWARD_ONLY STATIC FOR
SELECT Name, Color, ProductSubcategoryID
FROM Production.Product
WHERE Color = @vColor
AND ListPrice > @vListPrice
AND ProductSubcategoryID = @vCategory
OPEN @cProducts
END
714_05.sql: Rückgabe eines Cursors
Interessant ist nun natürlich, wie man einen solch ungewöhnlichen Ausgabeparameter in
der äußeren Anwendung nutzen kann. Dazu erstellt man eine Variable mit dem Datentyp CURSOR, verzichtet allerdings darauf, auch eine Abfrage anzugeben. Diese Variable
setzt man dann an die Stelle des Ausgabeparameters beim Aufruf der Prozedur, indem
man das OUTPUT-Schlüsselwort anschließt und die richtige Position oder den entsprechenden Parameternamen für die Zuordnung wählt.
476
Programmierbarkeit
Da der Cursor bereits innerhalb der Prozedur geöffnet wurde, kann man unmittelbar mit
der FETCH-Anweisung den ersten Datensatz abrufen und dann über die WHILE-Schleife
die schon bekannte Verarbeitung durchführen. Nach dieser Verarbeitung schließt man
den Cursor wieder in äußeren Anwendung und gibt den Speicher frei.
- Cursor und einfache Variablen erstellen
DECLARE @cProductsOut CURSOR,
@vName varchar(50), @vColor nvarchar(15),
@vCategory
int
-- Prozedur ausführen
EXEC Production.usp_GetProduct
@cProducts = @cProductsOut OUTPUT,
@vListPrice = 150,
@vColor = 'Black'
-- Abrufen 1
FETCH NEXT FROM @cProductsOut
INTO
@vName, @vColor, @vCategory
-- Schleife
WHILE (@@FETCH_STATUS = 0) BEGIN
-- Abrufen 2..n
FETCH NEXT FROM @cProductsOut
INTO
@vName, @vColor, @vCategory
PRINT @vName + ' ' + @vColor
END
477
Programmierbarkeit
-- Aufräumen
CLOSE @cProductsOut
DEALLOCATE @cProductsOut
714_05.sql: Abruf eines Cursors
7.2
Funktionen
Neben den Prozeduren gibt es auch noch die Möglichkeit, benutzerdefinierte Funktionen zu erstellen. Beide Modularten haben – wie in allen Datenbanken, die überhaupt
solche Strukturen anbieten – große Gemeinsamkeiten. Daher werden viele Aspekte
bereits bekannt sein, weswegen die Darstellung von Funktionen ein wenig kürzer ausfallen kann. Eine Funktion kann als Ausdruck, auf der rechten Seite einer Zuweisung
(was ebenfalls ein Ausdruck ist) oder auch in einer FROM-Klausel erscheinen. Die ersten
beiden Möglichkeiten sollten angesichts der verschiedenen, schon in der Datenbank
vorhandenen Systemfunktionen sowie natürlich aufgrund der gängigen SQL-Funktionen
schon bekannt sein. Neu ist möglicherweise die Überlegung, dass solche Funktionen
auch vom Benutzer erstellt werden und direkt in SQL-Anweisungen wie bspw. SELECT
aufgerufen werden können. Neu dürfte auch sein, dass eine Funktion innerhalb einer
FROM-Klausel anstelle einer Tabelle auftreten kann. In diesem Sinne ähnelt sie einer
Prozedur, die eine relationale Ergebnismenge zurückgibt, welche wiederum mit zusätzlichen Filtern, Sortierungen und Gruppierungen sowie auch Spaltenauswahlen genutzt
wird. Eine Prozedur hingegen könnte an dieser Stelle niemals stehen, sondern bildet
bereits die gesamte Abfrage an.
Ein wesentlicher Unterschied von Funktionen und Prozeduren besteht darin, dass eine
Funktion den Status einer Datenbank nicht ändern kann. Die folgende Liste enthält
Anweisungen, die innerhalb einer benutzerdefinierten Funktion überhaupt zulässig sind:
ƒ
478
Variablendeklarationen und zugehörige Zuweisungen, sowie Kontroll-strukturen
und gängige T-SQL-Programmstrukturen außer Ausnahme-behandlung. Die
Erstellung von Cursorn ist dagegen erlaubt.
Programmierbarkeit
ƒ
SELECT-Anweisungen, welche ihre Werte lokalen Variablen zuweisen, sowie die
Verarbeitung von Cursorn.
ƒ
Nutzung von lokalen Cursorn, die also innerhalb der Funktion erstellt und wieder
gelöscht werden. Die Rückgabe von Cursorn an den Klienten sowie die Nutzung
von Prozedur-Cursorn ist nicht zulässig.
ƒ
INSERT-, UPDATE- und DELETE-Anweisungen, die sich auf lokale table-
Variablen auswirken. Eine Änderung von tatsächlichen DB-Tabellen ist in
Funktionen nicht gestattet.
ƒ
EXECUTE-Anweisungen, die gespeicherte Prozeduren aufrufen, sofern sie die oben
angegebenen Regeln nicht verletzen, oder erweiterte gespeicherte Prozeduren.
Funktionen können daher verschiedene Eigenschaften haben, die ihnen automatisch
zugewiesen werden und die sich aus der Gesamtheit der in ihnen enthaltenen T-SQLAnweisungen ergeben. Folgende Anweisungen sind in 2005 verfügbar:
ƒ
IsDeterministic meint, dass eine Funktion bei gleichen Eingabedaten und
gleichem Datenbankstatus das gleiche Ergebnis liefert. Solche Funktionen, die
aktuelle Informationen wie Uhrzeiten abrufen, sind bspw. nicht deterministisch.
ƒ
IsPrecise prüft die Nutzung von Gleitkommatransaktionen, die in unpräzisen
Funktionen auftreten.
ƒ
IsSystemVerified gibt an, ob die Präzisions- und Determinismus-eigenschaften
vom MS SQL Server geprüft werden können.
ƒ
SystemDataAccess gibt an, ob die Funktion in der lokalen MS SQL Server-
Instanz auf Systemdaten wie den Systemkatalog oder virtuelle Systemtabellen
zugreift.
ƒ
UserDataAccess prüft, ob eine Funktion auf Benutzerdaten in der lokalen Instanz
von SQL Server zugreift, wobei benutzerdefinierte und temporäre Tabellen, aber
keine Tabellenvariablen eingeschlossen sind.
479
Programmierbarkeit
Die nachfolgende Abbildung ist analog zu derjenigen, welche bereits bei Prozeduren
zum Einsatz kam, aufgebaut. Sie stelle die verschiedenen Arten von Funktionen sowie
ihre typische Verwendungsweise vor.
Auf der rechten Seite befindet sich wieder ein Datenbanksymbol, welches zeigen soll,
dass sich diese Funktionen nicht in der Software, sondern vielmehr in der Datenbank
selbst befinden. Oben sieht man zwei Kästen, von denen der eine bspw. in .NET geschriebene, individuelle Software und der andere ein beliebiges T-SQL-Skript, wie es
im Management Studio ausgeführt werden kann, symbolisiert. Beide können die verschiedenen Prozeduren in T-SQL-Anweisungen, die zur Datenbank geschickt werden,
aufrufen.
Individuelle
Software
T-SQL
(Management Studio)
Relationale
Ergebnismenge
Rückgabewerte
Tabellenwertfunktion
SELECT
Skalarfunktionen
Datenbank
Tab 1
Tab 2
Tab 1
Tab 2
Tab 2
...
Bereich 1
Tab 3
...
Bereich 2
Abbildung 7.4: Typologie von Funktionen
Man unterscheidet im Wesentlichen zwei Arten von benutzerdefinierten Funktionen, die
wie die Prozeduren zuvor als Rauten innerhalb der Abbildung zu sehen sind:
480
Programmierbarkeit
ƒ
Skalarfunktionen entsprechen bekannten SQL-Funktionen, welche auf Basis keines,
eines oder mehrerer Parameter einen Rückgabewert zurückliefern, der überall dort,
wo ein Ausdruck erwartet wird, benutzt werden kann. Diese Funktionen können
ihre Werte aus Tabellen abrufen; sie können dagegen allerdings auch beliebige
Berechnungen ohne Tabellenbezug ausführen.
ƒ
Tabellenwerfunktionen dagegen führen eine SELECT-Anweisung aus und liefern
meist auf Basis einer oder mehrerer Tabellen ein relationales Ergebnis zurück. Da
diese Funktionen innerhalb einer FROM-Klausel aufgerufen werden, können alle
möglichen SELECT-Techniken auf diese Daten angewandt werden.
ƒ
.NET-Funktionen: Es ist möglich, in .NET geschriebene Funktionen aus DLLs im
MS SQL Server zu speichern und genauso zu verwenden wie in T-SQL
geschriebene. Dies wird allerdings nicht in diesem Buch erläutert, weil natürlich
.NET-Kenntnisse erforderlich sind.
An dieser Stelle folgte bei der Darstellung der Prozeduren die allgemeine Syntax für
CREATE, ALTER und DROP. Wenn Funktionen vorgestellt werden, ist dies jedoch nicht
sinnvoll, weil die zwei verschiedenen Arten von Funktionen eine unterschiedliche
Erstellungs-/Änderungssyntax aufweisen. Sie folgt später in eigenen Abschnitten.
Gleich bleibt für alles Irdische jedoch die Löschung:
DROP FUNCTION { [ schema_name. ] function_name } [ ,...n ]
7.2.1
Skalare Funktionen
Eine skalare Funktion liefert einen Wert zurück, sodass sie überall dort, wo ein Ausdruck erwartet wird, aufgerufen werden kann. Man erstellt sie ebenfalls über den CREATE-Befehl und kann sie in einem Schema speichern. Parameter werden mit ihrem Namen und dem vorangestellten @-Zeichen angegeben, können auch einen Standardwert
aufweisen oder natürlich NULL sein. Ein solcher Standardwert muss allerdings beim
Aufruf in einer SQL-Anweisung immer auch mit default angegeben werden. Man
kann nicht wie bei einer Prozedur diese Parameter auslassen. Die Besonderheit einer
skalaren Funktion besteht darin, dass sie einen Rückgabewert besitzt und dass dessen
481
Programmierbarkeit
Datentyp in der RETURNS-Klausel angegeben ist. Danach folgt nach einem optionalen
AS innerhalb von BEGIN und END die Reihe an T-SQL-Anweisungen, welche die Funktion speichert und die mit einer RETURN-Anweisung enden, welche den Rückgabewert
schließlich zurückliefern. Dies erinnert insgesamt sehr an eine Methode gängiger Programmiersprachen.
CREATE FUNCTION [ schema_name. ] function_name
( [ { @parameter_name [ AS ][ type_schema_name. ]
parameter_data_type
[ = default ] }
[ ,...n ]
]
)
RETURNS return_data_type
[ WITH <function_option> [ ,...n ] ]
[ AS ]
BEGIN
function_body
RETURN scalar_expression
END
[ ; ]
Ein Beispiel soll diese Technik zeigen. Zunächst prüft man mit Hilfe der OBJECT_IDFunktion, ob die entsprechende Funktion überhaupt schon in der Datenbank vorhanden
ist, um sie ggf. zu löschen. Dabei ist zu beachten, dass die verschiedenen Funktionsarten
482
Programmierbarkeit
eigene Testwerte haben. Eine skalare Funktion prüft man bspw. mit der Zeichenkette
FN.
Die Funktion soll nun endlich ein Problem in Angriff nehmen, das während des bisher
noch keine Berücksichtigung fand: die Währungsrechnung. Innerhalb der Bestellungen,
die von Kunden eingehen, gibt es eine Verknüpfung zu einer Währungstabelle namens
CurrenyRate, welche historisierte Wechselkurse enthält, und die wiederum mit einer
Tabelle Currency verbunden ist. Sie enthält die Namen von Währungen.
Die Funktion ufnGetBuyerCurrency erwartet die Nummer eines Verkaufs sowie
einen Währungsbetrag in Dollar, der umgerechnet werden soll. Da in einem Datensatz
der SalesOrderHeader-Tabelle mehrere Spalten vom Datentyp money enthalten sind,
soll diese Funktion dynamisch zwar den zurzeit der Bestellung gültigen Wechselkurs
ermitteln - doch die Summe, die umgerechnet werden soll, wird als Wert übergeben. So
kann man diese Funktion für die Gesamtsumme genauso wie für die Steuerlast oder die
Frachtkosten nutzen. Innerhalb der Funktion steht ein letztlich ganz gewöhnliches TSQL-Skript, welches den für die übergebene Bestellnummer gültigen Wechselkurs
ermittelt, die Berechnung durchführt und diese schließlich in der RETURNS-Klausel
zurückliefert. Letztlich ist es nur die RETURNS-Klausel, welche dieses innere Skript von
einem nicht gespeicherten unterscheidet. An seiner Stelle hätte man möglicherweise
sonst eher die PRINT-Anweisung verwendet.
-- Auf Existenz prüfen und ggf. löschen
IF OBJECT_ID (N'dbo.ufnGetBuyerCurrency', N'FN') IS NOT NULL
DROP FUNCTION dbo.ufnGetBuyerCurrency
GO
-- Erstellen
CREATE FUNCTION ufnGetBuyerCurrency(
@salesOrderID int,
483
Programmierbarkeit
@sum money
)
RETURNS varchar(15)
AS BEGIN
-- Lokale Variablen
DECLARE @averageRate float, @currency varchar(5)
-- SQL-Anweisungen
SELECT @averageRate = cr.AverageRate,
@currency = cr.ToCurrencyCode
FROM Sales.SalesOrderHeader AS sh
INNER JOIN Sales.CurrencyRate AS cr
ON sh.CurrencyRateID = cr.CurrencyRateID
WHERE sh.SalesOrderID = @salesOrderID
-- Rückgabe
RETURN RTRIM(LTRIM(STR(@sum * @averageRate) + ' ' + @currency))
END
721_01.sql: Skalarwertfunktion
Spektakulär ist nun insbesondere, dass die doch sehr schwierige Umrechnung aufgrund
der Verknüpfungen völlig in der Funktion gekapselt ist und die ansonsten im Buch
484
Programmierbarkeit
vermiedene Berücksichtigung der tatsächlichen Währungen überaus einfach in dieser
Funktion durchgeführt wird. Dies alles gelingt in einem T-SQL-Programm genauso gut,
lässt sich aber gerade auch in einer gewöhnlichen SELECT-Anweisung nutzen. Andere
typische SQL-Anweisungen könnten hier ebenfalls mit dieser Funktion umgangen werden. So wäre es möglich, den einzutragenden oder zu aktualisierenden Wert mit Hilfe
dieser Funktion zu erzeugen.
SELECT SalesOrderID,
dbo.ufnGetBuyerCurrency(SalesOrderID,
SubTotal) AS SubTotal,
dbo.ufnGetBuyerCurrency(SalesOrderID,
TaxAmt)
AS Tax,
dbo.ufnGetBuyerCurrency(SalesOrderID,
Freight)
AS Freight,
dbo.ufnGetBuyerCurrency(SalesOrderID,
TotalDue) AS Total
FROM Sales.SalesOrderHeader
WHERE SalesOrderID IN (43661, 43662)
721_01.sql: Skalarwertfunktion testen
Man erhält für die beiden angeforderten Bestellungen die jeweiligen Werte in kanadischen Dollar.
SalesOrderID SubTotal
Tax
Freight
Total
------------ ------------ --------- --------------- --------43661
57718 CAD
4617 CAD
1443 CAD
63778
CAD
485
Programmierbarkeit
43662
50789 CAD
4063 CAD
1270 CAD
56122
CAD
(2 Zeile(n) betroffen)
7.2.2
Tabellenwertfunktion
Die zweite Gruppe an Funktionsarten wird aus den beiden verschiedenen Arten von
Tabellenwertfunktionen gebildet. Man kann sie als parametrisierte Sicht verstehen, da
hier auf der einen Seite eine gespeicherte Abfrage unter einem eigenen Namen existiert,
diese hingegen auf der anderen Seite über Parameterwerte gefiltert werden kann. Im
Gegensatz zu einer Sicht oder einer gewöhnlichen Tabelle ist hier also für die Filterung
zunächst keine WHERE-Klausel notwendig, weil die vom Besitzer der Funktion als wesentlich erachteten Filtermöglichkeiten schon vorgegeben wurden und besonders einfach in ihrer Verwendung sind. Davon ist unberührt, zusätzliche Filterungen in der
WHERE-Klausel anzugeben.
7.2.2.1
Einfache Tabellenwertfunktion
Die erste Untergruppe der Tabellenwertfunktionen bezeichnet man als „einfach,“ weil
hier der Charakter einer Sicht besonders deutlich zum Tragen kommt. Diese Funktion
besitzt die Möglichkeit, Parameter anzulegen, die durch einen Datentyp beschrieben
sind und einen Standardwert oder NULL enthalten können. Im Gegensatz zu den Skalarfunktionen kündigt man hier den Rückgabewert in der RETURNS-Klausel mit dem Datentyp table an. Bei einer einfachen Tabellenwertfunktion folgt hier nun kein BEGIN…END-Block, sondern lediglich nach einem optionalen AS die SELECT-Anweisung
nach dem Schlüsselwort RETURN. Sie kann zwar die verschiedenen Parameter enthalten
und auch eine stattliche Größe erreichen, doch alles, was sich nicht in einer einzelnen
SELECT-Anweisung ausdrücken lässt, ist dann für diese einfache Form nicht mehr
geeignet.
CREATE FUNCTION [ schema_name. ] function_name
( [ { @parameter_name [ AS ] [ type_schema_name. ]
486
Programmierbarkeit
parameter_data_type
[ = default ] }
[ ,...n ]
]
)
RETURNS TABLE
[ WITH <function_option> [ ,...n ] ]
[ AS ]
RETURN [ ( ] select_stmt [ ) ]
[ ; ]
Als Beispiel möchte man nun die zuvor über die Skalarfunktion erstellte Ausgabe weiter
verwenden und unter Angabe der Bestellnummer, welche einfach weitergereicht wird,
die zuvor erzeugte Abfrage zurückliefern.
-- Auf Existenz prüfen und ggf. löschen
IF OBJECT_ID (N'dbo.ufnGetBuyerCurrencyTable', N'IF') IS NOT
NULL
DROP FUNCTION dbo.ufnGetBuyerCurrencyTable
GO
-- Erstellen
CREATE FUNCTION ufnGetBuyerCurrencyTable (
@salesOrderID int
)
RETURNS table
487
Programmierbarkeit
AS
-- Unmittelbares RETURN, kein BEGIN..END
RETURN (
SELECT SalesOrderID,
dbo.ufnGetBuyerCurrency(SalesOrderID, SubTotal) AS
SubTotal,
dbo.ufnGetBuyerCurrency(SalesOrderID, TaxAmt)
AS Tax,
dbo.ufnGetBuyerCurrency(SalesOrderID, Freight)
AS
Freight,
dbo.ufnGetBuyerCurrency(SalesOrderID, TotalDue) AS Total
FROM Sales.SalesOrderHeader
WHERE SalesOrderID = @salesOrderID)
GO
722_01.sql: Einfache Tabellenwertfunktion erstellen
Der Aufruf ist dann besonders einfach möglich, indem in der FROM-Klausel die entsprechende Funktion aufgerufen wird. Sie empfängt dann den Parameter, der eine Bestellnummer darstellt.
SELECT *
FROM dbo.ufnGetBuyerCurrencyTable(43661)
722_01.sql: Tabellenwertfunktion testen
Wie gerade eben in der Test-Abfrage gesehen, liefert der Asterikus (*) alle Spaltennamen, wie sie in der SELECT-Anweisung innerhalb der Funktion angegeben wurden.
Über die Angabe von Aliasnamen in der Funktion kann man hie also die Spaltennamen
ändern. Darüber hinaus erkennt man an der nachfolgenden Ausgabe, dass weitere SQL-
488
Programmierbarkeit
Anweisungen innerhalb der SELECT-Anweisung, welche die Funktion aufruft, genau
über die Spaltennamen eingegeben werden können.
SalesOrderID SubTotal
Tax
Freight
Total
------------ ---------- --------- ---------- ----------43661
57718 CAD
4617 CAD
1443 CAD
63778 CAD
(1 Zeile(n) betroffen)
7.2.2.2
Erweiterte Tabellenwertfunktion
Als erweiterte Tabellenwertfunktion bezeichnet man tatsächlich eine erweiterte Form
der zuvor angegebenen. Sie besitzt ebenfalls die Möglichkeit, Parameter über die bekannte Methode anzugeben, präsentiert allerdings eine ganz andere Form der RETURNSAnweisung. Sie dient auf der einen Seite dazu, die Rückgabevariable vom Typ table
zu deklarieren, um sie später auffüllen zu können, und auf der anderen Seite auch die
Struktur der Tabelle anzugeben. Innerhalb dieser Tabellendefinition folgt die übliche
Auflistung an Spaltennamen und Datentypen, wie sie auch innerhalb einer tableDefinition üblich ist, weil es sich ja auch genau um diesen Datentyp handelt. Diese
erweiterte Form der Tabellenwertfunktionen erlaubt es dann, in ihrem BEGIN…ENDBlock, wie in einer Prozedur beliebigen T-SQL-Anweisungen zu verwenden. Die Tabellenvariable füllt man dann wie jede andere Variable vom Typ table über DMLOperationen auf, aktualisiert die Daten oder löscht sie. Diese Tabelle wir dann quasi
zurückgeliefert – quasi deshalb, weil die RETURN-Klausel leer ist und die Rückgabevariable schon in der RETURNS-Klausel angekündigt wurde.
CREATE FUNCTION name( [ { @parameter_name [ AS ] [
type_schema_name. ] parameter_data_type
[ = default ] }
[ ,...n ]
]
489
Programmierbarkeit
)
RETURNS @return_variable TABLE < table_type_definition >
[ WITH <function_option> [ ,...n ] ]
[ AS ]
BEGIN
function_body
RETURN
END
[ ; ]
Die Tabellenvariable kann man über die folgende allgemeine Syntax beschreiben und
gleichzeitig anlegen:
<table_type_definition>, (
{ <column_definition> <column_constraint> ,
| <computed_column_definition> } ,
[ <table_constraint> ] [ ,...n ], ) }
Das nächste Beispiel zeigt, wie man eine solche erweiterte Tabellenfunktion erstellt, mit
der grundsätzlich alle Anforderungen umgesetzt werden können. Sie soll eine relational
fragwürdige, dafür allerdings als table-Variable lohnenswerte Darstellung von Abteilungen und zugehörigen Mitarbeitern erstellen. Innerhalb der RETURNS-Klausel definiert man den Aufbau der zurück gelieferten Tabellenstruktur, welche aus zwei Spalten
für die Abteilung und drei Spalten für den Mitarbeiter besteht. Innerhalb der Anweisungen der Funktion gibt es dann einen Cursor, der verarbeitet wird und welcher die Daten
so in die Tabellenvariable einfügt, dass beim Abruf durch die NULL-Werte automatisch
auch eine Gruppierung (ähnlich eines kleinen Berichts) stattfindet. Dass genau diese
Tabellenvariable, deren Werte über solche Anweisungen wie INSERT, UPDATE oder
490
Programmierbarkeit
DELETE erstellt oder bearbeitet werden, an den Klienten geliefert werden soll, ist bereits
im Kopf der Funktion angegeben, und muss daher nicht noch einmal in der RETURNKlausel angegeben werden.
-- Existenzprüfung
IF OBJECT_ID (N'dbo.ufnGetEmployeesTable', N'TF') IS NOT
NULL
DROP FUNCTION dbo.ufnGetEmployeesTable
GO
-- Erstellung
CREATE FUNCTION dbo.ufnGetEmployeesTable (
@vGroupName nvarchar(50) )
-- Genaue Angabe der Rückgabestruktur
RETURNS @retEmployees TABLE (
GroupName
nvarchar(50)
NULL,
DepName
nvarchar(50)
NULL,
EmployeeID int
NULL,
EmpTitle
nvarchar(50)
NULL,
EmpName
nvarchar(255) NULL
)
AS BEGIN
-- Variablen- und Cursor-Deklaration
DECLARE @vDepID int, @vDepName nvarchar(20)
DECLARE cDepartment CURSOR FOR
491
Programmierbarkeit
SELECT DepartmentID, Name
FROM HumanResources.Department
WHERE GroupName = @vGroupName
-- Öffnen und Vorab-Laden
OPEN cDepartment
FETCH NEXT FROM cDepartment
INTO @vDepID, @vDepName
-- Schleife
WHILE @@FETCH_STATUS = 0
BEGIN
-- Einfügen Kopfzeile einer Abteilung
INSERT INTO @retEmployees (GroupName, DepName)
VALUES (@vGroupName, @vDepName)
-- Einfügen Mitarbeiterzeilen
INSERT INTO @retEmployees (EmployeeID, EmpTitle, EmpName)
SELECT emp.EmployeeID,
emp.Title,
FirstName + ' ' + LastName
FROM HumanResources.Employee AS emp
INNER JOIN Person.Contact AS c
ON emp.ContactID = c.ContactID
492
Programmierbarkeit
INNER JOIN HumanResources.EmployeeDepartmentHistory
AS dep
ON dep.EmployeeID = emp.EmployeeID
WHERE dep.DepartmentID = @vDepID
-- Nächster Abruf
FETCH NEXT FROM cDepartment
INTO @vDepID, @vDepName
END
-- Aufräumen
CLOSE cDepartment
DEALLOCATE cDepartment
-- Leere Rückgabe bzw. Rückgabe der Tabelle
RETURN
END
722_02.sql: Erweiterte Tabellenfunktion
Schließlich setzt man diese Tabellenwertfunktion genauso ein wie die vorherige. Innerhalb der FROM-Klausel kann man sie nun unter Angabe eines Abteilungsgruppennamens, zu dem eine Reihe von Untergruppen gehören, aufrufen. Interessant ist auch hier,
zu sehen, wie nicht alle Spalten der zurückgelieferten Tabelle genutzt werden, sondern
nur drei.
SELECT GroupName, DepName, EmpName
FROM dbo.ufnGetEmployeesTable('Manufacturing')
722_02.sql: Erweiterte Tabellenfunktion abrufen
493
Programmierbarkeit
Man erhält eine Ausgabe, die möglicherweise auch besser zeigt, welche Art Datenstruktur überhaupt gewählt wurde: wie eine Überschrift kann man Ober- und Untergruppe
der Abteilung sehen, unter die sich dann die einzelnen Datensätze der Angestellten
anfügen. Dies ist relational zwar bedenklich, wird aber von vielen Datenbanksystemen
genau auf diese Weise automatisch im Bereich der automatischen Berichtserstellung
angeboten.
GroupName
DepName
EmpName
-------------- ---------------------- ----------------Manufacturing
Production
NULL
NULL
NULL
Guy Gilbert
NULL
NULL
JoLynn Dobney
Manufacturing
Production Control
NULL
NULL
NULL
Peter Krebs
NULL
NULL
A. Scott Wright
7.2.3
Optionen
Die Funktionen besitzen noch verschiedene zusätzliche Optionen, von denen nur zwei
(ENCRYPTION und EXECUTE AS) wiederum mit denen von Prozeduren und von denen
eine mit Sichten (SCHEMABINDING) übereinstimmen. Sie sollen hier vergleichend dargestellt und dann für die Funktionsoptionen mit Beispielen erläutert werden. Folgende
Optionen sind für eine Funktion möglich:
<function_option>::= {
[ ENCRYPTION ]
| [ SCHEMABINDING ]
| [ RETURNS NULL ON NULL INPUT | CALLED ON NULL INPUT ]
494
Programmierbarkeit
| [ EXECUTE_AS_Clause ] }
Folgende Optionen sind für eine Prozedur möglich:
<procedure_option> ::= {
[ ENCRYPTION ]
[ RECOMPILE ]
[ EXECUTE_AS_Clause ] }
Zur Erinnerung: Für Prozeduren legte RECOMPILE fest, dass kein Ausführungsplan
gespeichert wird, weil davon auszugehen war, dass keine sinnvolle Annahmen über den
Wert der Parameter getroffen werden konnten und daher die Berücksichtigung des ansonsten die Ausführung beschleunigenden Ausführungsplans die Ausführung tatsächlich nur behindern würde. Mit der Klausel EXECUTE AS konnte man den so genannten
Sicherheitskontext festlegen, was später noch einmal im Zusammenhang erläutert wird.
Mit ENCRYPTION konnte man festlegen, dass der Quelltext verschlüsselt wird und nur
mit großen Berechtigungen wieder lesbar gemacht wird.
Für Funktionen gibt es neben diesen schon von Prozeduren bekannten Optionen noch
die folgenden weiteren:
ƒ
SCHEMABINDING legt fest, dass Funktionen, die in anderen Objekten, welche
gleichfalls schemagebunden sind, nicht gelöscht werden können.
ƒ
RETURN NULL ON NULL INPUT liefert bei Funktionsaufruf mit lauter NULL-
Werten als Parameter automatisch den Wert NULL zurück, ohne die Funktion
überhaupt auszuführen. Die Standardvariante CALLED ON NULL INPUT führt
dagegen die Funktion aus.
7.2.3.1
Schemabindung
Das Konzept der Schemabindung, das auch für Sichten existiert, in diesem Buch allerdings nicht ausführlich erläutert wurde, nun aber für Funktionen wenigstens eingeführt
werden soll, könnte man mit der referenziellen Integrität von Daten und den entspre-
495
Programmierbarkeit
chenden Beziehungen zwischen Tabellen vergleichen. Hat man eine Funktion erstellt,
die nicht nur im Rahmen von T-SQL-Anweisungen und wieder anderen Prozeduren,
Funktionen oder Triggern genutzt wird, sondern die sogar bei der Erstellung von Sichten und Tabellen als Skalarfunktion zum Einsatz kommt, dann ist es wichtig, dass man
sie nicht einfach löschen oder so verändern kann, dass die von ihr abhängigen Objekte
ungültig werden. In einer Sicht kann eine solche Funktion sowohl als Skalar- als auch
in Form einer Tabellenwertfunktion erscheinen, wenn die Sicht eine solche Funktion in
einer Spalte für eine Berechnung aufruft oder als Abfragequelle benutzt. In einer Tabelle kann sie für berechnete Spalten oder CHECK-Bedingungen zum Einsatz kommen. In
allen anderen Objekten kann sie in diversen T-SQL-Anweisungen zum Einsatz kommen. Was soll nun geschehen, wenn die Funktion gelöscht wird, obwohl eine
Sicht/Tabelle weiterhin diese Funktion nutzt? Wie kann eine Prozedur funktionieren,
wenn die wichtigste Berechnung in Form der Funktion nicht mehr korrekt ausgeführt
wird? Als Antwort gibt es zwei Alternativen: Es gibt bei Ausführung eine sehr unangenehme Fehlermeldung, oder es gibt einen automatischen bzw. vom Benutzer eingerichteten Mechanismus, der das Löschen verhindert. Dies entspricht der referenziellen Integrität, in der ein Datensatz der Eltern-Tabelle ebenfalls nicht gelöscht werden kann,
wenn noch ein Kind-Datensatz auf ihn verweist. Dies kann man einrichten oder auch
unterlassen - mit dem Ergebnis, eine sehr flexible Datenbank ohne Ärger bei Löschoperationen zu besitzen, die allerdings schnell dazu neigt, völlig inkonsistent zu werden.
Mit der Option SCHEMABINDING legt man nun fest, dass die Funktion an DB-Objekte
gebunden ist, welche auf diese Funktion verweisen. Dadurch sind Lösch- und Änderungsoperationen untersagt. Folgende Bedingungen müssen zutreffen, wenn die Schemabindung genutzt werden soll:
ƒ
Bei der Funktion muss es sich um eine in T-SQL geschriebene Funktion handeln.
ƒ
Andere benutzerdefinierte Funktionen und Sichten, welche die Funktion nutzen,
müssen gleichfalls schemagebunden sein.
ƒ
Verweise auf andere Objekte innerhalb der Funktion sind mit dem zweiteiligen
Namen schema.name angegeben.
496
Programmierbarkeit
ƒ
Die abhängigen Objekte und die Funktion gehören zur selben Datenbank.
ƒ
Der Benutzer, welche die Funktion erstellt hat, besitzt die REFERENCESBerechtigung für die DB-Objekte, auf die die Funktion verweist.
In der Datei 723_01.sql befindet sich ein vollständiges Beispiel, in dem verschiedene
Teile der vorherigen Beispiele aufgegriffen werden, um es leichter verständlich zu
machen. Daher ist es allerdings auch nicht vollständig abgedruckt, um den Blick auf das
wesentliche zu lenken. Zunächst erstellt man eine Funktion mit Schemabindung, die
wiederum die bekannte Währungsumrechnung unter dem schon bekannten Namen
durchführt.
-- Auf Existenz prüfen und ggf. löschen
IF OBJECT_ID (N'Sales.ufnGetBuyerCurrency', N'FN') IS NOT
NULL
DROP FUNCTION Sales.ufnGetBuyerCurrency
GO
-- Funktion mit Schema-Bindung erstellen
CREATE FUNCTION Sales.ufnGetBuyerCurrency (
@salesOrderID int,
@sum money
)
RETURNS varchar(15)
WITH SCHEMABINDING
AS BEGIN
...
723_01.sql: Anlegen einer Funktion mit Schemabindung
497
Programmierbarkeit
Danach erstellt man eine ebenfalls schemagebundene Sicht, welche die gerade erstellte
Funktion aufruft. Dies wäre dann auch ein Beispiel, in dem man eine Sicht und eine
Tabellenwertfunktion miteinander vergleichen kann, denn grundsätzlich liefert die Sicht
auch die verschiedenen Bestellungen mit ihren umgerechneten Preisspalten.
-- Sicht erstellen, welche die Funktion benötigt
IF OBJECT_ID (N'Sales.vBuyerCurrency', N'V') IS NOT NULL
DROP VIEW Sales.vBuyerCurrency
GO
CREATE VIEW Sales.vBuyerCurrency
WITH SCHEMABINDING AS
SELECT SalesOrderID,
Sales.ufnGetBuyerCurrency(SalesOrderID,
SubTotal) AS SubTotal,
...
723_01.sql: Anlegen einer Sicht mit Schemabindung
Nachdem man die Sicht erfolgreich getestet hat, versucht man, die Funktion zu löschen,
womit man genau die Regeln der Schemabindung verletzt.
-- Test von Sicht und gleichzeitig der Funktion
SELECT *
FROM Sales.vBuyerCurrency
WHERE SalesOrderID BETWEEN 43661 AND 43663
GO
-- Löschversuch der Funktion
498
Programmierbarkeit
DROP FUNCTION Sales.ufnGetBuyerCurrency
723_01.sql: Test und Löschversuch der Funktion
Man erhält die sehr deutliche Fehlermeldung:
Meldung 3729, Ebene 16, Status 1, Zeile 1
Das DROP FUNCTION von 'Sales.ufnGetBuyerCurrency' ist nicht
möglich, da das 'vBuyerCurrency'-Objekt darauf verweist.
In de Datei 723_02.sql findet man das gesamte Beispiel noch einmal, wobei hier allerdings die gesamte Schemabindung entfernt wurde. Alles funktioniert hervorragend und
genauso wie zuvor. Sogar der Löschversuch gelingt problemlos. Lediglich der Test, ob
denn die Sicht nach dem Löschen der Funktion noch ausgeführt wird, bringt folgende
Fehlermeldung, die es an Deutlichkeit ebenfalls nicht zu wünschen übrig lässt, hervor.
Auch wenn man sie wohlwollend liest, muss man zugeben, dass Schemabindung überaus nützlich zu sein scheint.
Meldung 4121, Ebene 16, Status 1, Zeile 1
Die "Sales"-Spalte oder die benutzerdefinierte Funktion bzw.
das benutzerdefinierte Aggregat "Sales.ufnGetBuyerCurrency"
wurde nicht gefunden, oder der Name ist mehrdeutig.
Meldung 4413, Ebene 16, Status 1, Zeile 1
Die Sicht oder Funktion 'Sales.vBuyerCurrency' konnte
aufgrund von Bindungsfehlern nicht verwendet werden.
7.2.3.2
NULL-Werte ausgeben
Schließlich gibt es noch zwei Optionen, die sich mit der Übergabe von NULL-Werten
beschäftigen. Die Standardvariante CALLED ON NULL INPUT gibt vor, dass die Funktion durchaus aufgerufen werden soll, wenn alle übergebenen Parameter NULL sein
sollten. Wenn innerhalb der Prozedur dafür Sorge getragen wurde, dass ein sinnvoller
Wert oder auch NULL als geeigneter Rückgabewert ermittelt wird, dann ist dies auch
499
Programmierbarkeit
durchaus sinnvoll. Die Variante RETURNS NULL ON NULL INPUT dagegen liefert
automatisch den Wert NULL zurück, wenn alle übergebenen Werte NULL sind. Dies ist
dann interessant, wenn die Funktion in einem solchen Fall tatsächlich NULL zurückliefern soll und die Ausführung der Funktion zu lange dauert. Durch den fehlenden Aufruf
erhält man das gleiche Ergebnis bei deutlich kürzerer Gesamtausführungszeit.
Das nachfolgende Beispiel kann durch Ändern der Kommentare bei gleich bleibender
Ausführung zu ganz anderen Ergebnissen kommen und dieses Phänomen eindrucksvoll
vorführen. Wenn die Option RETURNS NULL ON NULL INPUT vorgeben ist, dann
liefert der Aufruf mit lauter NULL-Werten tatsächlich NULL, obwohl ganz am Ende der
Funktion in einer Fallunterscheidung dieser Wert extra abgefangen und in den Zahlwert
0 umgewandelt wird. Im anderen Fall sorgt die Option CALLED ON NULL INPUT dafür, dass die Funktion – wie sonst auch – ausgeführt wird und im Fall von lauter NULLWerten der Zahlwert 0 ausgegeben wird, wie ihn die Fallunterscheidung ermittelt.
CREATE FUNCTION Sales.ufnGetBuyerCurrency (
@salesOrderID int,
@sum money
)
RETURNS varchar(15)
-- Liefert NULL bei Angabe eines NULL-Preises
WITH RETURNS NULL ON NULL INPUT
-- Liefert den Wert ermittelten Wert 0 (Standard)
--WITH CALLED ON NULL INPUT
AS BEGIN
...
-- Rückgabe
500
Programmierbarkeit
SET @result = RTRIM(LTRIM(STR(@sum * @averageRate) + ' '
+ @currency))
IF @result IS NULL BEGIN
RETURN '0'
END
RETURN @result
END
723_03.sql: Einstellungen für NULL-Parameter
Der Aufruf sollte dann mit folgender Anweisung getestet werden:
SELECT Sales.ufnGetBuyerCurrency(NULL, NULL)
7.2.4
APPLY-Operator
Es gibt noch einen weiteren neuen Operator, der zur T-SQL-Syntax der Version 2005
hinzugefügt wurde, und der innerhalb der FROM-Klausel genutzt werden kann. Dabei
kann man diesen Operator sowohl mit Unterabfragen als auch mit Tabellenwertfunktionen nutzen. Die allgemeine Syntax lautet:
left_table_source { CROSS | OUTER } APPLY right_table_source
Beide Ausdrücke links und rechts vom APPLY-Operator stellen Tabellenausdrücke dar.
Dabei können beide Ausdrücke neben Unterabfragen auch Tabellenwertfunktionen
enthalten. Die rechte kann als Argument eine ganze Spalte aus dem Tabellenausdruck
auf der linken Seite empfangen. Dies ist für die linke nicht möglich. Sofern keine solche Tabellenwertfunktion genutzt wird, kommt für den rechten Tabellenausdruck eine
korrelierte Unterabfrage zum Einsatz kommen.
Die Funktionsweise des Operators lässt sich wie folgt beschreiben: Der rechte Ausdruck
wertet den linken Ausdruck aus, um Ergebnisse zu ermitteln. Dies lässt sich entweder
durch eine Korrelation erklären oder durch die Übergabe von Rückgabewerten. Da er
501
Programmierbarkeit
insbesondere für die Nutzung mit Funktionen geschaffen wurde, wird er in diesem Kapitel beschrieben.
Das erste Beispiel zeigt allerdings zunächst seine Funktionsweise anhand von zwei
Unterabfragen und damit auch diese Einsatzalternative. Auf der linken Seiten des Operators ruft man die Tabelle ProductSubcategory auf. Sie liefert die Eingabedaten für
den rechten Ausdruck, der in Form einer Korrelationsunterabfrage auftritt. Dabei handelt es sich um die Daten aus der Product-Tabelle, welche zu den Kategorien aus dem
linken Tabellenausdruck passen. Anstelle eines solchen Operators hätte man im Normalfall einfach beide Tabellen über einen INNER JOIN verbunden, um das gleiche
Ergebnis zu erzielen. Der Operator CROSS sorgt dafür, dass nur die Datensätze, welche
in beiden Tabellen Treffer finden, in die Ergebnismenge kommen.
SELECT sc.Name AS Category, p.Name, ProductNumber
FROM Production.ProductSubcategory AS sc
CROSS APPLY
(SELECT Name, ProductNumber, Color, Size
FROM Production.Product
WHERE ProductSubCategoryID =
sc.ProductSubCategoryID) AS p
WHERE sc.Name LIKE '%Bike%'
724_01.sql: APPLY und Unterabfrage
Im nächsten Beispiel soll das Schlüsselwort OUTER präsentiert werden, welches inhaltlich so funktioniert wie ein OUTER JOIN. Die nachfolgende Abfrage beschafft alle
Produktdaten und zeigt – aufgrund der LEFT OUTER JOIN-Verknüpfung – für diejenigen Produkte, die einer Kategorie zugeordnet sind, auch die entsprechende Kategorie
an. In der WHERE-Klausel beschränkt man dies auf diejenigen Datensätze, die keiner
Kategorie zugeordnet sind. In der zweiten Lösung geschieht dies über die gleiche Abfrage wie zuvor, wobei hier einfach nur OUTER APPLY verwendet wird.
502
Programmierbarkeit
SELECT *
FROM Production.Product AS p
LEFT OUTER JOIN Production.ProductSubcategory AS sc
ON p.ProductSubcategoryID = sc.ProductSubcategoryID
WHERE p.ProductSubcategoryID IS NULL
SELECT p.Name, p.ProductNumber, sc.Name
FROM Production.Product AS p
OUTER APPLY
(SELECT *
FROM Production.ProductSubcategory AS sc
WHERE p.ProductSubcategoryID =
sc.ProductSubcategoryID) AS sc
WHERE p.ProductSubcategoryID IS NULL
724_01.sql: APPLY und Unterabfrage
Solange gewöhnliche Tabellen, Sichten oder Unterabfragen zum Einsatz kommen können, ist es sicherlich empfehlenswert, die Standard-Techniken zu benutzen. Das nachfolgende Beispiel allerdings zeigt, wie man mit Hilfe einer Tabellenwertfunktion den
APPLY-Operator nutzen kann. Die Funktion ufnGetProducts() erwartet eine SubCategoryID und liefert für diesen Schlüsseln die zugeordneten Produkte in Form einer
Tabelle, bestehend aus dem Produktnamen, Nummer, Größe und Farbe.
Nachdem die Funktion erstellt wurde, lässt sie sich rechts vom APPLY-Operator verwenden. Eine linksseitige Verwendung ist auch erlaubt, wobei hier allerdings kein sol-
503
Programmierbarkeit
cher Übergabewert möglich ist. Anstelle einer Korrelation wie zuvor übergibt man den
vom linksseitigen Tabellenausdruck gelieferten Wert der Unterkategorienummer.
CREATE FUNCTION dbo.ufnGetProducts ( @SubCategoryID int )
RETURNS table AS
RETURN ( SELECT Name, ProductNumber, Color, Size
FROM Production.Product
WHERE ProductSubCategoryID = @SubCategoryID)
GO
SELECT sc.Name AS Category, p.Name, ProductNumber
FROM Production.ProductSubcategory AS sc
CROSS APPLY
dbo.ufnGetProducts(sc.ProductSubCategoryID) AS p
WHERE sc.Name LIKE '%Bike%'
724_01.sql: APPLY und Tabellenwertfunktion
7.3
Verwaltungsarbeiten
Die Abschnitt gibt neben ausführlichen Hinweisen zur Sicherheit von Modulen auch
zusätzliche Informationen, wie Informationen über Funktionen und Prozeduren abgerufen werden können.
7.3.1
Katalogsichten für Objekte
Die Abfrage SELECT * FROM sys.sql_modules liefert eine Aufstellung der in der
jeweiligen Datenbank, in der Abfrage ausgeführt wird, vorhandenen Funktionen, Prozeduren und Trigger. Es handelt sich dabei um eine so genannte Katalogsicht, welche
504
Programmierbarkeit
Daten über das System in relationaler Form liefern, die ansonsten nur in der Oberfläche
abgerufen werden können.
Folgende Spalten sind in der Ergebnismenge enthalten:
ƒ
object_id (int) enthält eine in der Datenbank eindeutige ID des Objekts.
ƒ
definition (nvarchar(max)) enthält den T-SQL-Quelltext des Moduls oder NULL,
wenn es verschlüsselt ist.
ƒ
uses_ansi_nulls (bit) gibt an, ob das Modul mit SET ANSI_NULLS ON erstellt
wurde und ist immer 0 für Regeln und Standardwerte.
ƒ
uses_quoted_identifier
(bit)
gibt
an,
ob
das
Modul
mit
SET
QUOTED_IDENTIFIER ON erstellt wurde.
ƒ
is_schema_bound (bit) gibt an ob, das Modul mit der Option SCHEMABINDING
erstellt wurde.
ƒ
uses_database_collation (bit) liefert 1, wenn die richtige Auswertung der
schemagebundenen Definition des Moduls abhängig von der Standardsortierung
der Datenbank ist, ansonsten 0. Sollte hier eine Abhängigkeit bestehen, wird eine
Änderung der Standardsortierung der Datenbank verhindert.
ƒ
is_recompiled (bit) gibt an, ob die Prozedur mit der Option WITH RECOMPILE
erstellt wurde.
ƒ
null_on_null_input (bit) gibt an, ob das Modul so erstellt wurde, dass die
Übergabe von NULL-Werten der Wert NULL folgt.
ƒ
execute_as_principal_id (int) liefert die ID des Besitzers in EXECUTE AS.
Der Standardwert ist NULL oder EXECUTE AS CALLER. Ansonsten wird die ID des
Benutzers bei EXECUTE AS SELF oder EXECUTE AS <user> geliefert. Der Wert
-2 entsteht bei EXECUTE AS OWNER.
505
Programmierbarkeit
Die Abfrage SELECT * FROM sys.objects liefert die benutzerdefinierten Objekte
und u.a. Funktionen und Prozeduren, aber keine Trigger, da sie nicht schemagebunden
sind. Trigger findet man dagegen in sys.triggers.
In einigen Spalten werden mit Hilfe von Buchstabenkürzeln Objekttypen angegeben.
Dies sind die folgenden, wobei .NET-bezogene nicht in diesem Buch erklärt wurden
und einige Objekte im DBA-Buch zu finden sind: AF = Aggregatfunktion (.NET), C =
CHECK-Einschränkung, D = DEFAULT (Einschränkung oder eigenständig), F = FOREIGN
KEY-Einschränkung, PK = PRIMARY KEY-Einschränkung, P = Gespeicherte SQLProzedur, PC = Prozedur (.NET), FN = SQL-Skalarfunktion, FS = Skalarfunktion
(.NET), FT = Tabellenwertfunktion (.NET), R = Regel (Version 2000, eigenständig),
RF = Replikationsfilterprozedur, SN = Synonym, SQ = Dienstwarteschlange, TA =
DML-Trigger (.NET), TR = DML-Trigger, IF = Einfache Tabellenwertfunktion, TF =
Tabellenwertfunktion, U = Tabelle (benutzerdefiniert), UQ = UNIQUE-Einschränkung,
V = Sicht, X = Erweiterte gespeicherte Prozedur, IT = Interne Tabelle.
Die
gleiche
Struktur weisen die Sichten sys.system_objects
sys.all_objects auf. Diese Struktur besteht aus folgenden Spalten:
und
ƒ
name (sysname) enthält den Objektnamen.
ƒ
object_id (int) enthält die innerhalb der Datenbank eindeutige Objekt-ID.
ƒ
schema_id (int) enthält die Schema-ID, in dem das Objekt enthalten ist. Für die in
Version 2005 vorhandenen Systemobjekte in einem Schema ist dies immer IN
(schema_id('sys'), schema_id('INFORMATION_SCHEMA').
ƒ
principal_id (int) enthält die Besitzer-ID, falls es nicht der Schemabesitzer ist,
oder NULL bei folgenden Objekten: C, D, F, PK, R, TA, TR, UQ
ƒ
parent_object_id (int) enthält die Eltern-Objekt-ID oder 0.
ƒ
type (char(2)) enthält den Objekttyp: AF, C, D, F, PK, P, PC, FN, FS, FT, R, RF,
SN, SQ, TA, TR, IF, TF, U, UQ, V, X, IT
506
Programmierbarkeit
ƒ
type_desc (nvarchar(60)) beschreibt das Objekt mit den folgenden Werten:
AGGREGATE_FUNCTION,
CHECK_CONSTRAINT,
FOREIGN_KEY_CONSTRAINT,
DEFAULT_CONSTRAINT,
PRIMARY_KEY_CONSTRAINT,
SQL_STORED_PROCEDURE,
CLR_STORED_PROCEDURE,
SQL_SCALAR_FUNCTION,
CLR_SCALAR_FUNCTION,
CLR_TABLE_VALUED_FUNCTION, RULE, REPLICATION_FILTER_PROCEDURE,
SYNONYM,
SERVICE_QUEUE,
CLR_TRIGGER,
SQL_INLINE_TABLE_VALUED_FUNCTION,
USER_TABLE,
SQL_TRIGGER,
SQL_TABLE_VALUED_FUNCTION,
UNIQUE_CONSTRAINT,
VIEW,
EXTENDED_STORED_PROCEDURE, INTERNAL_TABLE
ƒ
create_date (datetime) enthält das Erstelldatum.
ƒ
modify_date (datetime) enthält das letzte Änderungsdatum über ALTER oder
durch einen Index.
ƒ
is_ms_shipped (bit) gibt an, ob es eine interne SQL Server-Komponente ist.
ƒ
is_published (bit) gibt an, ob das Objekt veröffentlicht wurde.
ƒ
is_schema_published (bit) gibt an, ob nur das Schema des Objekts
veröffentlicht wurde.
Die Sicht sys.procedures liefert eine Zeile für jede Prozedur vom Typ
sys.objects.type = P, X, RF und PC. Zusätzlich zu den Spalten von sys.objects
gibt es noch folgende Spalten:
ƒ
is_auto_executed (bit) liefert bei Prozeduren in der master-DB 1, wenn die
Prozedur Serverstart automatisch ausgeführt wird, sonst 0.
ƒ
is_execution_replicated (bit) gibt an, ob die Ausführung der Prozedur
repliziert wird.
ƒ
is_repl_serializable_only (bit) gibt an, ob die Ausführung der Prozedur nur
repliziert wird, wenn die Transaktion serialisiert werden kann.
507
Programmierbarkeit
ƒ
skips_repl_constraints (bit) gibt an ob, die Prozedur Einschränkungen
überspringt, die mit NOT FOR REPLICATION angegeben sind.
7.3.2
Funktionen
Neben den zuvor vorgestellten Katalogsichten gibt es noch verschiedene Funktionen,
mit deren Hilfe Objekteigenschaften abgefragt werden können.
Die Funktion OBJECT_ID() liefert die Objekt-ID eines Objekts. Die allgemeine Syntax
lautet:
OBJECT_ID (
'[ database_name . [ schema_name ] . | schema_name . ]
object_name' [ ,'object_type' ] )
Das nächste Beispiel prüft auf Existenz und ermittelt dann weitere Daten.
IF OBJECT_ID (N'AdventureWorks.Production.
usp_GetProduct', N'P') IS NOT NULL
BEGIN
SELECT OBJECT_ID(N'AdventureWorks.Production.
usp_GetProduct') AS 'Object ID'
SELECT name, type_desc
FROM sys.procedures
WHERE [Object_ID] = OBJECT_ID(N'AdventureWorks.
Production.usp_GetProduct')
END
732_01.sql: Abrufen der Objekt-ID
Man erhält als Ergebnis:
508
Programmierbarkeit
Object ID
----------320720195
name
type_desc
----------------- ---------------------usp_GetProduct
SQL_STORED_PROCEDURE
Die Funktion OBJECT_NAME() liefert den Objektnamen. Die Funktion hat die allgemeine Syntax:
OBJECT_NAME ( object_id )
Im nächsten Beispiel ermittelt man den Objektnamen über die ID.
SELECT *
FROM sys.objects
WHERE name = OBJECT_NAME(
OBJECT_ID(N'Production.usp_GetProduct'))
732_01.sql: Ermittlung des Objektnamens
OBJECTPROPERTY() mit der allgemeinen Syntax OBJECTPROPERTY ( id , property ) liefert jede beliebige Eigenschaft zu einem Objekt zurück. Darunter fallen die
Art des Objekts, seine unterschiedlichen Zustände und seine möglichen Zustände. Die
Liste enthält so viele Einträge, dass auf die Dokumentation verwiesen wird. Es lässt sich
allerdings mit Sicherheit jede Information abrufen, die benötigt wird. Die Eigenschaft
wird als Zeichenkette erwartet und stammt aus der in der Dokumentation aufgeführten
Liste. Die Rückgabewerte sind meistens 1 für TRUE und 0 für False. In wenigen Ausnahmen gibt es noch weitere Werte.
Die Funktion OBJECTPROPERTYEX ( id , property ) liefert weitere Eigenschaften zurück. Sollten die Eigenschaften in der vorherigen Funktion nicht verfügbar sein,
509
Programmierbarkeit
dann kann man noch Hoffnung haben, in der ebenfalls beeindruckend langen Liste dieser Funktion fündig zu werden.
Die Funktion OBJECT_DEFINITION ( object_id ) liefert den Quelltext und ähnelt
damit der Prozedur sp_helptext [ @objname = ] 'name' [ , [
@columnname = ] computed_column_name ]. Die Prozedur sp_help [ [
@objname = ] 'name' ] liefert schließlich eine relationale Ergebnismenge, d.h. eine
Tabelle zurück, in der ebenfalls verschiedene Informationen zu einem Objekt aufgeführt
sind. Die Tabellenstruktur ist bei den einzelnen Objektarten sehr unterschiedlich.
7.3.3
Sicherheit
Die Ausführung von gespeicherten Prozeduren beinhaltet verschiedene sicherheitsrelevante Aspekte, da hier kleine Softwarebausteine direkt in der Datenbank ausgeführt
werden, deren gespeicherte Anweisungen verschiedene Schema-Objekte ansprechen
können. Dies betrifft grundsätzlich alle Datenbanken, in denen Prozeduren erstellt werden können. Hier muss man sich lediglich vorstellen, dass ein Benutzer einem anderen
Benutzer die Ausführrechte an seiner Prozedur erteilt, welche in ihren Anweisungen
eigentlich für den ausführenden Benutzer verbotene Schema-Objekte benutzt.
Im Normalfall greift eine Reihe von T-SQL-Anweisungen nacheinander auf mehrere
Objekte zu. Wenn diese Objekte nicht nur Tabellen sind, sondern auch Prozeduren und
Funktionen, ist es leicht vorstellbar, dass innerhalb dieser Module wiederum auf anderen Module bzw. wenigstens auf Tabellen oder Sichten zugegriffen wird. Dies wird als
Kette bezeichnet, da ein Objekt das nächste aufruft. Dabei gelten besondere Sicherheitsregeln, die nicht denen entsprechen, als hätte ein Benutzer, der die Kette angestoßen hat,
selbst die einzelnen Objekte angesprochen. Mit dem Begriff der Besitzkette wird nun
das Prinzip beschrieben, dass die von einem Objekt nachfolgend aufgerufenen Objekte
mit Hilfe einer speziellen Sicherheitsverwaltung tatsächlich aufrufbar sind. So soll ein
Leistungsabfall vermieden werden, der entstehen würde, wenn permanent einzelne
Berechtigungsprüfungen durchgeführt werden, wie dies bei einem getrennten, nacheinander erfolgenden Aufruf der Fall wäre.
510
Programmierbarkeit
Wird innerhalb einer Kette aufgerufen, dann prüft der MS SQL Server zunächst, ob der
Besitzer (= Benutzer) des aufrufenden Objekts auch tatsächlich der Besitzer des aufgerufenen Objekts ist. Ist dies der Fall, hat der Besitzer (=Benutzer) auch die entsprechenden Berechtigungen am nachfolgend aufgerufenen Objekt und die Berechtigungen werden nicht weiter ausgewertet. Was soll allerdings geschehen, wenn dies nicht der Fall
ist? Der MS SQL Server 2000 prüfte hier zunächst danach, ob die gespeicherte Prozedur
und die angesprochenen Objekte im gleichen Schema liegen, ob die Aktivität statisch ist
und damit kein dynamisches SQL enthält und ob schließlich die Aktivität nur DMLOperationen (SELECT, INSERT, UPDATE und DELETE) enthält oder eine andere gespeicherte Prozedur aufruft. Trafen alle Fälle zu, so konnte ein anderer Benutzer, der nur die
Ausführrechte einer Prozedur, aber keine direkten Rechte an den durch die Prozedur
bearbeiteten Objekten besaß, sehr wohl die Prozedur benutzen. Im MS SQL Server
2005 wurde dieses Berechtigungskonzept zunächst übernommen, sodass hier Abwärtskompatibilität und Vergleichbarkeit vorherrscht. Allerdings bietet die neue Version
auch nun verfeinerte Vorgabemöglichkeiten bei der Prozedurerstellung.
Schließlich ist es auch möglich, datenbankübergreifende Besitzverkettungen zu ermöglichen. Sie ist standardmäßig deaktiviert.
In der Abbildung soll das Grundproblem noch einmal grafisch dargestellt werden:
Verschiedene Objekte haben verschiedene Besitzer, wobei Benutzer B selbst gar kein
Besitzer irgendeines Objekts ist, sondern er nur von Benutzer A die Berechtigungen
erhalten hat, die Prozedur von Benutzer A auszuführen. In (1) ist er autorisiert, die Prozedur auszuführen, weil er die Berechtigung von A erhalten hat. Diese Prozedur ruft in
(2) eine Sicht von Benutzer C ab, wobei nun allerdings die vollständigen Berechtigungen abgerufen werden, weil sich beide Besitzer unterscheiden. Sofern hier auch Benutzer B für die Sicht autorisiert ist, werden die Daten zurückgeliefert. Diese Sicht wirkt
sich nun wiederum in (3) auf eine Tabelle aus, deren Besitzer Benutzer D ist. Da hier
erneut ein Besitzerwechsel stattfindet, müssen die gesamten Berechtigungen abgerufen
werden, und auch Benutzer B wird auf Nutzungsberechtigung dieser Tabelle überprüft.
Schließlich ist in (4) auch noch die datenbankübergreifende Besitzverkettung dargestellt, die hier funktioniert, weil sie entsprechend aktiviert wurde. Benutzer B hat die
511
Programmierbarkeit
Berechtigung von Benutzer A an dieser Sicht und darf daher sogar von einer anderen
DB die Daten abrufen.
Die Besitzkette ist also mehrfach unterbrochen, weil sich die Besitzer unterscheiden.
Unter eine fortlaufenden Besitzkette versteht man dagegen den verketteten Aufruf von
Objekten eines einzigen Benutzers.
Benutzer A
Prozedur
Benutzer A
1
4
Sicht 1
Benutzer A
Benutzer B
2
Sicht 1
Benutzer C
Datenbank B
3
Tabelle 1
Benutzer D
Datenbank A
Abbildung 7.5: Besitzketten
Unter Einsatz der neuen Klausel EXECUTE AS kann man nun innerhalb einer Prozedur
den Sicherheitskontext einer Prozedur genau festlegen. Folgende allgemeine Syntax
512
Programmierbarkeit
existiert für Funktionen, Prozeduren und Trigger, wobei die Syntax für Trigger noch
zusätzliche Erweiterungen besitzt:
Funktionen (außer inline table-valued-Funktionen),
Prozeduren und DML-Trigger
{ EXEC | EXECUTE } AS { CALLER | SELF | OWNER | 'user_name'
}
Die Bedeutung der verschiedenen Einstellungen ergeben sich fast schon aufgrund ihres
Namens:
ƒ
CALLER (Standardwert) legt fest, dass die Prozedur im Sicherheitskontext des
Aufrufenden, d.h. des Benutzers, ausgeführt wird. Dies ist auch der Standard unter
der Vorgänverversion MS SQL Server 2000.
ƒ
SELF legt fest, dass die Prozedur im Sicherheitskontext des Besitzers der Prozedur
oder desjenigen, der den ALTER-Befehl abgesetzt hat, ausgeführt wird. Dies
entspricht der <user_name>-Option, wobei hier als Benutzername automatisch der
erstellende oder ändernde Benutzer eingetragen wird.
ƒ
OWNER legt fest, dass die Prozedur nur im Sicherheitskontext des Besitzers
ausgeführt wird. Hier ist keine Rolle oder Gruppe möglich, nur ein einzelnes
Benutzerkonto.
ƒ
<user_name> legt einen speziellen Benutzer fest, in desse Sicherheitskontext die
Prozedur ausgeführt werden soll. Dabei darf der Benutzer kein(e) Gruppe, Rolle,
Zertifikat, Schlüssel oder integriertes Konto sein.
Schließlich soll noch in Transact SQL kurz erklärt werden, wie die Ausführberechtigung erteilt und wieder entzogen wird. Die beiden Befehle GRANT und REVOKE werden
im Administrationsbuch noch ausführlich erläutert, sodass hier nur eine Kurzfassung
folgt. Folgende Rechte können für Prozeduren und Funktionen erteilt und entzogen
werden. Sie werden gleichzeitig über die ALL-Option zusammen gefasst.
ƒ
Skalarfunktionen: EXECUTE, REFERENCES.
513
Programmierbarkeit
ƒ
Tabellenwertfunktionen: DELETE, INSERT, REFERENCES, SELECT, UPDATE.
ƒ
Prozeduren: EXECUTE, SYNONYM, DELETE, INSERT, SELECT, UPDATE.
Die allgemeine Syntax für GRANT, mit dem Berechtigungen zugewiesen werden können,
lautet:
GRANT <permission> [ ,...n ] ON
[ OBJECT :: ][ schema_name ]. object_name
[ ( column [ ,...n ] ) ]
TO <database_principal> [ ,...n ]
[ WITH GRANT OPTION ]
[ AS <user_name> ]
<permission> ::=
ALL [ PRIVILEGES ] | permission [ ( column [ ,...n ] ) ]
Im Fall von Prozeduren und Funktionen können alle Berechtigungen mit ALL erteilt
werden, was sich auf die oben angegebenen Berechtigungen bezieht. Dies ist gleichzeitig auch die Liste der zu erstellenden einzelnen Berechtigungen. Um also eine einfache
Berechtigung zu erstellen, verwendet man:
GRANT EXECUTE ON OBJECT::Production.usp_GetProduct TO kunde;
Folgende wesentliche Parameter sind zu verwenden:
ƒ
ON [ OBJECT :: ] [ schema_name ] . object_name gibt das Objekt an,
für das Berechtigungen erteilt werden sollen.
ƒ
TO <user_name> enthält den Benutzernamen, für den Berechtigungen erteilt
werden können. Dies können eine ganze Reihe an unterschiedlichen Konten sein:
Datenbankbenutzer,
einem
Windows-Anmeldename
zugeordneter
514
Programmierbarkeit
Datenbankbenutzer, einer Windows-Gruppe zugeordneter Datenbankbenutzer,
keinem Serverprinzipal zugeordneter Datenbankbenutzer, Datenbankrolle oder eine
Anwendungsrolle. Diese erfordern teilweise zusätzliche Eigenschaften und
Einträge.
ƒ
WITH GRANT OPTION erlaubt die Weitergabe der Rechte.
ƒ
AS <user_name> enthält die Rolle, unter der die Berechtigung erteilt wird und
entspricht inhaltlich dem in TO <user_name> aufgelisteten Bereich.
Die allgemeine Syntax für REVOKE, mit dem Berechtigungen entzogen werden können,
lautet:
REVOKE [ GRANT OPTION FOR ] <permission> [ ,...n ] ON
[ OBJECT :: ][ schema_name ]. object_name
[ ( column [ ,...n ] ) ]
{ FROM | TO } <user_name> [ ,...n ]
[ CASCADE ]
[ AS <user_name> ]
<permission> ::=
ALL [ PRIVILEGES ] | permission [ ( column [ ,...n ] ) ]
Folgende wesentliche Parameter sind zu verwenden:
ƒ
ON [ OBJECT :: ] [ schema_name ] . object_name enthält das Objekt,
für das Rechte entzogen werden sollen.
ƒ
{ FROM | TO } <user_name> enthält u.a. die bereits bei GRANT aufgelisteten
Konten für den Benutzer, dem Rechte entzogen werden sollen.
515
Programmierbarkeit
ƒ
GRANT
OPTION hebt die Berechtigung auf, Rechte weiterzugeben, ohne die
Berechtigung an sich aufzuheben.
ƒ
CASCADE gibt an, dass allen nachfolgenden Benutzern, welche Rechte von dem
Benutzer erhalten haben, dem sie nun entzogen werden, ebenfalls die Rechte
entzogen werden (kann wie ein Flächenbrand wirken).
Im Falle von Prozeduren und Funktionen können alle oben angegebenen Berechtigungen mit ALL entzogen werden. Dies ist gleichzeitig auch die Liste der zu aufzuhebenden
einzelnen Berechtigungen. Analog zum vorherigen Fall würde man die Berechtigungen
folgendermaßen entziehen:
REVOKE EXECUTE ON OBJECT::Production.usp_GetProduct
FROM kunde;
516
Index
Index
517
Index
518
Index
Index
.NET-Funktionen 481
ALL 193
[NOT] EXISTS 195
ALTER 252
[NOT] IN 90
ALTER PROCEDURE 458
Abfrage speichern 40
AND 92
Abfrage-Editor: Grafisch 43
Ansicht 53
Abfrage-Editor 39
ANSI-SQL-Verknüpfungen 161
Abgeleitete Tabellen 182; Mehrstufige
Verschachtelung 184; Vereinfachte
Berechnungen 187
ANY 196
ABS 132
ACOS 132
ADO.NET 471
AdventureWorks 66; Tabellenbereiche
71
Aggregate 396; Akkumulation 407;
Fenster-Aggregate 410; Gleitender
Durchschnitt 408; Hitparaden 412
APPLY-Operator 501
ASCII 140
ASIN 132
Assemblies: Definition 66
ATAN 132
ATN2 132
Äußere Verknüpfung 164
Authentifizierungsmodus 25
AVG 114, 403
Aggregatfunktionen 403
Bedingungen 87
Akkumulation 407
Berechnete Bedingungen 100
Aktualisieren: Grafisch 48
Berechnete Spalten 97
Aliasnamen: Spalten 84; Tabellen 85
519
Index
Berechtigungen:
Erteilen 514
Entziehen
515;
Bereiche 426
Berichts- und Gruppenuntersummen
214
Besitzkette 512
Boolesche Operatoren 91
CALLED ON NULL INPUT 500
CASE 428, 434; Mit Selektor 198;
Ohne Selektor 201
CEILING 132
CHAR 140
CHARINDEX 140
CHECK-Bedingung 35
CHECKSUM 403
CHECKSUM_AGG 403
Clustered Index 37
Common Table Expression 380
COMPUTE [BY] 214
CONSTRAINT 249
COS 132
COT 132
COUNT 403
520
COUNT und COUNT_BIG 115
COUNT_BIG 403
CREATE PROCEDURE 455
CREATE FUNCTION 482, 489
CREATE TABLE 51, 240, 242
CROSS APPLY 502
CTE
380; Aggregate
398;
Datenmanipulation 393; Dynamisch
384; Mehrfach 386; Pivot 435;
Prinzip
381; Rekursiv
391;
Spaltennamen 383
Cursor: Rückgabe aus Prozedur 475
Cursor-Rückgabe 475
DataTable 472
DATEADD 126
DATEDIFF 126
DATENAME 126
Datenbankdiagramme 233
Datenschnittstelle mit Prozedur 463
DATEPART 126
Datums- und Zeitfunktionen 125
DAY 127
DEFAULT 467
Index
DEGREES 132
FLOOR 133
DELETE 49; CTE 395
FOR REPLICATION 457
DENSE_RANK 403
FOREIGN KEY-Klausel 246
Dienstkonto 24
Fremdschlüssel anlegen 248
DIFFERENCE 140
Funktionen
478; Definition
54;
Definition
63;
Einfache
Tabellenwertfunktionen
486;
Erweiterte Tabellenwertfunktionen
489; NULL-Werte ausgeben 499;
Schemabindung
495; Sicherheit
510; Skalarfunktionen
481;
Tabellenwertfunktionen
486;
Typologie 480
DISTINCT 109
Dokumentation 57
DROP 252
DROP FUNCTION 481
Duplikate ein-/ausblenden 109
Einfache Unterabfragen 175
ENCRYPTION 474
ENCRYPTION 457
ENCRYPTION 494
Ergebnismenge aus Prozedur 460
EXCEPT 106, 168
EXEC 465
EXECUTE AS 457, 494, 512
EXP 133
Express-Edition 23
Fenster-Aggregate 410
Fensterrangfunktion 402
GETDATE 127
GETUTCDATE 127
Gleitender Durchschnitt 408
GRANT 514
GROUP BY 118
GROUPING 212, 403
Gruppenbedingungen 120
Gruppensuchbedingung: Definition 82
Gruppieren 117
Gruppierung: Definition 82
HAVING 121
521
Index
Hierarchien 389
Hitparaden 412
IDENTITY 242
Index 36
INNER JOIN 162
Skript generieren
anlegen 222
236; Tabellen
Management-Studio: Einschränkungen
34;
Schlüssel
32;
Spalteneigenschaften 33; Statistiken
37; Tabellen anzeigen 31; Trigger
35
Innere Verknüpfung 161
Manuelle Verknüpfungen 153
INSERT: CTE 394
MAX 114, 403
INTERSECT 107
Mengen-Operatoren 102
JOIN: INNER JOIN 162
MIN 114, 403
JOIN: OUTER JOIN 165
MONTH 127
Katalogsichten für Objekte 504
Korrelierte Spaltenunterabfragen 191
MS SQL Server 2005: Download 21;
Standardinstallation 23
Korrelierte Unterabfragen 189
Namensnotation 465
Kreuzverknüpfung 159
NCHAR 141
LEFT 141
Neukompilierung von Prozeduren 474
LEN 141
Non-Clustered Index 37
LOG 133
NTILE 403, 426, 427
Löschen: Grafisch 49
OBJECT_DEFINITION() 510
LOWER 141
OBJECT_ID 244
LTRIM 141
OBJECT_ID() 508
Management Studio 29; Beziehungen
anlegen 230; Datenbankdiagramme
233; Prozeduren öffnen 458; SQL-
OBJECTPROPERTY() 509
522
OBJECTPROPERTYEX() 509
Index
Objekt-Explorer 30
Positionsnotation 465
Operatoren: Mathematische Operatoren
97
POWER 133
Operatoren
88, 97; AND
92;
Boolesche Operatoren 91; IN 90;
IS [NOT] NULL 90; OR 92
Operatoren: Mengen-Operatoren 102
Operatoren: ALL 193
Operatoren: [NOT] EXISTS 195
Operatoren: SOME 196
Operatoren: ANY 196
OR 92
Programmierbarkeit 59, 451
Projektmappe 42
Projektverwaltung 41
Prozeduren
451; Cursor-Rückgabe
475; Datenschnittstelle
463;
Definition
59; Ergebnismenge
zurückliefern
460;
Neukompilierung
474;
Rückgabewerte
469; Sicherheit
510; Standardwerte 467; Syntax
455; Typologie
453, 460;
Verschlüsselung 474
ORDER BY 112
Prozedurparameter 465
OUTER APPLY 503
Qualifizierte Spaltennamen 86, 156
OUTER JOIN 164
Quantile 426
OUTPUT 467, 470, 475
QUOTENAME 141
OUTPUT 457
RADIANS 133
OVER 396, 427; Aggregate 402
RAND 133
PARTITION BY 403, 416
Rangfolgefunktionen 403, 421
PATINDEX 141
Rangfolgen 204, 418
PI 133
RANK 403
Pivot: CTE 437; Dynamisch
Klassisch 431
444;
Rekursive CTE 391
523
Index
Relation 53
Sortierreihenfolge 25
REPLACE 141
Sortierung 112; Definition 83
REPLICATE 142
SOUNDEX 142
RETURNS NULL ON NULL INPUT
500
sp_helptext 475
REVERSE 142
REVOKE 515
RIGHT 142
ROUND 133
ROW_NUMBER 403
ROW_NUMBER() 413
RTRIM 142
Rückgabewerte 469
SCHEMABINDING 496
Schemabindung 495
Selbstverknüpfung 170, 251
SELECT 43; Grundstruktur 81
Sicht: Definition 53
SIGN 133
SIN 133
Skalarfunktionen 481
SOME 196
524
SPACE 142
Spaltenliste: Definition 82
Spaltenunterabfrage 400
Spaltenunterabfragen 180
SqlCommand 472
SqlDataAdapter 471
SqlDataReader 471
SqlException 473
SqlInfoMessageEventArgs 473
SQRT 134
SQUARE 134
Standard-Aggregate 114
Standardwerte 467
STDEV 403
STDEVP 403
STR 142
STUFF 142
SUBSTRING 142
Index
Suchbedingung: Definition 82
TOP n 205
SUM 114, 403
Trigger: Definition 65
Synonym 54
Trigger 35
sys.objects 506
UNICODE 142
sys.procedures 507
UNION [ALL] 105
Systemfunktionen 147
UNPIVOT 435, 444
Tabelle: Definition
generieren 236
53; SQL-Skript
Tabelle erstellen: Grafisch 51
Tabellen: Ändern durch ALTER 252;
Erweiterte Tabellendefinition 246;
Fremdschlüssel anlegen
248;
Grafisch anlegen 223; Löschen mit
DROP 252; Mit SQL erstellen 238
Tabellenangabe: Definition 82
Tabellenausdrücke 379
Tabellen-Designer 34
Tabellenwerfunktionen 481
Tabellenwertfunktionen 486; Einfach
486; Erweitert 489
Unterabfrage: Einzelne Werte 176
Unterabfragen 175; Einfach 175;
Korrelierte
Spaltenunterabfragen
191; Korrelierte Unterabfragen 189;
Spaltenunterabfrage
180;
Wertelisten 177
Untersummen 207
UPDATE 48; CTE 394
UPPER 143
VAR 403
VARP 403
VARYING 475
VARYING 457
table-Variable 490
Verknüpfung: Äußere 164; Einfach
154; Innere 161; Mehrfach 158;
Selbstverknüpfung 170
TAN 134
Verschlüsselung 474
Testversion 21
Verzögerte Namensauflösung 459
TABLESAMPLE 122
525
Index
Verzweigungen 197
Würfel 210
Vorlagen-Explorer 55
YEAR 127
Wertedominante Tabelle 165
Zeichenketten verknüpfen 99
WITH 380
Zeichenkettenfunktionen 140
WITH CUBE 214
Zeilennummern erstellen 412
WITH RECOMPILE 467
Zufällige Datenauswahl 122
WITH ROLLUP 214
526
527
528
529
530
531
532
Herunterladen