Dieser Abschnitt ersetzt keine SQL Referenz und kein PostgreSQL
Handbuch. Es wird nur exemplarisch auf einige Details
eingegangen. Dabei stehen PostgreSQL-spezifische Eigenschaften im
Vordergrund.
Der bereits kurz erwähnt interaktive Kommandointerpreter ist
sicherlich das wichtigste Programm.
psql versteht etliche Optionen:
-d Datenbank
|
Zu dieser Datenbank verbinden |
-h Servername
|
Über TCP/IP zu diesem Server verbinden |
-p Port
|
Diesen Port verwenden (Voreinstellung 5432) |
-U Benutzer
|
Als Benutzer anmelden |
-c Kommando
|
Dieses Kommando ausführen |
-f Datei
|
Diese SQL Datei ausführen |
-o Datei
|
Ausgaben die Datei schreiben |
-s |
Einzelschrittmodus: jedes SQL Kommando bestätigen |
-E |
zeigt das ausgeführte SQL-Kommando bei internen Befehlen (z.B. \d) an. |
Nach den Optionen gibt man eine Datenbank an, sofern man nicht
-d verwendet. Dahinter kann man noch einen Benutzernamen
schreiben, sofern man nicht -U verwendet.
Um als Superuser postgres zur Datenbank test zu verbinden,
schreibt man also beispielsweise:
root@linux #
psql -U postgres -d test
|
Je nach Einstellung der Authentifizierung wird nun nach einen
Passwort gefragt. Es erscheint das Datenbankprompt.
Hat man PostgreSQL mit der readline-Unterstützung übersetzt, kann
man ebenso wie in der Bash die Tabulator-Taste drücken, um Befehle
und Objekte zu erweitern
|
Am Prompt kann man SQL Befehle eingeben:
test=# CREATE TEMPORARY TABLE temp test-# ( feld1 int UNIQUE NOT NULL, test(# feld2 varchar(100000) DEFAULT NULL ); NOTICE: CREATE TABLE / UNIQUE will create implicit index 'temp_feld1_key' for table 'temp' CREATE
|
Man sieht, das SQL Kommandos mit Semikolon abgeschlossen werden
und dann automatisch ausgeführt werden. Das Prompt zeigt
an, ob man in einer Klammer ist, eine kleine Hilfe. Das
Beispielkommando hat nun eine einfach Testtabelle erzeugt. Diese
kann man nun mit Daten füllen:
test=# INSERT INTO TEMP (feld1, feld2) VALUES (1234, 'hallo'); INSERT 1532564 1
|
Die Ausgabe enthält eine merkwürdige Nummer. Das ist der OID, der
object identifier. Diese sollte man nicht weiter beachten (es
handelt sich um eine Art automatisches Indexfeld, ist aber höchst
unportabel, und wird nur intern benötigt).
Über psql kann man auch in Transaktionen arbeiten:
Die Tabelle enthält einen Datensatz:
test=# SELECT count(*) FROM temp; count ------- 1 (1 row)
|
Transkation beginnen:
Tabelle temp leermachen (alles löschen):
test=# DELETE FROM temp; DELETE 1
|
Die Tabelle ist jetzt auch Sicht der Transaktion leer:
test=# SELECT count(*) FROM temp; count ------- 0 (1 row)
|
Transaktion abbrechen:
Es ist nichts geändert worden:
test=# SELECT * FROM temp; feld1 | feld2 ------+------- 1234 | hallo (1 row)
|
Die Temporäre Tabelle verfällt automatisch, wenn man die
Verbindung schließt.
|
psql verfügt über eine Reihe sogenannter interner Kommandos.
Diese beginnen mit einem \ (Backslash). Einige der wichtigesten
internen Kommandos sind:
\? |
kurze Hilfe zu allen Backslash Kommandos |
\d Objekt
|
Objekt beschreiben. Ist Objekt beispielsweise eine Tabelle, so
werden die Spalten und Typen angezeigt. Auch definierte
Indizes werden aufgelistet. Wird Objekt nicht angegeben,
werden alle Tabellen aufgelistet, die existieren (außer
natürlich temporäre Tabellen).
|
\dKürzel
|
Liste die zu Kürzel passenden Objekte: Tabellen (t), Indizes
(i), Sequenzen (s), Views (v), Privilegien (p), Systemtabellen
(S), große Objekte (l), Aggregatfunktionen (a), Kommentare (d;
Objektname muss folgen), Funktionen (f), Operatoren (o) und
Datentypen (T).
Durch ein Leerzeichen kann man noch ein Objekt angeben. \dp
temp zeigt beispielsweise die Privilegien für die Tabelle
temp an (was nur funktioniert, wenn es keine temporäre Tabelle
ist).
|
\e Datei
|
Öffnet das letzte Kommando oder Datei im Editor. Hilfreich,
um lange Kommandos wie CREATE TABLE zu bearbeiten und zu
speichern.
|
\l |
Listet alle Datenbanken auf. |
\q |
Beendet psql |
\x |
Erweiterte Ausgabe |
\H |
HTML Ausgabe |
\c Datenbank
\c - Benutzer
|
Verbindet zu einer neuen Datenbank oder zur aktuellen mit
einem neuen Benutzer. Dies ist in etwa mit dem
USE vergleichbar, das andere DBMS verwenden.
|
Es folgt ein Beispiel für das Ausgabeformat. Zunächst soll die
Ausgabe der oben erwähnten Testtabelle nicht feld1 und feld2
beinhalten, sondern Nummer und Textfeld. Wenn man diese
Bezeichner case-sensitiv haben möchte (Tabellen- und Feldnamen
sind sonst case-insensitiv, das heißt, Groß-/Kleinschreibung wird
nicht beachtet), muss man diese quoten:
test=# SELECT feld1 AS "Nummer", feld2 AS "Textfeld" FROM temp; Nummer | Textfeld --------+---------- 1234 | hallo (1 row)
|
Nach \x sieht die Ausgabe so aus:
test=# SELECT feld1 AS "Nummer", feld2 AS "Textfeld" FROM temp; -[ RECORD 1 ]--- Nummer | 1234 Textfeld | hallo
|
Dies macht bei großen Tabellen Sinn, wenn nicht mehr alle Spalten
nebeneinander auf den Bildschirm passen.
|
Neben der interaktiven Verwendung kann man
psql dazu verwenden,
SQL Skripte auszuführen, beispielsweise Skripte, die
Datenbankschemata erzeugen. Man kann
psql sogar dazu verwenden,
Shell-Skripte mit rudimentärer Datenbankfunktionalität zu
versehen; hier ist die Verwendung von Perl::DBI oder anderen
Methoden jedoch oft einfacher und sauberer.
|
|
pgaccess ist eine graphisches Frontend, mit dem etliche
Standardaufgaben erledigt werden können. Das Anlegen von Tabellen
beispielsweise macht sich mit diesem Frontend wesentlich besser,
also mit psql .
Über das Menü kann man zu einer Datenbank verbinden. Im folgenden
Dialog können Server- und Datenbankname sowie ein Benutzerkonto
angegeben werden.
Im Hauptfenster kann man rechts die anzuzeigende Objekte wählen.
Hier kann man beispielsweise zwischen Tabellen, Views und
Sequenzen auswählen. Im linken Teil werden dann die
entsprechenden Objekte aufgelistet und können ausgewählt werden.
Nach einem Doppelklick auf eine Tabelle bekommt man ein Fenster,
in dem der Inhalt dargestellt wird und geändert werden kann.
Klickt man eine Tabelle nur einmal an, so kann man weitere
Funktionen anwenden, beispielsweise Design. Hier öffnet sich
ein Fenster, in dem man komfortabel Indizes hinzufügen kann oder
neue Felder anhängen kann.
Seit Version 7.3 ist pgaccess nicht mehr Bestandteil der PostgreSQL-
Distribution und muss separat von http://www.pgaccess.org besorgt
werden.
|
RedHat vertreibt eine eigene Version von PostgreSQL. Diese entspricht
ungefähr der Version 7.2.3 und ist unter
http://www.redhat.com/software/database/ erhältlich.
RedHat stellt alle Änderungen am DBMS und auch sein graphisches
Administrationsfrontend unter die GPL. Dieses läuft auch mit einer
konventionellen PostgreSQL Installation und ist unter
http://sources.redhat.com/rhdb/ zu finden. Es ist hübscher als
pgaccess und bietet im Bereich der Verwaltung mehr Optionen als dieses,
kann dafür aber nicht zur Definition von TCL-Formularen herangezogen
werden.
Die neuen Funktionen der Version 7.3 (Schemata) werden allerdings
noch nicht unterstützt.
|
Dies ist ein Webfrontend und setzt einen Webbrowser voraus. Dieses
Frontend verfügt über sehr viele nützliche Funktionen.
Tabellendaten können als HTML Tabelle betrachtet und editiert
werden, beliebige Abfragen können erstellt und ausgeführt werden.
Tabellen selbst können einfach und komfortabel bearbeitet werden,
so können neue Felder hinzugefügt oder gelöscht werden. Weiterhin
stehen Kopier- und Dumpfunktionen bereit. Auch Berechtigungen
können komfortabel verwaltet werden. Die zur Verfügung stehenden
Optionen sind sinnvoll in Auswahlfeldern aufgelistet. Bei Bedarf
ist es auch möglich, eigene SQL Kommandos einzugeben und
ausführen zu lassen.
Eine weitere schöne Funktion ist die Verlinkung zu jeweils
passenden Seiten der PostgreSQL Dokumentation.
Wer Webfrontends mag, wird dieses Frontend wohl lieben. Es lohnt
sich allemal, sich dieses zu installieren. Natürlich muss
unbedingt darauf geachtet werden, den Zugang zu diesem Frontend
zu schützen, da der Zugriff auf das Frontend Zugriff auf die
Datenbank gestattet - und zwar als Superuser!
|
Dieser Abschnitt geht kurz auf Transaktionen ein. Transaktionen
sind notwendig, um Änderungen atomar, dass heißt, ganz oder gar
nicht, durchführen zu können.
Im Folgenden wird oft der englische Ausdruck lock verwendet.
Wörtlich übersetzt bedeutet er in etwa sperren. Hier ist
gemeint, ein Objekt so zu benutzen, dass es niemand anders
gleichzeitig benutzen kann. Lock wird später noch genauer
erklärt.
Das klassische Beispiel für den Bedarf ist das Buchungsbeispiel.
Angenommen, es existieren zwei Kontotabellen. Möchte man nun eine
Buchung gegen diese beiden Tabellen machen, muss in jede Tabelle
ein neuer Datensatz angelegt werden. Dazu muss man zwei INSERT
INTO SQL Kommandos ausführen lassen.
Nun könnte es ja passieren, dass eines der beiden Kommandos
klappt, das andere jedoch nicht. In diesem Fall würden die Konten
nicht mehr stimmen, da die Summen nicht mehr passen. Man hätte
inkonsistente Daten und ein Problem.
Daher fasst man beide Kommandos zu einer Transaktion zusammen.
Eine Transaktion klappt entweder ganz, oder gar nicht. Geht also
eines der SQL Kommandos schief, so hat auch das andere
automatisch keinen Effekt (es wird gegebenenfalls rückgängig
gemacht).
Transaktionen sind für andere erst sichtbar, wenn sie
abgeschlossen wurden. Das bedeutet im Beispiel, dass nach dem
Ausführen der ersten Kommandos ein anderer Client diese Änderung
überhaupt nicht sieht. Erst wenn das andere Kommando erfolgreich
war und die Transaktion beendet wurde, werden die Änderungen
sichtbar. Somit stimmen die Summen zu jedem Zeitpunkt.
Wenn innerhalb einer Transaktion Daten gelesen werden, und von
einer anderen Transaktion in dieser Zeit geändert werden, so wird
die Transaktion automatisch abgebrochen. Auch hier kann es nicht
passieren, dass Daten versehentlich zurückgeschrieben werden, die
inzwischen an anderer Stelle geändert wurden.
|
Implementiert wird ein sogenanntes Multiversion Concurrency
Control (MVCC). Das bedeutet, das Abfragen einer Transaktion die
Daten so sehen, wie sie zu einem bestimmten Zeitpunkt waren,
unabhängig davon, ob sie inzwischen von einer anderen Transaktion
geändert wurden. Dies verhindert, dass eine Transaktion einen Teil
Daten vor und einen anderen nach einer nebenläufig
abgeschlossenen Transaktion lesen kann und verhindert so
inkonsistentes Lesen: die Transaktionen werden von einander
isoliert. Der Hauptunterschied zu Lock Verfahren ist, dass
MVCC Locks für das Lesen nicht mit Locks für das Schreiben in
Konflikt stehen. Somit blockiert das Schreiben nie das Lesen und
das Lesen nie das Schreiben.
Eine wichtige Einschränkung gibt es: Transaktionen können in
PostgreSQL nicht geschachtelt werden (es gibt also keine
Untertransaktionen).
|
PostgreSQL unterstützt zwei Transaktionslevel: read committed
und serializable. Verwendet eine Transaktion read committed,
so kann es vorkommen, dass sie Daten erneut liest,
aber andere Daten erhält als beim ersten Lesen
(nicht-wiederholbares Lesen, non-repeatble reads). Auch
sogenanntes Phantom-Lesen (phantom reads) kann vorkommen. Vom
Phantom-Lesen spricht man, wenn sich in einer Transaktion die
Ergebnissätze von Suchbedingungen ändern können. Sogenanntes
schmutziges Lesen (dirty reads), also das Lesen von Änderungen
aus anderen, nicht abgeschlossenen Transaktionen kann jedoch nicht
auftreten. Dieser Transaktionslevel ist die Voreinstellung. Er
ist einfach anzuwenden, schnell und für die meisten Anwendungen
ausreichend.
Verwendet eine Transaktion serializable, können diese beiden
unerwünschten Effekte nicht auftreten. Man benutzt diesen Level
für Anwendungen, die komplexe Abfragen und Änderungen durchführen.
|
Transaktionen werden durch das SQL Kommando BEGIN eingeleitet.
Dies ist nicht standardkonform; ANSI fordert, das immer
implizit eine Transaktion begonnen wird. PostgreSQL bietet
jedoch wie viele andere DBMS auch eine sogenanntes auto commit
Funktion an, dies ist auch das Standardverhalten.
Jedes SQL Kommando wird dann so aufgefasst, als wäre
es eine einzelne Transaktion (es wird also sozusagen ein
implizites COMMIT nach jedem SQL Kommando ausgeführt).
Möchte man nun eine aus mehreren Anweisungen bestehende Transaktion
beginnen, schreibt man einfach BEGIN als erstes Kommando. Dies passt auch
gut zum eingebettetem SQL, da die SQL Kommandos dadurch in einen
schicken BEGIN - END Block eingeschlossen sind.
Grundsätzlich gibt es zwei Möglichkeiten, eine Transaktion zu
beenden. Eine Anwendung kann eine Transaktion selbst abbrechen.
Hierzu dient das Kommando ROLLBACK. Keine der Änderungen der
Transaktion wird ausgeführt. Eine Anwendung kann die Transaktion
auch positiv beenden. Dazu wird END oder COMMIT verwendet. Die
Transaktion wird genau dann durchgeführt, wenn sie fehlerfrei war. In
diesem Fall werden alle Änderungen (oder die eine komplexe
Transaktionsänderung) übernommen (sichtbar). Trat in der
Transaktion ein Fehler auf, so gibt es natürlich keine
Möglichkeit, sie doch noch positiv zu beenden, da dies zu
Inkonsistenzen führen würde. In solchen Fällen kann die Anwendung
(je nach Art des Fehlers) die Transaktion wiederholen. Dies ist
natürlich nicht sinnvoll, wenn beispielsweise ein Tabelle fehlt.
Dann wird auch die Wiederholung fehlschlagen.
So ist also sichergestellt, dass Transaktionen nur vollständig
(und vollständig erfolgreich), oder überhaupt nicht durchgeführt
werden.
Hat man mit BEGIN eine Transaktion begonnen, so ist zunächst die
Datenbankvoreinstellung des Transaktionslevel (read committed)
aktiv. Solange die Transaktion noch nicht begonnen wurde, kann
der Transaktionslevel noch geändert werden. Dazu wird das
SQL Kommando SET TRANSACTION ISOLATION LEVEL verwendet. Als
Parameter wird READ COMMITTED oder SERIALIZABLE angegeben. Damit
ist der Transaktionslevel eingestellt. Ein Client kann auch einen
eigene Voreinstellung setzen, wenn beispielsweise Transaktionen
grundsätzlich serializable sein sollen. Das SQL Kommando lautet
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL und
erwartet die gleichen Parameter wie das vorherige Kommando.
|
Verwendet man Transaktionen, so kann es natürlich vorkommen, dass
eine Transaktion vom DBMS beendet wird, weil eine andere
Transaktion Daten geändert hat; insbesondere, wenn serializable
verwendet wird. In solchen Fällen wird die Transaktion in der
Regel einfach von vorn beginnend vollständig wiederholt (die
Anwendung führt diese also erneut aus).
Derartige Effekte minimiert man oft, in dem man Datensätze, von
denen man schon weiß, dass man sie ändern muss, schon mal für
selbiges vormerkt. Dies geschieht mit dem SQL Kommando SELECT
FOR UPDATE. Nun weiß das DBMS, dass diese Datensätze der
Transaktion gehören. Möchte eine andere Transaktion hier auch
Daten ändern, so wartet diese automatisch, bis die erste
Transaktion beendet wurde (also bestätigt oder abgebrochen). Dann
erst wird die Aktion ausgeführt. Mit dem SQL Kommando LOCK TABLE
können auch komplette Tabellen gesperrt werden. Verwendet man
diese Mechanismen sorgfältig, vereinfacht sich die Handhabung;
spätere Transaktionsabbrüche treten nicht auf, da die Daten ja
bereits verwendet werden.
Es kann passieren, dass sich Transaktionen gegenseitig
ausschließen. Würde beispielsweise Transaktion A die Tabelle A
sperren und Transaktion B Tabelle B und anschließend Tabelle A sperrt,
kommt es zu einer solchen Situation, wenn Transaktion A auch versucht,
Tabelle B zu sperren. Transaktion B kann ja Tabelle A nicht sperren,
weil diese schon von Transaktion A bereits gesperrt ist und blockiert, bis
Transaktion A beendet wurde. Transaktion A wiederum wartet auf
Transaktion B, um Tabelle B sperren zu können. Man spricht von
einem Deadlock - beide Transaktionen haben sich gegenseitig
blockiert.
PostgreSQL erkennt solche Fälle automatisch. Eine der
beiden Transaktionen wird mit einem entsprechendem Deadlock-Fehler
abgebrochen, woraufhin die andere durchgeführt werden kann. Auch
hier wiederholt die Anwendung einfach die Transaktion. Da nun
keine andere mehr läuft, wird es diesmal klappen.
Bei der Arbeit mit komplexen Transaktionen muss man damit rechnen,
dass eine Transaktion durch solche oder ähnliche Gründe
abgebrochen wird. In der Software ist also vorzusehen,
Transaktionen wiederholen zu können. Da im Falle eines
Transaktionsabbruches ja überhaupt keine Daten geändert werden,
geht das unproblematisch. Man beginnt einfach von vorn.
|
Sperre oder Lock bedeutet, dass der Inhaber oder Eigentümer dessen davor
geschützt ist, dass jemand anders eine Sperre erzeugt, der dieser
widerspricht. Es gibt verschiedene Arten von Sperren. Lese-Locks
beispielsweise schließen sich nicht gegenseitig aus (es können ja
problemlos mehrere Transaktionen die gleichen Daten lesen),
jedoch schließt ein Lese-Lock einen Schreib-Lock aus. Schreib-Locks
schließen sich und Lese-Locks aus. Letztere nennt man daher auch
exklusiv, keine anderere Sperre kann neben einem Schreib-Lock
ausgeführt sein.
Die folgende Aufstellung ist unvollständig.
AccessShareLock |
(lesender Zugriff) Lese-Lock, der automatisch auf Tabellen
gesetzt wird, aus denen gelesen wurden. Schließt
AccessExclusiveLock aus.
|
RowShareLock |
(lesender Zugriff auf Zeilen) Wird durch SELECT FOR UPDATE und
LOCK TABLE IN ROW SHARE MODE
gesetzt. Schließt ExclusiveLock und AccessExclusiveLock Modi
aus.
|
RowExclusiveLock |
(exklusiver Zugriff auf Zeilen) Wird durch UPDATE, DELETE,
INSERT und LOCK TABLE IN ROW
EXCLUSIVE MODE gesetzt. Schließt ShareLock,
ShareRowExclusiveLock, ExclusiveLock und AccessExclusiveLock
Modi aus.
|
AccessExclusiveLock |
(exklusiver Zugriff) Gesetzt durch ALTER TABLE, DROP TABLE,
VACUUM FULL und LOCK
TABLE. Schließt alle Modi aus. Selbst
SELECT in anderen Transaktionen blockiert in diesem Fall.
|
|
Datensätze werden mit SELECT FOR UPDATE gesperrt. Dies schließt
Änderungen an genau diesen Datensätzen aus. Wie bereits
angedeutet, schließt dies kein Lesen aus (Schreiben blockiert
kein Lesen).
|
Wie bereits gesagt, werden Transaktionen bei Fehlern automatisch
abgebrochen. Alle Kommandos werden ignoriert:
Transaktion beginnen:
Es Kommando geht schief, zum Beispiel weil syntaktisch falsch:
test=# SYNTAX ERROR; ERROR: parser: parse error at or near "SYNTAX"
|
Die Transaktion ist abgebrochen worden. Alle Kommandos werden ab
jetzt ignoriert:
test=# DELETE FROM temp; NOTICE: current transaction is aborted, queries ignored until end of transaction block *ABORT STATE*
|
Selbst wenn man versucht, die Transaktion positiv zu beenden,
wird nichts geändert (die Transaktion wird also trozdem
abgebrochen):
Die Antwort COMMIT heißt nicht, dass wirklich etwas committed
wurde. Hier wurde ja ein Rollback durchgeführt. Dieses Verhalten
ist bei Skripts sehr nützlich. Die Kommandos schreibt man einfach
ein einen BEGIN; - END; Block (End ist das gleiche wie
Commit). Bei einem Fehler wird keine Änderung ausgeführt - die
Datenbank sieht genauso aus, wie vorher. Man kann das Skript
korrigieren und erneut ausführen.
An dieser Stelle sei noch einmal daran erinnert, dass
Strukturkommandos (wie CREATE und DROP) nicht den
den Transaktionsregeln unterliegen.
|
Es ist möglich, Bedingungen (CONSTRAINTS) an Tabellen zu
definieren. Beispielsweise könnte man fordern, dass die Summe über
alle Felder einer Tabelle null sein muss. Möchte man nun zu einem
Datensatz drei addieren, muss man also von einem anderen drei
abziehen. Doch kurz dazwischen ist die Bedingung ja verletzt,
denn die Summe ist ja dann nicht mehr null, sondern drei!
Bedingungen können daher in Transaktionen aufgeschoben werden
(DEFERRED). Das bedeutet, sie werden erst am Ende der Transaktion
geprüft. Eine Bedingung kann dies aber auch verhindern.
Bedingungen können so definiert werden, dass sie immer sofort
geprüft werden. Bedingungen können aber auch so definiert werden,
dass die Prüfung per Voreinstellung aufgeschoben wird, oder das
die Bedingung explizit aufgeschoben werden kann.
Um Bedingungen aufzuschieben, die sofort geprüft werden sollen, aber auch
aufgeschoben werden dürfen, verwendet man das SQL Kommando SET
CONSTRAINTS ALL DEFERRED. Anstatt ALL kann man auch den Namen der
Bedingung angeben (das wird auch oft gemacht). Anstatt DEFERRED
kann auch IMMEDIATE eingestellt werden. Damit hat das den
Gegenteiligen Effekt. Bedingungen, die per Voreinstellung
aufgeschoben werden, werden dennoch sofort ausgeführt.
Schiebt man also eine Prüfung auf, so wird diese am (bisher
positiven) Ende der Transaktion durchgeführt. Stellt sich nun
heraus, dass die Bedingung verletzt ist, wird die Transaktion
abgebrochen (und die Bedingung bleibt dadurch erfüllt).
|
|
Es gibt einige Variablen, die das Verhalten des DBMS (für den
entsprechenden Clienten) beeinflussen. Über Variablen wird
beispielsweise gesteuert, wie Datumsangaben aussehen. Dies ist
nicht standard konform (mit Ausnahme von TIME ZONE, hier wurde
der Standard erweitert).
Variablen werden mit SET gesetzt und mit SHOW abgefragt. Mit SET
wird eine Variable auf einen Wert gesetzt. Zwischen der Variable
und dem Wert steht TO (oder ein Gleichheitszeichen).
Hier werden nur zwei wichtige Variablen erwähnt. Die Variable
DATESTYLE setzt die Form der Datumsrepräsentation. Mögliche
Werte sind German, ISO und andere.
Auch die Zeitzone kann man setzen. Hier verwendet man SET TIME
ZONE. ANSI erlaubt als Parameter nur eine Zahl, beispielsweise
SET TIME ZONE 2. Dies ist natürlich ungünstig, da die Sommer- und
Winterzeit Unterscheidung von der Anwendung getroffen werden muss
(Ist Berlin nun gerade -1 oder -2? Das hängt vom Datum ab!).
PostgreSQL erlaubt jedoch auch SET TIME ZONE
'Europe/Berlin'.
An einem Beispiel wird gezeigt, wie man die aktuelle Uhrzeit mit
Datum in Californien (Zeitzone PST8PDT) im ISO Format
(amerikanische Notation) und in Berlin (Zone CET, Central
European Time, deutsche Notation) ausgeben lassen kann.
test=# SET TIME ZONE 'PST8PDT'; SET DATESTYLE TO ISO; SELECT now(); SET VARIABLE SET VARIABLE now ------------------------------- 2003-01-02 11:30:17.698728-08 (1 row)
|
test=# SET TIME ZONE 'CET'; SET DATESTYLE TO German; SELECT now(); SET VARIABLE SET VARIABLE now -------------------------------- 02.01.2003 20:32:46.387261 CET (1 row)
|
|
PostgreSQL unterstützt unter anderem die SQL92 Datentypen.
Ingesammt werden viele Typen unterstützt und eigene können
definiert werden. Beispiele sind int (Ganzzahlen), double
precision (8 Byte Fließkomma), serial (Autoinkrementeller int),
varchar (variable lange Zeichenketten), bytea (Binäre
Zeichenkette, wie ANSI BLOB), timestamp (Datum und
Uhrzeit), boolean (Wahrheitswert) und viele andere.
Typ-Umwandlungen werden durchgeführt, in dem man den
Zieltyp durch zwei Doppelpunkte :: getrennt an den Typ anfügt:
'123'::int.
Dies konvertiert die Zeichenkette 123 in einen Ganzzahltyp mit
dem Wert einhundertdreiundzwanzig.
|
Neben den normalen Operatoren (OR, AND, +, -, *, || usw.) gibt
viele weitere, beispielsweise Quadratwurzel (|/), LIKE und ILIKE
(Patternmatching wie bei LIKE, aber case-insensitiv) auch reguläres
Patternmatching (~, ~* und andere). Die Operatoren verhalten sich
je nach Datentyp korrekt. Addiert man mit dem Operator +
beispielsweise ein timestamp und ein intervall (also now() +
intervall '2 hours'), kommt das erwartete Ergebnis heraus.
|
PostgreSQL stellt viele Funktionen bereit. Viele mathematische
Funktionen sind verfügbar (sin(), cos(), abs(), random() usw).
Daneben gibt es viele Zeichenkettenfunktionen (lower(),
substring(), initcap(), translate(), encode(), um nur einige zu
nennen). Auch die Zeit- und Datumsfunktionen sind sehr
interessant und leistungsfähig. Beispielsweise gibt es
current_timestamp (oder auch kurz now, eine
klassische PostgreSQL-Erweiterung), extract (liefert Datumsteile,
SELECT EXTRACT(MINUTE FROM TIMESTAMP '2001-02-16 20:38:40');
führt also zu 38) und age (berechnet die Differenz zwischen zwei
Zeitstempeln).
|
Das Erzeugen und Planen von Datenbanken findet sich im Abschnitt
Administration.
|
Wie in jedem anderen RDBMS werden natürlich auch Tabellen
unterstützt. Diese werden mit CREATE TABLE erzeugt. Dieses
Kommando ist gut ANSI konform. Es gibt temporäre Tabellen, die
automatisch gelöscht werden. Tabellen und Spalten können
Bedingungen besitzen, das sind beispielsweise Funktionen, die es
verhindern können, sinnlose Daten einzutragen (2 stellige
Postleitzahlen beispielsweise). Wie bereits in
Arbeiten mit Bedingungen genannt, können die Prüfungen
gegebenenfalls auf das Transaktionsende verschoben werden.
Fremdschlüssel sind Sonderformen von Bedingungen und werden auch
unterstützt. Hiermit kann man gewährleisten, dass in eine
Tabellenspalte nur solche Werte eingetragen werden können, die bereits
in der Spalte einer anderen Tabelle definiert sind. Hat man z.B. eine
Tabelle mit Herstellern und eine mit Teilen, in welcher der Hersteller
vermerkt wird, kann sichergestellt werden, dass kein ungültiger
Hersteller in letzterer eingetragen wird).
Bei Fremdschlüsseln kann beispielsweise eine Aktion
angegeben werden, die ausgeführt werden soll, falls der Fremdschlüssel
gelöscht wird: NO ACTION, RESTRICT (dann ist das ein Fehler),
CASCADE (die den Schlüssel referenzierenden Datensätze auch automatisch
löschen, Vorsicht, dass können dann evtl. eine ganze Menge sein!),
SET NULL (Wert auf NULL setzen), SET DEFAULT (auf Voreinstellung
setzen).
Beispiele ähnlich denen aus der PostgreSQL Dokumentation |
-- Eine Tabelle mit Primärschlüssel und einfachem Aufbau
CREATE TABLE films (
code CHARACTER(5) CONSTRAINT films_pkey PRIMARY KEY,
title CHARACTER VARYING(40) NOT NULL,
distributors_id DECIMAL(3) NOT NULL,
date_prod DATE,
kind CHAR(10),
len INTERVAL HOUR TO MINUTE
);
-- Beispieldatensatz
INSERT INTO films (code, title, distributors_id) VALUES ('FilmA', 'Der Film A', 123);
-- Eine Tabelle mit einem Autoinkrement und einer einfachen Bedingung
CREATE TABLE distributors (
id DECIMAL(3) PRIMARY KEY DEFAULT NEXTVAL('serial'),
name VARCHAR(40) NOT NULL CHECK (name <> '')
);
-- Ein Tabelle mit Bedingung (distributors_id muss größer als 100 sein, der Name
-- darf nicht leer sein, sonst gibt es einen Fehler
-- Das Feld modtime wird automatisch auf "jetzt" gesetzt, wenn ein
-- Datensatz eingefügt wird.
CREATE OR REPLACE TABLE distributors (
id DECIMAL(3) UNIQUE,
name VARCHAR(40),
modtime TIMESTAMP DEFAULT CURRENT_TIMESTAMP
CONSTRAINT cst_valid_distributors_id CHECK (id > 100 AND name <> '')
);
-- Datensatz einfügen:
INSERT INTO distributors (id, name) VALUES (123, 'Name');
-- Nochmal geht schief, weil id eindeutig sein muss
-- Das geht auch schief:
-- INSERT INTO distributors (id, name) VALUES (001, 'Name');
-- denn: "ExecAppend: rejected due to CHECK constraint cst_valid_distributors_id"
-- (muss ja > 100 sein)
-- Eine Tabelle mit Fremdschlüssel und benannten Bedingungen.
-- "varchar" heißt einfach: kann beliebig lang werden (also fast,
-- bei ca 1000 MB ist Ende)
CREATE TABLE lager (
id SERIAL PRIMARY KEY,
films_code CHARACTER(5),
distributors_id DECIMAL(3),
info VARCHAR DEFAULT NULL,
CONSTRAINT fk_lager_distributors_id FOREIGN KEY (distributors_id) REFERENCES distributors(id)
ON DELETE RESTRICT,
CONSTRAINT fk_lager_films_code FOREIGN KEY (films_code) REFERENCES films(code)
ON DELETE RESTRICT DEFERRABLE
);
-- Datensatz einfügen
INSERT INTO lager (id, films_code, info) VALUES (123, 'FilmA', 'hallo');
-- Das geht schief:
-- INSERT INTO lager (id, films_code, info) VALUES (124, 'FilmA', 'hallo');
-- denn: "fk_lager_distributors_id referential integrity violation
-- - key referenced from lager not found in distributors"
-- Das geht auch schief:
-- DELETE FROM distributors;
-- denn: "fk_lager_distributors_id referential integrity violation
-- - key in distributors still referenced from lager" |
Tabellen können mit dem Kommando ALTER TABLE geändert werden.
Diese Kommando hat viele Formen.
Einige Beispiele:
Beispiele: ALTER TABLE |
-- Eine Spalte anfügen:
ALTER TABLE lager ADD COLUMN plz VARCHAR(8);
-- Eine Spalte ändern:
ALTER TABLE lager ALTER COLUMN plz SET DEFAULT 'unsortiert';
-- Eine Spalte umbennen:
ALTER TABLE lager RENAME COLUMN plz TO zipcode;
-- Bedingung hinzufügen (PLZ muss fünfstellig sein)
ALTER TABLE lager ADD CONSTRAINT cst_zipchk CHECK (char_length(zipcode) = 5);
-- Bedingung entfernen
ALTER TABLE lager DROP CONSTRAINT cst_zipchk RESTRICT;
-- Tabelle umbennnen
ALTER TABLE lager RENAME TO lagermitplz;
ALTER TABLE lagermitplz RENAME TO lager;
-- Eigentümer ändern
ALTER TABLE lager OWNER TO steffen;
|
Ab Version 7.3 wird endlich auch SQL 92 ALTER TABLE DROP COLUMN
unterstützt. Gibt es einen Index, eine Bedingung oder einen
Fremdschlüssel der die zu löschende Spalte referenziert, muss die
Option CASCADE mit angegeben werden.
Für ältere Versionen hat sich folgende Vorgehensweise bewährt:
Man muss die Tabelle neu erzeugen. Diese Funktion
wird übrigens von phpPgAdmin unterstützt (das heißt, es gibt
einen DROP Knopf, der im Prinzip das tut). Im Folgenden wird ein
Workaround gezeigt. Es werden hier gleich noch ein paar weitere
Kommandos demonstriert.
Beispiele |
-- Workaround für fehlendes:
-- ALTER TABLE lager DROP COLUMN zipcode;
-- Daten in Temp-Tabelle:
BEGIN;
-- Tabelle exklusiv schützen:
LOCK TABLE lager IN ACCESS EXCLUSIVE MODE;
-- LOCK TABLE lager; macht das gleiche (Voreinstellung ist ACCESS EXCLUSIVE)
CREATE TEMPORARY TABLE temp AS SELECT id, films_code, distributors_id, info FROM lager;
-- lager Tabelle neu erstellen
DROP TABLE lager;
CREATE TABLE lager (
id SERIAL PRIMARY KEY,
films_code CHARACTER(5),
distributors_id DECIMAL(3),
info VARCHAR DEFAULT NULL,
CONSTRAINT fk_lager_distributors_id FOREIGN KEY (distributors_id) REFERENCES distributors(id)
ON DELETE RESTRICT,
CONSTRAINT fk_lager_films_code FOREIGN KEY (films_code) REFERENCES films(code)
ON DELETE RESTRICT
DEFERRABLE
);
-- Achtung, die Berechtigungen und Bedingungen der Tabelle müssen
-- noch gesetzt werden!
-- neue Tabelle füllen
INSERT INTO lager SELECT * FROM temp;
-- vielleicht noch prüfen
-- SELECT * FROM lager LIMIT 100;
DROP TABLE temp;
-- nicht unbedingt notwendig, passiert sonst bei Ende der
-- Sitzung automatisch
-- Transaktion abschließen
END;
|
Füllt man (beispielsweise neue) Tabellen mit sehr vielen Daten,
so ist INSERT langsam. Die schnellste Möglichkeit ist das Füllen
über COPY. Bei sehr vielen Datensätzen spart es auch Zeit, die
Indizes zu löschen und anschließend neu zu erzeugen. Traut man
den Daten, weil diese beispielsweise aus einem Backup kommen, so
bringt es auch oft sehr viel Zeitersparnis, wenn man die Trigger
und Bedingungen löscht und nach dem Füllen wieder neu anlegt.
Eine Erweiterung ist die Möglichkeit CREATE TABLE AS, die eine
Tabelle aus einer SELECT-Abfrage erzeugt. Das ist äquivalent zu
einer INSERT INTO Erweiterung, mit der auch Tabellen erzeugt
werden können (beides ist nicht Standard-SQL). Um standardkonform
zu sein, muss man zunächst ein CREATE TABLE machen und diese dann
über INSERT ... SELECT füllen.
|
Views sehen aus Sicht der Anwendung aus wie Tabellen. Manchmal
werden sie sogar als virtuelle Tabellen bezeichnet. Es sind
Sichten auf Tabellen. Eine View stellt eine Abfrage (ein SELECT
Kommando) dar. Diese Abfrage kann beispielsweise nur einige der
Spalten einer Tabelle enthalten. Die Abfrage kann auch über einen
join mehrere Tabellen verbinden und so Werte aus verschiedenen
Tabellen anzeigen.
Ein großer Vorteil von Views ergibt sich, wenn man sich an die
Privilegien erinnert. Über Views kann man es erreichen, dass nur
bestimmte Felder sichtbar sind. In diesem Fall definiert man
einen View über die erlaubten Felder und gibt dem entsprechenden
Benutzer Rechte auf den View - nicht aber auf die Tabelle.
Momentan können Views so erstmal nur zum Lesen von Daten, nicht
jedoch zum Ändern benutzt werden. Möchte man Daten auch ändern
können, so verwendet man eine PostgreSQL Erweiterung, eine Regel.
Im später später folgenden Abschnitt zu Regeln wird dies
exemplarisch erklärt.
|
Das Cursorkonzept stammt aus eingebettetem SQL (ESQL). Eingebettet
heißt, dass man SQL Anweisungen direkt in Programmquelltexte
einbettet (diese Programmiertechnik wurde inzwischen durch
Standards wie ODBC weitgehend abgelöst; ESQL wird jedoch auch
heute noch verwendet und auch von PostgreSQL unterstützt). In
PostgreSQL stehen Cursors unabhängig von der Verwendung von ESQL
zur Verfügung. Man kann sie beispielsweise auch über
psql
interaktiv verwenden.
Einem aktiven Cursor ist eine Menge von Datensätzen assoziiert,
die über eine Abfrage, also über ein SELECT Kommando, ausgewählt
wurden. Man kann nun einzelne Datensätze oder Teilmengen der
Datensatzmenge über den Cursor holen. Der Cursor merkt sich dabei
die Position. Holt man beispielsweise dreimal einen Datensatz aus
einem Cursor, so erhält man automatisch die ersten drei
Datensätze. Der Cursor zählt sozusagen mit, was auch den Namen
erklärt. Eine Besonderheit ist, dass man über Cursors (in
PostgreSQL, das gilt nicht generell) auch rückwärts gehen kann,
also dass man Datensätze mehrfach holen kann.
Cursors funktionieren in PostgreSQL nur in Transaktionen. Um einen
Cursor zu verwenden, muss dieser zunächst deklariert werden. Man
kann sich vorstellen, dass man einer Abfrage einen (temporären)
Namen gibt. Dann kann man Datensätze (die Ergebnisse der Abfrage)
holen. Man kann den Cursor auch verschieben, beispielsweise, um
Datensätze auszulassen oder erneut zu verarbeiten. Wird der
Cursor nicht mehr benötigt, so wird er mit CLOSE geschlossen.
Besonderheiten in PostgreSQL sind, dass aus einem Cursor nicht
über absolute Positionen gelesen werden kann und das Cursordaten
nicht geändert werden können (es gibt kein DECLARE FOR UPDATE).
Ein Cursor ist also immer READ ONLY. Durch die
Transaktionsforderung ist er auch immer INSENSITIVE, auch wenn dies
nicht explizit mit angeben wurde. Auch SCROLL ist nicht notwendig, da
ein Cursor immer SCROLL kann. Es muss auch kein OPEN auf einen
Cursor gemacht werden.
Ein einfaches Beispiel folgt.
cursor.sql |
-- Die Tabelle sieht so aus:
test=> SELECT code, title FROM films WHERE distributors_id = 124;
code | title
-------+------------------
MM-dt | Mädchen, Mädchen
IJ1 | Indiana Jones 1
IJ2 | Indiana Jones 2
IJ3 | Indiana Jones 3
(4 rows)
-- Transaktion starten
test=> BEGIN;
BEGIN
-- Einen Cursor für Indiana Jones deklarieren.
test=> DECLARE ijfilme INSENSITIVE CURSOR FOR
test-> SELECT code, title FROM films
test-> WHERE code LIKE 'IJ%'
test-> ORDER BY code
test-> FOR READ ONLY;
DECLARE
-- Ersten Datensatz holen
test=> FETCH NEXT FROM ijfilme;
code | title
-------+-----------------
IJ1 | Indiana Jones 1
(1 row)
-- Zweiten Datensatz holen (1 ist wie NEXT)
test=> FETCH 1 FROM ijfilme;
code | title
-------+-----------------
IJ2 | Indiana Jones 2
(1 row)
-- Einen Datensatz zurückgehen:
test=> FETCH -1 FROM ijfilme;
code | title
-------+-----------------
IJ1 | Indiana Jones 1
(1 row)
-- Die nächsten zwei Datensätze holen:
test=> FETCH 2 FROM ijfilme;
code | title
-------+-----------------
IJ2 | Indiana Jones 2
IJ3 | Indiana Jones 3
(2 rows)
-- Hier ist Ende:
test=> FETCH 1 FROM ijfilme;
code | title
------+-------
(0 rows)
-- weit Zurückspringen (an den Anfang)
test=> MOVE -100 FROM ijfilme;
MOVE 3
-- wieder am Anfang
test=> FETCH 1 FROM ijfilme;
code | title
-------+-----------------
IJ1 | Indiana Jones 1
(1 row)
-- Rest holen
test=> FETCH ALL FROM ijfilme;
code | title
-------+-----------------
IJ2 | Indiana Jones 2
IJ3 | Indiana Jones 3
(2 rows)
-- Den letzen nochmal (wie -1)
test=> FETCH PRIOR FROM ijfilme;
code | title
-------+-----------------
IJ3 | Indiana Jones 3
(1 row)
-- Cursor schließen
test=> CLOSE ijfilme;
CLOSE
-- Transaktion abbrechen
test=> ROLLBACK;
ROLLBACK |
|
Ein Index dient dazu, Datensätze mit bestimmten Eigenschaften
schnell zu finden. Hat man beispielsweise eine Tabelle films wie
im Beispiel Tabellen und sucht den Film mit dem code FilmA, so
müsste ja die gesamte Tabelle durchsucht werden (und dazu vor
allem von Festplatte geladen werden), dann müsste jeder code
geprüft werden, ob er denn dem gesuchten entspricht.
Hier verwendet man einen Index. Ein Index gilt für eine bestimmte
Tabellenspalte, also beispielsweise für code. Er kann aber auch
aus mehreren zusammengesetzten Spalten bestehen. Ein Index ist
eine effiziente Speicherung aller code Werte und einem Verweis
auf die Stelle, an der der zugehörige Datensatz gespeichert ist.
Wie genau die Speicherung funktioniert, hängt vom Typ des Index
ab. Es gibt beispielsweise HashIndizes und binäre Bäume.
Sucht man nun FilmA, so wird nur der Index geladen, der ja viel
kleiner ist, als die ganze Tabelle. Es wird an der entsprechenden
Stelle nachgesehen (bei einem Hash geht das bei einer
Gleichoperation mit einem Zugriff), dann direkt die richtige
Stelle (oder die richtigen Stellen) der Tabelle geladen. Das ist
dann wesentlich schneller.
Indizes sind aber nicht immer günstig. Hat man beispielsweise
viele Datensätze, beispielsweise alle, so muss eh sehr viel von
der Tabelle geladen werden. Hier bremst es nur, zusätzlich den
Index zu laden (der Abfrageplaner würde in solchen Fällen den
Index aber automatisch nicht verwenden, weil er das auch weiß,
mehr dazu später). Das gleiche Verhalten kann man auch bei
kleinen Tabellen erwarten (wenn man beispielsweise 100 aus 1000
Datensätzen liest, ist ein Index oft nicht günstig und wird nicht
verwendet). Ein Index verlangsamt auch Änderungen, da nicht nur
die Tabelle, sondern auch der Index aktualisiert werden muss.
Ein Index kann auch Eindeutigkeit (UNIQUE) fordern. Genauer
gesagt, wird Eindeutigkeit in Tabellen garantiert, in dem ein
UNIQUE Index angelegt wird. Dies sollte man aber lieber durch ein
sauberes ALTER TABLE ... ADD CONSTRAINT erledigen. Das dann ein
Index verwendet wird, ist ein Implementierungsdetail von
PostgreSQL.
Die bereits kurz erwähnten Speichertypen von Indizes sind:
BTREE (Lehman-Yao B-Baum), RTREE (R-Baum mit Guttman's "quadratic
split" Algorithmus), HASH (Litwin's lineares hashen) und GIST
(Generalized Index Search Trees, verallgemeinerter Index Suchbaum).
BTREE kann bei den Operationen <, <=, =, >=, > verwendet werden.
RTREE bei den Operationen <<, &<, &>, >>, @, ~=, && und ein HASH bei
=.
Indexes kann man per Hand erzeugen. Dazu gibt es das
nicht-standard SQL Kommando CREATE INDEX. Zum Löschen gibt es
analog DROP INDEX. Ein Index auch hat immer einen Namen. Meistens
setzt man diesen aus Tabellen- und Feldnamen zusammen. Ein
Beispiel für einen Index test1_id_idx über die Spalte id der
Tabelle test1:
CREATE UNIQUE INDEX test1_id_idx ON test1 USING BTREE (id);
Es ist sogar möglich, Indizes für Funktionsergebnisse zu
definieren. Verwendet man beispielsweise oft:
SELECT * FROM test1 WHERE lower(col1) = 'value';
so hilft einem ein Index über col1 hier ja nichts. Man kann aber
einen Index für lower(col1) erzeugen, der dann wieder verwendet
wird:
CREATE INDEX test1_lower_col1_idx ON test1 (lower(col1));
Indizes können auch nur über Teile gelegt werden, in dem man eine
WHERE Bedingung hinzufügt. So kann man beispielsweise sehr
häufige Werte ausklammern und von Geschwindigkeitsvorteilen bei
seltenen Werten profitieren (bei häufigen Werten werden Indizes
oft gar nicht verwendet, weil langsam). Eine genaue Diskussion
würde diesen Rahmen hier jedoch sprengen.
Eine Erweiterung ist die Möglichkeit, Indizes neu zu erstellen.
Oft kann man diese einfach löschen und neu anlegen, was den
Vorteil hat, dass nur die zu lesenden Datensätze gelockt werden.
Hat man jedoch kaputte Indizes, kann man diese mit REINDEX neu
erstellen lassen. Dies wird nur durchgeführt, wenn der Index als
kaputt bekannt ist, oder man FORCE mit angibt.
Es gibt drei Varianten des Kommandos: REINDEX INDEX (erzeugt den
folgenden Index neu), REINDEX TABLE (erzeugt für die folgend
genannte Tabelle alle Indizes neu) und REINDEX DATABASE
(erzeugt für die folgend genannte Datenbank alle Indizes neu).
REINDEX DATABASE my_database FORCE;
Hilft bei Problemen also (was in der Praxis jedoch im Prinzip NIE
benötigt wird; aber wenn, dann hilft das).
|
Man kann sich eigene Funktionen definieren. Hierzu stehen neben
SQL noch weitere Sprachen bereit. SQL ist in manchen Punkten
beschränkt oder umständlich. Hier hilft einem eine Sprache wie
PL/pgSQL oder PL/Perl weiter.
So kann man sich Funktionen schreiben, die beispielsweise
komplizierte Bedingungen prüfen können (vielleicht Quersumme der
ID muss gerade sein). Funktionen kann man auch direkt aufrufen.
In PostgreSQL ruft man selbst definierte Funktionen genauso auf,
die eingebaute: einfach über SELECT. Eine Funktion hallo mit
zwei Parametern könnte man beispielsweise aufrufen:
SELECT hallo(123, 'hallo parameter');
Eine Erweiterung von PostgreSQL ist die Möglichkeit,
Aggregatfunktionen selbst zu definieren (CREATE AGGREGATE).
Aggregatfunktionen sind Funktionen wie min oder max, die
beispielsweise in Gruppierten SELECT Anweisungen verwendet
werden.
|
Trigger sind relativ SQL99 konform (es gibt einfach zu umgehende
Ausnahmen). Trigger können nicht auf einzelne Spalten
angewendet werden. Über einen Trigger kann man vor oder nach den
Ereignissen INSERT, DELETE oder UPDATE auf eine Tabelle eine Funkion
aufrufen (man sagt, der Trigger feuert bei einem Ereignis). Diese
Funktion kann dann die Daten prüfen, ändern oder sonst was
unternehmen. PostgreSQL bietet erweitert dazu auch Regeln
(Rules).
Über Trigger kann man, wie auch mit Bedingungen,
Konsistenzbedingungen realisieren. Werden beispielsweise
Schlüssel geändert, so kann man über einen Trigger vielleicht
abhängige Datensätze entsprechend anpassen.
|
Regeln sind eine PostgreSQL Erweiterung. Ähnlich wie Trigger
reagieren sie auf ein Ereignis SELECT, INSERT, DELETE oder UPDATE
auf eine Tabelle. Optional kann noch eine Bedingung angegeben
werden, die ebenfalls erfüllt sein muss, damit die Regel greift.
Die Regel definiert dann, ob gar nicht passieren soll (NOTHING),
ob zusätzlich oder ob anstatt (INSTEAD) des eigentlichen
Kommandos ein anderes ausgeführt werden soll.
Über Regeln kann man, wie auch mit Bedingungen,
Konsistenzbedingungen realisieren.
Regeln werden bei PostgreSQL oft in Verbindung mit Views
verwendet. Dr.Ruud ( rvtol@isolution.nl) postete eine
beispielhafte Regelschablone:
rules-template.sql |
CREATE VIEW <virtual-table> AS SELECT * FROM <actual-table>;
CREATE RULE <virtual-table>_ins AS ON INSERT TO <virtual-table>
DO INSTEAD
INSERT INTO <actual-table> ( <field-1>, <field-2>, ... , <field-n> )
VALUES ( new.<field-1>, new.<field-2>, ... , new.<field-n> );
CREATE RULE <virtual-table>_upd AS ON UPDATE TO <virtual-table>
DO INSTEAD
UPDATE <actual-table>
SET <field-1> = new.<field-1>,
<field-2> = new.<field-2>,
...
<field-n> = new.<field-n>
WHERE <primary-key> = old.<primary-key>;
CREATE RULE <virtual-table>_del AS ON DELETE TO <virtual-table>
DO INSTEAD
DELETE FROM <actual-table>
WHERE <primary-key> = old.<primary-key>;
|
|
Eine Sequenz ist eine Zählfunktion, die hauptsächlich bei
Autoinkrementfeldern angewendet werden. Bei jedem Aufruf liefert
eine Sequenz einen größeren Wert (dies funktioniert natürlich
auch vollständig in Transaktionen). Man kann auch den letzten
Wert abfragen, den man in der Transaktion erhalten hat und so
herausfinden, welchen Wert der letzte Datensatz im
Autoinkrementfeld erhalten hat.
Sequenzen werden beim Feldern vom Typ serial automatisch
erzeugt. Der Name wird automatisch bestimmt. Es kommt zu einem
Fehler beim Anlegen der Tabelle, wenn der Name bereits vergeben
ist. Man kann ein Sequenz auch für mehrere verschiedene Felder
verwenden, und so tabellenübergreifend eindeutige Werte erzeugen.
Daher werden automatisch erzeugte Sequenzen nicht automatisch mit
dem Löschen von Tabellen gelöscht.
Man kann Sequenzen auch explizit über CREATE SEQUENCE erzeugen.
Die Funktionen nextval('seq') und currval('seq') liefern den
nächsten bzw. aktuellen (zuletzt gelieferte nextval) zurück. Mit
setval('seq', 1234) kann man den Wert eine Sequenz direkt setzen.
Dies braucht man beispielsweise, wenn man IDs hat, die von der
Sequenz noch gar nicht erzeugt wurden, weil jemand einen Wert bei
INSERT direkt angegeben hat. In solchen Fällen erreicht die
Sequenz irgendwann diesen Wert (oder den ersten dieser Werte),
daraufhin klappt das INSERT nicht, weil die ID sicherlich
eindeutig sein muss, die Sequenz wird auch nicht erhöht
(Transaktionsabbruch) und man kommt nicht weiter. Hier hilft es,
die Sequenz auf den höchsten verwendeten Wert zu setzen. Hat man
eine Tabelle lager mit einem Autoinkrementfeld id, so heißt die
automatisch erzeugte Sequenz lager_id_seq. Um diese anzupassen,
kann man einfach schreiben:
test=# SELECT setval('lager_id_seq', (SELECT max(id) FROM lager) ); setval -------- 3 (1 row)
|
Danach funktioniert das Autoinkrementfeld wieder. Vor solchen
Phänomenen kann man sich schützen, wenn man Regeln verwendet, die
ein direktes Setzen solcher Felder verhindern.
|
Neben SQL unterstützt PostgreSQL weitere Datenbanksprachen.
Arbeitet man mit SQL, so kann man bestimmte Dinge teils nur
schwierig formulieren.
SQL ist eine sehr mächtige Sprache, wenn man sie beherrscht. Im
Gegensatz zu prozeduralen Sprachen beschreibt man jedoch keine
Algorithmen. Möchte man beispielsweise alle Werte des Feldes
gehalt einer Tabelle mitarbeiter um 10 Prozent erhöhen, würde
man prozedural formulieren: gehe jeden Datensatz durch, und für
jeden Wert setze Wert gleich Wert mal 1.1. In SQL schreibt man
das jedoch einfach so hin:
UPDATE mitarbeiter SET gehalt = gehalt * 1.1;
Man beschreibt also in etwa Änderungen. Zusätzlich kann man hier
natürlich auch Bedingungen angeben (nur, wenn gehalt kleiner als
5000 ist beispielsweise). Diesen grundlegenden Unterschied muss
man unbedingt verstehen, wenn man mit SQL arbeitet. In der Praxis
sieht man manchmal Skripte, die die Datensätze einer Tabelle
einzeln durchgehen, einen Test machen, und eine Änderung
schreiben. So etwas macht man in der Regel einfach mit einem
passendem SQL Kommando; das hat noch den angenehmen Nebeneffekt,
viel schneller zu sein.
In PostgreSQL kann man auch Unterabfragen verwenden:
Beispiel: Unterabfrage |
UPDATE mitarbeiter
SET gehalt = gehalt +
(SELECT bonus FROM bonustabelle WHERE art = 'weihnachtsgeld');
|
Mit derartigen Konstrukten kann man Operationen durchführen, die
in prozeduralen Sprachen nur sehr umständlich gemacht werden
können.
Die Sprache PL/pgSQL ist im Lieferumfang von PostgreSQL. Sie
ähnelt PL/SQL von Oracle. Diese Sprache ist beliebt, um
Triggerfunktionen zu implementieren. In PL/pgSQL sind
Kontrollstrukturen verfügbar (beispielsweise Schleifen). Diese
Sprache ist an SQL angelehnt und daher sehr leicht erlernbar und
einfach zu benutzen.
Neben Zuweisungen, der Möglichkeit dynamische SQL Kommandos
auszuführen und Bedingungen auszuwerten, stehen mehrere Schleifen
zur Verfügung. Mit FOR kann gezählt oder über Datensätze
iteriert werden, auch mit LOOP und WHILE kann man Schleifen bilden.
Bedingungen sind flexibel (IF-THEN-ELSIF-ELSE). Ein Blick in die
Dokumentation ist sicherlich interessant, PL/pgSQL sollte zum
Handwerkszeug eines Datenbankbenutzers gehören.
Als Beispiel folgt eine Triggerfunktion. Da die gesamte Funktion
in einfache Anführungszeichen eingeschlossen ist, müssen innerhalb
der Funktion alle einfachen Anführungszeichen durch zwei
aufeinanderfolgende ersetzt werden (leider etwas unübersichtlich).
Beispiel: triggerbeispiel.sql |
-- Eine Beispieltabelle für Angestellte
CREATE TABLE emp (
empname text, -- Name
salary integer, -- Gehalt
last_date timestamp, -- Letztes Datum
last_user text -- Letzter Benutzer
);
-- Der Trigger.
-- Es ist eine Funktion, die einen Datensatz zurückliefert.
CREATE FUNCTION emp_stamp () RETURNS OPAQUE AS '
-- Dieses Begin kommt von PL/pgSQL. Es startet keine
-- neue Transaktion!
BEGIN
-- Prüfen, ob empname und salary (Gehalt) angegeben wurde
IF NEW.empname ISNULL THEN
-- RAISE erzeugt einen Fehler (EXCEPTION)
-- Die Transaktion wird dadruch abgebrochen.
RAISE EXCEPTION ''empname darf nicht NULL sein'';
END IF;
IF NEW.salary ISNULL THEN
-- anstelle des % steht dann der Name
RAISE EXCEPTION ''% ohne Gehalt?!'', NEW.empname;
END IF;
-- Wer arbeitet für uns und muss dafür bezahlen?
IF NEW.salary < 0 THEN
RAISE EXCEPTION ''% mit negativen Gehalt?!'', NEW.empname;
END IF;
-- Es wird das letze Änderungsdatum und der Änderungsbenutzer gesetzt.
-- Selbst wenn bei INSERT oder UPDATE last_user angegeben wird, so
-- wird dennoch immer current_user verwendet. Es ist also
-- nicht mehr möglich, einen falschen Eintrag zu erzeugen.
NEW.last_user := current_user;
-- now sollte hier besser als Funktion und besser gegen
-- den standardkonformen Namen current_timestamp ersetzt werden:
-- NEW.last_date := current_timestamp;
NEW.last_date := ''now'';
-- Den (geänderten) Datensatz zurückliefern (wird dann eingetragen)
RETURN NEW;
END;
-- Das END kommt - wie auch BEGIN - von PL/pgSQL und beeinflußt
-- die aktive Transaktion nicht
' LANGUAGE 'plpgsql';
-- Diese Funktion als Trigger setzen. Danach wird sie bei INSERT
-- oder UPDATE automatisch gestartet.
CREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON emp
FOR EACH ROW EXECUTE PROCEDURE emp_stamp();
|
|
PL/Perl kann auf zwei Arten installiert werden (siehe Abschnitt
Sprachen im administrativen Teil). Im trusted Modus kann die
Sprache gefahrlos benutzt werden, darf jedoch nicht alles. So
dürfen zum Beispiel keine externen Module geladen werden. Im
untrusted Modus geht das. Man kann so beispielsweise Mails
verschicken. Da hierdurch jeder, der eine Funktion schreiben
und starten darf, die Unix-Rechte des Unix-Benutzers postgres
(oder unter welchem Benutzer das DBMS läuft) erhält, muss man hier
vorsichtig und sorgfältig arbeiten.
Die Verwendung von PL/Perl ist sehr intuitiv. NULL Werte werden
in Perl als undef dargestellt. Parameter werden wie gewohnt über
$_ erreicht. Zusammengesetzte Datentypen werden als Referenzen
auf Hashes übergeben, was eine sehr komfortable Handhabung
erlaubt.
Fehler werden durch Aufruf der Funktion elog gemeldet. elog
verhält sich analog zu RAISE.
Leider gibt es (noch?) einige Einschränkungen bei der Verwendung.
So kann PL/Perl leider nicht dazu verwendet werden,
Triggerfunktionen zu schreiben. Es ist aber möglich, einen
Trigger in PL/pgSQL zu schreiben, und hier einfach eine PL/Perl
Funktion aufruft.
Beispiel: plperl.sql |
-- Eine Funktion, die den größeren Wert zurückliefert.
-- Ist eine der Werte NULL, so wird der andere zurückgeben.
-- Sind beide NULL, ergibt die Funktion auch NULL
CREATE FUNCTION perl_max (integer, integer) RETURNS integer AS '
my ($a,$b) = @_;
if (! defined $a) {
if (! defined $b) { return undef; }
return $b;
}
if (! defined $b) { return $a; }
if ($a > $b) { return $a; }
return $b;
' LANGUAGE plperl;
-- Ein Beispiel mit einen zusammengesetzten Datentyp (hier employee)
CREATE TABLE employee (
name text,
basesalary integer,
bonus integer
);
-- Als Parameter kommt ein employee, also z.B. ein Datensatz aus
-- dieser Tabelle
CREATE FUNCTION empcomp(employee) RETURNS integer AS '
my ($emp) = @_;
return $emp->{''basesalary''} + $emp->{''bonus''};
' LANGUAGE plperl;
|
|
|
Eine PostgreSQL Erweiterung erlaubt es, das mehrere Clienten sich
synchronisieren. Dazu kann ein Client über LISTEN ein Objekt
beobachten. Ruft ein anderer Client NOTIFY auf diesem Objekt auf,
so wird ersterer (und alle anderen LISTENer) benachrichtigt.
Hat er kein Interesse mehr an Notifikationen, ruft er ein
UNLISTEN auf das Objekt auf. LISTEN ist nicht blockierend; die
Notifikation erfolgt asynchron.
Dies wird wohl selten verwendet und ist nicht portabel. Oft kann
man ähnliches Verhalten auch über Datensatz-Locks über Tabellen
erreichen.
|
Wie im Abschnitt Optimierung mit EXPLAIN noch genauer erklärt
wird, wird eine Abfrage vom Planer in Abfragepläne
umgewandelt. Um sich für den richtigen (also den schnellsten)
Abfrageplan entscheiden zu können, muss beispielsweise geschätzt
werden, wie viele Daten von Festplatte gelesen werden müssen. Das
hängt von der Tabellengröße ab.
Die Tabellengrößen werden von PostgreSQL in einer speziellen
Tabelle pg_class gespeichert. Meistens werden jedoch nicht alle
Datensätze benötigt, sondern nur ein Teil. Dieser wird oft über
eine Bedingung definiert. Dadurch wird es schwierig zu schätzen,
wie viele Daten geladen werden müssen, da man dazu ja wissen muss,
wie oft die Bedingung erfüllt ist.
Um diese Abschätzung durchführen zu können, werden
Statistiktabellen geführt, beispielsweise pg_stats. In diesen
Tabellen werden statistische Informationen über Tabelleninhalte
gespeichert, beispielsweise die häufigsten Werte, die Anzahl der
NULL Werte, die Anzahl der verwendeten Werte (es kann ja Tabellen
mit 1000 Einträgen geben, die nur 4 verschiedene Werte verwenden)
und andere. Mit diesen Informationen errechnet der Planer seine
Abschätzungen.
Die Statistiken werden natürlich nicht ständig aktualisiert, das
wäre ja sehr bremsend (um statistische Korrelation zu berechnen,
muss ja in jedem Fall jeder Datensatz gelesen werden). Statt
dessen werden die Statistiken durch das SQL Kommando ANALYZE oder
VACUUM ANALYZE aktualisiert, dass man demzufolge regelmäßig (zum
Beispiels nachts und nach großen Änderungen) ausführen sollte.
|
Natürlich ist es immer interessant, Abfragen auf Geschwindigkeit
zu optimieren. Bei langsamen Abfragen ist es interessant, den
Grund zu kennen. Vielleicht fehlt ja nur ein Index oder sitzt
ungünstig?
Verarbeitet PostgreSQL eine Abfrage, so wird vom DBMS ein Abfrageplan
erstellt. Dies wird durch den sogenannten Planer erledigt. Dieser
legt fest, in welcher Reihenfolge die Daten organisiert werden
und ob (und welche) Indizes verwendet werden. Dazu prüft er die
verschiedenen Möglichkeiten auf Effizienz. Er erstellt also
erstmal viel Pläne und wählt dann den Plan aus, der die geringsten
Kosten hat, also am schnellsten geht.
Es gibt das EXPLAIN Kommando, das den Abfrageplan für die Abfrage
anzeigt (eine PostgreSQL Erweiterung). Man erhält die geschätzten
Kosten, bis mit der Ausgabe begonnen werden könnte, und die
gesamten Kosten. Als Einheit wird in etwa Festplattenzugriffe
verwendet. Die anderen beiden Zahlen sind die geschätzte Anzahl an
Datensätzen (etwas richtiger ist hier der Begriff Tupel), die
zurückgegeben werden, und die geschätzte Größe eines Datensatzes.
Eine Abfrage besteht aus mehreren Teilen. Die Kosten jedes Teiles
schließen immer die aller nach unten folgenden Teile ein.
Ein paar Beispiele dazu.
regression=# EXPLAIN SELECT * FROM tenk1; NOTICE: QUERY PLAN:
Seq Scan on tenk1 (cost=0.00..333.00 rows=10000 width=148)
|
Man sieht: Es ist ein vollständiges durchgehen der Tabellen tenk1
notwendig (Seq Scan heißt sequentiell). Mit der Ausgabe kann
sofort begonnen werden, nach 333 Zugriffen ist sie nach 10000
Datensätzen beendet. Die 333 Zugriffe entstehen hier übrigens
durch 233 Diskzugriffe und 10000 * cpu_tuple_cost (Voreinstellung
ist 0.01), also 233 + 100 == 333.
regression=# EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 1000; NOTICE: QUERY PLAN:
Seq Scan on tenk1 (cost=0.00..358.00 rows=1007 width=148)
|
Man sieht, das immer noch die gesamte Tabelle gelesen werden
muss. Es werden weniger Datensätze erwartet (natürlich ist der
Wert nur geschätzt und nicht wirklich aussagekräftig). Die Kosten
sind durch die zusätzlich benötigte Vergleichszeit etwas
gestiegen. Es wird immer noch kein Index verwendet, weil er sich
nicht lohnt.
Oft liefert Abfragen jedoch nicht solche Mengen an Daten:
regression=# EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 50; NOTICE: QUERY PLAN:
Index Scan using tenk1_unique1 on tenk1 (cost=0.00..181.09 rows=49 width=148)
|
Hier ist die Bedingung so, dass nur noch 49 Datensätze erwartet
werden. Daher entscheidet der Planer, den Index zu verwenden.
Da hier nur 50 Datensätze erwartet werden, ist die Verwendung
eines Index billiger, obwohl jeder einzelne Datensatz langsamer
geladen wird (Festplatten lesen Folgedaten schneller).
regression=# EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 < 50 regression-# AND t1.unique2 = t2.unique2; NOTICE: QUERY PLAN:
Nested Loop (cost=0.00..330.41 rows=49 width=296) -> Index Scan using tenk1_unique1 on tenk1 t1 (cost=0.00..181.09 rows=49 width=148) -> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.00..3.01 rows=1 width=148)
|
In diesem etwas komplizierteren Beispiel wird zusätzlich eine
zweite Tabelle benutzt, die über einen Join verbunden ist. Man
sieht, das der Planer einen Indexscan ausgewählt hat. Durch den
Join entsteht ein Loop mit zwei Teilen. Zunächst wird die Tabelle
tenk1 über den Index durchgearbeitet. Die Kosten sind natürlich
die gleichen im Beispiel davor (gleiche Bedingung: WHERE unique1
< 50). Mit dem Wert unique2, der aus tenk1 gelesen wurde (genauer
gesagt, sind das ja insgesamt 49 Werte!), wird nun ein passender
Eintrag in tenk2 gesucht. Der Planer erwartet genau einen
Treffer und verwendet daher wieder einen Index (der
glücklicherweise auch verfügbar ist). Diese Teilabfrage gilt für
einen konstanten Wert unique2 (je einen der insgesamt 49). Damit
ist der Zugriff vergleichsweise billig (3).
Die zweite Teilabfrage wird nun für jeden der 49 Werte
durchgeführt. Die Kosten sind also 49 * 3 == 147. Dazu kommen die
181 des vorherigen Teils (der die 49 Werte überhaupt erstmal
lieferte), macht zusammen 147 + 181 == 328. Dazu kommt noch etwas
Rechenzeit für den Join (hier ca. 2). Macht dann zusammen 330.
330 sind auch die Kosten, die der Planer für den Loop
ausgerechnet hat. Es 49 Datensätze (es wird ja erwartet, das
jeweils ein Datensatz passt), nur das die etwas größer sind, also
vorher (sind ja durch einen Join verbunden).
Der Planer hat sich entschieden, einen nested-loop join (etwa:
geschachtelte Schleife) zu verwenden. Man kann über Variablen
den Planer beeinflussen. In der Praxis bringt das so gut wie nie
Vorteile. Beispielsweise kann man dem Planer sagen, dass er
nested-loop join nicht verwenden soll:
regression=# set enable_nestloop = off; SET VARIABLE
***layout: achtung, zwei Zeilen Prompt! regression=# EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 < 50 regression-# AND t1.unique2 = t2.unique2; NOTICE: QUERY PLAN:
Hash Join (cost=181.22..564.83 rows=49 width=296) -> Seq Scan on tenk2 t2 (cost=0.00..333.00 rows=10000 width=148) -> Hash (cost=181.09..181.09 rows=49 width=148) -> Index Scan using tenk1_unique1 on tenk1 t1 (cost=0.00..181.09 rows=49 width=148)
|
Der Planer kommt nun mit einem anderen Plan. Zunächst werden
wieder die 49 Datensätze aus tenk1 mit Indexunterstützung
geladen. Die Werte werden nun aber erst alle geladen, und in
einem Hash gespeichert. Man sieht das gut an den Anfangskosten
für den Hash: sie entsprechen den Gesamtkosten für den Indexscan
(da der Hash anschließend gebaut wird, und sehr schnell fertig
ist).
Anschließend wird die Tabelle tenk2 sequentiell durchsucht, ob
irgendwo der Wert unique2 aus der Tabelle zu einem der im Hash
gespeicherten Werte passt. Dies muss ja nun für alle 10.000
Datensätze gemacht werden (nestloop ist ja verboten).
Sobald mit dem Scan über tenk2 begonnen wurde, sind die ersten
Treffer zu erwarten. Die Anfangskosten des Joins entsprechen also
den Kosten, die anfallen, bis mit tenk2 begonnen werden kann
(vorher kommt ja keine Ausgabe), also den Gesamtkosten des
ersten Indexscans. Dazu kommen die 333 für den sequentiellen Scan
über tenk2, macht 514.09. Die restlichen 50 gehen für
Rechenleistung drauf; schließlich muss mit jedem der 10000
Datensätze eine Test auf den vorher gespeicherten Hash gemacht
werden. Die erwarten Kosten sind wesentlich höher als vorhin,
daher hat der Planer vorhin auch einen nestloop verwendet.
Weitere Variablen, die bestimmte Pläne vermeiden, sind:
ENABLE_HASHJOIN, ENABLE_INDEXSCAN, ENABLE_MERGEJOIN, ENABLE_SEQSCAN
ENABLE_SORT und ENABLE_TIDSCAN. Auch diese können auf off
gesetzt werden, um anzuzeigen, dass sie zu vermeiden sind. Wie
bereits gesagt, lassen sich nur schwer Fälle konstruieren, wo das
was bringt.
EXPLAIN kann auch um ANALYZE erweitert werden. Dann wird die
Abfrage tatsächlich ausgeführt, und auch die wirklichen Werte
werden ausgegeben.
regression=# EXPLAIN ANALYZE regression-# SELECT * FROM tenk1 t1, tenk2 t2 regression-# WHERE t1.unique1 < 50 AND t1.unique2 = t2.unique2; NOTICE: QUERY PLAN:
Nested Loop (cost=0.00..330.41 rows=49 width=296) (actual time=1.31..28.90 rows=50 loops=1) -> Index Scan using tenk1_unique1 on tenk1 t1 (cost=0.00..181.09 rows=49 width=148) (actual time=0.69..8.84 rows=50 loops=1) -> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.00..3.01 rows=1 width=148) (actual time=0.28..0.31 rows=1 loops=50) Total runtime: 30.67 msec
|
Hier wird die tatsächlich benötigte Zeit in Millisekunden
angezeigt. Man erkennt auch, dass der Planer sich gering
verschätzt hat, anstatt 49 Datensätzen sind es 50. Es läßt sich
abschätzen, das ein Festplattenzugriff (die Einheit von
EXPLAIN) hier in etwa 10 Millisekunden dauert.
Dies mag als Einführung ausreichen. Das Verstehen dieser Ausgaben
erfordert Übung. Man kann so erkennen, ob und wann Indexe
verwendet werden, ob sie günstig sind, oder vielleicht gar nicht
benötigt sind. Dann sollte man sie löschen, dass spart Zeit bei
Aktualisierungen.
|
|