Thực thi KHÔNG NULL cho tập hợp các cột có ràng buộc CHECK chỉ cho các hàng mới


7

Tôi có một bảng và cần thêm một cột mới mà không có giá trị mặc định:

Hạn chế:

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

Cột:

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

Câu hỏi:

Làm cách nào tôi có thể thêm các ràng buộc cho các giá trị tiếp theo chứ không phải cho các giá trị đã có trong đó? (Nếu không, tôi sẽ nhận được ràng buộc kiểm tra lỗi "" bị vi phạm bởi một số hàng).

Điều này có liên quan đến câu hỏi trước đây của tôi: LRI: kiểm tra ràng buộc bị vi phạm bởi một số hàng


1
Bây giờ bảng có cột không có ràng buộc hoặc cột và ràng buộc không? Hay không? Câu hỏi không rõ ràng.
ypercubeᵀᴹ

@ ypercubeᵀᴹ nó rõ ràng cho tôi. Như bạn có thể thấy ở trên, tôi đang thêm cột vào một bảng hiện có (đã có ràng buộc)

5
Làm thế nào có thể có một ràng buộc đề cập myob_idmà không có cột tồn tại trong bảng?
ypercubeᵀᴹ

@ ypercubeᵀᴹ: Rõ ràng, các lệnh trong câu hỏi phải được sắp xếp lại. Các ràng buộc sẽ được điều chỉnh sau khi cột mới được thêm vào.
Erwin Brandstetter

Câu trả lời:


4

Đánh dấu tất cả các hàng hiện có của bạn là cũ:

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

UPDATE integrations.billables SET is_old = true;

Và thiết lập các ràng buộc để bỏ qua các hàng cũ:

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

(Vâng, IS NULLkiểm tra đó hoạt động. Xem ở đây .)

Ưu điểm của cơ chế này:

  • Ràng buộc vẫn còn hiệu lực
  • Bạn có thể tiếp tục cập nhật các hàng cũ mà không cần điền giá trị này

Nhược điểm:

  • Một tình huống tương tự xuống đường sẽ lộn xộn. Bạn sẽ phải thêm một booleancột thứ hai hoặc một số bước nhảy khác cho cột mới thứ hai.
  • Nếu bạn muốn buộc các hàng cập nhật được cung cấp một giá trị, điều này sẽ không làm điều đó.
  • Điều này có khả năng lạm dụng, vì ai đó chỉ có thể lật is_oldcờ đến true. (Tuy nhiên, điều này có thể được giải quyết. Xem bên dưới.) Đây không phải là điều đáng quan tâm nếu người dùng cuối không thể truy cập trực tiếp vào cơ sở dữ liệu và bạn có thể tin tưởng các nhà phát triển không làm những điều kỳ quặc với dữ liệu.

Nếu bạn đang lo lắng về ai đó thay đổi lá cờ, bạn có thể thiết lập kích hoạt để ngăn chặn bất kỳ chèn hoặc cập nhật từ thiết is_oldđể 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()
;

Bạn vẫn phải tin tưởng rằng không ai có thể sửa đổi lược đồ cơ sở dữ liệu sẽ xuất hiện và bỏ trình kích hoạt của bạn hoặc một cái gì đó, nhưng nếu họ sẽ làm điều đó, họ cũng có thể loại bỏ ràng buộc.

Đây là bản demo SQLFiddle . Lưu ý rằng hàng "nên bỏ qua" không nằm trong đầu ra (như chúng ta mong muốn).


Câu trả lời của bạn rất thú vị. Tuy nhiên, tôi phải cập nhật hơn 40 triệu hàng. Điều đó là không thể = \

1
Tôi thực sự nghi ngờ rằng điều đó là không thể . Nó có thể mất một thời gian dài , nhưng đó là một vấn đề rất khác. Việc bạn có thể đối phó với khung thời gian đó hay không tùy thuộc vào các ràng buộc của bạn. Bạn có thể kiểm tra mất bao lâu và một khi bạn biết, bạn có thể đưa ra quyết định. Bạn có đủ khả năng để ngoại tuyến lâu không? Bạn có thể đặt cơ sở dữ liệu / ứng dụng ở trạng thái chỉ đọc trong khi nó chạy không? (Đối với phần sau, bạn có thể thực hiện thay đổi trong một bản sao và sau đó chuyển sang cơ sở dữ liệu mới.)
jpmc26

@LucasPossamai Có lẽ nhanh hơn nhiều so với bạn nghĩ. Tôi vừa thử nghiệm trên một bảng với hơn 14 triệu hàng. Thêm cột mất 273023 ms (4,5 phút). Các UPDATEmất 349.422 ms (phút 5.8). Áp dụng tương tự CHECKmất 20996 ms (21 giây). Đó là tổng cộng khoảng 10 phút. Đây là tất cả trên một máy có công suất khá thấp (RAM 4 GB, CPU lõi kép). Bạn có ít hơn 3 lần số lượng hàng. Nếu thời gian chạy phát triển tuyến tính, mà tôi mong đợi, bạn sẽ nói về một nửa giờ ngừng hoạt động, cực kỳ hợp lý trong nhiều bối cảnh. Nhưng hãy tự kiểm tra thời gian chạy.
jpmc26

