Erstellen Sie eine eindeutige Einschränkung mit Nullspalten

252

Ich habe eine Tabelle mit diesem Layout:

CREATE TABLE Favorites
(
  FavoriteId uuid NOT NULL PRIMARY KEY,
  UserId uuid NOT NULL,
  RecipeId uuid NOT NULL,
  MenuId uuid
)

Ich möchte eine eindeutige Einschränkung erstellen, die dieser ähnelt:

ALTER TABLE Favorites
ADD CONSTRAINT Favorites_UniqueFavorite UNIQUE(UserId, MenuId, RecipeId);

Dies ermöglicht jedoch mehrere Zeilen mit derselben (UserId, RecipeId), wenn MenuId IS NULL. Ich mag , damit NULLin MenuIdeinen Favoriten zu speichern , die kein Menü hat zugeordnet, aber ich will nur höchstens eine dieser Zeilen pro Benutzer / Rezept Paar.

Die Ideen, die ich bisher habe, sind:

  1. Verwenden Sie eine fest codierte UUID (z. B. alle Nullen) anstelle von Null.
    Hat MenuIdjedoch eine FK-Einschränkung für die Menüs jedes Benutzers, so müsste ich dann für jeden Benutzer ein spezielles "Null" -Menü erstellen, was ein Ärger ist.

  2. Überprüfen Sie das Vorhandensein eines Null-Eintrags stattdessen mit einem Trigger.
    Ich denke, das ist ein Ärger und ich mag es, Auslöser zu vermeiden, wo immer dies möglich ist. Außerdem vertraue ich ihnen nicht, um sicherzustellen, dass meine Daten niemals in einem schlechten Zustand sind.

  3. Vergessen Sie es einfach und überprüfen Sie, ob in der Middleware oder in einer Einfügefunktion ein Null-Eintrag vorhanden ist, und haben Sie diese Einschränkung nicht.

Ich benutze Postgres 9.0.

Gibt es eine Methode, die ich übersehen habe?

Mike Christensen
quelle
Warum sind mehrere Zeilen mit demselben ( UserId, RecipeId) zulässig, wenn MenuId IS NULL?
Drux

Antworten:

382

Erstellen Sie zwei Teilindizes :

CREATE UNIQUE INDEX favo_3col_uni_idx ON favorites (user_id, menu_id, recipe_id)
WHERE menu_id IS NOT NULL;

CREATE UNIQUE INDEX favo_2col_uni_idx ON favorites (user_id, recipe_id)
WHERE menu_id IS NULL;

Auf diese Weise kann es nur eine Kombination von (user_id, recipe_id)wo geben menu_id IS NULL, wodurch die gewünschte Einschränkung effektiv implementiert wird.

Mögliche Nachteile: Sie können nicht auf einen Fremdschlüssel verweisen (user_id, menu_id, recipe_id), Sie können nicht CLUSTERauf einem Teilindex basieren, und Abfragen ohne übereinstimmende WHEREBedingung können den Teilindex nicht verwenden. (Es ist unwahrscheinlich, dass Sie eine drei Spalten breite FK-Referenz wünschen - verwenden Sie stattdessen die PK-Spalte).

Wenn Sie einen vollständigen Index benötigen , können Sie die WHEREBedingung alternativ löschen favo_3col_uni_idxund Ihre Anforderungen werden weiterhin durchgesetzt.
Der Index, der jetzt die gesamte Tabelle umfasst, überlappt sich mit dem anderen und wird größer. Abhängig von typischen Abfragen und dem Prozentsatz der NULLWerte kann dies nützlich sein oder auch nicht. In extremen Situationen kann es sogar hilfreich sein, alle drei Indizes (die beiden Teilindizes und die Gesamtsumme oben) beizubehalten.

Nebenbei: Ich rate, in PostgreSQL keine gemischten Fallbezeichner zu verwenden .

