Normalisierung per DML-Queries eines bestehenden Datenbankinhalts (LS Beispiellösung Teil 3)
https://oer-informatik.de/lets_meet-beispielloesung_normalisierung
https://bildung.social/@oerinformatik/110207682728246503
tl/dr; Dies ist der dritte Teil (Normalisierung) einer Beispiellösung für die “Let’s Meet” Datenbank-Lernsituation. Die Daten sollen nun über Zeichenkettenoperationen, JOIN, UNION und Subselects im Bestand normalisiert und in neue Tabellen übernommen werden. (Zuletzt geändert am 13.06.2023)
Dieser Artikel ist Teil einer mehrteiligen Serie, in der
die eigentliche Aufgabestellung das Ausgangsszenario vorstellt und den Datenbank-Export als Tabelle verlinkt,
im ersten Teil der Beispiellösung das Zielsystem modelliert wird (ER-Modell, Relationenmodell, physisches Modell / DDL),
im zweiten Teil der Beispiellösung SQL-Befehle für den Import aus der Tabellenkalkulation erstellt werden und
im dritten Teil der Beispiellösung die Ausgangsdaten über Zeichenkettenfunktionen, Datumsfunktionen, JOINS und Vereinigung von Abfragen schließlich Daten in das neue Zielsystem übernommen werden.
Allen Artikel enden mit einem SQL-Skript, mit dem Daten als Ausgangspunkt für die nächste Etappe genutzt werden können, falls es zu Problemen bei der Eigenentwicklung kam.
Ausgangslage
Die eigentliche Aufgabestellung der “Let’s Meet” Datenbank-Lernsituation kann hier nachgelesen werden. Voraussetzung für die in diesem Teil beschriebene Normalisierung ist eine fertig modellierte und importierte Datenbank (siehe Teil 1 und Teil 2).
Schnelleinstieg ohne Vorkenntnisse: Vorbereitung des DB-Containers
Die Grundlagen der beiden vorigen Artikel münden in einer Datenbank, die sich mit SQL-Skripten erzeugen und füllen lässt. Wer so weit ist, der muss nur seinen Container wieder starten (sofern dieser nicht noch läuft) und die folgenden Zeilen überspringen bis zum Absatz “Normalisierung vorbereiten in tblUrsprung”.
Bei wem dies nicht geglückt ist, der kann mit folgenden Schritten einen befüllten Datenbank-Container erzeugen, der direkt startklar ist. Hierfür wird nur ein leeres Verzeichnis und Docker benötigt. In das Verzeichnis müssen alle SQL-Skripte mit benötigten Befehlen kopiert werden, die zuvor erstellt (oder heruntergeladen) wurden. Die Skripte werden in alphabetischer Reihenfolge ausgeführt. In der Bash und Powershell könnte das z.B. so aussehen:
Die folgenden Befehle sind für die PowerShell. In der Linux-Bash muss der -OutFile "..."
-Teil weggelassen werden!
cd ~/Documents
mkdir psqldocker
cd psqldocker
wget https://gitlab.com/oer-informatik/db-sql/lets-meet-db/-/blob/raw/beispielloesung/postgresql/letsmeet_1_struktur_ddl_postgre.sql -OutFile "letsmeet_1_struktur_ddl_postgre.sql"
wget https://gitlab.com/oer-informatik/db-sql/lets-meet-db/-/blob/raw/beispielloesung/postgresql/letsmeet_2_import_einzeiler_postgre.sql -OutFile "letsmeet_2_import_einzeiler_postgre.sql"
Dieses SQL-Skript wird automatisch ausgeführt, wenn wir in diesem Ordner mit folgendem Befehl einen postgreSQL-Container erzeugen und starten:
(unter Linux muss statt ${PWD}:
mit runden Klammern $(pwd):
geschrieben werden)
Das dauert diesmal etwas, weil 1576 Zeilen importiert werden müssen. Wenn alles geklappt hat, dann sollte im Container die interaktive (-it
) PostgreSQL-Kommandozeile (psql
) für Nutzer postgres
mit Passwort postgres
geöffnet werden können:
Die psql-Kommandozeile sieht etwa so aus:
Und ein Befehl wie dieser hier sollte im Idealfall die Anzahl der Zeilen (1576) ausgeben:
Mit \q
kann die Konsole wieder verlassen werden.
Weitere Details im Umgang mit PostgreSQL im Container und mit dem Zugriff eines Frontends finden sich hier.
Normalisierung vorbereiten in tblUrsprung
Die Strategie der Normalisierung und Anpassung der Daten im Bestand ist immer die gleiche:
Zunächst werden die Daten lesend so aufbereitet, wie wir sie später benötigen (
SELECT
-Statments, ohne Änderung des Datenbestands)Die Struktur der Datenbank wird so angepasst, wie wir sie benötigen, um die aufbereiteten Daten zu speichern (z.B. neue Spalten per
ALTER TABLE
anfügen)Basierend auf dem
SELECT
-Statement aus (1) wird ein SQL-DML-Statement (DML = Data Manipulation Language) erstellt, dass die Daten ändert / einfügt (UPDATE
oderINSERT
)Mit Hilfe von
SELECT
-Statements wird im Anschluss geprüft, ob die Abfrage wie gewünscht funktioniert hat.
Wir führen diese Anpassungen in zwei Schritten durch: zunächst bereiten wir alle Daten in tblUrsprung
so auf, wie wir sie benötigen. In einem zweiten Schritt übernehmen wir diese Daten dann in die jeweiligen Zieltabellen. In den folgenden Absätzen geht es zunächst um die Anpassungen in tblUrsprung
.
Das Namensfeld in Vorname/Nachname aufteilen
Als Erstes kümmern wir uns um Vorname / Nachname. Die Techniken und Befehle, die wir hier verwenden, werden uns bei einer Vielzahl an Anpassungen helfen. Es geht wie angekündigt im Dreischritt voran: (1) Daten zunächst ohne Datenänderung umformen/filtern (SELECT
), (2) Datenbank anpassen (ALTER TABLE
), (3) Daten ändern (INSERT
/ UPDATE
):
- Lesend die Daten aufbereiten
Um die bestehenden Daten aufzubereiten, müssen wir Zeichenkettenfunktionen bemühen. Eine erste Übersicht für verschiedene DBMS und Links, um die passenden Funktionen aus den Dokumentationen zu finden, ist hier beschrieben.
Die wichtigsten Zeichenkettenoperationen der DBMS, die uns hier helfen, sind folgende:
Teilstrings erzeugen (rechts, links, oder positionsabhängig):
SUBSTRING(...)
(in PostgreSQL)Zeichenkettenlängen bestimmen:
CHAR_LENGTH()
(in PostgreSQL)Position von Zeichen in Zeichenketten:
POSITION(...)
(in PostgreSQL)Zeichen von Whitespaces entfernen:
TRIM(...)
(in PostgreSQL)
Für unsere jeweilige Aufgabe müssen wir jetzt nur noch einen passenden Separator finden, mithilfe dessen wir die Zeichenkette zerteilen können.

