Steffen: Datenbanknormalisierung

Hallo,

ich habe in meiner Datenbank 2 Tabellen.
#1 Kunden
#2 Lieferanten

Beide dieser Tabellen beinhalten Kontaktdaten.
z.B. Straße, Hausnummer, PLZ, Ort, Telefon, FAX

Somit hätte ich jetzt 2 Tabellen, die teilweise gleich aufgebaut sind.

z.B.
#1 Kunden

  • id + Kundennummer + Straße + Hausnummer + PLZ + Ort + Telefon + FAX +

#2 Lieferanten

  • id + Lieferantennummer + Straße + Hausnummer + PLZ + Ort + Telefon + FAX +

Ich möchte jetzt die Kontaktdaten in eine separate Tabelle auslagern.
(Bietet sich an, aber ist es auch gescheit?)

Jetzt habe ich

#1 Kunden

  • id + Kundennummer

#2 Lieferanten

  • id + Lieferantennummer

#3 Kontaktdaten

  • id + Straße + Hausnummer + PLZ + Ort + Telefon + FAX + fk-Kunden + fk-Lieferanten

Per Constraint auf der Tabelle Kontaktdaten versuche ich jetzt dafür zu sorgen, dass fk-Kunden oder (XOR) fk-Lieferanten nicht NULL ist.

Mir erscheinen die beiden Foreign-Keys auf der Tabelle Kontaktdaten sehr umständlich. Vor allem, wenn man aus weiteren Tabellen (z.B. Mitarbeiter, ...) diese Tabelle benutzen möchte.

Gibt es dafür bessere/schönere/andere Lösungen?

