O que há de errado com colunas anuláveis ​​em chaves primárias compostas?

149

O ORACLE não permite valores NULL em nenhuma das colunas que compreendem uma chave primária. Parece que o mesmo se aplica à maioria dos outros sistemas "corporativos".

Ao mesmo tempo, a maioria dos sistemas também permite restrições exclusivas em colunas anuláveis.

Por que as restrições exclusivas podem ter NULLs, mas as chaves primárias não podem? Existe uma razão lógica fundamental para isso, ou isso é mais uma limitação técnica?

Roman Starkov
fonte

Respostas:

216

As chaves primárias destinam-se à identificação exclusiva de linhas. Isso é feito comparando todas as partes de uma chave com a entrada.

Por definição, NULL não pode fazer parte de uma comparação bem-sucedida. Até uma comparação NULL = NULLconsigo mesma ( ) falhará. Isso significa que uma chave contendo NULL não funcionaria.

Additonally, NULL é permitido em uma chave estrangeira, para marcar um relacionamento opcional. (*) Permitir no PK também quebraria isso.


(*) Uma palavra de cautela: Ter chaves estrangeiras anuláveis ​​não é um design de banco de dados relacional limpo.

Se houver duas entidades Ae Bonde Aopcionalmente pode ser relacionado B, a solução limpa é criar uma tabela de resolução (digamos AB). Que a tabela ligaria Acom B: Se não é um relacionamento, então ele iria conter um registro, se houver não é , então ele não iria.

Tomalak
fonte
5
Alterei a resposta aceita para esta. A julgar pelos votos, esta resposta é a mais clara para mais pessoas. Ainda sinto que a resposta de Tony Andrews explica melhor a intenção por trás desse design; verifique também!
Roman Starkov 16/02
2
P: Quando você deseja um NULL FK em vez de falta de uma linha? R: Somente em uma versão de um esquema não normalizado para otimização. Em esquemas não triviais, problemas não normalizados como esse podem causar problemas sempre que novos recursos são necessários. ah, a multidão de web design não se importa. Eu adicionaria pelo menos uma nota de cautela sobre isso, em vez de parecer uma boa idéia de design.
zxq9 17/02/2015
3
"Ter chaves estrangeiras anuláveis ​​não é um design de banco de dados relacional limpo." - um design de banco de dados sem nulos (sexta forma normal) invariavelmente aumenta a complexidade, as economias de espaço obtidas são geralmente superadas pelo trabalho extra do programador necessário para obter esses ganhos.
Dai
1
e se for uma tabela de resolução ABC? com C opcional
Bart Calixto
1
Tentei evitar escrever "porque o padrão proíbe", pois isso realmente não explica nada.
amigos estão
62

Uma chave primária define um identificador exclusivo para cada linha de uma tabela: quando uma tabela possui uma chave primária, você tem uma maneira garantida de selecionar qualquer linha nela.

Uma restrição exclusiva não identifica necessariamente todas as linhas; ele só especifica que se uma linha tem valores em suas colunas, em seguida, eles devem ser exclusivos. Isso não é suficiente para identificar exclusivamente cada linha, que é o que uma chave primária deve fazer.

Tony Andrews
fonte
10
No Sql Server, uma restrição exclusiva que possui uma coluna anulável, permite que o valor 'nulo' nessa coluna apenas uma vez (dados idênticos para as outras colunas da restrição). Portanto, essa restrição exclusiva se comporta essencialmente como um pk com uma coluna anulável.
Gerard
Eu confirmo o mesmo com o Oracle (11.2) #
Alexander Malakhov
2
No Oracle (não sei sobre o SQL Server), a tabela pode conter muitas linhas em que todas as colunas em uma restrição exclusiva são nulas. No entanto, se algumas colunas na restrição exclusiva não forem nulas e algumas forem nulas, a exclusividade será aplicada.
Tony Andrews
Como isso se aplica ao UNIQUE composto?
Dims
1
@ Dims Tal como acontece com quase tudo nos bancos de dados SQL "depende da implementação". Na maioria dos dbs, uma "chave primária" é na verdade uma restrição UNIQUE abaixo. A idéia de "chave primária" não é realmente mais especial ou poderosa do que o conceito de ÚNICO. A diferença real é que, se você tem dois aspectos independentes de uma tabela que podem ser garantidos como UNIQUE, não possui um banco de dados normalizado por definição (você está armazenando dois tipos de dados na mesma tabela).
Zxq9 26/06/2015
46

Fundamentalmente, nada está errado com um NULL em uma chave primária de várias colunas. Mas ter uma delas tem implicações que o designer provavelmente não pretendia, e é por isso que muitos sistemas lançam um erro ao tentar fazer isso.

Considere o caso das versões de módulo / pacote armazenadas como uma série de campos:

CREATE TABLE module
  (name        varchar(20) PRIMARY KEY,
   description text DEFAULT '' NOT NULL);

