Tạo một ràng buộc PostgreSQL để ngăn các hàng kết hợp duy nhất


9

Hãy tưởng tượng bạn có một bảng đơn giản:

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

Tôi cần tạo một ràng buộc duy nhất đặc biệt không thành công trong tình huống sau: các is_activegiá trị khác nhau không thể cùng tồn tại cho cùng một namegiá trị.

Ví dụ về điều kiện được phép:

Lưu ý: chỉ mục duy nhất nhiều cột đơn giản sẽ không cho phép kết hợp như thế này.

A    | 0
A    | 0
B    | 0

Ví dụ về điều kiện được phép:

A    | 0
B    | 1

Ví dụ về điều kiện thất bại:

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

Lý tưởng nhất, tôi cần ràng buộc duy nhất hoặc chỉ mục một phần duy nhất. Triggers là vấn đề hơn đối với tôi.

Nhân đôi A,0cho phép, nhưng (A,0) (A,1)không.

Câu trả lời:


17

Bạn có thể sử dụng một ràng buộc loại trừ với btree_gist,

-- This is needed
CREATE EXTENSION btree_gist;

Sau đó, chúng tôi thêm một ràng buộc nói:

"Chúng tôi không thể có 2 hàng giống nhau namevà khác nhau 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 <>
    );

Một số lưu ý:

  • is_activecó thể là số nguyên hoặc boolean, không tạo ra sự khác biệt cho ràng buộc loại trừ. (thực tế là như vậy, nếu cột là boolean bạn cần sử dụng (is_active::int) WITH <>.)
  • Hàng ở đâu namehoặc is_activelà null sẽ bị bỏ qua bởi các ràng buộc và do đó được cho phép.
  • Ràng buộc chỉ có ý nghĩa nếu bảng có nhiều cột hơn. Mặt khác, nếu bảng chỉ có 2 cột này, một UNIQUEràng buộc về (name)một mình sẽ dễ dàng và phù hợp hơn. Tôi không thấy bất kỳ lý do nào để lưu trữ nhiều hàng giống hệt nhau.
  • Thiết kế vi phạm 2NF. Mặc dù các ràng buộc loại trừ sẽ cứu chúng ta khỏi các bất thường cập nhật, nhưng nó có thể không từ các vấn đề về hiệu suất. Nếu bạn có ví dụ 1000 hàng với name = 'A'và bạn muốn cập nhật trạng thái is_active từ 0 đến 3, tất cả 1000 sẽ phải được cập nhật. Bạn nên kiểm tra xem bình thường hóa thiết kế sẽ hiệu quả hơn. (Bình thường hóa ý nghĩa trong trường hợp này để loại bỏ trạng thái is_active khỏi bảng và thêm bảng 2 cột có tên, is_active và một ràng buộc duy nhất trên (name). Nếu is_activelà boolean, nó có thể bị xóa hoàn toàn và bảng phụ chỉ là một bảng cột duy nhất, lưu trữ chỉ tên "hoạt động".)

is_active không thể là boolean,ERROR: data type boolean has no default operator class for access method "gist"
Evan Carroll

1
@EvanCarroll Tôi không thể nhớ mình đã kiểm tra tốt như thế nào khi đăng. Nhưng nó hoạt động với intsmallint.
ypercubeᵀᴹ

Cũng hoạt động bằng cách sử dụng EXCLUDE USING gist (name WITH =, (is_active::int) WITH <>)nếu đó là boolean. Và câu hỏi đã 01không true, falsevì vậy, không chắc là tôi đã thử nghiệm với
booleans

Tốt thôi, tôi đã sử dụng một ràng buộc loại trừ đối với dba.stackexchange.com/a/175922/2639 và tôi gặp vấn đề khi sử dụng booleans nên tôi đã tìm kiếm. Tôi nghĩ btree_gist che bool nhưng nó không.
Evan Carroll

3

Đây không phải là trường hợp bạn có thể sử dụng một chỉ mục duy nhất. Bạn có thể kiểm tra điều kiện trong trình kích hoạt, ví dụ:

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

Kiểm tra nó ở đây.

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.