Die PostgreSQL-Datenbank

Postmodern

von Patrick Gundlach


PostgreSQL ist ein modernes Datenbanksystem, das sich hinter bekannten kommerziellen Systemen in Punkto Leistung, Stabilität und Flexibilität nicht zu verstecken braucht. In diesem Artikel wird die Installation des Systems besprochen, eine Beispieldatenbank eingerichtet und ein Perl-Programm vorgestellt, um Daten auszuwerten.

PostgreSQL zeichnet sich unter anderem durch seine Typenvielfalt aus. Neben den üblichen SQL-Typen (int, char, etc.) gibt es bei PostgreSQL auch geometrische Typen wie Kreise und Polygone. Außerdem können aus den vorhandenen Typen neue, zusammengesetzte Typen geschaffen werden. Das errinert an Records in Pascal oder Structs in C.

Geschichte

Die erste Version von Postgres wurde im Jahr 1989 vorgestellt. 1994 wurde Postgres um die Abfragesprache SQL (Structured Query Language) erweitert und der Name wurde auf Postgres95 geändert. Kurze Zeit später, im Jahr 1996 hat man wieder eine Namensänderung in PostgreSQL vorgenommen. Inzwischen liegt PostgreSQL (im Folgenden einfach Postgres genannt) für sehr viele Unix-ähnliche Plattformen vor. Dank des freien Quelltextes (Postgres unterliegt dem Berkley Copyright) wird die Datenbank stetig weiterentwickelt und besitzt eine sehr hohe Stabilität. Leider gibt es noch keine Versionen für MacOS oder Windows.

Installation

Um Postgres einzurichten, ist es wichtig, erst einmal die Dateien unter /etc/postgresql/ zu ändern. Alle Konfigurationsdateien sind sehr gut dokumentiert und sollten keine größeren Schwierigkeiten bereiten. Damit Postgres funktioniert, muß der postmaster-Daemon laufen. Das kann man z.B. mit dem Befehl

$ ps ax | grep postmaster

überprüfen. Erscheint hier keine Ausgabe, so muß das Programm postmaster als Benutzer postgres gestartet werden. Läuft postmaster, dann sollte als erstes ein Benutzer für die Datenbanken eingerichtet werden. Hier wird der Benutzer pg, der schon als normaler Unix-User existiert, eingerichtet (als root ausführen):

$ su postgres
$ createuser pg

Jetzt müssen ein paar Fragen beantwortet werden:

Enter user's postgres ID or RETURN to use unix user ID: 1000 -> [RETURN]
Is user "pg" allowed to create databases (y/n)                  [y]
Is user "pg" allowed to add users? (y/n)                        [n]

Die eingegebenen Antworten finden sich in den eckigen Klammern. createuser meldet dann hoffentlich:

createuser: pg was successfully added

Damit ist der Benutzer eingerichtet, kann sich einloggen und

source /etc/postgresql/postgresql.env

ausführen. Danach kann mit dem tool createdb eine neue Datenbank eingerichtet werden. Was gibt es spannenderes als eine CD-Datenbank?

Einrichten einer Datenbank

$ createdb cddb

errichtet eine neue Datenbank mit Namen cddb. Jetzt kann das Postgres eigene Abfragetool gestartet werden:

$ psql cddb

Postgres begrüßt den Benutzer mit der folgenden Ausgabe:

Welcome to the POSTGRESQL interactive sql monitor:
  Please read the file COPYRIGHT for copyright terms of POSTGRESQL

   type \? for help on slash commands
   type \q to quit
   type \g or terminate with semicolon to execute query
 You are currently connected to the database: cddb

cddb=>

Die letzte Zeile gibt den Namen der aktuellen Datenbank an (cddb) und der Doppelpfeil (=>) sagt, daß jetzt SQL-Befehle oder Spezialbefehle, die mit einem Backslash (\) eingeleitet werden, eingegeben werden können. SQL-Befehle werden grundsätzlich mit einem Semikolon beendet. Sie können sich auch über mehrere Zeilen erstrecken. Dann schreibt psql einen einfachen Pfeil (->) an den Anfang der Zeile.

In die CD-Datenbank soll folgendes eingetragen werden: Band-Namen, Titel, Erscheinungsdatum und eine Markierung, ob es sich bei dem Titel um eine Doppel-CD handelt oder nicht. Die Markierung kann die Werte t (true) oder f (false) erhalten. Ein Beispieleintrag sieht so aus:

Deep Purple, Stormbringer,  1974, f

