Buat batasan PostgreSQL untuk mencegah baris kombinasi unik

9

Bayangkan Anda memiliki tabel sederhana:

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

Saya perlu membuat batasan unik khusus yang gagal pada situasi berikut: is_activenilai yang berbeda tidak dapat hidup berdampingan untuk nilai yang sama name.

Contoh kondisi yang diizinkan:

Catatan: indeks unik multi-kolom sederhana tidak akan mengizinkan kombinasi seperti ini.

A    | 0
A    | 0
B    | 0

Contoh kondisi yang diizinkan:

A    | 0
B    | 1

Contoh kondisi gagal:

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

Idealnya, saya perlu batasan unik atau indeks parsial unik. Pemicu lebih bermasalah bagi saya.

Ganda A,0diizinkan, tetapi (A,0) (A,1)tidak.

Andrii Skaliuk
sumber

Jawaban:

17

Anda dapat menggunakan batasan pengecualian dengan btree_gist,

-- This is needed
CREATE EXTENSION btree_gist;

Kemudian kami menambahkan batasan yang mengatakan:

"Kami tidak dapat memiliki 2 baris yang memiliki yang sama namedan berbeda 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 <>
    );

Beberapa catatan:

  • is_activebisa bilangan bulat atau boolean, tidak membuat perbedaan untuk batasan pengecualian. (sebenarnya itu benar, jika kolomnya boolean, Anda perlu menggunakan (is_active::int) WITH <>.)
  • Baris di mana nameatau is_activenol akan diabaikan oleh batasan dan dengan demikian diizinkan.
  • Batasan masuk akal hanya jika tabel memiliki lebih banyak kolom. Kalau tidak, jika tabel hanya memiliki 2 kolom ini, UNIQUEkendala (name)sendirian akan lebih mudah dan lebih sesuai. Saya tidak melihat alasan untuk menyimpan beberapa baris yang identik.
  • Desain melanggar 2NF. Meskipun batasan pengecualian akan menyelamatkan kita dari pembaruan anomali, mungkin tidak dari masalah kinerja. Jika Anda memiliki misalnya 1000 baris dengan name = 'A'dan Anda ingin memperbarui status is_active dari 0 hingga 3, semua 1000 harus diperbarui. Anda harus memeriksa apakah normalisasi desain akan lebih efisien. (Menormalkan arti dalam hal ini untuk menghapus status is_active dari tabel dan menambahkan tabel 2-kolom dengan nama, is_active dan kendala unik (name). Jika is_activeboolean, itu bisa dilucuti total dan tabel tambahan hanya tabel kolom tunggal, menyimpan hanya nama "aktif".)
ypercubeᵀᴹ
sumber
is_active tidak boleh boolean,ERROR: data type boolean has no default operator class for access method "gist"
Evan Carroll
1
@ EvanCarroll Saya tidak ingat seberapa baik saya menguji ini ketika saya memposting. Tetapi ini bekerja dengan intdan smallint.
ypercubeᵀᴹ
Juga berfungsi menggunakan EXCLUDE USING gist (name WITH =, (is_active::int) WITH <>)jika itu boolean. Dan pertanyaannya adalah 0dan 1, tidak truedan falsejadi agak tidak mungkin saya diuji dengan boolean;)
ypercubeᵀᴹ
Semua bagus, saya menggunakan batasan pengecualian di dba.stackexchange.com/a/175922/2639 dan saya punya masalah menggunakan booleans jadi saya pergi mencari. Saya pikir btree_gist menutupi bools tetapi tidak.
Evan Carroll
3

Ini bukan kasus di mana Anda dapat menggunakan indeks unik. Anda dapat menguji kondisi di pemicu, misalnya:

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

Uji di sini.

klin
sumber