Erzwingen Sie NOT NULL für eine Reihe von Spalten mit einer CHECK-Einschränkung nur für neue Zeilen

7

Ich habe eine Tabelle und muss eine neue Spalte ohne Standardwert hinzufügen:

Zwang:

ALTER TABLE integrations.billables 
DROP CONSTRAINT IF EXISTS cc_at_least_one_mapping_needed_billables,
ADD CONSTRAINT cc_at_least_one_mapping_needed_billables 
CHECK ((("qb_id" IS NOT NULL) :: INTEGER +
    ("xero_id" IS NOT NULL) :: INTEGER +
    ("freshbooks_id" IS NOT NULL) :: INTEGER +
    ("unleashed_id" IS NOT NULL) :: INTEGER +
    ("csv_data" IS NOT NULL) :: INTEGER +
    ("myob_id" IS NOT NULL) :: INTEGER) > 0);

Säule:

ALTER TABLE integrations.billables
DROP COLUMN IF EXISTS myob_id,
ADD COLUMN myob_id varchar(255);

Frage:

Wie kann ich die Einschränkung für die nächsten Werte hinzufügen und nicht für diejenigen, die bereits vorhanden sind? (Andernfalls würde die Fehlerprüfungsbeschränkung "" durch eine Zeile verletzt).

Dies hängt mit meiner vorherigen Frage zusammen: FEHLER: Die Prüfbedingung wird durch eine Zeile verletzt

Gemeinschaft
quelle
1
Hat die Tabelle jetzt die Spalte ohne Einschränkung oder die Spalte und die Einschränkung? Oder auch nicht? Die Frage ist nicht klar.
Ypercubeᵀᴹ
@ ypercubeᵀᴹ es ist mir klar. Wie Sie oben sehen können,
5
Wie kann es eine Einschränkung geben, die myob_idohne die in der Tabelle vorhandene Spalte erwähnt wird ?
Ypercubeᵀᴹ
@ ypercubeᵀᴹ: Natürlich müssen die Befehle in der Frage neu angeordnet werden. Die Einschränkung muss angepasst werden, nachdem die neue Spalte hinzugefügt wurde.
Erwin Brandstetter

Antworten:

4

Markieren Sie alle vorhandenen Zeilen als alt:

ALTER TABLE integrations.billables
ADD COLUMN is_old BOOLEAN NOT NULL DEFAULT false;

UPDATE integrations.billables SET is_old = true;

Und richten Sie die Einschränkung so ein, dass alte Zeilen ignoriert werden:

ALTER TABLE integrations.billables
ADD CONSTRAINT cc_at_least_one_mapping_needed_billables 
CHECK (
    NOT(("qb_id", "xero_id", "freshbooks_id", "unleashed_id", "csv_data", "myob_id") IS NULL)
    OR is_old
);

(Ja, diese IS NULLPrüfung funktioniert. Siehe hier .)

Vorteile dieses Mechanismus:

  • Die Einschränkung bleibt gültig
  • Sie können alte Zeilen weiterhin aktualisieren, ohne diesen Wert einzugeben

Nachteile:

  • Eine ähnliche Situation auf der Straße wird chaotisch sein. Sie müssten eine zweite booleanSpalte oder einen anderen Reifensprung für die zweite neue Spalte hinzufügen .
  • Wenn Sie erzwingen möchten , dass aktualisierten Zeilen ein Wert zugewiesen wird, ist dies nicht der Fall.
  • Dies kann zu Missbrauch führen, da jemand einfach die is_oldFlagge umdrehen könnte true. (Dies kann jedoch behoben werden. Siehe unten.) Dies ist kein Grund zur Sorge, wenn Endbenutzer nicht direkt auf die Datenbank zugreifen können und Sie darauf vertrauen können, dass die Entwickler keine verrückten Dinge mit den Daten tun.

Wenn Sie sich über jemanden Änderung der Flagge besorgt, könnten Sie einen Trigger einrichten, um alle Einsätze oder Updates zu verhindern Einstellung is_oldzu true:

CREATE FUNCTION throw_error_on_illegal_old()
  RETURNS trigger
  LANGUAGE plpgsql
  AS $$
  BEGIN
    IF NEW.is_old THEN
      -- Need to make sure we don't try to access
      -- OLD in an INSERT
      IF TG_OP = 'INSERT' THEN
        RAISE 'Cannot create new with is_old = true';
      ELSE
        IF NOT OLD.is_old THEN
          RAISE 'Cannot change is_old from false to true';
        END IF;
      END IF;
    END IF;
    -- If we get here, all tests passed
    RETURN NEW;
  END
  $$