Erwin Brandstetter
quelle
1
@Erwin Brandsetter: In Bezug auf die " Mixed Case Identifiers " Bemerkung: Solange keine doppelten Anführungszeichen verwendet werden, ist die Verwendung von Mixed Cased Identifiers absolut in Ordnung. Es gibt keinen Unterschied bei der Verwendung aller Kleinbuchstaben (wieder: nur wenn keine Anführungszeichen verwendet werden)
a_horse_with_no_name
14
@a_horse_with_no_name: Ich nehme an, Sie wissen, dass ich das weiß. Das ist tatsächlich einer der Gründe, warum ich von seiner Verwendung abraten würde . Personen, die die Einzelheiten nicht so gut kennen, werden verwirrt, wie bei anderen RDBMS-Kennungen, bei denen (teilweise) zwischen Groß- und Kleinschreibung unterschieden wird. Manchmal verwirren sich die Leute. Oder sie erstellen dynamisches SQL und verwenden quote_ident () wie sie sollten und vergessen, Bezeichner jetzt als Kleinbuchstaben zu übergeben! Verwenden Sie in PostgreSQL keine Bezeichner für gemischte Groß- und Kleinschreibung, wenn Sie dies vermeiden können. Ich habe hier eine Reihe von verzweifelten Anfragen gesehen, die von dieser Torheit herrühren.
Erwin Brandstetter
3
@a_horse_with_no_name: Ja, das stimmt natürlich. Aber wenn Sie sie vermeiden können: Sie möchten keine gemischten Fallbezeichner . Sie dienen keinem Zweck. Wenn Sie sie vermeiden können: Verwenden Sie sie nicht. Außerdem: Sie sind einfach nur hässlich. Zitierte Identifikationen sind ebenfalls hässlich. SQL92-Bezeichner mit Leerzeichen sind ein Fehltritt eines Ausschusses. Benutze sie nicht.
Wildplasser
2
@ Mike: Ich denke, Sie müssten mit dem SQL-Standard-Komitee darüber sprechen, viel Glück :)
mu ist zu kurz
1
@buffer: Die Wartungskosten und der Gesamtspeicher sind grundsätzlich gleich (mit Ausnahme eines geringen festen Overheads pro Index). Jede Zeile wird nur in einem Index dargestellt. Leistung: Wenn Ihre Ergebnisse beide Fälle umfassen, kann sich ein zusätzlicher einfacher Gesamtindex auszahlen. Wenn nicht, ist ein Teilindex in der Regel schneller als ein vollständiger Index, hauptsächlich aufgrund der geringeren Größe. Fügen Sie die Indexbedingung zu Abfragen hinzu (redundant), wenn Postgres nicht herausfindet, dass es einen Teilindex selbst verwenden kann. Beispiel.
Erwin Brandstetter
75

Sie können einen eindeutigen Index mit einer Koaleszenz auf der Menü-ID erstellen:

CREATE UNIQUE INDEX
Favorites_UniqueFavorite ON Favorites
(UserId, COALESCE(MenuId, '00000000-0000-0000-0000-000000000000'), RecipeId);

Sie müssten nur eine UUID für die COALESCE auswählen, die im "echten Leben" niemals auftreten wird. Sie würden im wirklichen Leben wahrscheinlich nie eine Null-UUID sehen, aber Sie könnten eine CHECK-Einschränkung hinzufügen, wenn Sie paranoid sind (und da sie wirklich darauf aus sind, Sie zu erreichen ...):

alter table Favorites
add constraint check
(MenuId <> '00000000-0000-0000-0000-000000000000')
mu ist zu kurz
quelle
1
Dies birgt den (theoretischen) Fehler, dass Einträge mit menu_id = '00000000-0000-0000-0000-000000000000' falsche eindeutige Verstöße auslösen können - aber das haben Sie bereits in Ihrem Kommentar angesprochen.
Erwin Brandstetter
2
@ Muistooshort: Ja, das ist eine richtige Lösung. Vereinfachen Sie es (MenuId <> '00000000-0000-0000-0000-000000000000')jedoch. NULList standardmäßig erlaubt. Übrigens gibt es drei Arten von Menschen. Die Paranoiden und Leute, die keine Datenbanken machen. Die dritte Art stellt gelegentlich verwirrt Fragen zu SO. ;)
Erwin Brandstetter
2
@Erwin: Meinst du nicht "die paranoiden und die mit kaputten Datenbanken"?
Mu ist zu kurz
2
Diese hervorragende Lösung macht es sehr einfach, eine Nullspalte eines einfacheren Typs, z. B. eine Ganzzahl, in eine eindeutige Einschränkung aufzunehmen.
Markus Pscheidt
2
Es ist wahr, dass eine UUID nicht nur aufgrund der damit verbundenen Wahrscheinlichkeiten, sondern auch, weil es sich nicht um eine gültige UUID handelt , nicht auf diese bestimmte Zeichenfolge kommt . Ein UUID-Generator kann an keiner Position eine hexadezimale Ziffer verwenden. Beispielsweise ist eine Position für die Versionsnummer der UUID reserviert.
Toby 1 Kenobi
1

Sie können Favoriten ohne zugehöriges Menü in einer separaten Tabelle speichern:

