Erstellen Sie eine PostgreSQL-Einschränkung, um eindeutige Kombinationszeilen zu vermeiden

9

Stellen Sie sich vor, Sie haben eine einfache Tabelle:

name | is_active
----------------
A    | 0
A    | 0
B    | 0
C    | 1
...  | ...

Ich muss eine spezielle eindeutige Einschränkung erstellen, die in der folgenden Situation fehlschlägt: Unterschiedliche is_activeWerte können nicht für denselben nameWert nebeneinander existieren .

Beispiel für einen zulässigen Zustand:

Hinweis: Ein einfacher mehrspaltiger eindeutiger Index lässt eine solche Kombination nicht zu.

A    | 0
A    | 0
B    | 0

Beispiel für einen zulässigen Zustand:

A    | 0
B    | 1

Beispiel für einen fehlgeschlagenen Zustand:

A    | 0
A    | 1
-- should be prevented, because `A 0` exists
-- same name, but different `is_active`

Idealerweise benötige ich eine eindeutige Einschränkung oder einen eindeutigen Teilindex. Auslöser sind für mich problematischer.

Doppelt A,0erlaubt, aber (A,0) (A,1)nicht.

Andrii Skaliuk
quelle

Antworten:

17

Sie können eine Ausschlussbeschränkung mit verwenden btree_gist:

-- This is needed
CREATE EXTENSION btree_gist;

Dann fügen wir eine Einschränkung hinzu, die besagt:

"Wir können nicht zwei Zeilen haben, die gleich nameund verschieden sind is_active" :

ALTER TABLE table_name
  ADD CONSTRAINT only_one_is_active_value_per_name
    EXCLUDE  USING gist
    ( name WITH =, 
      is_active WITH <>      -- if boolean, use instead:
                             -- (is_active::int) WITH <>
    );

Einige Notizen:

  • is_activekann ganzzahlig oder boolesch sein, macht keinen Unterschied für die Ausschlussbeschränkung. (Tatsächlich ist es so, wenn die Spalte boolesch ist, müssen Sie verwenden (is_active::int) WITH <>.)
  • Zeilen, in denen nameoder is_activenull ist, werden von der Einschränkung ignoriert und sind daher zulässig.
  • Die Einschränkung ist nur dann sinnvoll, wenn die Tabelle mehr Spalten enthält. Andernfalls wäre eine UNIQUEEinschränkung für sich (name)allein einfacher und angemessener , wenn die Tabelle nur diese beiden Spalten enthält . Ich sehe keinen Grund, mehrere identische Zeilen zu speichern.
  • Das Design verletzt 2NF. Die Ausschlussbeschränkung bewahrt uns zwar vor Aktualisierungsanomalien, möglicherweise jedoch nicht vor Leistungsproblemen. Wenn Sie beispielsweise 1000 Zeilen mit haben name = 'A'und den is_active-Status von 0 auf 3 aktualisieren möchten, müssen alle 1000 aktualisiert werden. Sie sollten prüfen, ob eine Normalisierung des Designs effizienter ist. (Normalisierung der Bedeutung in diesem Fall, um den Status is_active aus der Tabelle zu entfernen und eine zweispaltige Tabelle mit dem Namen is_active und einer eindeutigen Einschränkung hinzuzufügen (name). Wenn dies is_activeboolesch ist, kann es vollständig entfernt werden und die zusätzliche Tabelle nur eine einzelne Spaltentabelle, die gespeichert wird nur die "aktiven" Namen.)
ypercubeᵀᴹ
quelle
is_active kann nicht boolesch sein,ERROR: data type boolean has no default operator class for access method "gist"
Evan Carroll
1
@EvanCarroll Ich kann mich nicht erinnern, wie gut ich das getestet habe, als ich gepostet habe. Aber es funktioniert mit intund smallint.
Ypercubeᵀᴹ
Funktioniert auch mit, EXCLUDE USING gist (name WITH =, (is_active::int) WITH <>)wenn es boolesch ist. Und die Frage hat 0und 1, nicht trueund falseso ist es eher unwahrscheinlich, dass ich mit Booleschen
Werten
Alles gut, ich habe eine Ausschlussbeschränkung für dba.stackexchange.com/a/175922/2639 verwendet und hatte ein Problem mit der Verwendung eines Booleschen Werts, also habe ich gesucht. Ich dachte, btree_gist deckt Bools ab, aber das tut es nicht.
Evan Carroll
3

Dies ist kein Fall, in dem Sie einen eindeutigen Index verwenden können. Sie können den Zustand in einem Trigger testen, z.

create or replace function a_table_trigger()
returns trigger language plpgsql as $$
declare
    active int;
begin
    select is_active into active
    from a_table
    where name = new.name;

    if found and active is distinct from new.is_active then
        raise exception 'The value of is_active for "%" should be %', new.name, active;
    end if;
    return new;
end $$;

Testen Sie es hier.

klin
quelle