6. Fluggäste

Fluggäste werden durch Personen repräsentiert, die an verschiedenen Flügen teilnehmen können. Diese Abstraktion der Fluggäste vermeidet Mehrfachspeicherungen von Personendaten. Daten, die mehrfach gespeichert werden können immer zu Inkonsistenzen der Daten führen. Wird bei der Änderung eines Datenwertes eine Mehrfachspeicherung Kopie vergessen, so wird dadurch ein neuer Datensatz erzeugt, der durch die ausgebliebene Änderung bereits veraltet ist.

Bevor wir eine Tabelle zur Verwaltung von Personen anlegen, definieren wir noch einen Datentypen, der die Anrede der Person gewährleistet.

CREATE TYPE address AS ENUM ('MS', 'MR');

Der DML-Befehl CREATE TYPE erzeugt den Datentyp address als Aufzählung (enumaration). Eine Variable vom Typ address kann entweder den Wert MS (Miss) oder MR (Mister) annehmen.

CREATE TABLE persons (
 person_id serial,
 address address,
 prename varchar(128) NOT NULL,
 surname varchar(128)NOT NULL,
 nationality char(2) NOT NULL,
 date_of_birth date NOT NULL,
 passport_number varchar(256) NOT NULL,
 CONSTRAINT pk_person_id PRIMARY KEY (person_id),
 CONSTRAINT fk_nationality FOREIGN KEY (nationality) REFERENCES countries (country_id)
);

Als Primärschlüssel verwenden wir hier den Surrogat-Schlüssel person_id vom Datentyp serial. Surrogat-Schlüssel werden der Einfachheit halber häufig Tabellen hinzugefügt. Sie versehen jeden Datensatz mit einer eindeutigen Zahl. Da diese Zahl nur zum internen Gebrauch gedacht ist, muss der Fall einer Änderung eines Primärschlüssel meist nicht überdacht werden. Außerdem lassen sich dadurch auch aus mehreren Feldern zusammengesetzte Primärschlüssel vermeiden. Zudem speichert die Tabelle persons noch die Anrede, den Vor-, Nachnamen, die Nationalität, das Geburtsdatum und die Passnummer einer Person. Das Geburtsdatum ist vom Datentyp date. Dieser Datentyp umfasst keinen Zeitanteil. Die Nationalität wird über den Fremdschlüssel nationality ausgedrückt.

Die Definition eines Datenbankfeldes vom PostgreSQL eigenen Datentyp serial hat einen Nebeneffekt. Zugleich wird ein Zähler (Sequenz) angelegt, dessen aktueller Wert in das Feld übertragen werden kann. Dazu wird der DDL-Befehl CREATE SEQUENCE verwendet.

INSERT INTO persons (address, prename, surname, nationality, date_of_birth, passport_number) VALUES ('MR', 'Andreas', 'Meier', 'DE', '01.01.1970', 'M7020100');
INSERT INTO persons (address, prename, surname, nationality, date_of_birth, passport_number) VALUES ('MS', 'Manina', 'Lehmann', 'DE', '23.12.1950', 'F5037945');
INSERT INTO persons (address, prename, surname, nationality, date_of_birth, passport_number) VALUES ('MS', 'Anja', 'Pfeiffer', 'DE', '05.06.1969', 'F6948129');
INSERT INTO persons (address, prename, surname, nationality, date_of_birth, passport_number) VALUES ('MS', 'Alexandra', 'Hofer', 'AT', '12.11.1974', 'A00G221');

Eine Eigenschaft des Datentyps serial besteht darin, dass der aktuelle Wert des Zählers in das Feld von diesem Typ eingefügt wird, sofern kein Wert durch eine INSERT INTO-Anweisung gegeben ist. Anschließend wird der Zähler erhöht. Um die automatische Belegung des Feldes person_id zu überprüfen fragen wir über folgende SELECT-Anweisung alle Flughäfen ab.

SELECT * FROM persons;

 person_id | address |  prename  | surname  | nationality | date_of_birth | passport_number
-----------+---------+-----------+----------+-------------+---------------+-----------------
         1 | MR      | Andreas   | Meier    | DE          | 1970-01-01    | M7020100
         2 | MS      | Manina    | Lehmann  | DE          | 1950-12-23    | F5037945
         3 | MS      | Anja      | Pfeiffer | DE          | 1969-06-05    | F6948129
         4 | MS      | Alexandra | Hofer    | AT          | 1974-11-12    | A00G221