Jetzt kommt es häufiger vor, daß von einer Band mehrere CDs vorhanden sind. Um nicht für jede CD den Bandnamen immer wieder eingeben zu müssen, werden die Daten aufgeteilt. In einer Tabelle werden die Band-Namen zusammen mit einer dazugehörigen Ziffer gespeichert. In der anderen Tabelle wird der Titel der CD vermerkt, sowie eine Ziffer, die auf den entsprechenden Band-Namen verweist. Diese Ziffer wird auch (Fremd-)Schlüssel genannt. Zum einen wurde damit Speicherplatz gespart, (jeder Bandname wird nur einmal gepeichert und nicht für jede CD) zum anderen gibt es weniger Möglichkeiten für Fehler. Wird z.B. die Schreibweise einer Band korrigiert, so werden damit garantiert alle dazugehörigen CDs abgedeckt. Diesen Prozeß (Aufteilen der Daten in verschiedene Tabellen) nennt man ,,normalisieren`` und er ist eines der wichtigsten Kriterien für eine gut zu handhabende Datenbankstruktur. Mehr zu solchen Überlegungen finden sich unter anderem in [1] und [2].

Die Tabellen werden mit SQL-Befehlen eingerichtet. Als Einführung in die Sprache SQL kann ich [3] und [4] empfehlen. In der Regel wird bei Postgres nicht zwischen Groß- und Kleinschreibung unterschieden. In vielen Lehrbüchern (und hier) werden SQL-Kommandos der Übersichtlichkeit halber oft in Großbuchstaben geschrieben. Die Tabelle bands wird nun mit dem folgenden Befehl erstellt:

cddb=> CREATE TABLE bands (
cddb->   BandID   int,
cddb->   BandName text );

Wird die Tabelle erfolgreich angelegt, so antwortet Postgres mit

CREATE

Jetzt müssen wir noch die Tabelle für die einzelnen Titel anlegen:

cddb=> CREATE TABLE cd (
cddb->   CdID   int,
cddb->   BandID int,
cddb->   CdName text,
cddb->   ErschDatum int,
cddb->   DoppelCd  bool );

Eine Übersicht über die erstellten Tabellen erhält man mit dem Befehl \d:

cddb=> \d

Database    = cddb
 +------------------+----------------------------------+----------+
 |  Owner           |             Relation             |   Type   |
 +------------------+----------------------------------+----------+
 | pg               | bands                            | table    |
 | pg               | cd                               | table    |
 +------------------+----------------------------------+----------+

Dann können die ersten Bandnamen eingetragen werden:

cddb=> INSERT INTO bands
cddb->      VALUES (1,'Judas Priest');

Postgres antwortet mit einer Ausgabe wie INSERT 18378 1. Der Inhalt der neu erstellten Tabelle kann mit dem Befehl

cddb=> SELECT * FROM bands;

betrachtet werden:

bandid|bandname
------+------------
     1|Judas Priest
(1 row)

Dazu passend die Titel eintragen:

cddb=> INSERT INTO cd
cddb->   VALUES (1,1,'Unleashed in the East',1979,'f');
cddb=> INSERT INTO cd
cddb->   VALUES (2,1,'Hero, Hero',1995,'f');

Um eine Tabelle zu erhalten, bei der die Bandnamen zusammen mit den Titeln auftauchen, muß eine etwas kompliziertere Abfrage geschrieben werden, da die Informationen in zwei verschiedenen Tabellen gespeichert sind:

cddb=> SELECT BandName, CdName FROM cd,bands WHERE cd.BandID=bands.BandID;
bandname    |cdname
------------+---------------------
Judas Priest|Unleashed in the East
Judas Priest|Hero, Hero
(2 rows)

Für interessantere Abfragen geben wir weitere Daten ein:

cddb=> INSERT INTO bands VALUES (2,'Deep Purple');
cddb=> INSERT INTO cd VALUES (3,2,'Who do you think we are?',1973,'f');
cddb=> INSERT INTO cd VALUES (4,2,'Stormbringer',1974,'f');
cddb=> INSERT INTO bands VALUES (3,'Fish');

Bei größeren Datenmengen lohnt es sich einzelne Reihen, zum Beispiel die Reihe CdID, zu indizieren. Das heißt, daß Postgres eine zusätzliche Tabelle anlegt, mit der es Datensätze anhand der CdID wesentlich schneller findet. Wenn ein neuer Eintrag in die Tabelle kommt, wird jetzt auch die Tabelle mit dem Index erweitert. Dadurch dauert das Anlegen eines neuen Datensatzes zwar etwas länger, diese werden dafür aber deutlich schneller gefunden. Ein anderes Problem besteht auch in der Vergabe der Schlüssel BandID und CdID. Es gibt keine Vorkehrung dafür, daß die Zahlen bei den Einträgen nicht doppelt vorkommen. Daher müß Postgres angewiesen werden, auf doppelte Zahlen zu achten, und sie dem Benutzer verbieten. Damit keine CdID doppelt vergeben werden kann, wird ein Index mit eindeutigen Schlüsseln erzeugt:

