Heinz-Gerd Raymans SQL An imprint of Pearson Education München • Boston • San Francisco • Harlow, England Don Mills, Ontario • Sydney • Mexico City Madrid • Amsterdam 4 Dies und Das In diesem vorletzten Kapitel habe ich für Sie noch eine Menge zusätzlicher Informationen zusammengestellt, bei denen es sich teilweise um Ergänzungen zu dem im zweiten Kapitel behandelten Stoff handelt. Exkurs zur Datenbankstruktur 4.1.1 Struktur einer MySQL-Datenbank Wie Sie schon wissen, werden MySQL-Datenbanken durch entsprechende Unterverzeichniseinträge im File-System gebildet, wobei die einzelnen Tabellen durch entsprechende Dateien in diesen Unterverzeichnissen repräsentiert werden. Aus dieser sehr engen Verzahnung zwischen dem vom Betriebssystem kontrollierten Dateisystem auf der einen und dem Datenbanksystem auf der anderen Seite resultiert die Tatsache, dass Sie für die in MySQL gespeicherten Objekte manchmal auf die exakte Schreibweise (Groß-/Kleinschreibung) achten müssen. Aus Sicht des Betriebssystems stellt das MySQL-DBMS lediglich eine Liste von Unterverzeichnissen dar, die alle direkt unter einem bestimmten Wurzelverzeichnis angelegt werden. Dabei ist ein besonderes Unterverzeichnis mit dem Namen „mysql“ immer vorhanden. Hierbei handelt es sich um die MySQL-Systemdatenbank, in der sich eine Reihe von vordefinierten Tabellen befinden, mit denen die zugelassenen Benutzer und deren Zugriffsberechtigungen gespeichert werden. Dieses Wurzelverzeichnis kann im Rahmen der MySQL-Konfiguration vorgegeben werden; standardmäßig zeigt es auf das \data-Unterverzeichnis der MySQL-Installation. Aber selbst mit einem SQL-Editor kann man feststellen, wo sich dieses Wurzelverzeichnis befindet. Hierzu muss man wissen, das MySQL viele Betriebsparameter zur Laufzeit in so genannten Systemvariablen vorhält, die mit Hilfe einer speziellen Variante des schon bekannten show-Kommandos abgefragt werden können. show variables [like <Einschränkung] Dies und Das 331 4 Nitty Gritty • Take that! 4.1 Wenn Sie diesen Befehl einmal ohne weiteren Parameter verwenden, dann erhalten Sie am Bildschirm eine Übersicht über sämtliche bereitgestellte Systemvariablen. Variable_name Value ================================================== ansi_mode OFF back_log 50 basedir c:\mysql\ bdb_cache_size 8388600 bdb_log_buffer_size 32768 bdb_home c:\mysql\data\ Mit Hilfe der like-Klausel können Sie hingegen jede einzelne Variable gezielt abfragen, d.h. auch den aktuellen Wert der datadir-Variablen, die das verwendete Wurzelverzeichnis enthält, könnten Sie auf diese Weise ermitteln. Nitty Gritty • Take that! 4 show variables like 'datadir' Variable_name Value ================================================== datadir c:\mysql\data\ Eine ausführliche Beschreibung sämtlicher verfügbarer Systemvariablen finden Sie in der Online-Hilfe Ihres MySQL-Systems. Suchen Sie in dem HTML-Dokument beispielsweise nach der Zeichenfolge „show variables“ und wiederholen Sie den Suchvorgang anschließend ein paar Mal, um das entsprechende Kapitel zu finden. Jede neue Datenbank führt also zu einem weiteren Unterverzeichnis, und jede angelegte Tabelle führt dort zu neuen Dateien. Dabei entspricht der Dateiname zunächst einmal dem bei der create table-Anweisung vergebenen Namen, wobei die genaue Anzahl der Dateien und die vergebene Dateinamenserweiterung von der bei der Anlage verwendeten type-Klausel abhängt. Das MySQL-System besitzt zwar eine einheitliche Kommandoebene, bedient sich aber unterschiedlichster Zugriffsmethoden, die teilweise sogar von unterschiedlichen Herstellern erstellt bzw. gewartet werden. 332 Exkurs zur Datenbankstruktur Standardmäßig werden alle Tabellen vom Typ „MYISAM“ angelegt, und hierbei entsteht zunächst einmal eine FRM-Datei, in der die Tabellenstruktur abgelegt wird. Daneben existieren eine Datendatei (*.MYD) und eine Datei (*.MYI), in der alle Indexeinträge gespeichert werden. Dagegen werden bei den Barkley_DB-Tabellen Daten und Indexeinträge in einer gemeinsamen Datei (*.DB) gespeichert; außerdem entsteht wieder eine FRM-Datei, in der sich wieder die Tabellenstruktur befindet. Die gesamte Struktur eines MySQL-Systems ist einfach und dennoch effizient. Der Vor- und manchmal auch Nachteil eines derartig durchschaubaren Systems ist allerdings, dass man häufig geneigt und in der Lage ist, mit einfachen Betriebssystembefehlen in die Datenbankstruktur einzugreifen; ich kenne sonst keine Datenbank (lassen wir Access einmal außen vor), bei der man mit dem Windows-Explorer oder einem entsprechenden FTP- oder Telnet-Programm Datenbanken oder Tabellen so einfach umbenennen oder kopieren kann. Und noch eine Erkenntnis ist zwar banal, aber trotzdem unterschiedlich zu vielen anderen Datenbanksystemen. Der Erfolg einer Einfügeoperation hängt bei MySQL letztendlich von dem noch verfügbaren Platz auf dem Datenträger bzw. dem verwendeten Datei-System ab, d.h., das Datenbanksystem konkurriert permanent mit allen anderen Prozessen, die ebenfalls Platz auf dem gleichen Datenträger bzw. Datei-System beanspruchen bzw. verbrauchen. Falls Ihnen der Begriff des „Datei-Systems“ nichts sagt, so möchte ich im Folgenden einen kurzen Erklärungsversuch starten. Üblicherweise bieten die heutzutage gängigen Betriebssysteme die Möglichkeit, einen Datenträger (Festplatte) in beliebige logische Bereiche aufzuteilen. Auch unter Windows/DOS kann man einen weiteren Laufwerksbuchstaben also nicht unbedingt mit einer zweiten physiDies und Das 333 4 Nitty Gritty • Take that! Die Größe einer MySQL-Datenbank ist übrigens zunächst einmal nur durch den bereitgestellten Platz des Datenträgers bzw. des DateiSystems begrenzt. Mit jedem neuen Datensatz einer Tabelle wird die zugehörige Datendatei größer. Folglich wächst auch der von dem zur Datenbank gehörenden Unterverzeichnis beanspruchte Platz, d.h., die Datenbankgröße hängt direkt von dem Wachstumsverhalten ihrer zugehörigen Tabellen ab. kalischen Festplatte gleichsetzen. Unter Windows/DOS nennt man diese logischen Bereiche üblicherweise Partition (vgl. „Festplatte partitionieren“), wohingegen man in der UNIX-Welt von der Anlage bzw. Definition eines Datei-Systems spricht. 4.1.2 Struktur einer SQL-Server-Datenbank Auch der SQL-Server benutzt zum Speichern der verschiedenen Datenbankobjekte natürlich das vom Betriebssystem kontrollierte Dateisystem, jedoch ist das Zusammenspiel abstrakter geregelt. Eine SQL-Server-Installation muss zunächst einmal nur den Weg zur Primärdatei der so genannten Master-Datenbank finden. Von da an ist der Zugriff auf wichtige Systemtabellen gegeben und anschließend kann sich die Datenbank über sämtliche verfügbare Festplatten erstrecken. Nitty Gritty • Take that! 4 Zunächst einmal besitzt eine SQL-Server-Installation die Datenbanken „master“ und „tempdb“, wobei im Rahmen einer Standardinstallation meistens weitere Datenbanken angelegt werden. Dabei ist die Master-Datenbank das Herzstück des SQL-Servers, denn in ihr befinden sich zentrale Systemtabellen, ohne die das gesamte DBMS nicht lauffähig ist. Das beginnt beispielsweise mit der Systemtabelle „sysdatabases“, in der jede verfügbare Datenbank mit ihrer Primärdatei registriert wird. Schon jetzt sollte klar sein, dass man beim SQL-Server den dort verwalteten Datenbanken nicht mit Betriebssystemkommandos zu Leibe rücken sollte. Wenn Sie hier Verzeichnis- oder Dateinamen ändern, dann passen anschließend vielleicht die in den Systemtabellen gespeicherten Einträge nicht mehr und anschließend kann die zugehörige Datenbank oder sogar das Gesamtsystem nicht mehr hochgefahren werden. Eine SQL-Server-Datenbank besteht mindestens aus zwei Dateien. Eine dieser beiden Dateien dient zur Aufnahme der datenbankindividuellen Systemtabellen und zur Speicherung der Benutzertabellen nebst zugehörigen Daten; in der anderen Tabelle wird das Transaktionsprotokoll der Datenbank gespeichert. 334 Exkurs zur Datenbankstruktur 64/6HUYHU PDVWHU WHPSGE SULPDU\ SULPDU\ P\GE SULPDU\ JUS ORJ ORJ 'DWHQEDQNHQ 'DWHLJUXSSHQ ORJ )HVWSODWWHQ Für den Datenbereich ist die Datenbank allerdings nicht direkt mit dieser Datendatei gekoppelt, sondern zwischen der Datenbank auf der einen und der Datei auf der anderen Seite liegt noch einmal der Begriff der so genannten Dateigruppe. Eine Datenbank basiert nämlich auf einer oder mehreren Dateigruppen und jede Dateigruppe kann aus einer oder mehreren Dateien bestehen, die überall auf den verfügbaren Festplatten verstreut sein können (vgl. Abb. 4.1). Jede Datenbank besitzt also mindestens einen Protokollbereich und basiert auf mindestens einer Dateigruppe, der mindestens eine Datei zugeordnet ist, in der die Datenbankobjekte mitsamt den darin enthaltenen Daten gespeichert werden. Schon während der Anlage einer Datenbank wird sowohl dem Protokoll- als auch dem Datenbereich eine bestimmte Größe zugeordnet, wobei diese Dateien in der Tat sofort diese vorgegebene Größe annehmen, d.h., wenn Sie hierbei großzügigerweise ein paar hundert Dies und Das 335 4 Nitty Gritty • Take that! Bild 4.1: Schematischer Aufbau der logischen Datenbank und Dateigruppen auf der einen und der physischen Dateien und Festplatten auf der anderen Seite Mbyte vergeben, dann muss dieser Platz zum einen auf dem Datenträger verfügbar sein und er steht zum anderen den übrigen Prozessen ab sofort nicht mehr zur Verfügung. Auf diese Weise ist es also möglich, dass selbst leere Datenbanken mehrere hundert Megabytes groß sein können. Das hat natürlich den Vorteil, dass die Datenbank anschließend nicht permanent um Platz auf dem Datenträger ringen muss; auf der anderen Seite kann es hierbei passieren, dass Sie eine „Datenbank voll“-Meldung erhalten, obwohl auf der Festplatte noch reichlich Platz vorhanden ist. Sie werden es vielleicht kaum glauben, aber in der Praxis ist das nicht immer unproblematisch. Datenbanken werden häufig mit Hilfe vorgefertigter Skripts erstellt und wenn sich Datenbanken im Rahmen eines Projekts anschließend wie von selbst vervielfältigen (noch eine Demodatenbank für Herrn X), dann führt das zu einem entsprechenden Platzverbrauch auf dem Server, obwohl alle einzelnen Datenbanken relativ leer sind. Nitty Gritty • Take that! 4 Zumindest für Test-, Spiel- oder auch Entwicklungsdatenbanken kann man dem aber entgegenwirken, indem man die Datenbankdefinition von vornherein sehr klein wählt und den zugehörigen Dateibereich so konfiguriert, dass dieser im Bedarfsfall automatisch wächst. Eine solche Wachstumserlaubnis kann man dabei uneingeschränkt erteilen, d.h., die Datenbank kann anschließend so lange wachsen, bis die Festplatte voll ist, oder man legt eine Obergrenze fest, bis zu der die zugehörige Datei wachsen darf. Vorgaben bei der Tabellenanlage Jede neue Tabelle wird bei der Anlage automatisch in einer zur Datenbank gehörenden Datendatei angelegt. Konkret wird die Tabelle allerdings zunächst der zur Datenbank gehörenden primären Dateigruppe zugeordnet, und erst danach legt das DBMS fest, in welcher der zugehörigen Datendateien das neue Objekt angelegt wird. Prinzipiell haben Sie die Möglichkeit, Einfluss auf diese Entscheidung zu nehmen, indem Sie die gewünschte Dateigruppe bei der Anlage der Tabelle vorgeben. create table … ( <Spalten- und ConstraintDefinitionen>) [on <Dateigruppe> ] [textimage_on <Dateigruppe>] 336 Exkurs zur Datenbankstruktur Hierzu müssen Sie nach der Definition aller zur Tabelle gehörenden Spalten und Constraint-Definitionen die gewünschte Dateigruppe, in deren Dateien die neue Tabelle angelegt werden soll, mit Hilfe der onKlausel vorgeben. Zusätzlich existiert auch noch die Klausel „textimage_on“, mit deren Hilfe Sie die Speicherung der text- und image-Spalten in eine andere Dateigruppe und damit in andere Dateien auslagern können. Sicherlich fragen Sie sich gerade, worum es im letzten Absatz überhaupt ging, so dass ich jetzt damit beginnen werde, Ihnen ein paar Hintergrundinformationen über die Struktur einer Oracle-Datenbank zu vermitteln. Wie Sie schon wissen, unterscheidet sich Oracle vom SQL-Server beispielsweise dadurch, dass für jede Oracle-Datenbank eine eigene Instanz der Datenbanksoftware läuft. Dementsprechend gibt es natürlich keine Notwendigkeit für zentrale System- oder Temporär-Datenbanken, denn jede Oracle-Datenbank ist für sich allein eigenständig. Zu jeder Oracle-Datenbank gehören in der Regel mehrere (gespiegelte) Kontrolldateien, die das Herzstück der Datenbank darstellen und mit den zentralen Systemtabellen der Systemdatenbank des SQL-Servers vergleichbar sind. In dieser Kontrolldatei ist beispielsweise definiert, wo ein bestimmter Systembereich der Oracle-Datenbank (physikalisch) gespeichert ist. Ist dieser Systembereich erst Dies und Das 337 4 Nitty Gritty • Take that! 4.1.3 Struktur einer Oracle-Datenbank In Kapitel 2.2.1 haben Sie gesehen, dass es auch in einer Oracle-Datenbank prinzipiell möglich ist, eine Tabelle mit Hilfe einer einfachen create table-Anweisung anzulegen. Allerdings muss ich gestehen, dass ich solch einfache Anweisungen in meinem bisherigen OracleDasein noch niemals gesehen habe. Das mag zum Teil damit zusammenhängen, dass diese einfache Form des Befehls aufgrund ungeschickter Einstellungen im Profil des Benutzerkontos gar nicht mehr funktioniert; zum anderen mag es daran liegen, dass solche Datendefinitionsanweisungen immer weniger getippt, sondern immer häufiger generiert werden, so dass auch für die zusätzlichen Klauseln des create table-Kommandos zumindest irgendwelche Standards zur Anwendung kommen. einmal gefunden, dann ist es aufgrund der dort gespeicherten Informationen möglich, die weiteren zur Datenbank gehörenden Dateien zu öffnen. Letztendlich speichert auch Oracle die Datenbankinformationen in ganz gewöhnlichen Dateien, die Sie genau wie beim SQL-Server beliebig auf den verfügbaren Festplatten bzw. File-Systemen verteilen können. Eine solche Datei bzw. mehrere solcher Dateien bilden zunächst einmal das logische Konstrukt des so genannten Tablespaces, der in gewisser Weise mit einer SQL-Server-Dateigruppe vergleichbar ist. Damit ist die Struktur einer Oracle- und SQL-Server-Datenbank von einem gewissen Standpunkt aus betrachtet gar nicht so unterschiedlich (vgl. Abb. 4.2). Nitty Gritty • Take that! 4 Unterschiedlich ist eigentlich nur, dass vom diesem Bereichs- bzw. Tablespace-Konzept in Oracle-Datenbanken meistens wesentlich mehr Gebrauch gemacht wird. Schon mit der Anlage einer einfachen OracleDatenbank erhalten Sie sechs verschiedene Tablespace-Definitionen, die zur Aufnahme verschiedener System- und Benutzerobjekte vorgesehen sind. Mit der Installation zusätzlicher Softwareprodukte erhält man üblicherweise ebenfalls weitere Tablespace-Definitionen; so verteilt eine PeopleSoft-Installation die zugehörigen Tabellen beispielsweise auf ca. 60 hierfür angelegte Tablespaces. Logisches Modell Datenbank Tablespaces Tablespace 1 Physische Dateien Datei 1 Tablespace 2 Datei 2 Festplatten Bild 4.2: Schematischer Aufbau einer Oracle-Datenbank 338 Exkurs zur Datenbankstruktur Datei 3 Wie Sie der Abbildung 4.2 entnehmen können, basiert ein solcher Tablespace genau wie eine Dateigruppe auf einer oder mehreren Dateien. Ähnlich wie beim SQL-Server und entgegen der MySQL-Verfahrensweise wird die Größe einer solchen Datei schon mit der Definition des Tablespace festgelegt und bei der Anlage auf dem Datenträger beansprucht. Das impliziert somit wieder alle schon im vorhergehenden Kapitel beschriebenen Vor- und Nachteile, denn auch in Oracle können leere Datenbanken eine beachtliche Größe besitzen; dafür muss das DBMS nicht permanent mit anderen Prozessen um Speicherplatz ringen. Es gibt sogar noch weitere Gemeinsamkeiten mit dem zuvor beschriebenen SQL-Server-DBMS, denn auch die in Oracle definierten Tablespaces bzw. die dazugehörigen Dateien können dynamisch wachsen. create table <Name der Tabelle> ( <Definition der Spalten...> ) [<Tablespace-Klausel>] [<Storage-Klausel>]; Wenn Sie bei der Tabellenanlage diese beiden Klauseln weglassen, dann werden hierfür Standardwerte (vgl. Kap. 5.3.2) verwendet, was aber nicht unbedingt immer zu einem funktionierenden Datenbankobjekt führt. Beim Tablespace verwendet Oracle beispielsweise entweder den dem Benutzer zugeordneten Standardwert oder den Tablespace „system“ und häufig ist es nicht erlaubt, in diesem zuletzt genannten Bereich Daten zu speichern, d.h., eine anschließende Einfügeanweisung würde mit einer entsprechenden Fehlermeldung abbrechen. Wenn Sie bei der Tabellenanlage die zusätzliche Storage-Klausel weglassen, dann verwendet das DBMS hierfür die im Tablespace verankerten Standardwerte. Dies und Das 339 4 Nitty Gritty • Take that! Bei einem solchen intensiven Gebrauch des Dateigruppenkonzepts ist es schon mehr als nahe liegend, dass sich dieses Prinzip auch bei der Anlage der einzelnen Datenbankobjekte niederschlägt, weshalb auch die create table-Anweisung üblicherweise Zusatzoptionen enthält, mit denen festgelegt wird, wo und wie sich eine Tabelle in der Datenbank ausbreiten soll. Speicherbelegung Im Folgenden möchte ich einmal etwas genauer auf die Speicherbelegung des zu einer Tabelle gehörenden Segments eingehen. Wie schon gesagt, können Sie mit Hilfe der Storage-Klausel festlegen, wie und wie viele Segmentbereiche (vgl. Abb. 4.3) zum Speichern der Tabellendaten angelegt werden. Den letzten Teil des vorhergehenden Satzes könnte man auch allgemeiner formulieren, denn er gilt nicht nur für Tabellen, sondern überall da, wo Sie eine storage-Klausel angeben können. Festplatte Segment 1. Extend 2Kb 2Kb 2Kb 2Kb 2Kb 2Kb 2. Extend 2Kb 2Kb 2Kb 2Kb 2Kb 2Kb 4 Nitty Gritty • Take that! 2Kb 2Kb 2Kb 2Kb 2Kb 2Kb 7DEOHVSDFH 2Kb 2Kb 2Kb 2Kb 2Kb 2Kb 2Kb 2Kb 2Kb 2Kb 2Kb 2Kb Bild 4.3: Segmente, Extends und Datenblöcke „Segment“ steht hierbei als Überbegriff für alle diejenigen Objekte, die in der Datenbank Platz zum Speichern von System- oder Benutzerdaten benötigen. Jede angelegte Tabelle ist also genauso wie jede Indexdefinition ein solches Segment. Die einzelnen Ausprägungen dieser Klausel werden dabei hinter dem Wörtchen storage in Klammern spezifiziert. storage (initial ... next ... minextents ... maxextents ... pctincrease ...) Hinter den verschiedenen Schlüsselwörtern müssen Sie die jeweils benötigten Werte angeben. Ansonsten ist neben der Bedeutung der einzelnen Ausprägungen eigentlich nur noch wichtig, dass Sie deren Reihenfolge zum einen wahlfrei und zum anderen auch einzeln bzw. 340 Exkurs zur Datenbankstruktur in beliebiger Kombination verwenden können. Die genaue Bedeutung der einzelnen storage-Parameter können Sie der Tabelle 4.1 entnehmen. Bedeutung initial Legen Sie hier bei Bedarf die Größe des ersten Segmentbereichs in Kilo- (K) oder Megabyte (M) fest. next Mit diesem Parameter können Sie die Größe der weiteren Segmentbereiche (Extents) in Kilo- (K) oder Megabyte (M) festlegen. minextents Hier können Sie die Anzahl der Extents festlegen, die beim Anlegen der Tabelle bzw. des Objekts erzeugt werden. Standardmäßig legt Oracle immer ein Extent an, denn ohne einen einzigen Segmentbereich ist das Objekt gar nicht vorhanden. maxextents Geben Sie mit diesem Parameter die maximale Anzahl der zusätzlichen Segmentbereiche vor oder verwenden Sie statt einer Zahl das Wörtchen „unlimited“, damit die Tabelle bzw. das Objekt uneingeschränkt wachsen kann. pctincrease Die hier vorgegebene Zahl wird als Prozentwert interpretiert, um die jeder neue Extent vergrößert wird. Der Standardwert hierfür beträgt 50%. Sollen die neuen Segmente nicht automatisch vergrößert werden, dann müssen Sie pctincrease zusammen mit dem Wert 0 verwenden. Tabelle 4.1: Beschreibung der storage-Klausel Zwei weitere Klauseln, die Sie bei der Anlage einer Tabelle oder auch anderer datenspeichernder Objekte verwenden können, heißen pctfree bzw. pctused. Mit pctfree reservieren Sie in jedem Extent des Segments eine Art Platzreserve in Prozent, die dem Wachstum durch Änderungen von bestehenden Datensätzen vorbehalten ist. Standardmäßig verwendet Oracle für diesen Wert 10%, d.h., nach dem vollständigen Auffüllen des Extents sind immer noch 10% des Platzes frei, der anschließend für Änderungen der in dem Segmentbereich gespeicherten Daten verwendet wird. Dies und Das 341 4 Nitty Gritty • Take that! StorageKlausel Mit der anderen pctused-Klausel legen Sie eine Art Schwellenwert (in Prozent) fest, der vorgibt, wie weit die Belegung eines einstmals vollen Segmentbereichs wieder sinken muss, bevor wieder neue Datensätze hineingeschrieben werden. Der Standardwert für diesen Parameter liegt bei 40 Prozent. Die pctused-Klausel verhindert somit, dass der Status eines Extents permanent zwischen voll und leer wechselt, denn entsprechend dem Standardwert beginnt Oracle erst wieder damit, neue Datensätze in den Extent zu schreiben, wenn seine Belegung unter 40% sinkt. Danach wird er allerdings wieder so lange mit neuen Datensätzen gefüllt, bis der in pctfree festgelegte Wert erneut erreicht wird. Unterlegen wir das Ganze mal mit Hilfe eines kleinen Beispiels und legen wir hierzu die Gehaltstabelle noch einmal an, wobei wir diesmal die Tablespace- und Storage-Klausel verwenden wollen. Nitty Gritty • Take that! 4 create table gehalt ( persnr varchar2(10) not null, g_ab date default sysdate not null, la varchar2(3), betrag decimal(9,2), constraint gehalt_pk primary key(persnr, g_ab), constraint gehalt_001 check (betrag >= 0), constraint gehalt_002 foreign key (la) references lohnarten(la) ) tablespace hrlarge storage (initial 1M next 1M minextents 2 maxextents 10) pctused 50 pctfree 20; So wie die Gehaltstabelle jetzt angelegt wurde, belegt sie von vornherein 2Mbyte (2x 1048576 Byte) im Tablespace „hrlarge“. Jede weitere benötigte Erweiterung (Extent) wird ebenfalls als 1Mbyte-Block angelegt, wobei insgesamt zehn solcher Extents möglich sind. 342 Exkurs zur Datenbankstruktur Wichtige Systemtabellen Im bisherigen Verlauf dieses SQL-Buches haben Sie schon mehrfach verschiedene Systemtabellen bzw. Views kennen gelernt, mit deren Hilfe Sie Informationen über die Datenbank bzw. über die darin enthaltenen Objekte sammeln konnten. Auch für die Speicherbelegung existieren solche Systemobjekte. Da wäre zunächst einmal die View v$tablespace zu nennen, mit deren Hilfe Sie die Namen der vordefinierten Tablespaces ermitteln können. select name from v$tablespace; NAME -----------------------------SYSTEM RBS USERS TEMP TOOLS INDX AMAPP BDAPP BNAPP BNLARGE … Mit Hilfe einer Abfrage auf die View dba_segments können Sie die definierte Speicherbelegung einer jeden Tabelle bzw. eines jeden Segments abfragen. Bei einer solchen Abfrage können Sie je nach Bedarf den Namen des Eigentümers, den Tablespace oder den Namen des Segments, das ist beispielsweise der Name der Tabelle oder des Index, oder den Segmenttyp (z.B. „TABLE“ oder „INDEX“) als einschränkende Kriterien verwenden. select bytes as B, extents as E, initial_extent as IE, next_extent as NE, min_extents as MIN, max_extents as MAX from dba_segments where segment_name = 'GEHALT'; B E IE NE MIN MAX --------- ---- -------- ---------- ---- ------2129920 2 1048576 1048576 2 10 Dies und Das 343 Nitty Gritty • Take that! 4 4.2 Verwenden von Skripten Werden mehrere SQL-Befehle gemeinsam gespeichert, so dass sie vom DBMS als eine Einheit verarbeitet werden, dann spricht man bei diesem Befehlspaket üblicherweise von einem Skript. Dabei muss man zunächst einmal beachten, dass nicht jedes DBMS die Verwendung von Skripten unterstützt. Access Beginnen bei unserer Betrachtung beispielsweise beim AccessDBMS. Wenn Sie hier im Abfragefenster mehrere Befehle hintereinander erfassen und anschließend absenden, dann erhalten Sie eine Fehlermeldung, auch wenn alle im Skript enthaltenen Anweisungen fehlerfrei sind. Nitty Gritty • Take that! 4 Bild 4.4: Ausführen eines Skripts im Access-Abfragefenster MySQL MySQL ist da nicht besser, auch wenn der mitgelieferte SQL-Monitor oder das von mir erstellte Worksheet Ihnen etwas anderes vorgaukelt. „Gaukeln“ ist hier das richtige Wort, denn die scheinbare Fähigkeit, ganze Stapel von SQL-Anweisungen verarbeiten zu können, ist eine Illusion, da die im SQL-Fenster erfasste Befehlskette mit Hilfe des Programms in einzelne Anweisungen zerlegt wird. Dabei wird jede im Skript enthaltene Anweisung einzeln an das DBMS geschickt, 344 Verwenden von Skripten wonach das zugehörige Ergebnis in Empfang genommen und im Ausgabefenster angezeigt wird. Aber auch ohne diese Skriptfähigkeit lassen sich in MySQL statementübergreifende Aktivitäten realisieren, sofern die verschiedenen Statements mit derselben Verbindung zum DBMS bzw. im Rahmen der gleichen Sitzung abgesendet werden. Bei der Beschreibung der last_insert_id-Funktion haben Sie hierfür auch schon ein Beispiel kennen gelernt, denn zunächst wird hierbei mit Hilfe einer Einfügeabfrage ein neuer Datensatz in die Tabelle geschrieben und anschließend wird der hierbei automatisch vergebene Autowert mittels Auswahlabfrage und last_insert_id-Funktion abgefragt. insert into ..... select last_insert_id() Die beginnen in MySQL grundsätzlich mit einem Klammeraffen „@“, dem der eigentliche Variablenname folgt, den Sie mit Hilfe beliebiger alphanumerischer Zeichen bilden können. Die Wertzuweisung zu einer solchen Variablen kann unter anderem mit Hilfe des set-Kommandos erfolgen. set @text = 'Guten Morgen!'; set @my_datum = '2001-01-01'; Anschließend können Sie diese Variable beispielsweise im Rahmen der bisher kennen gelernten Abfragen verwenden und im einfachsten Fall wird der aktuelle Inhalt mit Hilfe einer select-Anweisung abgefragt. Dies und Das 345 4 Nitty Gritty • Take that! Eine solche abfrageübergreifende Kommunikation mit dem MySQLDBMS kann allerdings noch weitergehender genutzt werden. MySQL bietet Ihnen nämlich die Möglichkeit, eigene Benutzervariablen zu definieren, mit deren Hilfe Sie beispielsweise Zwischenergebnisse von einer Abfrage in die nächste übertragen können. Sofern Ihr SQLProgramm (z.B. MySQL-Worksheet oder Ihr konkret eingesetztes Anwendungsprogramm) also das Ablaufen von Skripten simuliert, so kann man auf diese Weise doch noch das ein oder andere Abfrageszenario quasi in einem Schritt ausführen; doch beginnen wir ganz von vorne, nämlich bei der Definition solcher Variablen. select @text, @my_datum; @text @my_datum ================================================== Guten Morgen! 2001-01-01 Allerdings können solche Variablen auch an beliebigen anderen Stellen in der Abfrage auftauchen, so dass sich mit ihrer Hilfe beispielsweise auch flexible where-Bedingungen realisieren lassen. Ein Anwendungsbeispiel wäre vielleicht die Parametrierung immer wiederkehrender Änderungsabfragen, bei denen Sie alle variablen Kriterien zunächst durch entsprechende Variablen ersetzen. Nehmen wir einmal an, die Gehaltsaktualisierung eines Mitarbeiters soll nach dem folgenden Schema ablaufen: Zunächst soll der eventuell schon vorhandene Datensatz für den Mitarbeiter gelöscht werden, so dass anschließend in jedem Fall eine Einfügeanweisung verwendet werden kann. Ein solches Abfrageskript könnte etwa folgendermaßen aussehen. Nitty Gritty • Take that! 4 delete from gehalt where persnr = @persnr and g_ab = @g_ab; insert into gehalt(persnr, g_ab, la, betrag) values (@persnr, @g_ab, '100', @betrag); Jetzt haben wir alle variablen Bestandteile dieser Abfrage durch entsprechende benutzerdefinierte Variablen ersetzt, d.h., bevor wir die Abfragen jetzt an das DBMS senden können, müssen zunächst die Variablen mit geeigneten Werten vorbelegt werden. set @persnr = '4711'; set @g_ab = '2001-01-01'; set @betrag = 1566.22; delete from gehalt where persnr = @persnr and g_ab = @g_ab; insert into gehalt(persnr, g_ab, la, betrag) values (@persnr, @g_ab, '100', @betrag); Viel interessanter sind solche Variablen allerdings in speziellen Fällen der Anwendungsentwicklung, denn mit ihrer Hilfe kann in man- 346 Verwenden von Skripten chen Fällen ein „Workaround“ für die nicht unterstützten Unterabfragen geschaffen werden. Nehmen wir einmal an, Sie müssen die aktuellen Gehaltsdaten des Mitarbeiters „4711“ am Bildschirm anzeigen. In MySQL müssten Sie jetzt zunächst einmal das entsprechende Gültigkeitsdatum abfragen. select max(g_ab) from gehalt where persnr='4711'; Das von dieser Abfrage gelieferte Datum müssten Sie sich jetzt im Programm merken und innerhalb einer zweiten Abfrage verwenden. select * from gehalt where persnr='4711' and g_ab = '2001-01-01'; Mit Hilfe der Benutzervariablen lassen sich diese beiden Abfragen aber doch noch verschmelzen, denn der Wert einer Variablen kann auch mit Hilfe einer select-Abfrage gesetzt werden. select @mytvar := max(g_ab) from gehalt where persnr='4711'; select * from gehalt where persnr='4711' and g_ab = @mytvar; Beachten Sie hierbei allerdings, dass die Wertzuweisung diesmal nicht allein mit einem Gleichheitszeichen erfolgt, sondern dass Sie vor diesem Gleichheitszeichen auch noch einen Doppelpunkt kodieren müssen. Ein weiteres Anwendungsbeispiel für solche Skripts findet sich auch immer dann, wenn es darum geht, Datenbankobjekte anzulegen. Soll eine neue Anwendung installiert werden, so müssen hierfür vielleicht auch eine Reihe von Tabellen und Indizes in der Datenbank erzeugt werden, wobei sich alle diese DDL-Anweisungen in einem Skript befinden. Oder Sie spielen eine neue Version des Softwarepakets ein, wozu auch eine Aktualisierung der Datenbankstruktur notwendig ist. Wenn Sie DDL-Anweisungen für MySQL in ein Skript packen, dann sollten Sie wenigstens beim Löschen von Tabellen die in dem Zusam- Dies und Das 347 Nitty Gritty • Take that! 4 menhang verfügbare exists-Klausel verwenden, so dass während der Skriptausführung keine bzw. weniger Fehler ausgegeben werden, da das die anschließende Kontrolle umfangreicher SQL-Stapel vereinfacht. drop table if exists MyTable; SQL-Server, Oracle Der SQL-Server und Oracle unterstützen die Verwendung ganzer Abfragestapel, wobei solche Skriptdateien in der Praxis manchmal mehrere hundert bis tausend Zeilen Umfang haben. Der Unterschied zu MySQL ist hierbei der, dass das Abspielen des Skripts eben nicht mit Hilfe eines getürkten Anwendungsprogramms simuliert werden muss, sondern das gesamte SQL-Paket wird diesmal wirklich vollständig an das DBMS zur Ausführung übergeben. Nitty Gritty • Take that! 4 Bei den in diesem Pakt zusammengefassten Abfragen kann es aber sein, dass zwischen den einzelnen Abfragen Abhängigkeiten bestehen, die einer Ausführung aus Sicht der im DBMS vorgelagerten Statementprüfung entgegenstehen. So verlangt der SQL-Server beispielsweise, dass bestimmte DDL-Anweisungen (z.B. „create view“) immer am Anfang eines Abfragestapels stehen. Aus diesen Gründen haben Sie sowohl beim SQL-Server als auch in Oracle die Möglichkeit, das gesamte Skript noch einmal in logische Teilschritte zu unterteilen, so dass anschließend jeweils immer nur diese Teilschritte vom DBMS am Stück geprüft und verarbeitet werden. Beim SQL-Server wird ein solches Stapelende durch die Anweisung „go“ definiert, wohingegen Sie in Oracle das Slash-Zeichen (/) verwenden können. Im Folgenden finden Sie hierfür noch einmal ein Beispiel für ein solches SQL-Server-Skript, das aufgrund der verwendeten go-Kommandos in drei Schritten ausgeführt wird. select count(*) from gehalt; drop view xx; go create view xx (persnr) as select persnr from gehalt go select count(*) from xx; 348 Verwenden von Skripten Möchten Sie das Skript in Oracle ebenfalls in diesen drei Teilschritten ausführen, so müssen Sie einfach das go-Kommando durch ein Slash-Zeichen (/) ersetzen. Wie schon mehrfach erwähnt, beinhaltet der Sprachumfang des SQLServers nicht nur die bisher verwendeten SQL-Anweisungen zum Anlegen, Bearbeiten oder Abfragen von Tabellen der Datenbank. Im Laufe der Zeit haben sich um die eigentlichen SQL-Anweisungen eine Vielzahl von Befehlen gereiht, so dass mittlerweile eine richtige Programmiersprache entstanden ist. Wie gesagt, lassen sich mit Transact-SQL richtige Programme erstellen. In der Praxis taucht ein solches Programmwerk zunächst einmal bei der Realisierung von Triggern oder gespeicherten Prozeduren auf, denn in diesen Objekten werden meistens nicht nur einfache SQL-Abfragen ausgeführt, sondern daneben existieren oftmals komplexe Berechnungen oder die Ausführung der Abfragen ist von speziellen Bedingungen abhängig. Aber auch gewöhnliche Skripts bestehen oftmals nicht nur aus der Anhäufung verschiedener SQL-Abfragen, sondern auch hier wird beispielsweise mit Hilfe verschiedener Prüfungen zur Laufzeit entschieden, ob diese oder jene Abfragen auszuführen sind. Ein sehr einfaches Anwendungsbeispiel für eine solche bedingungsabhängige Abfrageausführung besteht darin, Skripten mit verschiedenen DDLAnweisungen frei von Laufzeitfehlern zu gestalten, d.h., drop-Anweisungen dürfen in dem Fall nur dann ausgeführt werden, wenn das zu löschende Objekt überhaupt in der Datenbank existiert. Dies und Das 349 4 Nitty Gritty • Take that! 4.2.1 Transact-SQL Beim SQL-Server werden diese Spracherweiterungen als „TransactSQL“ bezeichnet. Natürlich würde es im Rahmen dieses Buches zu weit führen, an dieser Stelle systematisch und umfassend in die Transact-SQL-Programmierung einzuführen, aber einen kurzen Einblick möchte ich Ihnen schon gewähren und schließlich bin ich auch noch das Beispiel mit der mehrfach zitierten Änderungsschleife schuldig. Variablen verwenden Das folgende Beispiel zeigt Ihnen hierfür eine mögliche Vorgehensweise, wobei ich es so konstruiert habe, dass möglichst viele Sprachkonstrukte zur Anwendung kommen. declare @t integer; select @t = (select count(name) from sysobjects where name = 'xxgehalt'); if @t <> 0 begin print 'Tabelle wird gelöscht'; drop table xxgehalt; end; create table xxgehalt (xx char(10)); Nitty Gritty • Take that! 4 Zunächst einmal habe ich eine benutzerdefinierte Variable angelegt, was mit Hilfe der declare-Anweisung funktioniert. Diese Variablen beginnen alle mit einem Klammeraffen (@), dem der Variablenname folgt, der aus Buchstaben, Ziffern und dem Unterstrich (_) bestehen kann. Hinter der Variablen folgt die Festlegung des Variablentyps, wobei Sie hierbei alle Typenbezeichner verwenden können, die Sie zusammen mit der create table-Anweisung kennen gelernt haben. declare declare declare declare @v1 @v2 @v3 @v4 integer; varchar(10); datetime; numeric(9,2); Anschließend finden Sie das Paradebeispiel dafür, wie Sie einer solchen Abfrage einen Wert zuweisen können, denn das erfolgt in Transact-SQL immer mit Hilfe einer speziellen select-Anweisung. Dem Schlüsselwort „select“ folgen hierbei der Variablenname, ein Gleichheitszeichen und danach kommt ihr neuer Wert in Form einer Konstanten, eines zur Variablen passenden Ausdrucks oder sogar einer Datenbankabfrage, die allerdings (genau) einen entsprechenden Wert zurückliefern muss. 350 Verwenden von Skripten select @t = 34.2; select @t1 = 33 + @t * 2; select @t2 = (select max(...) from ...) + @t1; Solche Abfragen entsprechen den schon bekannten exists- oder wertliefernden Unterabfragen, denn diese beiden Unterabfragetypen liefern jeweils genau einen Wert zurück. Kontrollstrukturen Da die in einem Skript oder Programm enthaltenen Befehle und Anweisungen nicht immer der Reihe nach ausgeführt werden sollen, bietet Ihnen Transact-SQL verschiedene Möglichkeiten, den Ablauf der einzelnen Anweisungen zu kontrollieren. Die einfachste Form einer solchen Kontrolle bietet der if-Befehl mit dessen Hilfe die Ausführung einer oder einer Reihe von Anweisungen von einer Bedingung abhängig wird. If <Bedingung> begin ... Transact-SQL-Anweisungen end [else begin ... Transact-SQL-Anweisungen end; Die Konstruktion einer solchen if-Bedingung folgt den Regeln, die Sie bei der Erstellung von where-Bedingungen kennen gelernt haben, d.h., Sie können auch hier die damals beschriebenen Vergleichs- und Verknüpfungsoperatoren verwenden. Ist die if-Bedingung erfüllt, so wird die direkt dahinter programmierte Anweisung ausgeführt; optional können Sie auch eine Anweisung programmieren, die nur dann ausgeführt werden soll, wenn die if-Bedingung nicht erfüllt ist, indem Sie die if-Konstruktion mit Hilfe des Schlüsselwortes „else“ erweitern. Blockstruktur Sicherlich ist die Situation, dass man im Rahmen einer if- bzw. elseAusführung genau eine einzelne Anweisung ausführen muss, eher selten gegeben, sondern meistens sollen stattdessen mehrere ver- Dies und Das 351 Nitty Gritty • Take that! 4 schiedene Anweisungen ausgeführt werden. Aus diesem Grund gibt es in Transact-SQL genau wie in vielen anderen Programmiersprachen auch des Konzept der so genannten Programmblöcke. Mit Hilfe dieser Blöcke können Sie mehrere Anweisungen zu einer logischen Einheit bündeln, so dass sie anschließend beispielsweise aus Sicht der if-Anweisung eine einzige (Block-) Anweisung darstellen. Solche Programmblöcke werden in Transact-SQL mit Hilfe der Schlüsselwörter „begin“ und „end“ gebildet. Manchmal ist deren Verwendung optional, beispielsweise wenn nach der if-Anweisung wirklich nur ein einzelner Befehl folgt; in solchen Fällen kann die freiwillige Bildung von Programmblöcken eventuell die Lesbarkeit des Skripts verbessern. Manchmal sind sie aber notwendig, um bestimmte Ablaufkonstruktionen zu realisieren. Ich persönlich habe mir angewöhnt, Programmblöcke auch dann (freiwillig) zu verwenden, wenn im Rahmen einer if-Anweisung nur eine Anweisung ausgeführt werden soll. Nitty Gritty • Take that! 4 In unserem kleinen Beispiel führen wir innerhalb des if-Programmblocks die drop-Anweisung aus, mit der die später im Skript erstellte Tabelle gelöscht wird. Insgesamt führen wir diesen Löschblock allerdings nur aus, wenn die Tabelle schon in der Datenbank existiert. Durch diesen kleinen Eingriff stellen wir also sicher, dass unser Skript im Normalfall ohne Fehlermeldung läuft, d.h., wenn wir jetzt eine Fehlermeldung erhalten, dann ist wirklich etwas schief gelaufen. Neben der if-Anweisung kennt Transact-SQL auch noch weitere Kontrollstrukturen wie „case“ oder die whileSchleife, mit deren Hilfe sich ein Programmblock bedingungsabhängig wiederholen lässt. Mehr Informationen zu „case“ und „while“ finden Sie im Referenzteil. Cursor verwenden Manchmal ist es notwendig, im Rahmen eines solchen Skripts neben der Ausführung von DDL- oder Änderungsabfragen oder neben dem Abfragen einzelner Werte auch komplexere Auswahlabfragen auszuführen, die anschließend mehr als einen Datensatz liefern; Sie merken, wir nähern uns jetzt so langsam der mehrfach erwähnten Änderungsschleife. 352 Verwenden von Skripten Zur Selektion von Daten in einem Transact-SQL-Skript oder -Programm müssen Sie einen so genannten Cursor erstellen, mit dessen Hilfe Sie eine beliebige Abfrage erstellen und anschließend die zugehörigen Daten in einer Schleife Satz für Satz abrufen können. declare gehalt_cursor cursor for select persnr, g_ab, la, betrag from gehalt; Die Definition eines solchen Cursors beginnt genau wie die Anlage einer Benutzervariablen mit dem Schlüsselwort „declare“, dem der von Ihnen vorzugebende Name des Cursors folgt. Anschließend müssen Sie die Schlüsselwörter „cursor for“ spezifizieren, denen die vom Cursor auszuführende Auswahlabfrage folgt. Was jetzt folgt, entspricht fast dem sequenziellen Lesen irgendwelcher Textdateien, wie Sie es vielleicht schon von irgendeiner Programmiersprache her kennen, d.h., zunächst wird der Cursor geöffnet, dann werden alle enthaltenen Datensätze abgerufen, und am Ende wird der Cursor wieder geschlossen. open gehalt_cursor; ... close gehalt_cursor; deallocate gehalt_cursor; Das Öffnen des Cursors erfolgt durch den Einsatz der open-Anweisung, die Sie zusammen mit dem bei der Cursordefinition vergebenen Namen verwenden müssen und führt intern zur Ausführung der zugehörigen Abfrage, wobei beim erstmaligen Öffnen zusätzlich noch gewisse Initialisierungsroutinen gestartet werden, die das DBMS zum Bearbeiten des Cursors benötigt. Wird der Cursor nicht mehr benötigt, dann können Sie ihn mit Hilfe der close-Anweisung schließen; und sofern Sie ihn innerhalb Ihres Skripts oder Programms überhaupt nicht mehr benötigen, so müssen Sie auch die vom DBMS bereitgestellten Strukturen entfernen, indem Sie die deallocate-Anweisung verwenden. „Können“ hört sich in diesem Zusammenhang zugegebenerweise etwas bittend an; verwenden Sie die beiden Anweisungen auf jeden Fall, da hierdurch der vom Cursor verbrauchte Speicher freigegeben wird. Dies und Das 353 Nitty Gritty • Take that! 4 Zwischen den open- und close-Anweisungen können Sie nun die vom Cursor bzw. der zugehörigen Abfrage bereitgestellten Datensätze abrufen, was konkret durch den Einsatz des fetch-Befehls geschieht. fetch <Fetchmodus> from <Cursorname> [into <Variablenliste>] Das Abrufen der zum Cursor gehörenden Datensätze kann auf unterschiedliche Art und Weise erfolgen, wobei uns hier nur der Modus „next“ interessiert, der zum sequenziellen Abrufen der Datensätze führt. Hinter dem Schlüsselwort „from“ müssen Sie wieder den bei der Definition festgelegten Cursornamen angeben und mit Hilfe der into-Klausel übertragen wir die von der Abfrage gelieferten Spalten in entsprechende Benutzervariablen. fetch next from gehalt_cursor into @persnr; Nitty Gritty • Take that! 4 Jeder Aufruf von „fetch” führt intern zum Setzen der Systemvariablen „@@fetch_status“, mit deren Hilfe Sie feststellen können, ob der Lesevorgang erfolgreich war oder nicht; enthält diese Variable den Wert 0, so war der vorhergehende fetch-Aufruf erfolgreich und alle anderen Werte deuten auf einen Fehler hin. Mit Hilfe dieser Systemvariablen und einer while-Schleife lassen sich nun recht einfach alle vom Cursor gelieferten Datensätze abrufen. declare @persnr varchar(10); declare gehalt_cursor cursor for select persnr from gehalt; open gehalt_cursor; fetch next from gehalt_cursor into @persnr; while @@fetch_status = 0 begin print @persnr; fetch next from gehalt_cursor into @persnr; end; close gehalt_cursor; deallocate gehalt_cursor; 354 Verwenden von Skripten Konkretes Beispiel Betrachten wir nach diesem einführenden Beispiel noch ein kleines Anwendungsbeispiel. In diesem soll die Gehaltstabelle noch einmal aktualisiert werden, wobei für jeden Mitarbeiter der Datensatz zum 01.01.2001 aktualisiert werden soll. Ist dieser überhaupt nicht vorhanden, so soll er im Rahmen der Aktualisierung angelegt werden. declare declare declare declare @persnr @la @betrag @g_ab varchar(10); varchar(3); numeric; datetime; declare gehalt_cursor cursor for select a.persnr, a.g_ab, a.la, a.betrag from gehalt a where a.g_ab = (select max(g_ab) from gehalt a1 where a1.persnr = a.persnr and a1.g_ab <= '01-01-2001'); 4 Nitty Gritty • Take that! open gehalt_cursor; fetch next from gehalt_cursor into @persnr, @g_ab, @la, @betrag; while @@fetch_status = 0 begin if @g_ab = '01-01-2001' begin print 'Geändert ' + @persnr; update gehalt set betrag = betrag * 1.05 where current of gehalt_cursor; end; else begin print 'Neuer Satz ' + @persnr + ' ' + Dies und Das 355 convert(varchar,@g_ab,104); insert into gehalt (persnr, g_ab, la, betrag) values (@persnr, '01-01-2001', @la, @betrag * 1.05); end; fetch next from gehalt_cursor into @persnr, @g_ab, @la, @betrag; end; close gehalt_cursor; deallocate gehalt_cursor; Betrachten wir nun noch einmal dieses kleine Beispiel. Zunächst wird mit Hilfe der im Cursor gespeicherten Abfrage für jeden Mitarbeiter der zum 01.01.2001 gültige Datensatz selektiert. Das passiert mit Hilfe einer Unterabfrage, die das größte Gültigkeitsdatum liefert, das kleiner oder gleich unserem Stichtag (01.01.2001) ist. Nitty Gritty • Take that! 4 select a.persnr, a.g_ab, a.la, a.betrag from gehalt a where a.g_ab = (select max(g_ab) from gehalt a1 where a1.persnr = a.persnr and a1.g_ab <= '01-01-2001'); Falls Sie Verständnisprobleme mit dieser Abfrage haben, so möchte ich Sie auf das Kapitel 2.5.3 verweisen, wo diese spezielle „wertlieferende“ Unterabfrage näher erläutert wird. Die von der Abfrage gelieferten Daten werden nun mit Hilfe der fetch-Anweisung und der while-Schleife Satz für Satz abgerufen, wobei die eigentliche Verarbeitung innerhalb der Schleife stattfindet. Dort wird zunächst einmal geprüft, ob das selektierte Gültigkeitsdatum unserem Stichtag entspricht, denn in diesem Fall existiert bereits ein Datensatz zum 01.01.2001 und wir können die Aktualisierung des Datensatzes mit Hilfe einer update-Anweisung durchführen. Betrachten Sie bei dieser update-Anweisung allerdings einmal die where-Bedingung. Im Rahmen der hier gezeigten Cursorverarbeitung wird programmintern ein Zeiger auf den aktuell vom Cursor gelieferten Datensatz mitgeführt. Diesen Zeiger können Sie im Rahmen 356 Verwenden von Skripten einer Aktualisierungsabfrage nutzen, indem Sie in der where-Bedingung einfach die Klausel „where current of“ zusammen mit dem Cursornamen verwenden. where current of gehalt_cursor Selbstverständlich könnten Sie auch eine reguläre where-Bedingung („where persnr = @persnr and g_ab = @g_ab“) konstruieren, allerdings ist das „current of“ zweifellos praktischer. Das gilt vor allem dann, wenn man die aktuelle Reihe, z.B. wegen fehlender Schlüssel, gar nicht so einfach identifizieren kann. Entspricht das von unserer Abfrage gelieferte Gültigkeitsdatum nicht dem 01.01.2001, so müssen wir in der Tabelle einen neuen Datensatz einfügen, was konkret in dem else-Programmblock passiert. Übrigens, im Rahmen solcher Cursorabfragen können Sie beim SQL-Server auch die Klausel „for update“ (vgl. Kap. 2.6.6) verwenden, wodurch der aktuell gelesene Datensatz automatisch gesperrt wird. 4.2.2 PL/SQL Also noch einmal. Das gilt natürlich nur, wenn Sie schon das Kapitel 4.2.1 gelesen haben, denn jetzt geht es um die Skriptprogrammierung in einer Oracle-Datenbank. Wie schon gesagt, wird der gesamte Sprachumfang in Oracle mit PL/SQL bezeichnet. Ähnlich wie beim SQL-Server kann man PL/SQL auch in einem Oracle-DBMS an vielen verschiedenen Stellen einsetzen. Die einfachste Anwendungsform besteht auch diesmal wieder in der Programmierung eines Skripts, das neben einfachen SQL-Abfragen auch Anweisungen zur Programmsteuerung oder Cursor mit komplexer Logik zur Datenänderung enthält. Dies und Das 357 4 Nitty Gritty • Take that! Damit haben Sie jetzt ein Beispiel gesehen, wie man beim SQL-Server auch mit Hilfe eines Skripts Änderungen in der Datenbank durchführen kann. So ganz nebenbei haben Sie dabei vielleicht Ihre ersten Gehversuche in der Transact-SQL-Programmierung gemacht. Das soll an dieser allerdings Stelle genügen; mehr Informationen über Transact-SQL im Allgemeinen oder zu den einzelnen Befehlen finden Sie in der Online-Hilfe Ihrer SQL-Server-Installation. Daneben können auch in einer Oracle-Datenbank Objekte existieren, die üblicherweise PL/SQL-Sprachelemente enthalten. Neben den Triggern und Prozeduren existieren in Oracle aber noch weitere Objekte. So lassen sich mit PL/SQL beispielsweise benutzerdefinierte Funktionen erstellen, die Sie anschließend genau wie die vom System vordefinierten Funktionen verwenden können. Letztendlich gibt es in Oracle auch noch ein so genanntes Package-Objekt. Ein solches Paket entspricht in der sonstigen Datenverarbeitung einem Programm. Wie Sie vielleicht wissen, können Programme intern über eine Vielzahl von Prozeduren oder Funktionen verfügen, die von außen gar nicht oder nur teilweise sichtbar sind, d.h., das eigentliche Programm wird mit Hilfe einer speziellen Startprozedur gestartet und ruft anschließend die enthaltenen Prozeduren und Funktionen entsprechend der vorgegebenne Programmierung auf. Genau so ist es auch bei den Package-Objekten, denn auch hier verschmelzen eine Vielzahl programmierter Prozeduren oder Funktionen zu einer logischen Einheit. Nitty Gritty • Take that! 4 Blockstruktur PL/SQL ist wie viele andere Programmiersprachen auch blockorientiert, wobei diese Blockstruktur aus den Schlüsselwörtern „begin“ und „end“ gebildet wird, was zumindest mich irgendwie an die Programmiersprache Pascal erinnert. Diese Blöcke besitzen in einem PL/SQL-Programm zwei Aufgaben. Zum einen werden sie dazu gebraucht, ein solches Programm oder Skript insgesamt zu begrenzen und zum anderen können Sie die Blöcke dazu benutzen, logisch zusammenhängende Programmteile zu markieren. Neben kosmetischen Gründen gibt es hierfür in der Praxis allerdings auch einen interessanten Anwendungsfall, denn die einzelnen PL/SQL-Blöcke können verschiedene Fehlerbehandlungsroutinen beinhalten, wobei ich hierauf nicht weiter eingehen werde. Ein PL/SQL-Programm bzw. Skript hat damit etwas vereinfacht dargestellt zunächst folgenden schematischen Aufbau: begin Anweisungen des Blocks 1 358 Verwenden von Skripten begin Anweisungen des Blocks 1-1 begin ... end; end; begin ... end; end; Außerdem gilt, dass alle PL/SQL-Anweisungen, die im Übrigen formatfrei kodiert werden können, mit einem Semikolon (;) abgeschlossen werden. Das gilt, wie Sie ebenfalls dem Schema entnehmen können, mit Ausnahme der begin-Anweisung auch für das Schlüsselwort end, d.h., neben den einzelnen Anweisungen wird auch ein gesamter Block mit einem Semikolon beendet. Theoretisch kann jeder PL/SQL-Block einen eigenen Deklarationsteil besitzen, mit dessen Hilfe Sie alle benötigten Variablen oder Cursor definieren können. Die hierzu benötigte Anweisung heißt auch in PL/ SQL wieder „declare“. Ihr folgt der Name der Variablen, dem der gewünschte Datentyp folgt, wobei Sie hierbei alle Typenbezeichner verwenden können, die Sie zusammen mit der create table-Anweisung kennen gelernt haben. declare v1 v2 v3 v4 integer; varchar(10); date; numeric(9,2); Im Unterschied zu Transact-SQL beginnen die Variablen diesmal aber nicht mit einem speziellen Sonderzeichen. Außerdem leitet das declare-Kommando den gesamten Deklarationsteil ein, denn anschließend können Sie alle benötigten Variablen definieren, ohne hierbei jedes Mal das declare-Schlüsselwort erneut nennen zu müssen. Dies und Das 359 Nitty Gritty • Take that! 4 Variablen verwenden Kommen wir jetzt zum ersten konkreten Anwendungsbeispiel eines solchen PL/SQL-Skripts, bei dem wir das Löschen einer Tabelle wieder davon abhängig machen, ob die zugehörige Tabelle bereits in der Datenbank existiert. declare tt integer; begin tt := 0; select count(table_name) into tt from user_tables where table_name = 'XXGEHALT'; if tt > 0 then execute immediate 'drop table xxgehalt'; end if; execute immediate 'create table xxgehalt (xx varchar(10))'; end; Nitty Gritty • Take that! 4 In PL/SQL erfolgt für eine Variable die Wertzuweisung nicht mit Hilfe einer speziellen Anweisung, sondern die Zuweisung erfolgt genau wie in vielen anderen Programmiersprachen mit Hilfe einer Kombination aus Doppelpunkt und Gleichheitszeichen (:=). Variable := <Konstante> | <Ausdruck>; Bei einer solchen Wertzuweisung steht immer links die Variable und rechts befindet sich der neue Wert in Form einer Konstanten oder eines beliebigen Ausdrucks. Ansonsten ist hierbei eigentlich nur wichtig, dass die Variablen auf der linken und das Ergebnis auf rechten Seite des Zuweisungsoperators (:=) den gleichen oder einen kompatiblen Datentyp besitzen müssen. t := 34.2; t1 := 33 + t * 2; a b e r !! select max(...) into t2 from ... 360 Verwenden von Skripten Möchten Sie den Wert einer Variablen mit Hilfe einer Datenbankabfrage verändern, so können Sie hierzu eine gewöhnliche Auswahlabfrage erstellen, die Sie mit Hilfe der into-Klausel erweitern, hinter der Sie für jede selektierte Spalte eine passende Variable spezifizieren. Hierbei wird die erste Selektionsspalte in die erste aufgeführte Variable, die zweite Spalte in die zweite Variable usw. kopiert. select a, b, c, ... into var_a, var_b, var_c, ... from Genau wie beim SQL-Server gilt allerdings auch hier, dass diese Abfrage genau eine Ergebniszeile selektieren muss. Liefert die Abfrage kein Ergebnis oder ein Ergebnis in Form mehrerer Datensätze, so erhalten Sie bei der Ausführung des Skripts einen Laufzeitfehler. Aus diesem Grund ist es hier genau wie bei den Unterabfragen sinnvoll, auf Aggregatfunktionen (z.B. count, max usw.) zurückzugreifen. Kontrollstrukturen If <Bedingung> then ... PL/SQL-Anweisungen [else ... PL/SQL-Anweisungen] end if; Die Konstruktion einer solchen if-Bedingung folgt den Regeln, die Sie bei der Erstellung von where-Bedingungen kennen gelernt haben, d.h., Sie können auch hier die damals beschriebenen Vergleichs- und Verknüpfungsoperatoren verwenden. Ist die if-Bedingung erfüllt, so werden die direkt dahinter programmierten Anweisungen ausgeführt; optional können Sie auch eine Anweisungsfolge programmieren, die nur dann ausgeführt werden soll, wenn die if-Bedingung nicht erfüllt ist, indem Sie die if-Konstruktion mit Hilfe des Schlüsselwortes „else“ erweitern. Dies und Das 361 4 Nitty Gritty • Take that! Da die in einem Skript oder Programm enthaltenen Befehle und Anweisungen nicht immer der Reihe nach ausgeführt werden sollen, bietet Ihnen PL/SQL verschiedene Möglichkeiten, den Ablauf der einzelnen Anweisungen zu kontrollieren. Die einfachste Form einer solchen Kontrolle bietet der if-Befehl, mit dessen Hilfe die Ausführung einer oder einer Reihe von Anweisungen von einer Bedingung abhängig wird. In jedem Fall muss die if-Anweisung durch die Schlüsselwörter „end if“ abgeschlossen werden; außerdem erwartet Oracle das Wörtchen „then“, mit dem die gesamte Prüfbedingung abgeschlossen wird. Ein weiterer Unterschied zu Transact-SQL ist, dass der if-Befehl in Oracle nicht nur eine einzige Blockanweisung erlaubt, sondern Sie können hier diesmal beliebig viele Anweisungen kodieren. Das ist aufgrund des formellen Abschlusses der if-Anweisung möglich; der zum „if“ gehörende Programmteil beginnt hinter dem „then“ und geht entweder bis zum „else“ oder bis zum abschließenden „end if“. Entsprechendes gilt auch, wenn mehrere if-Anweisungen ineinander geschachtelt werden, wobei man das auch mit dem Setzen von Klammern bei komplexen numerischen Ausdrücken vergleichen könnte. Jeder geöffneten Klammer folgt irgendwann einmal eine schließende; jeder if-Anweisung muss irgendwann einmal ein abschließendes „end if“ folgen. Nitty Gritty • Take that! 4 In unserem kleinen Beispiel führen wir innerhalb des if-Programmblocks wieder die drop-Anweisung aus, mit der auch diesmal wieder die später im Skript erstellte Tabelle gelöscht wird. Allerdings können wir diese Anweisung diesmal nicht wie beim SQL-Server im Klartext programmieren, da die direkte Verwendung von DDL-Anweisungen in einem PL/SQL-Programm zu Umwandlungsfehlern führt. Derartige Befehle müssen in einem Oracle-Skript zur Laufzeit dynamisch abgesetzt werden, wofür es den Befehl „execute immediate“ gibt. Hinter dieser Anweisung folgt das auszuführende SQL-Statement in Form einer Textkonstante oder Textvariablen, das von dem Skript gestartet wird, wenn das Programm die entsprechende Stelle erreicht. Durch diesen Eingriff wird die Tabelle beim Ablauf des Skripts somit wieder nur dann gelöscht, wenn die Tabelle bereits in der Datenbank existiert; hierdurch läuft das Skript im Normalfall dann wieder ohne Fehlermeldung ab, was bei umfangreichen Installationsarbeiten die nachträgliche Kontrolle erleichtert. Anschließend wird die Tabelle wieder mit Hilfe eines geeigneten create table-Befehls angelegt. Das geht aus den vorhin genannten Gründen jedoch auch nur über den Umweg der gerade beschriebenen execute immediate-Anweisung. 362 Verwenden von Skripten Schleifen Wie schon mehrfach erwähnt wurde, läuft ein Programm nur selten sequenziell vom Start zum Programmende, sondern manche Programmteile werden nur unter bestimmten Bedingungen ausgeführt, wohingegen andere Teile häufiger wiederholt werden müssen. Solche Wiederholungen bzw. Schleifen können auch in PL/SQL mit Hilfe entsprechender Anweisungen programmiert werden. Je nach verwendeter Programmiersprache gibt es immer verschiedene Varianten, um diese Schleifen zu programmieren; so haben Sie im letzten Abschnitt bei der Einführung in Transact-SQL die while-Anweisung kennen gelernt, bei der die Wiederholung eines Programmblocks von einer Bedingung abhängig war. Eine solche Schleife existiert auch in PL/SQL, wobei insgesamt sogar die nachfolgend beschriebenen Schleifenkonstruktionen möglich sind. while loop-Schleife Bei der while loop-Schleife werden die in der Schleife programmierten Anweisung so lange ausgeführt, wie die hinter der while-Anweisung kodierte Bedingung erfüllt ist. Ist diese Bedingung schon beim Erreichen der Schleife nicht mehr erfüllt, dann wird die Schleife folglich gar nicht durchlaufen. Die Programmierung einer while loopSchleife entspricht dem folgenden Schema: while <Bedingung> loop <Anweisungen> end loop; T loop-Schleife Bei der loop-Schleife handelt es sich um eine Sonderform der soeben beschriebenen while loop-Schleife. Wie Sie schon an der Begriffsfwahl erkennen können, fehlt hierbei die while-Klausel und damit die Abbruchbedingung der Schleife, d.h., loop-Schleifen laufen zunächst einmal endlos lange. Damit das in der Praxis dann doch nicht passiert - denn Endlosschleifen gelten nach neuesten wissenschaftlichen Erkenntnissen als Fehler - gibt es die exit-Anweisung, mit der Sie die Schleife verlassen können, so dass die Verwendung der loopSchleife nach etwa folgendem Schema erfolgt: Dies und Das 363 4 Nitty Gritty • Take that! T loop <Anweisungen> if <Bedingung> then exit; end if; end loop; T for loop-Schleife Bei der for loop-Schleife, die manchmal auch einfach „Zählschleife“ genannt wird, legen Sie von vornherein explizit fest, wie oft die Schleife durchlaufen wird, indem eine Laufvariable entsprechend eines vorgegebenen Intervalls variiert wird, bis die hintere Intervallgrenze erreicht wird. Die Verwendung einer for loop-Schleife sieht in etwa folgendermaßen aus: 4 Normalerweise wird das vorgegebene Intervall in aufsteigender Reihenfolge durchlaufen, d.h., die Laufvariable nimmt mit jedem Schleifendurchlauf den nächsten Wert des Intervalls an. Nitty Gritty • Take that! for <Laufvariable> in [reverse] <Interval> loop <Anweisungen> end loop; Cursor verwenden Sollten Sie das Transact-SQL-Beispiel ausführlich gelesen haben, so folgt jetzt wieder eine kleine Wiederholung, denn es geht auch jetzt wieder darum, in einem PL/SQL-Skript eine normale Abfrage auszuführen und deren Datensätze zu empfangen bzw. zu verarbeiten. Auch in PL/SQL müssen Sie zur Selektion von Daten in einem Skript oder Programm einen so genannten Cursor erstellen, mit dessen Hilfe Sie eine beliebige Abfrage erstellen und anschließend die zugehörigen Daten in einer Schleife Satz für Satz abrufen können. Hierzu müssen Sie im Deklarationsteil des Skripts eine entsprechende Definition durchführen, was mit Hilfe des Schlüsselworts „cursor“ erfolgt. declare cursor gehalt_cursor is select persnr, g_ab, la, betrag from gehalt; 364 Verwenden von Skripten Hinter der cursor-Anweisung folgt der Name des Cursors und hinter der is-Klausel folgt die zugehörige Abfrage. Was jetzt folgt, entspricht fast dem sequenziellen Lesen irgendwelcher Textdateien, wie Sie es vielleicht schon von irgendeiner Programmiersprache her kennen, d.h., zunächst wird der Cursor geöffnet, dann werden alle enthaltenen Datensätze abgerufen und am Ende wird der Cursor wieder geschlossen. open gehalt_cursor; ... close gehalt_cursor; Das Öffnen des Cursors erfolgt durch Verwendung der open-Anweisung, die Sie zusammen mit dem bei der Cursordefinition verwendeten Namen benutzen müssen und was intern zur Ausführung der zugehörigen Abfrage führt. Wird der Cursor nicht mehr benötigt, so können Sie ihn mit Hilfe der close-Anweisung schließen. fetch <Cursorname> into <Variablenliste> Hinter „fetch“ folgt hierbei der Name des Cursors, von dem Sie die aktuelle Datenzeile abrufen wollen unter hinter „into“ müssen Sie die Liste der Variablen spezifizieren, in die die zugehörigen Daten kopiert werden sollen. fetch gehalt_cursor into persnr, g_ab, la, betrag; Jeder Aufruf von „fetch” führt auch in PL/SQL zum Setzen einer Statusvariablen, die diesmal allerdings im Cursorobjekt vorhanden ist. gehalt_cursor%notfound Diese Statusvariable wird wahr (true), wenn ein zuvor ausgeführter fetch-Befehl nicht erfolgreich ausgeführt werden konnte. Weitere Statusvariablen finden Sie in der folgenden Tabelle 4.2. Dies und Das 365 4 Nitty Gritty • Take that! Zwischen den open- und close-Anweisungen können Sie nun die vom Cursor bzw. der zugehörigen Abfrage bereitgestellten Datensätze abrufen, was konkret durch den Einsatz des fetch-Befehls passiert. Statusvariable Beschreibung %found ist true, wenn der vorherige fetch einen Datensatz geliefert hat. %isopen enthält den Wert true, wenn der Cursor geöffnet wurde. %notfound entspricht „not %found“ und ist damit nur dann wahr, wenn der vorherige fetch-Versuch nicht erfolgreich war. %rowcount zählt alle gelesenen Datensätze, d.h., der Wert dieser Statusvariablen erhöht sich mit jedem fetch-Befehl. Tabelle 4.2: Statusvariablen bei der Cursorverarbeitung in Oracle Damit haben wir jetzt das Rüstzeug, um das im letzten Kapitel erstellte Transact-SQL-Beispiel in PL/SQL umzuschreiben, was dann etwa folgendermaßen aussehen könnte. Nitty Gritty • Take that! 4 declare persnr varchar2(10); la varchar2(3); g_ab date; betrag numeric(9,2); cursor gehalt_cursor is select persnr, g_ab, la, betrag from gehalt a where g_ab = (select max(g_ab) from gehalt a1 where a1.persnr = a.persnr and a1.g_ab <= to_date('2001-01-01', 'YYYY-MM-DD')) for update of persnr; begin open gehalt_cursor; fetch gehalt_cursor into persnr, g_ab, la, betrag; while not gehalt_cursor%notfound loop 366 Verwenden von Skripten if g_ab = to_date('2001-01-01','YYYY-MM-DD') then update gehalt set betrag = betrag * 1.05 where current of gehalt_cursor; else insert into gehalt (persnr, g_ab, la, betrag) values (persnr, to_date('2001-01-01','YYYY-MM-DD'), la, betrag * 1.05); end if; fetch gehalt_cursor into persnr, g_ab, la, betrag; end loop; close gehalt_cursor; commit; end; Genau wie beim Transact-SQL-Beispiel führen wir auch hier den Update mit Hilfe der Klausel „current of“ durch. Dabei müssen Sie beachten, dass diese Klausel in Oracle nur dann funktioniert, wenn Sie in der Cursorabfrage das Statement „for update“ (vgl. Kap. 2.6.6) verwenden. Dieses Statement hätte ich beim SQL-Server zwar auch verwenden können, jedoch ist es dort optional und hier zwingend, damit der Zugriff auf den aktuellen Datensatz des Cursors mit „where current of“ funktioniert. Ansonsten unterscheidet sich die Oracle-Version einmal abgesehen von der einen oder anderen kleinen Formalität eigentlich nur noch dadurch, dass wir die durchgeführten Änderungen und die damit automatisch gestartete Transaktion mit Hilfe einer commit-Anweisung abschließen. Dies und Das 367 4 Nitty Gritty • Take that! Finden Sie nicht auch, dass dieses Skript verblüffend viel Ähnlichkeit zur Transact-SQL-Variante hat? Das war natürlich Absicht, denn das Abrufen der vom Cursor gelieferten Sätze kann man in Oracle etwas einfacher programmieren, aber darauf werde ich erst etwas später eingehen. Bleiben wir zunächst noch etwas bei den Gemeinsamkeiten. for-Schleifen für Cursor Wie schon gesagt, habe ich das letzte Beispiel so konstruiert, um zunächst einmal möglichst viele Gemeinsamkeiten zwischen der Transact-SQL- und der PL/SQL-Variante zu erhalten. Mit den in PL/SQL verfügbaren Mitteln kann man die Verarbeitung eines solchen Cursors nämlich etwas eleganter programmieren, in dem man anstelle einer while- oder loop-Schleife die for-Schleife verwendet. Im ureigentlichen Sinne handelt es sich bei der for-Schleife um eine Zählschleife, mit deren Hilfe eine Zählvariable von einem Startwert aus mit einer voreingestellten Schrittweite bis zu einem Endwert hoch- bzw. herruntergezählt wird. Nitty Gritty • Take that! 4 Das kann man allerdings auch anders sehen und stattdessen behaupten, dass die Zählvariable durch eine Menge bestimmter (sortierter) Elemente bewegt wird und dabei den Wert eines jeden Elements annimmt. Aus diesem Blickwinkel (genau wie in den meisten modernen Programmiersprachen auch) wird die for-Schleife in PL/ SQL verwendet und von daher ist es nur noch ein kleiner Schritt, die zu durchlaufende Elementmenge nicht nur auf einfache Zahlen zu beschränken. Die vom Cursor bereitgestellten Datensätze bilden schließlich auch eine Menge, nämlich die vom ersten bis zum letzten selektierten Datensatz. Diese Menge von Datensätzen kann jetzt mit Hilfe der forSchleife durchlaufen werden. Hierbei ist die Laufvariable ein Objekt, das jeweils den vollständigen aktuellen Datensatz beinhaltet. declare cursor gehalt_cursor is select persnr, g_ab, la, betrag from gehalt a where g_ab = (select max(g_ab) from gehalt a1 where a1.persnr = a.persnr and a1.g_ab <= to_date('2001-01-01', 'YYYY-MM-DD')) for update of persnr; 368 Verwenden von Skripten