Im ersten Beispiel ist der Separator ein Komma (plus Leerzeichen), schematisch benötigen wir also etwa folgende Befehle für die Komponenten:

PostgreSQL-Beispiel:
select
namen,
trim(substring ( namen FROM 0 FOR position (', ' IN namen ) )) as lastname,
trim(substring ( namen FROM (position(', ' IN namen )+2) )) as firstame,
char_length ( namen ) as Gesamtlaenge,
position (', ' IN namen ) as Kommaposition,
char_length ( trim(substring ( namen FROM 0 FOR position (', ' IN namen ) ))) as LaengeNachname,
char_length ( trim(substring ( namen FROM (position(', ' IN namen )+2) ))) as LaengeVorname
from letsMeetMigrationScheme.tblUrsprung;
- Die nötigen neuen Spalten erzeugen
Die Ergebnisse dieser Abfrage speichern wir zunächst in einem neuen Attribut (eine neue Spalte) in tblUrsprung
. Es vereinfacht das Verfahren erheblich, wenn wir die Werte nicht direkt in die eigentliche Zieltabelle tblUser
übernehmen, sondern unser Gesamtproblem atomisieren und kleinschrittig lösen. Wir benötigen also neue Attribute und müssen unsere Tabelle anpassen:
ALTER TABLE letsMeetMigrationScheme.tblUrsprung ADD firstname CHAR(32);
ALTER TABLE letsMeetMigrationScheme.tblUrsprung ADD lastname CHAR(32);
- Die Daten in die neuen Zeilen übernehmen (
SELECT
inUPDATE
umformen)
Es folgt der eigentliche datenverändernde Schritt: Wir übernehmen die Projektion aus den SELECT
-Befehlen, die wir oben erstellt hatten, und weisen diese Werte den neuen Attributen zu. Durch diese Aufteilung können wir zunächst gefahrlos die Ergebnisse der Projektion begutachten, und erst, wenn wir keine Probleme mehr entdecken, die Daten schreiben.
Aus der Projektion TRIM(SUBSTR(namen, INSTR(namen,", ")+2)) AS firstname
wird so die Zuweisung firstname =trim(substring ( namen FROM (position(', ' IN namen )+2) ))
. In die Syntax eines UPDATE
-Befehls gegossen wird daraus:
UPDATE letsMeetMigrationScheme.tblUrsprung SET
lastname =trim(substring ( namen FROM 0 FOR position (', ' IN namen ) ) ),
firstname =trim(substring ( namen FROM (position(', ' IN namen )+2) ));
- Prüfen per
SELECT
-Befehl
Zur Überprüfung sollten wir uns mindestens die alten und neuen Attribute einmal anschauen und sortieren, ob es leere Felder gibt, um diese gesondert zu überprüfen. Hat alles geklappt?
Ein einfacher Weg, die Vollständigkeit eines Imports zu überprüfen, wenn man die Daten kennt, ist das Zählen der Zeichen:
SUM(CHAR_LENGTH(namen)) | SUM(CHAR_LENGTH(firstname)) | SUM(CHAR_LENGTH(lastname)) |
---|---|---|
24735 | 10307 | 11208 |
Diese vier Schritte wiederholen sich jetzt für die unterschiedlichen Werte, die wir aufteilen müssen. Ich nenne diese im Folgenden nur noch, ohne sie wieder ausführlich zu beschreiben.
Adresse aufteilen
PLZ/Ort von Straße/Nr trennen:
- Lesend die Daten aufbereiten
Im Feld adresse
stecken die Werte von vier Attributen. Wir teilen zunächst in die zwei Gruppen PLZ/Ort und Straße/Hausnummer, um dann in einem zweiten Schritt die einzelnen Werte zu ermitteln:
select
adresse,
trim(substring ( adresse FROM 0 FOR position (', ' IN adresse ) )) as street_nr,
trim(substring ( adresse FROM (position(', ' IN adresse )+2) )) as postcode_city,
char_length ( adresse ) as Gesamtlaenge,
position (', ' IN adresse ) as Kommaposition,
char_length ( trim(substring ( adresse FROM 0 FOR position (', ' IN adresse ) ))) as length_street_nr,
char_length ( trim(substring ( adresse FROM (position(', ' IN adresse )+2) ))) as length_postcode_city
from letsMeetMigrationScheme.tblUrsprung;
- Die nötigen neuen Spalten erzeugen
ALTER TABLE letsMeetMigrationScheme.tblUrsprung ADD postcode_city CHAR(128);
ALTER TABLE letsMeetMigrationScheme.tblUrsprung ADD street_nr CHAR(80);
- Die Daten in die neuen Zeilen übernehmen (
SELECT
inUPDATE
umformen)
UPDATE letsMeetMigrationScheme.tblUrsprung SET
street_nr = trim(substring ( adresse FROM 0 FOR position (', ' IN adresse ) )),
postcode_city = trim(substring ( adresse FROM (position(', ' IN adresse )+2) ));
- Prüfen per
SELECT
-Befehl
Straße von Nr trennen
- Lesend die Daten aufbereiten
select
street_nr,
substring (street_nr FROM '[0-9]+.*' ) as street_no,
substring (street_nr FROM '[^0-9]+' ) as street
from letsMeetMigrationScheme.tblUrsprung;
- Die nötigen neuen Spalten erzeugen
ALTER TABLE letsMeetMigrationScheme.tblUrsprung ADD street CHAR(64);
ALTER TABLE letsMeetMigrationScheme.tblUrsprung ADD street_no CHAR(16);
- Die Daten in die neuen Zeilen übernehmen (
SELECT
inUPDATE
umformen)
UPDATE letsMeetMigrationScheme.tblUrsprung SET
street = trim(substring (street_nr FROM '[^0-9]+' )),
street_no = trim(substring (street_nr FROM '[0-9]+.*' ));
- Prüfen per
SELECT
-Befehl
Prüfung anhand der umgewandelten Zeichen:
SELECT SUM(CHAR_LENGTH(street_nr)), SUM(CHAR_LENGTH(street)), SUM(CHAR_LENGTH(street_no))
FROM letsMeetMigrationScheme.tblUrsprung;
SUM(CHAR_LENGTH(street_nr)) | SUM(CHAR_LENGTH(street)) | SUM(CHAR_LENGTH(street_no)) |
---|---|---|
25036 | 19604 | 3856 |
PLZ von Ort trennen
- Lesend die Daten aufbereiten
select
postcode_city,
trim(substring (postcode_city FROM 0 FOR position (', ' IN postcode_city ) )) as postcode,
trim(substring ( postcode_city FROM (position(', ' IN postcode_city )+2) )) as city,
from letsMeetMigrationScheme.tblUrsprung;
- Die nötigen neuen Spalten erzeugen
ALTER TABLE letsMeetMigrationScheme.tblUrsprung ADD city CHAR(64);
ALTER TABLE letsMeetMigrationScheme.tblUrsprung ADD postcode CHAR(16);
- Die Daten in die neuen Zeilen übernehmen (
SELECT
inUPDATE
umformen)
UPDATE letsMeetMigrationScheme.tblUrsprung SET
postcode = trim(substring (postcode_city FROM 0 FOR position (', ' IN postcode_city ) )),
city = trim(substring ( postcode_city FROM (position(', ' IN postcode_city )+2) ));
- Prüfen per
SELECT
-Befehl
SELECT SUM(CHAR_LENGTH(postcode)), SUM(CHAR_LENGTH(city)), SUM(CHAR_LENGTH(postcode_city))
FROM letsMeetMigrationScheme.tblUrsprung;
SUM(CHAR_LENGTH(postcode)) | SUM(CHAR_LENGTH(city)) | SUM(CHAR_LENGTH(postcode_city)) |
---|---|---|
7822 | 14196 | 25170 |
Hobbys
Die fünf Hobbys voneinander trennen
Bei den Hobbys müssen wir unsere Strategie etwas anpassen, damit die Abfragen nicht zu kompliziert werden:
Wir trennen mit einer Abfrage immer das vorderste Hobby inkl. Priorität ab (trennen am “;”)
Den abgetrennten Hobbynamen speichern wir in einem neuen Attribut (erste Runde:
hobby1
, zweite Rundehobby2
usw.) um hier später Hobbynamen und Priorität zu trennenDen Text nach dem vordersten Hobby speichern wir ebenso in einer neuen Spalte
hobbyrest
, die wir dann wiederum in die oberste Abfrage geben und so das folgende Hobby separieren.
Im ganzen gibt es fünf Hobbys, es ist also etwas Copy&Past nötig:
- Lesend die Daten aufbereiten
select
hobbytext,
trim(substring (hobbytext FROM 0 FOR position ('; ' IN hobbytext ) )) as hobby1,
trim(substring (hobbytext FROM (position(', ' IN hobbytext )+2) )) as hobbyrest
from letsMeetMigrationScheme.tblUrsprung;
- Die nötigen neuen Spalten erzeugen
ALTER TABLE letsMeetMigrationScheme.tblUrsprung ADD hobbyrest CHAR(255);
ALTER TABLE letsMeetMigrationScheme.tblUrsprung ADD hobby1 CHAR(132);
ALTER TABLE letsMeetMigrationScheme.tblUrsprung ADD hobby2 CHAR(132);
ALTER TABLE letsMeetMigrationScheme.tblUrsprung ADD hobby3 CHAR(132);
ALTER TABLE letsMeetMigrationScheme.tblUrsprung ADD hobby4 CHAR(132);
ALTER TABLE letsMeetMigrationScheme.tblUrsprung ADD hobby5 CHAR(132);
- Die Daten in die neuen Zeilen übernehmen (
SELECT
inUPDATE
umformen)
UPDATE letsMeetMigrationScheme.tblUrsprung SET
hobby1 = trim(substring (hobbytext FROM 0 FOR position ('; ' IN hobbytext ) )),
hobbyrest = trim(substring (hobbytext FROM (position(', ' IN hobbytext )+2) ))
WHERE position ('; ' IN hobbytext ) >0;
UPDATE letsMeetMigrationScheme.tblUrsprung SET
hobby2 = trim(substring (hobbyrest FROM 0 FOR position ('; ' IN hobbyrest ) )),
hobbyrest = trim(substring (hobbyrest FROM (position(', ' IN hobbyrest )+2) ))
WHERE position ('; ' IN hobbytext ) >0;
UPDATE letsMeetMigrationScheme.tblUrsprung SET
hobby3 = trim(substring (hobbyrest FROM 0 FOR position ('; ' IN hobbyrest ) )),
hobbyrest = trim(substring (hobbyrest FROM (position(', ' IN hobbyrest )+2) ))
WHERE position ('; ' IN hobbytext ) >0;
UPDATE letsMeetMigrationScheme.tblUrsprung SET
hobby4 = trim(substring (hobbyrest FROM 0 FOR position ('; ' IN hobbyrest ) )),
hobbyrest = trim(substring (hobbyrest FROM (position(', ' IN hobbyrest )+2) ))
WHERE position ('; ' IN hobbytext ) >0;
UPDATE letsMeetMigrationScheme.tblUrsprung SET
hobby5 = trim(substring (hobbyrest FROM 0 FOR position ('; ' IN hobbyrest ) )),
hobbyrest = trim(substring (hobbyrest FROM (position(', ' IN hobbyrest )+2) ))
WHERE position ('; ' IN hobbytext ) >0;
- Prüfen per
SELECT
-Befehl
SUM(CHAR_LENGTH(hobbytext)) | SUM(CHAR_LENGTH(hobby1)) | SUM(CHAR_LENGTH(hobby2)) | SUM(CHAR_LENGTH(hobby3)) | SUM(CHAR_LENGTH(hobby4)) |
---|---|---|---|---|
176484 | 50743 | 47866 | 47866 | 47866 |
Die Hobby-Prioritäten von den einzelnen Hobbys trennen
- Lesend die Daten aufbereiten
Die Bearbeitung kann für die Spalten hobby1
bis hobby5
identisch erfolgen - wichtig ist dabei, an allen Stellen des SQL-Befehls auf das korrekte Feld zu verweisen!
select
hobby1,
trim(substring (hobby1 FROM 0 FOR position (' %' IN hobby1 ) )) as hobby1text,
CAST(trim(trim ( '%' FROM substring (hobby1 FROM (position(' %' IN hobby1 )+2) ))) AS INTEGER) as hobbyPRIO
from letsMeetMigrationScheme.tblUrsprung;
- Die nötigen neuen Spalten erzeugen
/* Struktur vorbereiten für die Hobby-Prioritäten */
ALTER TABLE letsMeetMigrationScheme.tblUrsprung ADD hobby1_prio INTEGER;
ALTER TABLE letsMeetMigrationScheme.tblUrsprung ADD hobby2_prio INTEGER;
ALTER TABLE letsMeetMigrationScheme.tblUrsprung ADD hobby3_prio INTEGER;
ALTER TABLE letsMeetMigrationScheme.tblUrsprung ADD hobby4_prio INTEGER;
ALTER TABLE letsMeetMigrationScheme.tblUrsprung ADD hobby5_prio INTEGER;
- Die Daten in die neuen Zeilen übernehmen (
SELECT
inUPDATE
umformen)
UPDATE letsMeetMigrationScheme.tblUrsprung SET
hobby1_prio = CAST(trim(trim ( '%' FROM substring (hobby1 FROM (position(' %' IN hobby1 )+2) ))) AS INTEGER),
hobby1 = trim(substring (hobby1 FROM 0 FOR position (' %' IN hobby1 ) ))
WHERE position (' %' IN hobby1 ) >0;
UPDATE letsMeetMigrationScheme.tblUrsprung SET
hobby2_prio = trim(trim ( '%' FROM substring (hobby2 FROM (position(' %' IN hobby2 )+2) ))) AS INTEGER),
hobby2 = trim(substring (hobby2 FROM 0 FOR position (' %' IN hobby2) ))
WHERE position (' %' IN hobby2 ) >0;
UPDATE letsMeetMigrationScheme.tblUrsprung SET
hobby3_prio = trim(trim ( '%' FROM substring (hobby3 FROM (position(' %' IN hobby3 )+2) ))) AS INTEGER),
hobby3 = trim(substring (hobby3 FROM 0 FOR position (' %' IN hobby3) ))
WHERE position (' %' IN hobby3 ) >0;
UPDATE letsMeetMigrationScheme.tblUrsprung SET
hobby4_prio = trim(trim ( '%' FROM substring (hobby4 FROM (position(' %' IN hobby4 )+2) ))) AS INTEGER),
hobby4 = trim(substring (hobby4 FROM 0 FOR position (' %' IN hobby4) ))
WHERE position (' %' IN hobby4 ) >0;
UPDATE letsMeetMigrationScheme.tblUrsprung SET
hobby5_prio = trim(trim ( '%' FROM substring (hobby5 FROM (position(' %' IN hobby5 )+2) ))) AS INTEGER),
hobby5 = trim(substring (hobby5 FROM 0 FOR position (' %' IN hobby5) ))
WHERE position (' %' IN hobby5 ) >0;
- Prüfen per
SELECT
-Befehl
SELECT
SUM(CHAR_LENGTH(hobby1)), SUM(hobby1_prio),
SUM(CHAR_LENGTH(hobby2)), SUM(hobby2_prio),
SUM(CHAR_LENGTH(hobby3)), SUM(hobby3_prio),
SUM(CHAR_LENGTH(hobby4)), SUM(hobby4_prio),
SUM(CHAR_LENGTH(hobby5)), SUM(hobby5_prio)
FROM letsMeetMigrationScheme.tblUrsprung
SUM(CHAR_LENGTH(hobby1)) | SUM(hobby1_prio) | SUM(CHAR_LENGTH(hobby2)) | SUM(hobby2_prio) | SUM(CHAR_LENGTH(hobby3)) | SUM(hobby3_prio) | SUM(CHAR_LENGTH(hobby4)) | SUM(hobby4_prio) | SUM(CHAR_LENGTH(hobby5)) | SUM(hobby5_prio) |
---|---|---|---|---|---|---|---|---|---|
43046 | 78483 | 40557 | 74184 | 40557 | 74184 | 40557 | 74184 | 40557 | 74184 |
Die Daten von tblUrsprung in die anderen Tabellen übernehmen
Jetzt sind wir so weit, dass wir uns an die eigentliche Zielstruktur wagen können.
Einige Tabellen sind über FOREIGN KEY
Constraints miteinander verknüpft. Bevor wir Daten in diese Tabellen eintragen müssen wir also sicherstellen, dass die nötigen Schlüssel in den verknüpften Tabellen existieren (oder - als Workaround - den FOREIGN KEY
Constraint temporär deaktivieren).
Es empfiehlt sich also - analog zur Erstellung der Struktur - zunächst die unabhängigen Tabellen zu befüllen. Eine Reihenfolge hatten wir bereits in Teil 1 erstellt:

tblGender
- Lesend die Daten aufbereiten
Um die unterschiedlichen Geschlechtszuweisungen auszulesen, müssen wir unsere Ursprungstabelle mit einer Aggregatsfunktion und Gruppierung zusammenfassen. Auf diesem Weg erhalten wir jedes Geschlecht nur einmal:
SELECT
tblUrsprung.geschlecht AS gender
FROM letsMeetMigrationScheme.tblUrsprung
GROUP by tblUrsprung.geschlecht;
- Die nötigen neuen Spalten erzeugen
Die nötige Tabelle wurde bereits in Teil 1 erzeugt (tblGender
)
- Die Daten in die neuen Zeilen übernehmen (SELECT in INSERT umformen)
INSERT INTO letsMeetMigrationScheme.tblGender(gender)
SELECT
tblUrsprung.geschlecht AS gender
FROM letsMeetMigrationScheme.tblUrsprung
GROUP by tblUrsprung.geschlecht;
- Prüfen per
SELECT
-Befehl
tblHobby
- Lesend die Daten aufbereiten
Unsere Hobbys stecken ja nun in fünf Spalten. Wir wollen diese aber zusammenfassen. Am einfachsten geht das, in dem wir fünf einzelne Abfragen (für jede hobby*
-Spalte eine) per UNION
zusammenfügen. Damit wir keine Dopplungen erhalten, nutzen wir das Schlüsselwort DISTINCT
.
Im Anschluss müssen wir noch die leeren Hobbyeinträge aus dem Abfrageergebnis entfernen. Um Dopplungen schneller entdecken zu können, habe ich die Ergebnisse sortieren lassen.
SELECT DISTINCT hobby FROM (
(SELECT DISTINCT hobby1 AS hobby
FROM letsMeetMigrationScheme.tblUrsprung)
UNION
(SELECT DISTINCT hobby2 AS hobby
FROM letsMeetMigrationScheme.tblUrsprung)
UNION
(SELECT DISTINCT hobby3 AS hobby
FROM letsMeetMigrationScheme.tblUrsprung)
UNION
(SELECT DISTINCT hobby4 AS hobby
FROM letsMeetMigrationScheme.tblUrsprung)
UNION
(SELECT DISTINCT hobby5 AS hobby
FROM letsMeetMigrationScheme.tblUrsprung)
) AS ursprung
WHERE hobby IS NOT NULL
ORDER BY hobby;
- Die nötigen neuen Spalten erzeugen
Die nötige Tabelle wurde bereits in Teil 1 erzeugt (tblHobby
)
- Die Daten in die neuen Zeilen übernehmen (
SELECT
inINSERT
umformen)
INSERT INTO letsMeetMigrationScheme.tblHobby(hobbyname)
(SELECT DISTINCT hobby FROM (
(SELECT DISTINCT hobby1 AS hobby
FROM letsMeetMigrationScheme.tblUrsprung)
UNION
(SELECT DISTINCT hobby2 AS hobby
FROM letsMeetMigrationScheme.tblUrsprung)
UNION
(SELECT DISTINCT hobby3 AS hobby
FROM letsMeetMigrationScheme.tblUrsprung)
UNION
(SELECT DISTINCT hobby4 AS hobby
FROM letsMeetMigrationScheme.tblUrsprung)
UNION
(SELECT DISTINCT hobby5 AS hobby
FROM letsMeetMigrationScheme.tblUrsprung)
) AS ursprung
WHERE hobby IS NOT NULL
);
- Prüfen per
SELECT
-Befehl
tblUser
- Lesend die Daten aufbereiten
Bevor wir die Werte aus tblUrsprung
in der Tabelle tblUser
speichern können, muss noch der Datentyp des Datums angepasst werden. Im Fall von _PostgreSQL bietet sich hierfür die Funktion TO_DATE
an 2. Für einige andere DBMS finden sich hier Übersichten zu Datumsfunktionen bzw. Links zur Dokumentation.
SELECT
tblUrsprung.user_id AS user_id,
tblUrsprung.firstname AS firstname,
tblUrsprung.lastname AS lastname,
tblUrsprung.telefon AS telephone,
tblUrsprung.street AS street,
tblUrsprung.street_no AS street_no,
tblUrsprung.postcode AS postcode,
tblUrsprung.city AS city,
'Deutschlang' AS country,
tblUrsprung.email AS email,
tblUrsprung.geschlecht AS gender,
TO_DATE(tblUrsprung.geburtstag,'DD.MM.YYYY') AS birthday
FROM letsMeetMigrationScheme.tblUrsprung
- Die nötigen neuen Spalten erzeugen
Die nötige Tabelle wurde bereits in Teil 1 erzeugt (tblUser
)
- Die Daten in die neuen Zeilen übernehmen (SELECT in INSERT umformen)
INSERT INTO letsMeetMigrationScheme.tblUser
(user_id,firstname,lastname, telephone, street, street_no, postcode, city, country, email, gender, birthday)
SELECT
tblUrsprung.user_id AS user_id,
tblUrsprung.firstname AS firstname,
tblUrsprung.lastname AS lastname,
tblUrsprung.telefon AS telephone,
tblUrsprung.street AS street,
tblUrsprung.street_no AS street_no,
tblUrsprung.postcode AS postcode,
tblUrsprung.city AS city,
'Deutschland' AS country,
tblUrsprung.email AS email,
tblUrsprung.geschlecht AS gender,
TO_DATE(tblUrsprung.geburtstag,'DD.MM.YYYY') AS birthday
FROM letsMeetMigrationScheme.tblUrsprung;
- Prüfen per
SELECT
-Befehl
tblHobby2User
- Lesend die Daten aufbereiten
Um die Zuweisungen der Hobbys zu Usern zu erhalten müssen wir - analog zu oben - wieder alle fünf Hobbyspalten auswerten. In jeder Subquery fügen wir einen JOIN
mit dem Hobbytext einer dieser fünf Spalten durch und fügen die Ergebnisse wieder per UNION
zusammen:
SELECT hobby_id, user_id, prio
FROM (
(SELECT letsMeetMigrationScheme.tblHobby.hobby_id AS hobby_id,
letsMeetMigrationScheme.tblUrsprung.user_id AS user_id,
letsMeetMigrationScheme.tblUrsprung.hobby1_prio AS prio
FROM letsMeetMigrationScheme.tblUrsprung
LEFT JOIN letsMeetMigrationScheme.tblHobby
ON letsMeetMigrationScheme.tblHobby.hobbyname = letsMeetMigrationScheme.tblUrsprung.hobby1)
UNION
(SELECT letsMeetMigrationScheme.tblHobby.hobby_id AS hobby_id,
letsMeetMigrationScheme.tblUrsprung.user_id AS user_id,
letsMeetMigrationScheme.tblUrsprung.hobby2_prio AS prio
FROM letsMeetMigrationScheme.tblUrsprung
LEFT JOIN letsMeetMigrationScheme.tblHobby
ON letsMeetMigrationScheme.tblHobby.hobbyname = letsMeetMigrationScheme.tblUrsprung.hobby2)
UNION
(SELECT letsMeetMigrationScheme.tblHobby.hobby_id AS hobby_id,
letsMeetMigrationScheme.tblUrsprung.user_id AS user_id,
letsMeetMigrationScheme.tblUrsprung.hobby3_prio AS prio
FROM letsMeetMigrationScheme.tblUrsprung
LEFT JOIN letsMeetMigrationScheme.tblHobby
ON letsMeetMigrationScheme.tblHobby.hobbyname = letsMeetMigrationScheme.tblUrsprung.hobby3)
UNION
(SELECT letsMeetMigrationScheme.tblHobby.hobby_id AS hobby_id,
letsMeetMigrationScheme.tblUrsprung.user_id AS user_id,
letsMeetMigrationScheme.tblUrsprung.hobby4_prio AS prio
FROM letsMeetMigrationScheme.tblUrsprung
LEFT JOIN letsMeetMigrationScheme.tblHobby
ON letsMeetMigrationScheme.tblHobby.hobbyname = letsMeetMigrationScheme.tblUrsprung.hobby4)
UNION
(SELECT letsMeetMigrationScheme.tblHobby.hobby_id AS hobby_id,
letsMeetMigrationScheme.tblUrsprung.user_id AS user_id,
letsMeetMigrationScheme.tblUrsprung.hobby5_prio AS prio
FROM letsMeetMigrationScheme.tblUrsprung
LEFT JOIN letsMeetMigrationScheme.tblHobby
ON letsMeetMigrationScheme.tblHobby.hobbyname = letsMeetMigrationScheme.tblUrsprung.hobby5)
) AS T
WHERE T.prio IS NOT NULL;
- Die nötigen neuen Spalten erzeugen
Die nötige Tabelle wurde bereits in Teil 1 erzeugt (tblHobby2User
)
- Die Daten in die neuen Zeilen übernehmen (
SELECT
inUPDATE
umformen)
INSERT INTO letsMeetMigrationScheme.tblHobby2User
(hobby_id,user_id,priority)
SELECT hobby_id, user_id, prio
FROM (
(SELECT letsMeetMigrationScheme.tblHobby.hobby_id AS hobby_id,
letsMeetMigrationScheme.tblUrsprung.user_id AS user_id,
letsMeetMigrationScheme.tblUrsprung.hobby1_prio AS prio
FROM letsMeetMigrationScheme.tblUrsprung
LEFT JOIN letsMeetMigrationScheme.tblHobby
ON letsMeetMigrationScheme.tblHobby.hobbyname = letsMeetMigrationScheme.tblUrsprung.hobby1)
UNION
(SELECT letsMeetMigrationScheme.tblHobby.hobby_id AS hobby_id,
letsMeetMigrationScheme.tblUrsprung.user_id AS user_id,
letsMeetMigrationScheme.tblUrsprung.hobby2_prio AS prio
FROM letsMeetMigrationScheme.tblUrsprung
LEFT JOIN letsMeetMigrationScheme.tblHobby
ON letsMeetMigrationScheme.tblHobby.hobbyname = letsMeetMigrationScheme.tblUrsprung.hobby2)
UNION
(SELECT letsMeetMigrationScheme.tblHobby.hobby_id AS hobby_id,
letsMeetMigrationScheme.tblUrsprung.user_id AS user_id,
letsMeetMigrationScheme.tblUrsprung.hobby3_prio AS prio
FROM letsMeetMigrationScheme.tblUrsprung
LEFT JOIN letsMeetMigrationScheme.tblHobby
ON letsMeetMigrationScheme.tblHobby.hobbyname = letsMeetMigrationScheme.tblUrsprung.hobby3)
UNION
(SELECT letsMeetMigrationScheme.tblHobby.hobby_id AS hobby_id,
letsMeetMigrationScheme.tblUrsprung.user_id AS user_id,
letsMeetMigrationScheme.tblUrsprung.hobby4_prio AS prio
FROM letsMeetMigrationScheme.tblUrsprung
LEFT JOIN letsMeetMigrationScheme.tblHobby
ON letsMeetMigrationScheme.tblHobby.hobbyname = letsMeetMigrationScheme.tblUrsprung.hobby4)
UNION
(SELECT letsMeetMigrationScheme.tblHobby.hobby_id AS hobby_id,
letsMeetMigrationScheme.tblUrsprung.user_id AS user_id,
letsMeetMigrationScheme.tblUrsprung.hobby5_prio AS prio
FROM letsMeetMigrationScheme.tblUrsprung
LEFT JOIN letsMeetMigrationScheme.tblHobby
ON letsMeetMigrationScheme.tblHobby.hobbyname = letsMeetMigrationScheme.tblUrsprung.hobby5)
) AS T
WHERE T.prio IS NOT NULL;
- Prüfen per
SELECT
-Befehl
tblGenderInterest
- Lesend die Daten aufbereiten
Abschließend müssen wir noch das Geschlecht, an dem die jeweiligen User Interesse haben, extrahieren. Da sich in der Spalte mehrere Bezeichnungen finden können, müssen wir als JOIN
-Bedingung prüfen, ob die jeweilige Geschlechtsbezeichnung in der Spalte enthalten ist (PostgreSQL ILIKE
mit den Wildcards ‘%’):
select letsmeetmigrationscheme.tblursprung.interesse, letsmeetmigrationscheme.tblursprung.user_id, letsmeetmigrationscheme.tblgender.gender
from letsmeetmigrationscheme.tblursprung
left join letsmeetmigrationscheme.tblgender
on (letsmeetmigrationscheme.tblursprung.interesse ILIKE '%' || letsmeetmigrationscheme.tblgender.gender || '%');
Die nötigen neuen Spalten erzeugen
Die Daten in die neuen Zeilen übernehmen (
SELECT
inUPDATE
umformen)
insert into letsMeetMigrationScheme.tblGenderInterest
(gender, user_id)
select letsmeetmigrationscheme.tblgender.gender, letsmeetmigrationscheme.tblursprung.user_id
from letsmeetmigrationscheme.tblursprung
left join letsmeetmigrationscheme.tblgender
on (letsmeetmigrationscheme.tblursprung.interesse ILIKE '%' || letsmeetmigrationscheme.tblgender.gender || '%');
- Prüfen per
SELECT
-Befehl
Fazit der Normalisierung
Damit wären die Daten der Ausgangstabelle in tblUrsprung
angepasst, getrennt, auf Tabellen aufgeteilt. Wir sind also weitgehend fertig. Was fehlt ist eine eingehende Prüfung, ob wirklich alles so geklappt hat, wie es soll.
Ein fertiges SQL-Skript, dass die bestehende Datenbank anpasst für PostgreSQL findet sich hier:
Eine gesamte lauffähige Struktur im DB-Containers
Einen Container, in dem die fertige Datenbank enthalten sind, lässt sich mit folgenden Befehlen erstellen:
Die folgenden Befehle sind für die PowerShell. In der Linux-Bash muss der -OutFile "..."
-Teil weggelassen werden!
cd ~/Documents
mkdir psqldocker
cd psqldocker
wget https://gitlab.com/oer-informatik/db-sql/lets-meet-db/-/blob/raw/beispielloesung/postgresql/letsmeet_1_struktur_ddl_postgre.sql -OutFile "letsmeet_1_struktur_ddl_postgre.sql"
wget https://gitlab.com/oer-informatik/db-sql/lets-meet-db/-/blob/raw/beispielloesung/postgresql/letsmeet_2_import_einzeiler_postgre.sql -OutFile "letsmeet_2_import_einzeiler_postgre.sql"
wget https://gitlab.com/oer-informatik/db-sql/lets-meet-db/-/blob/raw/beispielloesung/postgresql/letsmeet_3_normierung_postgre.sql -OutFile "letsmeet_3_normierung_postgre.sql"
Dieses SQL-Skript wird automatisch ausgeführt, wenn wir in diesem Ordner mit folgendem Befehl einen postgreSQL-Container erzeugen und starten:
(unter Linux muss statt ${PWD}:
mit runden Klammern $(pwd):
geschrieben werden)
Das dauert diesmal etwas, weil 1576 Zeilen importiert werden müssen. Wenn alles geklappt hat, dann sollte im Container die interaktive (-it
) PostgreSQL-Kommandozeile (psql
) für Nutzer postgres
mit Passwort postgres
geöffnet werden können:
Die psql-Kommandozeile sieht etwa so aus:
Und ein Befehl wie dieser hier sollte im Idealfall die Anzahl der Zeilen (1576) ausgeben:
Mit \q
kann die Konsole wieder verlassen werden.
Weitere Details im Umgang mit PostgreSQL im Container und mit dem Zugriff eines Frontends finden sich hier.
Links und weitere Informationen
Hinweis zur Nachnutzung als Open Educational Resource (OER)
Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Normalisierung per DML-Queries eines bestehenden Datenbankinhalts (LS Beispiellösung Teil 3)” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/lets_meet-beispielloesung_normalisierung veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/db-sql/lets-meet-db. Stand: 13.06.2023.
[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]