cddb=> CREATE UNIQUE INDEX I_CdID ON cd(CdID);

Jetzt kann keine CdID mehr doppelt vergeben werden. So wird der Eintrag

cddb=> INSERT INTO cd
cddb->   VALUES (4,3,'Internal Exile', 1991);

quittiert mit der Fehlermeldung:

ERROR:  Cannot insert a duplicate key into a unique index

Bei richtiger CdID hingegen funktioniert alles bestens:

cddb=> INSERT INTO cd
cddb->   VALUES (5,3,'Internal Exile', 1991);

Dann erzeugen wir einen eindeutigen Schlüssel für die BandID und einen Fremdschlüssel:

cddb=> CREATE UNIQUE INDEX I_BandID ON bands(BandID); 
cddb=> CREATE INDEX I_cd_BID ON cd(BandID);

Beim Betrachten der Daten fällt auf, daß vergessen wurde, die Spalte für die Doppelcd auszufüllen. Mit dem UPDATE-Kommando holen wir das nach:

cddb=> UPDATE cd     
cddb->  SET doppelcd = 'f'
cddb->  WHERE cdid = 5;

Ausgabe der Informationen

Eine Datenbank, in der wir Informationen speichern, nützt nichts, wenn man sich nicht die Daten ausgeben lassen kann. Natürlich könnte man jederzeit das Programm psql starten und die entsprechenden Abfragen starten. Aber wofür hat Postgres so schöne Schnittstellen für höhere Programmiersprachen? Man könnte zum Beispiel ein kleines Perl -Programm schreiben, und sich jederzeit eine aktuelle Übersicht über die vorhandenen CDs ausgeben zu lassen. Dazu wird ein passender DBI-Treiber benötigt (DBD-Pg). Es gibt auch noch eine andere Möglichkeit mit Perl Postgres-Datenbanken abzufragen, und zwar mit der Pg-Erweiterung. DBI hat den entscheidenden Vorteil, auch mit anderen Datenbanksystemen zusammen zu arbeiten, ohne daß die Syntax im Programm geändert werden muß. Ob der DBI-Treiber vorhanden ist, kann mit einem kurzen Perl Programm getestet werden (siehe Listing 1).

Listing 1: Ist ein PostgreSQL DBI-Treiber vorhanden?

#!/usr/bin/perl
use DBI;
@ary= DBI-> available_drivers; 
print "@ary\n";

Die Ausgabe sollte die Zeichenfolge Pg beinhalten. Folgende Ausgabe erhalte ich beim Programmaufruf auf meinem System:

ExampleP NullP Sponge Pg

Sollte Pg nicht in der Ausgabe erscheinen, so muß der Treiber installiert werden. Einfach vom CPAN-Server das entsprechende Paket [5] holen, entpacken und das README befolgen. Jetzt steht dem ersten Bericht nichts mehr im Wege. Das in Listing 2 beschriebene Perl-Skript gibt eine Tabelle ähnlich wie psql aus:

Listing 2: Ein erster Bericht mit Perl

#! /usr/bin/perl 
use DBI;                            # Initialisierung

$dbname='cddb';
$data_source="dbi:Pg:dbname=$dbname";$user='pg'; $password='';

$dbh = DBI -> connect ($data_source, $user, $passowrd ) 
    or die "Kann keine Verbindung zu $data_source herstellen: $DBI::errstr";

my $sth = $dbh->prepare( q{
    SELECT BandName, CdName 
        FROM cd,bands WHERE cd.BandID=bands.BandID}) 
    or die "Kann die Abfrage nicht vorbereiten: $DBI::errstr";

my $rc= $sth -> execute 
    or die "Kann die Abfrage nicht ausführen: $DBI::errstr";

printf ("%15.15s | %-30.30s\n","Bandname", "CD Titel");
print "----------------+-------------------------------\n";

while (($bandname, $cdname) =$sth->fetchrow_array){
    printf ("%15.15s | %-30.30s\n", $bandname,$cdname);
}

warn $DBI::errstr if $DBI::err;
$sth->finish;

Das Programm besteht aus drei Teilen. Als erstes wird versucht, eine Verbindung zu Postgres herzustellen. Dann wird die Abfrage vorbereitet und ausgeführt. Als letztes werden die Daten aus der Datenbank ausgelesen und auf dem Bildschirm ausgegeben. Die Dokumentation zu den speziellen Datenbankbefehlen kann man sich mit den Kommandos perldoc DBI und perldoc DBD::Pg für den Postgres-Treiber anschauen. Als weitere Möglichkeit bieten sich graphische Frontends für Datenbanken an. So kann man interaktive Abfragen tätigen, ohne daß der Benutzer SQL-Kenntnisse haben muß. Solche Systeme werden auch als Datensystem der 4. Generation bezeichnet. Als kleinen Einblick in diese Möglichkeiten möchte ich ein kurzes Programm vorstellen (siehe Listing 3), das ein Fenster mit den in der Datenbank gespeicherten Bandnamen anzeigt. Wählt man einen Eintrag in dieser Liste aus (Doppelklick mit der linken Maustaste), so wird die Datenbank erneut abgefragt; diesmal mit dem Bandnamen als Parameter. Das Ergebnis wird in einem zweiten Fenster dargestellt (siehe Abb. 1).


