Crie uma restrição do PostgreSQL para evitar linhas de combinação exclusivas

9

Imagine que você tem uma tabela simples:

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

Preciso criar uma restrição única e especial que falha na seguinte situação: is_activevalores diferentes não podem coexistir para o mesmo namevalor.

Exemplo de condição permitida:

Nota: o índice único simples de várias colunas não permitirá combinações como esta.

A    | 0
A    | 0
B    | 0

Exemplo de condição permitida:

A    | 0
B    | 1

Exemplo de condição com falha:

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

Idealmente, preciso de restrição ou índice parcial exclusivo. Os gatilhos são mais problemáticos para mim.

Duplo A,0permitido, mas (A,0) (A,1)não é.

Andrii Skaliuk
fonte

Respostas:

17

Você pode usar uma restrição de exclusão com btree_gist,

-- This is needed
CREATE EXTENSION btree_gist;

Em seguida, adicionamos uma restrição que diz:

"Não podemos ter 2 linhas que tenham o mesmo namee diferente 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 <>
    );

Algumas notas:

  • is_activepode ser inteiro ou booleano, não faz diferença para a restrição de exclusão. (na verdade, se a coluna for booleana, você precisará usar (is_active::int) WITH <>.)
  • Linhas em que nameou is_activeé nulo serão ignoradas pela restrição e, portanto, permitidas.
  • A restrição só faz sentido se a tabela tiver mais colunas. Caso contrário, se a tabela tiver apenas essas 2 colunas, uma UNIQUErestrição por (name)si só seria mais fácil e apropriada. Não vejo motivo para armazenar várias linhas idênticas.
  • O design viola 2NF. Embora a restrição de exclusão nos salve de anomalias de atualização, talvez não de problemas de desempenho. Se você possui, por exemplo, 1000 linhas name = 'A'e deseja atualizar o status is_active de 0 a 3, todas as 1000 terão que ser atualizadas. Você deve examinar se a normalização do design seria mais eficiente. (Normalizando o significado, neste caso, para remover o status is_active da tabela e adicionar uma tabela de 2 colunas com nome is_active e uma restrição exclusiva ativada (name). Se is_activefor booleano, pode ser totalmente removido e a tabela extra será apenas uma tabela de coluna única, armazenando somente os nomes "ativos".)
ypercubeᵀᴹ
fonte
is_active não pode ser booleano #ERROR: data type boolean has no default operator class for access method "gist"
Evan Carroll
11
@EvanCarroll Não me lembro de como testei isso quando postei. Mas funciona com inte smallint.
precisa saber é o seguinte
Também funciona usando EXCLUDE USING gist (name WITH =, (is_active::int) WITH <>)se for booleano. E a pergunta tem 0e 1, não truee falsepor isso é bastante improvável que eu testei com booleans;)
ypercubeᵀᴹ
Tudo bem, usei uma restrição de exclusão em dba.stackexchange.com/a/175922/2639 e tive um problema ao usar um booleano, então procurei. Eu pensei que btree_gist cobria bools, mas não.
Evan Carroll
3

Este não é um caso em que você pode usar um índice exclusivo. Você pode testar a condição em um gatilho, por exemplo:

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

Teste aqui.

klin
fonte