(4 rows)

Zur Teilnahme an einem Flug werden die Personen als Fluggäste aufgeführt. Dies geschieht durch die Tabelle passangers.

CREATE TABLE passangers (
 flight_number varchar NOT NULL,
 person_id integer NOT NULL,
 checkin boolean DEFAULT false,
 CONSTRAINT pk_flight_number_person_id PRIMARY KEY (flight_number, person_id),
 CONSTRAINT fk_flight_number FOREIGN KEY (flight_number) REFERENCES flights (flight_number),
 CONSTRAINT fk_person_id FOREIGN KEY (person_id) REFERENCES persons (person_id)
);

Da Personen nur einmal an einem Flug teilnehmen dürfen, ist die Angabe aus Flugnummer und Person eindeutig. Wir verwenden beide Felder zusammen als zusammengesetzten Primärschlüssel.

CONSTRAINT pk_flight_number_person_id PRIMARY KEY (flight_number, person_id)

Neben den Fremdschlüsseln flight_number und person_id enthält die Tabelle passangers noch das Feld checkin vom Datentyp boolean. Dieses Feld gibt an, ob ein Passagier bereits eingecheckt ist. Beim Anlegen des Datensatzes ist der Passagier auf keinen Fall eingecheckt. Deshalb verwenden wir den Initialisierungswert false.

INSERT INTO passangers (flight_number, person_id) VALUES ('AB 8510', 1);
INSERT INTO passangers (flight_number, person_id) VALUES ('AB 8550', 1);
INSERT INTO passangers (flight_number, person_id) VALUES ('AB 8510', 4);
INSERT INTO passangers (flight_number, person_id) VALUES ('LH 456', 1);
INSERT INTO passangers (flight_number, person_id) VALUES ('LH 456', 2);
INSERT INTO passangers (flight_number, person_id) VALUES ('LH 456', 3);
INSERT INTO passangers (flight_number, person_id) VALUES ('LH 456', 4);
INSERT INTO passangers (flight_number, person_id) VALUES ('LH 457', 1);
INSERT INTO passangers (flight_number, person_id) VALUES ('LH 457', 3);
INSERT INTO passangers (flight_number, person_id) VALUES ('LH 457', 4);
INSERT INTO passangers (flight_number, person_id) VALUES ('AB 9172', 1);
INSERT INTO passangers (flight_number, person_id) VALUES ('AB 9172', 2);
INSERT INTO passangers (flight_number, person_id) VALUES ('AB 9172', 4);
INSERT INTO passangers (flight_number, person_id) VALUES ('AB 9143', 1);
INSERT INTO passangers (flight_number, person_id) VALUES ('AB 9143', 2);
INSERT INTO passangers (flight_number, person_id) VALUES ('AB 9143', 4);

Um die eingefügten Daten in beiden Tabellen zu überprüfen verwenden wir wiederum eine SELECT-Anweisung, die uns die Anzahl von Fluggästen nach Ländern auszählen soll.

SELECT co.name AS country,
 count(person_id) AS total
FROM countries co
 LEFT JOIN persons ps ON (ps.nationality = co.country_id)
 LEFT JOIN passangers pg USING (person_id)
GROUP BY co.name
HAVING count(person_id) > 0
ORDER BY total DESC;

 country | total
---------+-------
 Germany |    11
 Austria |     5
(2 rows)

In dieser SELECT-Anweisung werden drei Tabellen miteinander verbunden. Zunächst werden die Tabellen countries und persons über die JOIN-Bedingung ON (ps.nationality = co.country_id) mittels eines LEFT JOINes verbunden. Diese Ergebnismenge wird zudem über einen LEFT JOIN mit der Tabelle passangers mittels der JOIN-Bedingung USING (person_id) verbunden. Die so gebildete Ergebnismenge wird durch die GROUP BY-Bedingung nach den Namen der Länder gruppiert und ausgezählt. Wiederum zählt die Aggregatfunktion count ein Primärschlüssel, person_id, so dass sich für Länder ohne Fluggäste eine 0 ergibt. Die HAVING-Bedingung schließt nun wiederum diese Länder aus. Die HAVING-Bedingung kann nur in Kombination mit der GROUP BY-Bedingung eingesetzt werden. Sie dient der Filterung der gruppierten Ergebnismenge.