web-dev-qa-db-ja.com

新しい行に対してのみCHECK制約を持つ列のセットにNOT NULLを適用します

テーブルがあり、デフォルト値なしで新しい列を追加する必要があります。

制約:

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);

カラム:

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

質問:

すでにそこにある値ではなく、次の値の制約を追加するにはどうすればよいですか? (そうしないと、エラーチェック制約「」が一部の行に違反します)。

これは私の前の質問に関連しています: エラー:チェック制約が一部の行に違反しています

7
user83914

すべての既存の行を古いものとしてマークします。

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

UPDATE integrations.billables SET is_old = true;

そして、古い行を無視するように制約を設定します。

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
);

(はい、そのIS NULLチェックは機能します。 ここ を参照してください。)

このメカニズムの利点:

  • 制約は有効なままです
  • この値を入力せずに古い行を更新し続けることができます

欠点:

  • 今後、同様の状況が発生します。 2番目の新しい列には、2番目のboolean列またはその他のフープジャンプを追加する必要があります。
  • wantを使用して、更新された行に強制的に値を指定した場合、これは行われません。
  • 誰かがis_oldフラグをtrueに変更するだけなので、これは悪用される可能性があります。 (ただし、これには対処できます。以下を参照してください。)これは、エンドユーザーがデータベースに直接アクセスできず、開発者がデータに奇妙なことをしないように信頼できる場合、心配する必要はありません。

誰かがフラグを変更することを心配しているare場合は、トリガーを設定して、挿入または更新によってis_oldtrueに設定されないようにすることができます。

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()
;

データベーススキーマを変更できる人が来てトリガーや何かを削除することは誰もないことを信頼する必要がありますが、そうする場合は制約も削除する可能性があります。

これが SQLFiddleデモ です。 「should skip」行は出力に含まれていないことに注意してください(希望どおり)。

4
jpmc26

serial列または整数列にnextvalが自動的に入力される場合(これにより、決してその列の明示的な値)、その列の値が特定の値より大きいかどうかをさらに確認できます。

(
  (("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
)

ここでvalueは、currval制約の変更/再作成時の列/対応するシーケンス。

このようにIS NOT NULLチェックは、YourSerialColumn値がvalueより大きい行にのみ適用されます。

これは David Spilletの提案 のバリエーションと見なすことができます。主な違いは、このソリューションでは構造の変更(パーティション化)が必要ないという事実にあります。ただし、どちらのオプションも、テーブル内の適切な列の存在に依存しています。そのような列がなく、この問題を解決するためにを追加できる場合は、パーティション化のアイデアを採用することが、これら2つの方法のより良いオプションである可能性があります。

10
Andriy M

制約をNOT VALIDとして追加するだけです

マニュアルから:

制約にNOT VALIDのマークが付けられている場合、テーブル内のすべての行が制約を満たすことを確認するために時間がかかる可能性のある初期チェックがスキップされます。制約は引き続き後続の挿入または更新に対して適用されます(つまり、[...]であり、新しい行が指定されたチェック制約と一致しない限り、制約は失敗します)

これはバージョン9.1までのPostgresでは不可能でした。 9.2以降では、チェック制約をNOT VALID(MS SQL ServerのWITH NOCHECKと同等)として定義できます。詳細は http://www.postgresql.org/docs/9.2/static/sql-altertable.html を参照してください。

私は、この種の事態を回避することが可能な場合、一般的に不満です。適切なパーティションキー(たとえば、入力フィールドの日付)がある場合の妥協点は、おそらくテーブルをパーティション化して、新しい行を含むパーティションにNOT NULL制約のみを適用できることです。どちらの場合も、テーブルのレコードの特定のサブセットでNULL値が予期される可能性があることを将来のDBA /開発者/その他が知るように、設計の選択が十分に文書化されていることを確認してください。

4
David Spillett

CHECK制約ははるかに簡単です。

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;

または単に:

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;

なぜそれが機能するのですか?

私はすでに NOT VALID 句を追加しました @ a_horseが言及しました 。このように、制約は新しく追加された行にのみ適用されます。また、可能なダンプ/復元サイクルを考慮する必要があります。詳細:

そして、あなたはそれを単一のコマンドですべて行うことができ、これは最速であり、可能な同時トランザクションが何か間違ったことをするのを防ぎます:

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フィドル

補足1:新しいmyob_idがなくても、同じ列セットにCHECK制約がすでにある場合、既存のすべての行がmyob_idとともに新しいCHECK制約も渡すため、問題はありません。

補足2:一部のRDBMSでは、パフォーマンスを最適化するためにvarchar(255)を使用することが理にかなっています。長さ修飾子が実際に長さを最大255に制限する必要がある場合にのみ意味があるので、これはPostgresと255には無関係です。

4