Bietet Rezepte, Lösungen und ausführliche Beispiele rund um gesundes SQL und zufriedene Datenbanken.
Nie gab es mehr Kontaktmöglichkeiten als heute und ständig kommen neue dazu. Bedeutet dies, ständig die Tabellen zu erweitern und jede Menge Leerfelder mit sich herum zu schleppen? Nein, mit dem richtigen Datenmodell und dem OUTER JOIN Befehl stehen wir als Datenbankentwickler dem gelassen gegenüber.
Um sicherzustellen, dass die verwendeten "eins zu viele" Fremdschlüsselbeziehungen (FOREIGN KEY) immer korrekte Ergebnisse liefern, wird die MySQL ENGINE InnoDb verwendet. Grundlage für stabile Fremdschlüsselbeziehungen sind die ON DELETE CASCADE bzw. ON DELETE RESTRICT Regeln.
* MySQL 5.1.44
/* Kontaktbezeichnungen */
CREATE TABLE kontakt_bez (
kontakt_bez_id INT NOT NULL,
bezeichnung VARCHAR(50) NOT NULL,
PRIMARY KEY (kontakt_bez_id)
) ENGINE = InnoDB;
/* Personen */
CREATE TABLE person (
person_id INT NOT NULL,
nachname VARCHAR(50) NOT NULL,
PRIMARY KEY (person_id)
) ENGINE = InnoDB;
/* person_hat_kontakt */
CREATE TABLE person_kontakt (
person_kontakt_id INT NOT NULL,
person_id INT NOT NULL,
kontakt_bez_id INT NOT NULL,
wert VARCHAR(50) NOT NULL,
bemerkung VARCHAR(50),
PRIMARY KEY (person_kontakt_id),
CONSTRAINT fk_person
FOREIGN KEY (person_id)
REFERENCES person (person_id)
ON DELETE CASCADE,
CONSTRAINT fk_kontakt_bez
FOREIGN KEY (kontakt_bez_id)
REFERENCES kontakt_bez (kontakt_bez_id)
ON DELETE RESTRICT
) ENGINE = InnoDB;
Nun noch die Testdaten dazu einlesen. Beginnen wir mit den Kontaktarten. Diese Tabelle ist natürlich beliebig erweiterbar und damit haben wird auch kein Problem mit zukünftigen Ergänzungen.
INSERT INTO kontakt_bez VALUES
( 1, 'Telefon privat' ),
( 2, 'Telefon geschäftlich' ),
( 3, 'Telefax geschäftlich' ),
( 5, 'E-Mail privat' ),
( 6, 'E-Mail geschäftlich' ),
( 7, 'Mobil privat' ),
( 8, 'Mobil geschäftlich' ),
( 9, 'Web-Seite' ),
(10, 'Facebook' ),
(11, 'Skype' ),
(12, 'Bankkonto Kaiman-Inseln' );
Hier ein paar Testpersonen und ein paar typische Kontaktdaten
INSERT INTO person VALUES
( 1, 'Vorname-1','Nachname-1' ),
( 2, 'Vorname-2','Nachname-2' ),
( 3, 'Vorname-3','Nachname-3' );
INSERT INTO person_kontakt VALUES
( 1, 1, 1, '0049-0089-12345678' , NULL),
( 2, 1, 9, 'https://twiedmann.de', NULL ),
( 3, 2, 5, '[email protected]', NULL ),
( 4, 2, 9, 'http://www.nachname2.de', NULL ),
( 5, 2, 7, '0152-00000000', 'lange klingeln lassen!' ),
( 6, 3, 2, '0049-0089-11111111', 'Sekretärin: Frau Immerda' ),
( 7, 3,12, 'X12-all-ES-mEiNs', 'Passwort liegt beim Pförtner' );
Zu allererst brauchen wir eine kleine Telefon-Liste aller angelegten Personen
SELECT p.vorname, p.nachname, kb.bezeichnung, pk.wert
FROM person p
JOIN person_kontakt pk
ON p.person_id = ph.person_id
JOIN kontakt_bez kb
ON k.kontakt_id = pk.kontakt_id
WHERE pk.kontakt_id = 1 -- // 'Telefon privat'
ORDER BY p.nachname, p.vorname;
+-----------+------------+----------------+--------------------+
| vorname | nachname | bezeichnung | wert |
+-----------+------------+----------------+--------------------+
| Vorname-1 | Nachname-1 | Telefon privat | 0049-0089-12345678 |
+-----------+------------+----------------+--------------------+
1 row in set (0.05 sec)
Hmm, die Abfrage in Listing 4 liefert allerdings nur die Personen, zu denen auch ein Telefon eingetragen ist. Macht nichts, da gibt es doch den OUTER JOIN. In unserem Fall also ein LEFT OUTER JOIN. Der Befehl OUTER ist optional und kann auch weggelassen werden.
SELECT p.vorname, p.nachname, kb.bezeichnung, pk.wert
FROM person p
LEFT JOIN person_kontakt pk
ON p.person_id = pk.person_id
LEFT JOIN kontakt_bez kb
ON kb.kontakt_bez_id = pk.kontakt_bez_id
WHERE pk.kontakt_bez_id = 1 -- // 'Telefon privat'
ORDER BY p.nachname, p.vorname;
+-----------+------------+----------------+--------------------+
| vorname | nachname | bezeichnung | wert |
+-----------+------------+----------------+--------------------+
| Vorname-1 | Nachname-1 | Telefon privat | 0049-0089-12345678 |
+-----------+------------+----------------+--------------------+
1 row in set (0.05 sec)
Zur allgemeinen Überraschung liefert auch diese Abfrage (Listing 5:) wieder nur die Personen, zu denen auch ein Telefon eingetragen ist, obwohl wir doch einen LEFT (OUTER) JOIN verwendet habe. Was läuft hier schief?
SELECT p.vorname, p.nachname, kb.bezeichnung, pk.wert
FROM person p
LEFT JOIN person_kontakt pk
ON p.person_id = pk.person_id
AND pk.kontakt_bez_id = 1 -- // 'Telefon privat'
LEFT JOIN kontakt_bez kb
ON kb.kontakt_bez_id = pk.kontakt_bez_id
ORDER BY p.nachname, p.vorname;
+-----------+------------+----------------+--------------------+
| vorname | nachname | bezeichnung | wert |
+-----------+------------+----------------+--------------------+
| Vorname-1 | Nachname-1 | Telefon privat | 0049-0089-12345678 |
| Vorname-2 | Nachname-2 | NULL | NULL |
| Vorname-3 | Nachname-3 | NULL | NULL |
+-----------+------------+----------------+--------------------+
3 rows in set (0.00 sec)
Aha, die gewünschte kontakt_bez_id muss auch im LEFT JOIN abgefragt werden. Bei Personen ohne hinterlegte Telefonnummer wird nun der Wert NULL ausgegeben.
Im nächsten Schritt brauchen wir mehrere Kontaktdaten (Telefon, Web-Seite und E-Mail) auf einer Liste. Die Ausgabe der Daten soll zuerst untereinander und anschließend nebeneinander erfolgen. Damit bei nicht vorhandenen Werten keine NULL ausgegeben wird, nutzen wir das Kommando COALESCE, um einen leeren String ('') auszugeben.
SELECT p.vorname, p.nachname,
COALESCE(kb.bezeichnung,'') AS bezeichnung,
COALESCE(pk.wert ,'') AS wert
FROM person p
LEFT JOIN person_kontakt pk
ON p.person_id = pk.person_id
AND pk.kontakt_bez_id IN (1,5,9)
LEFT JOIN kontakt_bez kb
ON kb.kontakt_bez_id = pk.kontakt_bez_id
ORDER BY p.nachname, p.vorname, kb.bezeichnung;
+-----------+------------+----------------+----------------------------+
| vorname | nachname | bezeichnung | wert |
+-----------+------------+----------------+----------------------------+
| Vorname-1 | Nachname-1 | Telefon privat | 0049-0089-12345678 |
| Vorname-1 | Nachname-1 | Web-Seite | https://twiedmann.de |
| Vorname-2 | Nachname-2 | E-Mail privat | [email protected] |
| Vorname-2 | Nachname-2 | Web-Seite | http://www.nachname2.de |
| Vorname-3 | Nachname-3 | | |
+-----------+------------+----------------+----------------------------+
5 rows in set (0.03 sec)
Und hier nun eine Liste mit Telefon (id=1) und Email (id=5) horizontal dargestellt. Damit dies möglich wird, müssen die Tabelle kontakt_bez und person_kontakt je zweimal als LEFT JOIN aufgeführt werden
SELECT p.vorname, p.nachname,
COALESCE(kb1.bezeichnung,'') AS bezeichnung1,
COALESCE(pk1.wert ,'') AS wert1,
COALESCE(kb5.bezeichnung,'') AS bezeichnung5,
COALESCE(pk5.wert , '') AS wert5
FROM person p
LEFT JOIN person_kontakt pk1
ON p.person_id = pk1.person_id
AND pk1.kontakt_bez_id = 1 -- // Telefon
LEFT JOIN kontakt_bez kb1
ON kb1.kontakt_bez_id = pk1.kontakt_bez_id
LEFT JOIN person_kontakt pk5
ON p.person_id = pk5.person_id
AND pk5.kontakt_bez_id = 5 -- // E-Mail
LEFT JOIN kontakt_bez kb5
ON kb5.kontakt_bez_id = pk5.kontakt_bez_id
ORDER BY p.nachname, p.vorname;
+-----------+------------+----------------+--------------------+---------------+----------------------------+
| vorname | nachname | bezeichnung1 | wert1 | bezeichnung5 | wert5 |
+-----------+------------+----------------+--------------------+---------------+----------------------------+
| Vorname-1 | Nachname-1 | Telefon privat | 0049-0089-12345678 | | |
| Vorname-2 | Nachname-2 | | | E-Mail privat | [email protected] |
| Vorname-3 | Nachname-3 | | | | |
+-----------+------------+----------------+--------------------+---------------+----------------------------+
3 rows in set (0.00 sec)
Abschließend noch eine MySQL Spezialausgabe mit GROUP_CONCAT, bei der alle vorhandenen Kontakte als CSV-Daten dargestellt werden.
SELECT p.vorname, p.nachname,
GROUP_CONCAT( CONCAT(kb.bezeichnung , ':' , pk.wert)
ORDER BY kb.bezeichnung ASC SEPARATOR ';'
) AS kontakt
FROM person p
LEFT JOIN person_kontakt pk
ON p.person_id = pk.person_id
LEFT JOIN kontakt_bez kb
ON kb.kontakt_bez_id = pk.kontakt_bez_id
GROUP BY p.nachname, p.vorname
ORDER BY p.nachname, p.vorname;
+-----------+------------+-------------------------------------------------------------------------------------------------------+
| vorname | nachname | kontakt |
+-----------+------------+-------------------------------------------------------------------------------------------------------+
| Vorname-1 | Nachname-1 | Telefon privat:0049-0089-12345678;Web-Seite:https://twiedmann.de |
| Vorname-2 | Nachname-2 | E-Mail privat:[email protected];Mobil privat:0152-00000000;Web-Seite:http://www.nachname2.de |
| Vorname-3 | Nachname-3 | Bankkonto Kaiman-Inseln:X12-all-ES-mEiNs;Telefon geschäftlich:0049-0089-11111111 |
+-----------+------------+-------------------------------------------------------------------------------------------------------+
3 rows in set (0.01 sec)
Die SQL-Abfragen werden mit diesem Datenmodel zwar immer etwas umfangreicher, aber dafür bleibt es absolut flexibel, was mögliche zukünftige Änderungen angeht. Wer sich jemals mit einem nicht normalisierten Datenmodell herumgeschlagen hat, der weiß wovon ich hier rede.