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

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!

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:

  1. Zunächst werden die Daten lesend so aufbereitet, wie wir sie später benötigen (SELECT-Statments, ohne Änderung des Datenbestands)

  2. 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)

  3. Basierend auf dem SELECT-Statement aus (1) wird ein SQL-DML-Statement (DML = Data Manipulation Language) erstellt, dass die Daten ändert / einfügt (UPDATE oder INSERT)

  4. 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):

  1. 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.

Die bisherige Spaltenamen enthält Vorname und Nachname
Die bisherige Spaltenamen enthält Vorname und Nachname

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

Am Beispiel einer Zeile skizziert die pos() und substring() Funktion
Am Beispiel einer Zeile skizziert die pos() und substring() Funktion

PostgreSQL-Beispiel:

  1. 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:

  1. Die Daten in die neuen Zeilen übernehmen (SELECT in UPDATE 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:

  1. 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:

  1. 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:

  1. Die nötigen neuen Spalten erzeugen
  1. Die Daten in die neuen Zeilen übernehmen (SELECT in UPDATE umformen)
  1. Prüfen per SELECT-Befehl
Straße von Nr trennen

  1. Lesend die Daten aufbereiten
  1. Die nötigen neuen Spalten erzeugen
  1. Die Daten in die neuen Zeilen übernehmen (SELECT in UPDATE umformen)
  1. Prüfen per SELECT-Befehl

Prüfung anhand der umgewandelten Zeichen:

SUM(CHAR_LENGTH(street_nr)) SUM(CHAR_LENGTH(street)) SUM(CHAR_LENGTH(street_no))
25036 19604 3856
PLZ von Ort trennen

  1. Lesend die Daten aufbereiten
  1. Die nötigen neuen Spalten erzeugen
  1. Die Daten in die neuen Zeilen übernehmen (SELECT in UPDATE umformen)
  1. Prüfen per SELECT-Befehl
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 Runde hobby2 usw.) um hier später Hobbynamen und Priorität zu trennen

  • Den 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:

  1. Lesend die Daten aufbereiten
  1. Die nötigen neuen Spalten erzeugen
  1. Die Daten in die neuen Zeilen übernehmen (SELECT in UPDATE umformen)
  1. 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

  1. 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!

  1. Die nötigen neuen Spalten erzeugen
  1. Die Daten in die neuen Zeilen übernehmen (SELECT in UPDATE umformen)
  1. Prüfen per SELECT-Befehl
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:

Abhängigkeit zwischen den Tabellen als Gantt-Diagramm
Abhängigkeit zwischen den Tabellen als Gantt-Diagramm

tblGender

  1. 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:

  1. Die nötigen neuen Spalten erzeugen

Die nötige Tabelle wurde bereits in Teil 1 erzeugt (tblGender)

  1. Die Daten in die neuen Zeilen übernehmen (SELECT in INSERT umformen)
  1. Prüfen per SELECT-Befehl

tblHobby

  1. 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.

  1. Die nötigen neuen Spalten erzeugen

Die nötige Tabelle wurde bereits in Teil 1 erzeugt (tblHobby)

  1. Die Daten in die neuen Zeilen übernehmen (SELECT in INSERT umformen)
  1. Prüfen per SELECT-Befehl

tblUser

  1. 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.

  1. Die nötigen neuen Spalten erzeugen

Die nötige Tabelle wurde bereits in Teil 1 erzeugt (tblUser)

  1. Die Daten in die neuen Zeilen übernehmen (SELECT in INSERT umformen)
  1. Prüfen per SELECT-Befehl

tblHobby2User

  1. 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;
  1. Die nötigen neuen Spalten erzeugen

Die nötige Tabelle wurde bereits in Teil 1 erzeugt (tblHobby2User)

  1. Die Daten in die neuen Zeilen übernehmen (SELECT in UPDATE 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;
  1. Prüfen per SELECT-Befehl

tblGenderInterest

  1. 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 ‘%’):

  1. Die nötigen neuen Spalten erzeugen

  2. Die Daten in die neuen Zeilen übernehmen (SELECT in UPDATE umformen)

  1. 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:

https://gitlab.com/oer-informatik/db-sql/lets-meet-db/-/blob/main/beispielloesung/postgresql/letsmeet_1_struktur_ddl_example_postgre.sql

https://gitlab.com/oer-informatik/db-sql/lets-meet-db/-/blob/main/beispielloesung/postgresql/letsmeet_2_import_einzeiler_postgre.sql

https://gitlab.com/oer-informatik/db-sql/lets-meet-db/-/blob/main/beispielloesung/postgresql/letsmeet_3_normierung_postgre.sql

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!

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.


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]


  1. https://www.postgresqltutorial.com/postgresql-date-functions/postgresql-to_date/

  2. https://www.postgresqltutorial.com/postgresql-date-functions/postgresql-to_date/

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: