Wie kann man eine Eins-zu-Viele-Beziehung zu einem privilegierten Kind haben?

22

Ich möchte eine Eins-zu-Viele-Beziehung, in der für jedes Elternteil eines oder null der Kinder als „Favorit“ markiert ist. Allerdings wird nicht jedes Elternteil ein Kind haben. (Stellen Sie sich die Eltern als Fragen auf dieser Website vor, die Kinder als Antworten und den Favoriten als akzeptierte Antwort.) Zum Beispiel:

TableA
    Id            INT PRIMARY KEY

TableB
    Id            INT PRIMARY KEY
    Parent        INT NOT NULL FOREIGN KEY REFERENCES TableA.Id

So wie ich es sehe, kann ich entweder die folgende Spalte zu Tabelle A hinzufügen:

    FavoriteChild INT NULL FOREIGN KEY REFERENCES TableB.Id

oder die folgende Spalte zu Tabelle B:

    IsFavorite    BIT NOT NULL

Das Problem beim ersten Ansatz besteht darin, dass ein nullwertfähiger Fremdschlüssel eingeführt wird, der meines Wissens nicht in normalisierter Form vorliegt. Das Problem mit dem zweiten Ansatz ist, dass mehr Arbeit geleistet werden muss, um sicherzustellen, dass höchstens ein Kind der Favorit ist.

Welche Art von Kriterien sollte ich verwenden, um zu bestimmen, welchen Ansatz ich verwenden möchte? Oder gibt es andere Ansätze, die ich nicht in Betracht ziehe?

Ich verwende SQL Server 2012.

cubetwo1729
quelle

Antworten:

19

Eine andere Möglichkeit (ohne Nullen und ohne Zyklen in den FOREIGN KEYBeziehungen) besteht darin, eine dritte Tabelle zum Speichern der "Lieblingskinder" zu haben. In den meisten DBMS benötigen Sie eine zusätzliche UNIQUEEinschränkung für TableB.

@Aaron erkannte schneller, dass die oben genannte Namenskonvention ziemlich umständlich ist und zu Fehlern führen kann. Es ist normalerweise besser (und wird Sie gesund halten), wenn Sie nicht alle IdSpalten in Ihren Tabellen haben und wenn die Spalten (die verbunden sind) in den vielen Tabellen, die angezeigt werden, den gleichen Namen haben. Also, hier ist eine Umbenennung:

Parent
    ParentID        INT NOT NULL PRIMARY KEY

Child
    ChildID         INT NOT NULL PRIMARY KEY
    ParentID        INT NOT NULL FOREIGN KEY REFERENCES Parent (ParentID)
    UNIQUE (ParentID, ChildID)

FavoriteChild
    ParentID        INT NOT NULL PRIMARY KEY
    ChildID         INT NOT NULL 
    FOREIGN KEY (ParentID, ChildID) 
        REFERENCES Child (ParentID, ChildID)

In SQL-Server (den Sie verwenden) können Sie auch die IsFavoriteerwähnte Bitspalte auswählen. Das eindeutige Lieblingskind pro Elternteil kann über einen gefilterten eindeutigen Index erreicht werden:

Parent
    ParentID        INT NOT NULL PRIMARY KEY

Child
    ChildID         INT NOT NULL PRIMARY KEY
    ParentID        INT NOT NULL FOREIGN KEY REFERENCES Parent (ParentID)
    IsFavorite      BIT NOT NULL

CREATE UNIQUE INDEX is_FavoriteChild
  ON Child (ParentID)
  WHERE IsFavorite = 1 ;

Der Hauptgrund dafür, dass Ihre Option 1 nicht empfohlen wird, zumindest nicht in SQL-Server, besteht darin, dass das Muster der kreisförmigen Pfade in den Fremdschlüsselreferenzen einige Probleme aufweist.

Lesen Sie einen ziemlich alten Artikel: SQL By Design: The Circular Reference

Beim Einfügen oder Löschen von Zeilen aus den beiden Tabellen tritt das Problem "Henne und Ei" auf. Welche Tabelle soll ich zuerst einfügen - ohne Einschränkung?

Um dies zu lösen, müssen Sie mindestens eine Spalte definieren, die nullwertfähig ist. (Technisch gesehen müssen Sie das nicht. Sie können alle Spalten haben, NOT NULLaber nur in DBMS, wie Postgres und Oracle, die aufschiebbare Einschränkungen implementiert haben. Siehe @ Erwins Antwort in einer ähnlichen Frage: Komplexe Fremdschlüssel-Einschränkung in SQLAlchemy, wie Dies kann in Postgres erfolgen. Dennoch fühlt sich dieses Setup an, als würde man auf dünnem Eis Schlittschuh laufen.

Überprüfen Sie auch eine fast identische Frage bei SO (aber für MySQL). Ist es in SQL in Ordnung, wenn zwei Tabellen aufeinander verweisen? wo meine antwort so ziemlich gleich ist. MySQL hat jedoch keine Teilindizes. Die einzigen möglichen Optionen sind die nullbaren FK und die zusätzliche Tabellenlösung.

ypercubeᵀᴹ
quelle
9

Es hängt davon ab, welche Priorität Sie haben. Möchten Sie die Arbeit vermeiden oder die strengsten Normalisierungsregeln einhalten?

Persönlich denke ich, dass es besser ist, IsFavoritein der Kindertabelle zu sitzen und bereit zu sein, sich an der Arbeit zu beteiligen, um sicherzustellen, dass höchstens ein Kind pro Elternteil der Favorit dieses Elternteils ist. Der Hauptgrund hat nichts mit dem nullbaren Fremdschlüssel zu tun: Mir gefällt die Idee überhaupt nicht, dass Fremdschlüssel in beide Richtungen zeigen.

@ypercubes Vorschlag ist ebenfalls ein guter Kompromiss.

Abgesehen davon, bitte, bitte, bitte verunreinigen Sie Ihr Schema nicht mit sinnlosen Spaltennamen wie Id. Ich würde es vorziehen, Idwenn der Name im gesamten Schema aussagekräftig wäre. Identifiziert es einen Autor? Ok, nenn es AuthorID. Stellt es ein Produkt dar? Ok ProductID. Handelt es sich um einen Mitarbeiter und in einigen Fällen um einen Manager? Ok EmployeeIDund ManagerIDfür mich sinnvoller als IDund Parent. Auch wenn es logisch erscheint, dies wegzulassen (und überflüssig zu machen), werden Sie beim Schreiben komplexer Joins (oder beim Veröffentlichen von Abfragen hier) auf jeden Fall einen gewissen Fluch spüren, wenn Sie versuchen, eine Reihe von Joins an diesem Punkt rückgängig zu machen zu Spalten wie a.Parent = b.ID... blecch.

Aaron Bertrand
quelle
1

Daten gehören in die untergeordnete Tabelle. Wir halten es mit einem Auslöser auf dem Tisch richtig, um sicherzustellen, dass eine einzige Aufzeichnung als Favorit markiert ist (oder in unserem Fall als bevorzugte Adresse).

@Ypercubes Idee einer separaten Tabelle ist jedoch auch gut.

HLGEM
quelle