Abb. 1: Datenbankabfrage mit Perl/TK

Das Programm benötigt eine aktuelle Perl/Tk-Bibliothek, die zum Beispiel unter [6] im Netz zu finden ist. Auf das spannende Thema Perl/Tk möchte ich hier nicht eingehen, daher habe ich so wenig wie möglich davon in das Programm integriert (Ein Bericht von einem anderen Autor zu diesem Thema ist allerdings in Planung). Die Bandnamen werden in eine Liste eingetragen. Sobald der Benutzer einen dieser Einträge auswählt, wird nachgeschaut, welcher Eintrag aktiv ist. Dieser Eintrag wird in der Variable $bname zwischengespeichert und für die Abfrage aufbewahrt. Dort wird mit WHERE bandname = '$bname' erreicht, daß Postgres nur die Einträge zurückliefert, die dieses Kriterium erfüllen.

Listing 3: Perl/Tk-Datenbankprogramm

#! /usr/bin/perl 

use DBI;
use Tk;

$top = MainWindow->new();
$band_list=$top-> Listbox ("width" => 30, "height" => 6)-> pack();
$cd_list=$top->  Listbox ("width"=> 30, "height" => 6 )-> pack();

$dbname='cddb';
$data_source="dbi:Pg:dbname=$dbname";$user='pg'; $password='';

$dbh = DBI -> connect ($data_source, $user, $passowrd ) 
    or die "Kann keine Verbingung zu $data_source herstellen: $DBI::errstr";

abfrage ("SELECT BandName FROM bands ORDER BY BandName");

while ($bandname =$sth->fetchrow_array){
    $band_list -> insert ('end', $bandname);
}

warn $DBI::errstr if $DBI::err;
$sth->finish;


$band_list->bind ('<Return>', \&getcdtitel);
$band_list->bind ('<Double-1>', \&getcdtitel);
$band_list->bind ('<q>', \&raus);
MainLoop();

sub abfrage {
    local ($q)=@_;
    $sth = $dbh->prepare($q) 
        or die "Kann die Abfrage nicht vorbereiten: $DBI::errstr";
    $rc= $sth -> execute 
        or die "Kann die Abfrage nicht ausführen: $DBI::errstr";
}


sub getcdtitel {
    my $bname=$band_list ->get('active');

    # Lösche die Liste, die vorher angzeigt wurde 
    $cd_list -> delete(0, $cd_list -> index('end'));

    # Passende CD Namen zu den Bandnamen (siehe Text)
    abfrage ("SELECT cdname FROM cd WHERE bandid IN
             (SELECT bandid FROM bands WHERE bandname = '$bname')");
    
    # und die gefundenen CD Titel in die Liste schreiben
    while (($cdtitel) =$sth->fetchrow_array){
        $cd_list-> insert ('end', $cdtitel); 
    }
}

sub raus { exit };

Kleine Datenbanken lassen sich mit Postgres schnell erstellen und mit gut angepaßten Programmen abfragen. Bei größeren Projekten ist es notwendig, sich vorher eine genaue Datenstruktur zu überlegen. Dann wird es immer wichtiger, die Theorie zu beherrschen. Postgres bietet sich dank der einfachen Installation und des geringen Resourcenbedarfs auch für Einsteiger an.

Infos

[1] H. Ebert, Scharfer Datenmix - c't 1993, Heft 9
[2] C. Zehnder, Informationssysteme und Datenbanken, Stuttgart: Teubner, 1989
[3] D. Petkovic, SQL - die Datenbanksprache, Hamburg: McGraw-Hill, 1990
[4] http://w3.one.net/~jhoffman/sqltut.htm
[5] ftp://ftp.gwdg.de/pub/languages/perl/CPAN/modules/by-module/DBD/DBD-Pg-0.89.tar.gz
[6] ftp://ftp.gwdg.de/pub/languages/perl/CPAN/modules/by-module/Tk/Tk800.012.tar.gz

Der Autor

Patrick Gundlach studiert Informatik in Dortmund. Wenn er nicht gerade - vom Pinguin überwacht - Xkobo spielt, macht er gerne mit seiner XT600 die Strecken ums Ruhrgebiet unsicher. Zu erreichen ist er unter gundlach@irb.cs.uni-dortmund.de.

Copyright © 1999 Linux-Magazin Verlag