Grüße

  • Steffen
  1. Moin Steffen,

    Gibt es dafür bessere/schönere/andere Lösungen?

    Warum zwei Tabellen? Warum machst du nicht eine Tabelle „contacts,” die sowohl die Lieferanten als auch die Kunden enthält? Unterscheidbar bleiben sie dann durch ein Attribut „contact_type” (AS ENUM('customer', 'vendor')).

    LG,
     CK

    1. Hallo,

      Gibt es dafür bessere/schönere/andere Lösungen?

      Warum zwei Tabellen? Warum machst du nicht eine Tabelle „contacts,” die sowohl die Lieferanten als auch die Kunden enthält? Unterscheidbar bleiben sie dann durch ein Attribut „contact_type” (AS ENUM('customer', 'vendor')).

      Ich muss ja die Kunden und Lieferanten spezifischen Daten auch irgendwo ablegen. Dafür gibt es dann die Kunden- und Lieferantentabellen. Es sind einfach verschiedene Daten, die sich nur teilweise überschneiden.

      z.B. weitere Felder
      Kunden -> + newsletter-erwünscht + bankeinzug +
      Lieferanten -> + uid + schnelllieferung + zahlungsziel +

      Grüße

      • Steffen
      1. Moin Steffen,

        Warum zwei Tabellen? Warum machst du nicht eine Tabelle „contacts,” die sowohl die Lieferanten als auch die Kunden enthält? Unterscheidbar bleiben sie dann durch ein Attribut „contact_type” (AS ENUM('customer', 'vendor')).

        Ich muss ja die Kunden und Lieferanten spezifischen Daten auch irgendwo ablegen. Dafür gibt es dann die Kunden- und Lieferantentabellen. Es sind einfach verschiedene Daten, die sich nur teilweise überschneiden.

        z.B. weitere Felder
        Kunden -> + newsletter-erwünscht + bankeinzug +
        Lieferanten -> + uid + schnelllieferung + zahlungsziel +

        Ich glaube, da würde ich aber dann eher den Weg gehen eine Tabelle „vendor_infos” (oder so) zu erstellen, die sich dann via foreign key auf die Kontakte bezieht. Dein Gefrickel (sorry)  mit dem CHECK-Constraint sowie den beiden unterschiedlichen foreign keys sieht für mich nach noch folgenden Schmerzen aus.

        LG,
         CK

        1. Hallo!

          Ich glaube, da würde ich aber dann eher den Weg gehen eine Tabelle „vendor_infos” (oder so) zu erstellen, die sich dann via foreign key auf die Kontakte bezieht. Dein Gefrickel (sorry)  mit dem CHECK-Constraint sowie den beiden unterschiedlichen foreign keys sieht für mich nach noch folgenden Schmerzen aus.

          Danke! Ich weiß! ;-) (Schmerzen)

          Das Problem würde sich ja recht leicht lösen lassen, wenn ich den FK umdrehe. (Ähnlich wie Du es beschreibst.)

          #1 Kunden

          • id + Kundennummer + FK-Kontaktdaten

          #2 Lieferanten

          • id + Lieferantennummer + FK-Kontaktdaten

          #3 Kontaktdaten

          • id + Straße + Hausnummer + PLZ + Ort + Telefon + FAX

          Allerdings kann ich dann in der Datenbank die Gültigkeit der Daten nicht mehr garantieren, weil
          1. Könnten Kunden und Lieferanten auf den selben Kontakt zeigen
          und
          2. Könnten Kontaktdaten existieren, die nicht mehr referenziert werden.

          Grüße

          • Steffen
          1. Moin Steffen,

            Allerdings kann ich dann in der Datenbank die Gültigkeit der Daten nicht mehr garantieren, weil

            1. Könnten Kunden und Lieferanten auf den selben Kontakt zeigen

            Das wäre für mich kein Fehler sondern eine valide Möglichkeit: ein Lieferant kann ja prinzipiell auch als Kunde zu euch kommen. Aber wenn du das verhindern möchtest, kannst du immer noch auf eine View referenzieren, die alle ctype != 'vendor' rausfiltert. Oder alternativ einen CHECK-Constraint/Trigger implementieren, der aufpasst, dass man keinen Datensatz einfügt, der auf Nicht-Vendor- bzw Nicht-Customer-Objekte verweist.

            und
            2. Könnten Kontaktdaten existieren, die nicht mehr referenziert werden.

            Das ist wahr. Da würde ich mit Triggern arbeiten, um das zu verhindern.

            LG,
             CK

            1. Tag,

              1. Könnten Kunden und Lieferanten auf den selben Kontakt zeigen

              Das wäre für mich kein Fehler sondern eine valide Möglichkeit: ein Lieferant kann ja prinzipiell auch als Kunde zu euch kommen. Aber wenn du das verhindern möchtest, kannst du immer noch auf eine View referenzieren, die alle ctype != 'vendor' rausfiltert. Oder alternativ einen CHECK-Constraint/Trigger implementieren, der aufpasst, dass man keinen Datensatz einfügt, der auf Nicht-Vendor- bzw Nicht-Customer-Objekte verweist.

              Das klingt interessant und muss ich mir näher anschauen.

              Kann ich in Check-Constraints auf VIEWs zugreifen?

              Oder meinst Du, dass ich in meine Kundentabelle nicht die Kontaktdaten per FK referenziere, sondern den VIEW "Kontaktdaten-nur-Kunden"?

              #1 Kunden

              • id + Kundennummer + fk-view-kunden-kontaktdaten

              #2 Kontaktdaten

              • id + TYPE(Kunde/Lieferant)

              #3 View (Kontaktdaten-nur-kunden)

              • id + ...

              Grüße

              • Steffen
            2. Moin!

              Das ist wahr. Da würde ich mit Triggern arbeiten, um das zu verhindern.

              Gibts eigentlich schon schlaue Methoden, diesen applikationsspezifischen Code pfleg- und versionierbar abzulegen?

              Mir gefällt der Gedanke nicht, dass die Applikation nur funktioniert, wenn die Datenbank ebenfalls ihren Teil des Codes korrekt implementiert bekommen hat. Außerdem sollte ein ungültiger bzw. invalider Datenschreibversuch nicht erst in der Datenbank scheitern.

              - Sven Rautenberg

              1. Hallo,

                Gibts eigentlich schon schlaue Methoden, diesen applikationsspezifischen Code pfleg- und versionierbar abzulegen?

                Naja, irgendwie gehören die Datenbank und die Applikation ja zusammen. Und die Datenbank muss für die Datengültigkeit sorgen. Und dafür muss sie zwangsweise auch etwas über die Applikation wissen.

                Wegen der Versionsverwaltung; das war/ist für mich auch ein großes Problem gewesen. Im Moment mache ich das wie folgt (mehr oder weniger automatisiert).

                Ich dumpe regelmäßig mein Datenbankschema ins SVN und entferne vor dem Checkin die Kommentare. So bekomme ich den relevanten Code ins SVN.

                Beim Update dumpe ich das Schema des Zielsystem und mache ein Diff (Postgres: apgdiff). Daraus erhalte ich dann fertige SQL-Statements, die ich (fast immer) am Zielsystem ausführen kann.

                Zur Sicherheit mache ich danach noch ein einfaches Diff.

                Anmerkung: Um die Version des Datenbankschemas kontrollieren zu können habe ich im Datenbankkommentar noch ein $Id$ drinnen.

                Mir gefällt der Gedanke nicht, dass die Applikation nur funktioniert, wenn die Datenbank ebenfalls ihren Teil des Codes korrekt implementiert bekommen hat. Außerdem sollte ein ungültiger bzw. invalider Datenschreibversuch nicht erst in der Datenbank scheitern.

                Ein ungültiger Datenschreibversuch sollte spätestens in der Datenbank scheitern.

                Grüße

                • Steffen
                1. Tach!

                  Gibts eigentlich schon schlaue Methoden, diesen applikationsspezifischen Code pfleg- und versionierbar abzulegen?

                  Es gibt Migrationsfunktionalitäten zumindest in anderen Systemen, bei denen je nach Versionsstand Up- und Downgrade-Scripts das jeweilige Delta hinzufügen oder entfernen. Ob das in einem der 3rd-Party-PHP-Datenbank-Layer enthalten ist, weiß ich nicht.

                  Naja, irgendwie gehören die Datenbank und die Applikation ja zusammen. Und die Datenbank muss für die Datengültigkeit sorgen. Und dafür muss sie zwangsweise auch etwas über die Applikation wissen.

                  So absolut würde ich das nicht in jedem Fall sehen. Wenn das Projekt so ausgelegt ist, dass nur eine einzige Applikation (abgesehen von Admin-Tools) auf den Datenbestand zugreift, warum soll dann nicht diese Applikation selbst für die Datengültigkeit sorgen?

                  dedlfix.

                  1. Hi!

                    Naja, irgendwie gehören die Datenbank und die Applikation ja zusammen. Und die Datenbank muss für die Datengültigkeit sorgen. Und dafür muss sie zwangsweise auch etwas über die Applikation wissen.

                    So absolut würde ich das nicht in jedem Fall sehen. Wenn das Projekt so ausgelegt ist, dass nur eine einzige Applikation (abgesehen von Admin-Tools) auf den Datenbestand zugreift, warum soll dann nicht diese Applikation selbst für die Datengültigkeit sorgen?

                    Ich finde schon, dass das nach Möglichkeit in der DB passieren sollte. Das ist das, was eine DB (u.a.) gut kann.

                    Das fängt mMn schein bei einfachen "not null" und "foreign keys" Contraints an. Willst Du sowas alles in der Applikation machen? Absolut nicht sinnvoll. Das bläst den Applikationscode unnötig auf. Das geht vielleicht dann, wenn sich Dein Applikation nicht weiter entwickelt und sich das Datenschema nicht mehr ändert.

                    Ich für meinen Teil habe es ganz gern, wenn ich mich auf ein gewisses Datenschema verlassen kann. Das spart mit viele Überprüfungen in der Applikation und macht diese auch besser wartbar.

                    Grüße

                    • Steffen
                    1. Tach!

                      Wenn das Projekt so ausgelegt ist, dass nur eine einzige Applikation (abgesehen von Admin-Tools) auf den Datenbestand zugreift, warum soll dann nicht diese Applikation selbst für die Datengültigkeit sorgen?
                      Ich finde schon, dass das nach Möglichkeit in der DB passieren sollte. Das ist das, was eine DB (u.a.) gut kann.
                      Das fängt mMn schein bei einfachen "not null" und "foreign keys" Contraints an. Willst Du sowas alles in der Applikation machen? Absolut nicht sinnvoll. Das bläst den Applikationscode unnötig auf.

                      "Absolut"? Ich finde es aber auch nicht sinnvoll, wenn die Applikation die meiste Zeit in catch-Bereichen verbringt, nur weil sie kein bisschen darauf achtet, was die Datenbank mag und was nicht. Die App sollte schon dem Schema entsprechend angelegt sein. Wenn im DBMS kein Null-Wert abgelegt werden kann, sollte auch die Anwendung nicht für dieses Feld mit Null-Werte arbeiten können und erst beim Insert auf die Nase fallen. Ich sehe es auch nicht als besonders sinnvoll an, sämtliche andere Datenvalidation in Stored Procedures oder ähnliches zu stecken.

                      dedlfix.

                      1. Hallo!

                        Wenn das Projekt so ausgelegt ist, dass nur eine einzige Applikation (abgesehen von Admin-Tools) auf den Datenbestand zugreift, warum soll dann nicht diese Applikation selbst für die Datengültigkeit sorgen?
                        Ich finde schon, dass das nach Möglichkeit in der DB passieren sollte. Das ist das, was eine DB (u.a.) gut kann.
                        Das fängt mMn schein bei einfachen "not null" und "foreign keys" Contraints an. Willst Du sowas alles in der Applikation machen? Absolut nicht sinnvoll. Das bläst den Applikationscode unnötig auf.

                        "Absolut"? Ich finde es aber auch nicht sinnvoll, wenn die Applikation die meiste Zeit in catch-Bereichen verbringt, nur weil sie kein bisschen darauf achtet, was die Datenbank mag und was nicht. Die App sollte schon dem Schema entsprechend angelegt sein.

                        Auf alle Fälle. Fehler sollte für den Benutzer so-und-so keine auftauchen. Also muss das UI schon so angelegt sein, dass der Benutzer nicht leer lassen kann.

                        Ein catch ist dafür aber auch nicht sinnvoll, weil die App sollte zu DB passen. Somit kann dieser Fehler gar nicht auftreten. Wenn es doch zu einem Fehler kommt ist es egal, ob von der DB oder von der App generiert.

                        Wenn im DBMS kein Null-Wert abgelegt werden kann, sollte auch die Anwendung nicht für dieses Feld mit Null-Werte arbeiten können und erst beim Insert auf die Nase fallen. Ich sehe es auch nicht als besonders sinnvoll an, sämtliche andere Datenvalidation in Stored Procedures oder ähnliches zu stecken.

                        Sag ich ja. Ich habe es zwar eher an SELECTs gedacht, aber wenn 'not-null' -> ich kann mich darauf verlassen, dass da ein Wert ist.

                        Aber es muss jeder für sich selber entscheiden, wie weit er geht. Ich finde jedenfalls, dass sich Datenkonsistenz wesentlich einfacher und mit viel weniger Code in der DB erzeugen lässt; nicht in der Applikation. Und konsistente Daten machen die Applikation schlanker.

                        Grüße

                        • Steffen
              2. Moin Sven,

                Das ist wahr. Da würde ich mit Triggern arbeiten, um das zu verhindern.

                Gibts eigentlich schon schlaue Methoden, diesen applikationsspezifischen Code pfleg- und versionierbar abzulegen?

                Es gibt Ansätze, aber noch nichts handfestes. Der üblichste Ansatz sind Migrations mit einem Schema-Dump nach einer neuer Migration. Bei PostgreSQL gibt es ein, zwei interessante Ansätze (z.B. diese Blogpost-Serie), aber das handfesteste, was ich kenne, ist das Management von Rails mit seinen Migrations und dem Schema-Dump (wobei ich auch hier auf SQL-Dump umstelle anstatt eines Ruby-Dumps).

                Mir gefällt der Gedanke nicht, dass die Applikation nur funktioniert, wenn die Datenbank ebenfalls ihren Teil des Codes korrekt implementiert bekommen hat.

                Ich glaube, von dem Gedanken kann man sich verabschieden. Datenbank und Applikation sind dermaßen miteinander verbunden, dass man ohne dass die Datenbank korrekt implementiert wurde eh nicht arbeiten kann.

                Außerdem sollte ein ungültiger bzw. invalider Datenschreibversuch nicht erst in der Datenbank scheitern.

                Das ist ein Philosophie-Ansatz. Es gibt Leute, die lagern solche Constraints und Verwaltungsinformationen fast vollständig in die Applikation aus (vielleicht abgesehen von foreign keys und unique constraints) und es gibt Leute, die machen das fast vollständig über die Datenbank. Welchen Weg ein Entwickler gehen will, muss er selber wissen, beide haben Vor- und Nachteile.

                Ich persönlich bin früher auch deiner Meinung gewesen, sehe da aber inzwischen keinen Sinn mehr drin und lagere vieles in die Datenbank aus, da sie das strukturell bedingt oft viel einfacher und effizienter kann. Und dabei spreche ich nicht von Format-Validierungen oder so etwas. :-)

                LG,
                 CK

          2. #1 Kunden

            • id + Kundennummer + FK-Kontaktdaten

            #2 Lieferanten

            • id + Lieferantennummer + FK-Kontaktdaten

            Würde ich nicht tun. Warum willst du das?
            Die Beziehung zwischen Kunde und Kontaktdaten (analog Lieferant-Kontakt) dürfte ziemlich 1:1 sein. Erwartest du hier eine Ersparnis an Daten?
            Kontaktdaten gehören für mich direkt zu dem Objekt für das sie gedacht sind. Außer natürlich du hast mehrere Kontakte pro Kunde.

            Selbst wenn ein Lieferant gleichzeitig Kunde ist, würde ich die Daten eben doppelt eingeben. Wahrscheinlich sind deine Kontaktpersonen für Einkauf und Verkauf sowieso verschieden.

            1. Hallo,

              #1 Kunden

              • id + Kundennummer + FK-Kontaktdaten

              #2 Lieferanten

              • id + Lieferantennummer + FK-Kontaktdaten

              Würde ich nicht tun. Warum willst du das?
              Die Beziehung zwischen Kunde und Kontaktdaten (analog Lieferant-Kontakt) dürfte ziemlich 1:1 sein. Erwartest du hier eine Ersparnis an Daten?
              Kontaktdaten gehören für mich direkt zu dem Objekt für das sie gedacht sind. Außer natürlich du hast mehrere Kontakte pro Kunde.

              Selbst wenn ein Lieferant gleichzeitig Kunde ist, würde ich die Daten eben doppelt eingeben. Wahrscheinlich sind deine Kontaktpersonen für Einkauf und Verkauf sowieso verschieden.

              das möchte ich auch nicht. Jede Zeile aus Kunden und Lieferanten soll immer eine eigene Zeile in Kontaktdaten bekommen. Selbst wenn Kunde gleich Lieferant ist, möchte ich 2 separate Datensätze.

              Was ich möchte, wenn sich das Schema meiner Kontaktdaten ändert, dann möchte ich es nur einmal machen. (Es gibt auch ein entsprechendes UI-Control.)
              Wenn ich in diesen Daten suche oder Indizes baue, dann brauch ich das nur hier einmal machen und es funktioniert, bzw. das Verhalten ist immer gleich.

              Grüße

              • Steffen
              1. Was ich möchte, wenn sich das Schema meiner Kontaktdaten ändert, dann möchte ich es nur einmal machen.

                Naja wie oft ändert man das Schema? Dann müsstest du eben zwei Tabellen ändern statt einer. Deswegen würde ich mir trotzdem überlegen ob du dir dafür die Umstände mit zwei Tabellen einhandeln willst. Jede neue Abfrage wird komplizierter weil du überall JOINs hast. Und das nur weil du sonst alle paar Monate mal zwei Tabellen statt einer anpassen musst?

                (Es gibt auch ein entsprechendes UI-Control.)

                Das kann ja nach wie vor eines bleiben.

                1. Hallo!

                  Was ich möchte, wenn sich das Schema meiner Kontaktdaten ändert, dann möchte ich es nur einmal machen.
                  Naja wie oft ändert man das Schema? Dann müsstest du eben zwei Tabellen ändern statt einer. Deswegen würde ich mir trotzdem überlegen ob du dir dafür die Umstände mit zwei Tabellen einhandeln willst. Jede neue Abfrage wird komplizierter weil du überall JOINs hast. Und das nur weil du sonst alle paar Monate mal zwei Tabellen statt einer anpassen musst?

                  Stimmt natürlich. Das ist ein schlechtes Argument.

                  Allerdings, was für mich auch ein Beweggrund war; ich mag es nicht, wenn Tabellen so unglaublich in die "Breite" gehen. Das wird es schnell unübersichtlich.

                  Im meiner Anwendung habe ich ja die Kontaktdaten und die Bankdaten in separate Tabellen ausgelagert, die dann von mehreren anderen Tabellen (Lieferanten, Kunden, ...) genutzt werden.

                  Wie geht Ihr mit "breiten" Tabellen um? In mehrere Tabellen aufteilen und per 1:1 verknüpfen?

                  Grüße

                  • Steffen
                  1. Allerdings, was für mich auch ein Beweggrund war; ich mag es nicht, wenn Tabellen so unglaublich in die "Breite" gehen. Das wird es schnell unübersichtlich.

                    Für wen?
                    Die Datenbank packt es. Du packst es auch einfacher wenn es um die Erstellung von Queries geht.

                    Wie geht Ihr mit "breiten" Tabellen um? In mehrere Tabellen aufteilen und per 1:1 verknüpfen?

                    Nein, aus den genannten Gründen bin ich froh wenn ich nur eine einzige Tabelle habe in der alles beisammen ist was zusammen gehört :-)

                    Ich will dich ja wirklich nicht umstimmen, wenn du einen Vorteil in deiner Struktur siehst dann sollst du das gerne so machen.
                    Aber wenn du "unübersichtlich" erwähnst, ich wüsste wirklich nicht was übersichtlicher ist wenn du zusammengehörende Daten auseinander reißt. Du hast die Datenfelder trotzdem, sparst also keinen Speicher. Nur musst du immer in zwei Tabellen nachsehen statt in einer. Und du musst die Daten konsistent halten. Entweder immer selbst ans löschen der anderen Tabelle denken, oder mit Triggern was aber auch kein bisschen übersichtlich ist.

                    Falls du öfter manuelle Queries absetzt und im Ergebnis nur ein paar bestimmte Spalten sehen willst, denke über eine View nach.