CREATE TABLE version
  (module      varchar(20) REFERENCES module,
   major       integer NOT NULL,
   minor       integer DEFAULT 0 NOT NULL,
   patch       integer DEFAULT 0 NOT NULL,
   release     integer DEFAULT 1 NOT NULL,
   ext         varchar(20),
   notes       text DEFAULT '' NOT NULL,
   PRIMARY KEY (module, major, minor, patch, release, ext));

Os 5 primeiros elementos da chave primária são partes regularmente definidas de uma versão de lançamento, mas alguns pacotes têm uma extensão personalizada que geralmente não é um número inteiro (como "rc-foo" ou "vanilla" ou "beta" ou qualquer outra pessoa para quem quem quatro campos é insuficiente pode sonhar). Se um pacote não tiver uma extensão, será NULL no modelo acima, e nenhum dano seria causado ao deixar as coisas dessa maneira.

Mas o que é um NULL? Supõe-se que representa uma falta de informação, um desconhecido. Dito isto, talvez isso faça mais sentido:

CREATE TABLE version
  (module      varchar(20) REFERENCES module,
   major       integer NOT NULL,
   minor       integer DEFAULT 0 NOT NULL,
   patch       integer DEFAULT 0 NOT NULL,
   release     integer DEFAULT 1 NOT NULL,
   ext         varchar(20) DEFAULT '' NOT NULL,
   notes       text DEFAULT '' NOT NULL,
   PRIMARY KEY (module, major, minor, patch, release, ext));

Nesta versão, a parte "ext" da tupla NÃO é NULL, mas o padrão é uma string vazia - que é semanticamente (e praticamente) diferente de um NULL. Um NULL é um desconhecido, enquanto uma sequência vazia é um registro deliberado de "algo que não está presente". Em outras palavras, "vazio" e "nulo" são coisas diferentes. É a diferença entre "não tenho valor aqui" e "não sei qual é o valor aqui".

Quando você registra um pacote que não possui uma extensão de versão, você sabe que ele não possui uma extensão; portanto, uma string vazia é realmente o valor correto. Um NULL só estaria correto se você não soubesse se tinha uma extensão ou não, ou você sabia que sim, mas não sabia o que era. É mais fácil lidar com essa situação em sistemas em que os valores de string são a norma, porque não há como representar um "número inteiro vazio" além da inserção de 0 ou 1, que acabará sendo acumulado em quaisquer comparações feitas posteriormente (que suas próprias implicações).

Aliás, as duas formas são válidas no Postgres (já que estamos discutindo RDMBSs "corporativos"), mas os resultados da comparação podem variar bastante quando você lança um NULL na mistura - porque NULL == "não sabe", então todos os resultados de uma comparação envolvendo um NULL acabam sendo NULL, pois você não pode saber algo que é desconhecido. PERIGO! Pense com cuidado: isso significa que os resultados da comparação NULL se propagam através de uma série de comparações. Isso pode ser uma fonte de erros sutis ao classificar, comparar etc.

O Postgres assume que você é adulto e pode tomar essa decisão por si mesmo. O Oracle e o DB2 assumem que você não percebeu que estava fazendo algo bobo e emitiu um erro. Esta é geralmente a coisa certa, mas nem sempre - você pode realmente não sei e ter um NULL em alguns casos e, portanto, deixando uma linha com um elemento desconhecido contra o qual comparações significativas são impossíveis é o comportamento correto.

Em qualquer caso, você deve se esforçar para eliminar o número de campos NULL permitidos em todo o esquema e duplamente quando se trata de campos que fazem parte de uma chave primária. Na grande maioria dos casos, a presença de colunas NULL é uma indicação de design de esquema não normalizado (em oposição a deliberadamente desnormalizado) e deve ser pensado muito antes de ser aceito.

[* NOTA: É possível criar um tipo personalizado que é a união de números inteiros e um tipo "inferior" que semanticamente significaria "vazio" em oposição a "desconhecido". Infelizmente, isso introduz um pouco de complexidade nas operações de comparação e, geralmente, ser verdadeiramente correto do tipo não vale o esforço na prática, pois você não deve permitir muitos NULLvalores em primeiro lugar. Dito isto, seria maravilhoso se os RDBMSs incluíssem um BOTTOMtipo padrão , além NULLde impedir o hábito de conflitar casualmente a semântica de "sem valor" com "valor desconhecido". ]

zxq9
fonte
5
Esta é uma resposta MUITO AGRADÁVEL e explica muito sobre valores NULL e suas implicações em muitas situações. Você, senhor, agora tem meu respeito! Nem na faculdade eu tive uma explicação tão boa sobre valores NULL dentro de bancos de dados. Obrigado!
Eu apoio a idéia principal desta resposta. Mas escrever como 'supostamente representa uma falta de informação, um desconhecido', 'semanticamente (e praticamente) diferente de um NULL', 'Um NULL é um desconhecido', 'uma string vazia é um registro deliberado de "algo que não está presente "',' NULL ==" não sei "', etc, são vagos, enganosos e realmente apenas mnemônicos para declarações ausentes sobre como NULL ou qualquer valor é ou pode ou deve ser usado - pelo resto do post . (Inclusive inspirando o design (ruim) dos recursos SQL NULL.) Eles não justificam ou explicam nada; eles devem ser explicados e desmascarados.
philipxy
21

