Restrição única de várias colunas do PostgreSQL e valores NULL

94

Eu tenho uma tabela como a seguinte:

create table my_table (
    id   int8 not null,
    id_A int8 not null,
    id_B int8 not null,
    id_C int8 null,
    constraint pk_my_table primary key (id),
    constraint u_constrainte unique (id_A, id_B, id_C)
);

E eu quero (id_A, id_B, id_C)ser distinto em qualquer situação. Portanto, as duas inserções a seguir devem resultar em um erro:

INSERT INTO my_table VALUES (1, 1, 2, NULL);
INSERT INTO my_table VALUES (2, 1, 2, NULL);

Mas não se comporta como o esperado, porque, de acordo com a documentação, dois NULLvalores não são comparados entre si, portanto, ambas as inserções passam sem erros.

Como posso garantir a minha restrição exclusiva, mesmo que id_Cpode ser NULLneste caso? Na verdade, a verdadeira questão é: posso garantir esse tipo de exclusividade no "sql puro" ou preciso implementá-lo em um nível superior (java no meu caso)?

Manuel Leduc
fonte
Então, digamos que você tenha valores (1,2,1)e (1,2,2)nas (A,B,C)colunas. Deve (1,2,NULL)ser permitido adicionar ou não?
ypercubeᵀᴹ
A e B não podem ser nulos, mas C pode ser nulo ou qualquer valor inteiro positivo. Portanto (1,2,3) e (2,4, nulo) são válidos, mas (nulo, 2,3) ou (1, nulo, 4) são inválidos. E [(1,2, nulo), (1,2,3)] não quebra a restrição exclusiva, mas [(1,2, nulo), (1,2, nulo)] deve quebrá-la.
Manuel Leduc
2
Existem valores que nunca aparecerão nessas colunas (como valores negativos?)
a_horse_with_no_name
Você não precisa rotular suas restrições na pág. Ele gera automaticamente um nome. Apenas para sua informação.
Evan Carroll

Respostas:

94

Você pode fazer isso em SQL puro . Crie um índice exclusivo parcial, além do que você possui:

CREATE UNIQUE INDEX ab_c_null_idx ON my_table (id_A, id_B) WHERE id_C IS NULL;

Dessa forma, você pode entrar (a, b, c)em sua tabela:

(1, 2, 1)
(1, 2, 2)
(1, 2, NULL)

Mas nada disso uma segunda vez.

Ou use doisUNIQUE índices parciais e nenhum índice completo (ou restrição). A melhor solução depende dos detalhes de seus requisitos. Comparar:

Embora seja elegante e eficiente para uma única coluna anulável no UNIQUEíndice, fica fora de controle rapidamente para mais. Discutindo isso - e como usar o UPSERT com índices parciais:

Apartes

Não há uso para identificadores de maiúsculas e minúsculas sem aspas duplas no PostgreSQL.

Você pode considerar uma serialcoluna como chave primária ou uma IDENTITYcoluna no Postgres 10 ou posterior. Relacionado:

Assim:

CREATE TABLE my_table (
   my_table_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY  -- for pg 10+
-- my_table_id bigserial PRIMARY KEY  -- for pg 9.6 or older
 , id_a int8 NOT NULL
 , id_b int8 NOT NULL
 , id_c int8
 , CONSTRAINT u_constraint UNIQUE (id_a, id_b, id_c)
);

Se você não espera mais de 2 bilhões de linhas (> 2147483647) durante a vida útil da sua tabela (incluindo desperdício e linhas excluídas), considere integer(4 bytes) em vez de bigint(8 bytes).

Erwin Brandstetter
fonte
1
Os documentos defendem esse método. A adição de uma restrição exclusiva criará automaticamente um índice de árvore B exclusivo na coluna ou no grupo de colunas listadas na restrição. Uma restrição de exclusividade que cobre apenas algumas linhas não pode ser gravada como uma restrição exclusiva, mas é possível impor essa restrição criando um índice parcial exclusivo.
Evan Carroll
12