CREATE TABLE FavoriteWithoutMenu
(
  FavoriteWithoutMenuId uuid NOT NULL, --Primary key
  UserId uuid NOT NULL,
  RecipeId uuid NOT NULL,
  UNIQUE KEY (UserId, RecipeId)
)
ypercubeᵀᴹ
quelle
Eine interessante Idee. Das Einfügen wird dadurch etwas komplizierter. Ich müsste zuerst prüfen, ob bereits eine Zeile vorhanden ist FavoriteWithoutMenu. Wenn ja, füge ich einfach einen Menülink hinzu - andernfalls erstelle ich zuerst die FavoriteWithoutMenuZeile und verknüpfe sie dann bei Bedarf mit einem Menü. Dies macht es auch sehr schwierig, alle Favoriten in einer Abfrage auszuwählen: Ich müsste etwas Seltsames tun, z. B. zuerst alle Menü-Links auswählen und dann alle Favoriten auswählen, deren IDs in der ersten Abfrage nicht vorhanden sind. Ich bin mir nicht sicher, ob mir das gefällt.
Mike Christensen
Ich halte das Einfügen nicht für komplizierter. Wenn Sie einen Datensatz mit NULL MenuIdeinfügen möchten, fügen Sie ihn in diese Tabelle ein. Wenn nicht, zum FavoritesTisch. Aber abfragen, ja, es wird komplizierter.
Ypercubeᵀᴹ
Wenn Sie alle Favoriten auswählen, ist dies nur eine einzige Verknüpfung nach links, um das Menü zu erhalten. Hmm ja, das könnte der richtige Weg sein.
Mike Christensen
Das EINFÜGEN wird komplizierter, wenn Sie dasselbe Rezept zu mehr als einem Menü hinzufügen möchten, da Sie eine EINZIGARTIGE Einschränkung für Benutzer-ID / Rezept-ID in FavoriteWithoutMenu haben. Ich müsste diese Zeile nur erstellen, wenn sie noch nicht vorhanden wäre.
Mike Christensen
1
Vielen Dank! Diese Antwort verdient eine +1, da es sich eher um eine datenbankübergreifende reine SQL-Sache handelt. In diesem Fall gehe ich jedoch auf die Teilindexroute, da keine Änderungen an meinem Schema erforderlich sind und es mir gefällt :)
Mike Christensen
-1

Ich denke, hier gibt es ein semantisches Problem. Meiner Ansicht nach kann ein Benutzer ein (aber nur ein ) Lieblingsrezept haben, um ein bestimmtes Menü vorzubereiten. (Das OP hat Menü und Rezept verwechselt; wenn ich mich irre: Bitte tauschen Sie MenuId und RecipeId unten aus.) Dies bedeutet, dass {Benutzer, Menü} ein eindeutiger Schlüssel in dieser Tabelle sein sollte. Und es sollte auf genau ein Rezept verweisen . Wenn der Benutzer kein Lieblingsrezept für dieses bestimmte Menü hat, sollte für dieses Schlüsselpaar {Benutzer, Menü} keine Zeile vorhanden sein . Außerdem: Der Ersatzschlüssel (FaVouRiteId) ist überflüssig: Zusammengesetzte Primärschlüssel sind für relationale Zuordnungstabellen perfekt gültig.

Das würde zu einer reduzierten Tabellendefinition führen:

CREATE TABLE Favorites
( UserId uuid NOT NULL REFERENCES users(id)
, MenuId uuid NOT NULL REFERENCES menus(id)
, RecipeId uuid NOT NULL REFERENCES recipes(id)
, PRIMARY KEY (UserId, MenuId)
);
Wildplasser
quelle
2
Ja das ist richtig. Außer in meinem Fall möchte ich einen Favoriten unterstützen, der zu keinem Menü gehört. Stellen Sie es sich wie Ihre Lesezeichen in Ihrem Browser vor. Sie können einfach eine Seite mit einem Lesezeichen versehen. Sie können auch Unterordner mit Lesezeichen erstellen und diese mit verschiedenen Titeln versehen. Ich möchte Benutzern erlauben, ein Rezept zu favorisieren oder Unterordner von Favoriten zu erstellen, die als Menüs bezeichnet werden.
Mike Christensen
1
Wie gesagt: Es geht nur um Semantik. (Ich habe offensichtlich über Essen nachgedacht) Einen Favoriten zu haben, "der zu keinem Menü gehört", macht für mich keinen Sinn. Sie können etwas nicht bevorzugen, das es nicht gibt, IMHO.
Wildplasser
Es scheint, als könnte eine gewisse Normalisierung der Datenbank helfen. Erstellen Sie eine zweite Tabelle, die Rezepte mit Menüs verknüpft (oder nicht). Obwohl es das Problem verallgemeinert und mehr als ein Menü zulässt, zu dem ein Rezept gehören könnte. Unabhängig davon ging es um eindeutige Indizes in PostgreSQL. Vielen Dank.
Chris