NULL == NULL -> false (pelo menos nos DBMSs)

Portanto, você não seria capaz de recuperar nenhum relacionamento usando um valor NULL, mesmo com colunas adicionais com valores reais.

Cogsy
fonte
1
Parece a melhor resposta, mas ainda não entendo por que isso é proibido na criação da chave primária. Se este foi apenas um problema de recuperação, você pode usar where pk_1 = 'a' and pk_2 = 'b'com valores normais e alternar para where pk_1 is null and pk_2 = 'b'quando houver nulos.
EoghanM
Ou ainda mais confiável, where (a.pk1 = b.pk1 or (a.pk1 is null and b.pk1 is null)) and (a.pk2 = b.pk2 or (a.pk2 is null and b.pk2 is null))/
Jordan Rieger 30/05
8
Resposta errada. NULL == NULL -> DESCONHECIDO. Não é falso. O problema é que uma restrição não é considerada violada se o resultado do teste for DESCONHECIDO. Isso geralmente faz com que SEJA como se a comparação produz falsa, mas realmente não.
Erwin Smout 25/02
4

A resposta de Tony Andrews é decente. Mas a resposta real é que essa tem sido uma convenção usada pela comunidade de bancos de dados relacionais e NÃO é uma necessidade. Talvez seja uma boa convenção, talvez não.

Comparar qualquer coisa com NULL resulta em DESCONHECIDO (terceiro valor de verdade). Assim, como foi sugerido com nulos, toda a sabedoria tradicional sobre igualdade sai pela janela. Bem, é assim que parece à primeira vista.

Mas não acho que isso seja necessariamente verdade e nem os bancos de dados SQL acham que o NULL destrói todas as possibilidades de comparação.

Execute no seu banco de dados a consulta SELECT * FROM VALUES (NULL) UNION SELECT * FROM VALUES (NULL)

O que você vê é apenas uma tupla com um atributo que tem o valor NULL. Portanto, a união reconheceu aqui os dois valores NULL como iguais.

Ao comparar uma chave composta que possui 3 componentes a uma tupla com 3 atributos (1, 3, NULL) = (1, 3, NULL) <=> 1 = 1 AND 3 = 3 AND NULL = NULL O resultado disso é DESCONHECIDO .

Mas poderíamos definir um novo tipo de operador de comparação, por exemplo. ==. X == Y <=> X = Y OU (X É NULO E Y É NULO)

Ter esse tipo de operador de igualdade tornaria sem problemas as chaves compostas com componentes nulos ou chaves não compostas com valor nulo.

Rami Ojares
fonte
1
Não, a UNION reconheceu os dois NULLs como não distintos. O que não é a mesma coisa que "igual". Tente UNION ALL e você terá duas linhas. E quanto ao "novo tipo de operador de comparação", o SQL já o possui. NÃO É DISTINTO DE. Mas isso por si só não é suficiente. Usar isso nas construções SQL, como NATURAL JOIN, ou a cláusula REFERENCES de uma chave estrangeira, exigirá ainda opções adicionais nessas construções.
Erwin Smout 25/02
Ah, Erwin Smout. Realmente um prazer conhecê-lo também neste fórum! Eu não sabia do SQL "NÃO É DISTINTO DE". Muito interessante! Mas parece que é exatamente isso que eu quis dizer com meu operador == de maquiagem. Você poderia me explicar por que diz isso: "isso por si só não é suficiente"?
Rami Ojares
A cláusula REFERENCES baseia-se na igualdade, por definição. Um tipo de REFERENCES que corresponda a uma tupla / linha filho com uma tupla / linha pai, com base nos valores de atributo correspondentes NOT NOT DISTINCT em vez de (o mais rigoroso) EQUAL, exigiria a capacidade de especificar essa opção, mas a sintaxe não permitir. O mesmo vale para NATURAL JOIN.
Erwin Smout 25/02
Para que uma chave estrangeira funcione, o referido deve ser exclusivo (ou seja, todos os valores devem ser distintos). O que significa que ele pode ter um único valor nulo. Todos os valores nulos poderiam se referir a esse nulo único se as referências fossem definidas com o operador NOT DISTINCT. Eu acho que seria melhor (no sentido de mais útil). Com JOINs (externo e interno), acho que os estritos iguais são melhores porque os "NULL MATCHES" se multiplicariam quando os nulos do lado esquerdo corresponderiam a todos os nulos do lado direito.
Rami Ojares
1

Eu ainda acredito que esta é uma falha fundamental / funcional provocada por um tecnicismo. Se você possui um campo opcional pelo qual pode identificar um cliente, agora precisa hackear um valor fictício nele, apenas porque NULL! = NULL, não é particularmente elegante, mas é um "padrão do setor"

Adriaan Davel
fonte