Eu tive o mesmo problema e encontrei outra maneira de ter um NULL exclusivo na tabela.

CREATE UNIQUE INDEX index_name ON table_name( COALESCE( foreign_key_field, -1) )

No meu caso, o campo foreign_key_fieldé um número inteiro positivo e nunca será -1.

Portanto, para responder ao Manual Leduc, outra solução pode ser

CREATE UNIQUE INDEX  u_constrainte (COALESCE(id_a, -1), COALESCE(id_b,-1),COALESCE(id_c, -1) )

Presumo que os IDs não serão -1.

Qual é a vantagem em criar um índice parcial?
No caso em que você não tem a cláusula NOT NULL, id_a, id_be id_cpode ser NULL juntos apenas uma vez.
Com um índice parcial, os 3 campos podem ser NULL mais de uma vez.

Luc M
fonte
3
> Qual é a vantagem de criar um índice parcial? A maneira como você fez isso COALESCEpode ser eficaz para restringir as duplicatas, mas o índice não seria muito útil na consulta, pois é um índice de expressão que provavelmente não corresponderá às expressões de consulta. Ou seja, a menos que SELECT COALESCE(col, -1) ...você não esteja atingindo o índice.
Bo Jeanes
@BoJeanes O índice não foi criado para um problema de desempenho. Ele foi criado para atender aos requisitos de negócios.
Luc M
8

Um Nulo pode significar que o valor não é conhecido para essa linha no momento, mas será adicionado, quando conhecido, no futuro (exemplo FinishDatepara uma corrida Project) ou que nenhum valor pode ser aplicado a essa linha (exemplo EscapeVelocitypara um buraco negro Star).

Na minha opinião, geralmente é melhor normalizar as tabelas eliminando todos os Nulos.

No seu caso, você deseja permitir NULLsem sua coluna, mas deseja que apenas um NULLseja permitido. Por quê? Que tipo de relacionamento é esse entre as duas tabelas?

Talvez você possa simplesmente mudar a coluna para NOT NULLe armazenar, em vez de NULL, um valor especial (como -1) que se sabe que nunca aparece. Isso resolverá o problema de restrição de exclusividade (mas pode ter outros efeitos colaterais possivelmente indesejados. Por exemplo, usar o -1significado "desconhecido / não se aplica" distorcerá qualquer soma ou cálculo médio da coluna. Ou todos esses cálculos terão de ser realizados em consideração o valor especial e ignorá-lo.)

ypercubeᵀᴹ
fonte
2
No meu caso, NULL é realmente NULL (id_C é uma chave estrangeira para table_c, por exemplo, portanto, não pode ter um valor -1), significa que não há relação entre "my_table" e "table_c". Portanto, tem uma significação funcional. A propósito [(1, 1,1, nulo), (2, 1,2, nulo), (3,2,4, nulo)] é uma lista válida de dados inseridos.
Manuel Leduc
1
Não é realmente um Nulo, como usado no SQL, porque você deseja apenas um em todas as linhas. Você pode alterar o esquema do banco de dados adicionando -1 a table_c ou adicionando outra tabela (que seria supertipo no subtipo table_c).
ypercubeᵀᴹ
3
Gostaria apenas de salientar ao @Manuel que a opinião sobre nulos nesta resposta não é universal e é muito debatida. Muitos, como eu, pensam que nulo pode ser usado para qualquer finalidade que você deseja (mas só deve significar uma coisa para cada campo e deve ser documentado, possivelmente no nome do campo ou um comentário coluna)
Jack Douglas
1
Você não pode usar um valor fictício quando sua coluna for uma CHAVE ESTRANGEIRA.
Luc M
1
+1 Estou com você: se queremos que uma combinação de colunas seja única, você deve considerar uma entidade na qual essa combinação de colunas é uma PK. O esquema do banco de dados dos OPs provavelmente deve mudar para uma tabela pai e uma tabela filho.
AK