;

CREATE TRIGGER billables_prohibit_marking_row_old
BEFORE INSERT OR UPDATE ON integrations.billables
FOR EACH ROW EXECUTE PROCEDURE throw_error_on_illegal_old()
;

Sie müssen immer noch darauf vertrauen, dass niemand, der das Datenbankschema ändern kann, Ihren Trigger oder etwas anderes fallen lässt, aber wenn er das tut, kann er auch die Einschränkung fallen lassen.

Hier ist eine SQLFiddle-Demo . Beachten Sie, dass die Zeile "sollte überspringen" nicht in der Ausgabe enthalten ist (wie gewünscht).

jpmc26
quelle
Ihre Antwort ist sehr interessant. Ich müsste jedoch mehr als 40 Millionen Zeilen aktualisieren. Es ist ziemlich unmöglich = \
1
Ich bezweifle ernsthaft, dass es unmöglich ist . Es kann lange dauern , aber das ist ein ganz anderes Problem. Ob Sie mit diesem Zeitrahmen umgehen können, hängt von Ihren Einschränkungen ab. Sie können testen, wie lange es dauert, und sobald Sie es wissen, können Sie eine Entscheidung treffen. Können Sie es sich leisten, so lange offline zu sein? Könnten Sie die Datenbank / Anwendung während der Ausführung in einen schreibgeschützten Zustand versetzen? (Für letzteres können Sie möglicherweise die Änderung in einem Klon vornehmen und dann zur neuen Datenbank wechseln.)
jpmc26
@ LucasPossamai Es ist wahrscheinlich viel schneller als Sie denken. Ich habe gerade an einem Tisch mit über 14 Millionen Zeilen getestet. Das Hinzufügen der Säule dauerte 273023 ms (4,5 Minuten). Das UPDATEdauerte 349422 ms (5,8 Minuten). Das Anwenden eines ähnlichen CHECKVorgangs dauerte 20996 ms (21 Sekunden). Das sind insgesamt ca. 10 Minuten. Dies alles war auf einem ziemlich leistungsschwachen Computer (4 GB RAM, Dual-Core-CPU). Sie haben etwas weniger als das Dreifache der Anzahl der Zeilen. Wenn die Laufzeit linear wächst, was ich erwartet hatte, sprechen Sie von einer halben Stunde Ausfallzeit, die in vielen Zusammenhängen äußerst vernünftig ist. Aber testen Sie die Laufzeiten selbst.
jpmc26
10

Wenn Sie eine serialSpalte oder eine Ganzzahl haben, die automatisch mit a gefüllt wird nextval(sodass Sie niemals neue Zeilen mit einem expliziten Wert für diese Spalte einfügen sollen), können Sie zusätzlich prüfen, ob der Wert dieser Spalte größer als ein bestimmter Wert ist ::

(
  (("qb_id" IS NOT NULL) :: INTEGER +
  ("xero_id" IS NOT NULL) :: INTEGER +
  ("freshbooks_id" IS NOT NULL) :: INTEGER +
  ("unleashed_id" IS NOT NULL) :: INTEGER +
  ("csv_data" IS NOT NULL) :: INTEGER +
  ("myob_id" IS NOT NULL) :: INTEGER) > 0
  OR
  YourSerialColumn <= value
)

wo die valuebestimmt werden sollen currvaldie Säule / entsprechende Sequenz zu der Zeit des Änderns / Neuerstellung der Einschränkung.

Auf diese Weise gelten die IS NOT NULLPrüfungen nur für die Zeilen, deren YourSerialColumnWert größer als ist value.

Hinweis

Dies kann als Variation des Vorschlags von David Spillet angesehen werden . Der Hauptunterschied liegt in der Tatsache, dass diese Lösung keine strukturellen Änderungen (Partitionierung) erfordert. Beide Optionen hängen jedoch vom Vorhandensein einer geeigneten Spalte in Ihrer Tabelle ab. Wenn es keine solche Spalte ist , und Sie können es hinzufügen , speziell dieses Problem zu lösen, mit der Partitionierung Idee gehen könnte die bessere Option dieser beiden sein.

Andriy M.
quelle
9

Fügen Sie einfach die Einschränkung als hinzu NOT VALID