10

Nếu bạn có một serialcột hoặc một số nguyên được tự động điền vào một nextval(để bạn không bao giờ phải chèn các hàng mới có giá trị rõ ràng cho cột đó), bạn có thể kiểm tra thêm xem giá trị của cột đó có lớn hơn một giá trị cụ thể không :

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

trong đó valuecần được xác định theo currvalcột / trình tự tương ứng tại thời điểm thay đổi / tạo lại ràng buộc.

Bằng cách này, IS NOT NULLkiểm tra sẽ chỉ áp dụng cho các hàng có YourSerialColumngiá trị lớn hơn value.

Ghi chú

Điều này có thể được xem như là một biến thể của đề xuất của David Spillet . Sự khác biệt chính nằm ở chỗ giải pháp này không yêu cầu thay đổi cấu trúc (phân vùng). Cả hai tùy chọn, tuy nhiên, dựa trên sự tồn tại của một cột phù hợp trong bảng của bạn. Nếu không có một cột như vậy và bạn có thể thêm nó cụ thể để giải quyết vấn đề này, đi với ý tưởng phân vùng có thể là lựa chọn tốt hơn của hai cái này.


9

Chỉ cần thêm các ràng buộc như NOT VALID

Từ hướng dẫn:

Nếu ràng buộc được đánh dấu NOT VALID, kiểm tra ban đầu có khả năng kéo dài để xác minh rằng tất cả các hàng trong bảng thỏa mãn ràng buộc được bỏ qua. Ràng buộc vẫn sẽ được thi hành đối với các lần chèn hoặc cập nhật tiếp theo (nghĩa là [...] và chúng sẽ thất bại trừ khi hàng mới khớp với các ràng buộc kiểm tra đã chỉ định)


4
Một cạm bẫy tiềm năng mà tôi thấy ở đây là nếu bạn dự định cột đó sẽ không có giá trị cho các hàng hiện có trong tương lai, thì bạn cũng không thể cập nhật bất kỳ cột nào khác của chúng.
David Jacobsen

1
@a_horse_with_no_name tôi có thể XÁC ĐỊNH các ràng buộc trong tương lai không? Để làm điều đó tôi sẽ phải chèn các giá trị vào các giá trị null đã tồn tại, phải không?

4

Điều này là không thể trong Postgres cho đến phiên bản 9.1. Từ 9.2 trở đi, bạn có thể xác định ràng buộc kiểm tra là NOT VALID(tương đương với WITH NOCHECKtrong MS SQL Server). Xem http://www.postgresql.org/docs/9.2/static/sql-altertable.html để biết thêm chi tiết.

Nói chung tôi không hài lòng với những điều mà ở đó hoàn toàn có thể tránh được. Một thỏa hiệp nếu bạn có khóa phân vùng phù hợp (ví dụ: ngày nhập trường) là bạn có thể phân vùng bảng và chỉ áp dụng NOT NULLràng buộc cho phân vùng chứa các hàng mới hơn. Trong cả hai trường hợp, đảm bảo rằng lựa chọn thiết kế được ghi lại tốt để các DBA / nhà phát triển / tương lai khác biết rằng các giá trị NULL có thể được mong đợi trong một tập hợp con nhất định của các bản ghi trong bảng.


4

CHECKRàng buộc của bạn có thể đơn giản hơn nhiều:

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;

Hoặc thậm chí chỉ:

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;

Tại sao nó hoạt động?

Tôi đã thêm NOT VALIDmệnh đề mà @a_horse đã đề cập . Bằng cách này, ràng buộc chỉ áp dụng cho các hàng mới được thêm vào. Bạn cũng phải xem xét các chu kỳ đổ / khôi phục có thể. Chi tiết:

Và bạn có thể thực hiện tất cả trong một lệnh duy nhất , nhanh nhất và ngăn các giao dịch đồng thời có thể xảy ra khi làm bất cứ điều gì sai:

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;

Câu đố SQL.

Ngoài ra 1: Nếu bạn đã có CHECKràng buộc trên cùng một tập hợp các cột, chỉ cần không có cột mới myob_id, thì sẽ không có vấn đề gì, vì mỗi hàng hiện có cũng sẽ vượt qua CHECKràng buộc mới myob_id.

Ngoài 2: Trong một số RDBMS, nên sử dụng varchar(255)để tối ưu hóa hiệu suất. Điều này không liên quan đến Postgres và 255 vì công cụ sửa đổi độ dài chỉ có ý nghĩa nếu bạn thực sự cần hạn chế độ dài tối đa là 255:

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.