Aus dem Handbuch:

Wenn die Einschränkung markiert ist NOT VALID, wird die möglicherweise langwierige Erstprüfung, um sicherzustellen, dass alle Zeilen in der Tabelle die Einschränkung erfüllen, übersprungen. Die Einschränkung wird weiterhin für nachfolgende Einfügungen oder Aktualisierungen erzwungen (d. H. [...] und sie schlagen fehl, es sei denn, die neue Zeile entspricht den angegebenen Prüfbeschränkungen).

ein Pferd ohne Name
quelle
4
Eine mögliche Gefahr, die ich hier sehe, ist, wenn Sie beabsichtigen, dass diese Spalte für vorhandene Zeilen in Zukunft null bleibt, können Sie auch keine ihrer anderen Spalten aktualisieren.
David Jacobsen
1
@a_horse_with_no_name Kann ich die Einschränkung in Zukunft validieren? Dazu muss ich Werte in die vorhandenen Nullwerte einfügen, habe ich Recht?
4

Dies war in Postgres bis Version 9.1 nicht möglich. Ab 9.2 können Sie eine Prüfbedingung definieren als NOT VALID(entspricht WITH NOCHECKin MS SQL Server). Weitere Informationen finden Sie unter http://www.postgresql.org/docs/9.2/static/sql-altertable.html .

Ich bin im Allgemeinen nicht zufrieden mit solchen Dingen, bei denen es überhaupt möglich ist, sie zu vermeiden. Ein Kompromiss, wenn Sie einen geeigneten Partitionierungsschlüssel haben (z. B. ein Eingabefeld), besteht darin, dass Sie die Tabelle möglicherweise partitionieren und die NOT NULLEinschränkung nur auf die Partition anwenden können, die neuere Zeilen enthält. Stellen Sie in beiden Fällen sicher, dass die Entwurfsauswahl gut dokumentiert ist, damit zukünftige Datenbankadministratoren / Entwickler / andere wissen, dass in einer bestimmten Teilmenge der Datensätze in der Tabelle möglicherweise NULL-Werte erwartet werden.

David Spillett
quelle
4

Ihre CHECKEinschränkung kann viel einfacher sein:

ALTER TABLE billables
ADD CONSTRAINT cc_at_least_one_mapping_needed_billables 
CHECK (qb_id         IS NOT NULL OR
       xero_id       IS NOT NULL OR 
       freshbooks_id IS NOT NULL OR
       unleashed_id  IS NOT NULL OR
       csv_data      IS NOT NULL OR
       myob_id       IS NOT NULL) NOT VALID;

Oder auch nur:

CONSTRAINT cc_at_least_one_mapping_needed_billables 
CHECK (NOT (qb_id,xero_id,freshbooks_id,unleashed_id,csv_data,myob_id) IS NULL) NOT VALID;

Warum funktioniert das?

Ich habe bereits die NOT VALIDKlausel hinzugefügt, die @a_horse erwähnt hat . Auf diese Weise gilt die Einschränkung nur für neu hinzugefügte Zeilen. Sie müssen auch mögliche Speicherauszugs- / Wiederherstellungszyklen berücksichtigen. Einzelheiten:

Und Sie können alles in einem einzigen Befehl erledigen , der am schnellsten ist und verhindert, dass mögliche gleichzeitige Transaktionen etwas falsch machen:

ALTER TABLE integrations.billables
  DROP CONSTRAINT IF EXISTS cc_at_least_one_mapping_needed_billables
, ADD COLUMN myob_id varchar(255)
, ADD CONSTRAINT cc_at_least_one_mapping_needed_billables 
    CHECK (NOT (qb_id,xero_id, freshbooks_id,unleashed_id, csv_data, myob_id) IS NULL)
    NOT VALID;

SQL Fiddle.

Nebenbei 1: Wenn Sie bereits die CHECKEinschränkung für denselben Spaltensatz hätten, nur ohne die neue myob_id, wäre dies kein Problem, da jede vorhandene Zeile auch die neue CHECKEinschränkung mit übergeben würde myob_id.

Nebenbei 2: In einigen RDBMS ist es sinnvoll varchar(255), die Leistung zu optimieren. Dies ist für Postgres und 255 irrelevant, da der Längenmodifikator nur dann sinnvoll ist, wenn Sie die Länge tatsächlich auf maximal 255 beschränken müssen:

Erwin Brandstetter
quelle