restrição única condicional

92

Eu tenho uma situação em que preciso impor uma restrição exclusiva em um conjunto de colunas, mas apenas para um valor de coluna.

Então, por exemplo, eu tenho uma tabela como Tabela (ID, Nome, RecordStatus).

RecordStatus só pode ter um valor 1 ou 2 (ativo ou excluído), e quero criar uma restrição exclusiva em (ID, RecordStatus) apenas quando RecordStatus = 1, já que não me importo se houver vários registros excluídos com o mesmo EU IRIA.

Além de escrever gatilhos, posso fazer isso?

Estou usando o SQL Server 2005.

np-hard
fonte
1
Este design é uma dor comum. Você considerou alterar o design para que os registros nocionalmente 'excluídos' sejam fisicamente excluídos da tabela e talvez movidos para uma tabela de 'arquivo'?
um
1
... porque a incapacidade de escrever uma restrição UNIQUE para impor uma chave simples deve ser considerada um 'cheiro de código', IMO. Se você não pode alterar o design (SQL DDL) porque muitas outras tabelas fazem referência a esta tabela, então aposto que seu SQL DML também sofre como resultado, ou seja, você deve se lembrar de adicionar ... AND Table.RecordStatus = 1 ' para a maioria das condições de pesquisa e junção envolvendo esta tabela e experimentando erros sutis quando ela é inevitavelmente omitida na ocasião.
um

Respostas:

36

Adicione uma restrição de verificação como esta. A diferença é que você retornará falso se Status = 1 e Contagem> 0.

http://msdn.microsoft.com/en-us/library/ms188258.aspx

CREATE TABLE CheckConstraint
(
  Id TINYINT,
  Name VARCHAR(50),
  RecordStatus TINYINT
)
GO

CREATE FUNCTION CheckActiveCount(
  @Id INT
) RETURNS INT AS BEGIN

  DECLARE @ret INT;
  SELECT @ret = COUNT(*) FROM CheckConstraint WHERE Id = @Id AND RecordStatus = 1;
  RETURN @ret;

END;
GO

ALTER TABLE CheckConstraint
  ADD CONSTRAINT CheckActiveCountConstraint CHECK (NOT (dbo.CheckActiveCount(Id) > 1 AND RecordStatus = 1));

INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 1);

INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 2);
-- Msg 547, Level 16, State 0, Line 14
-- The INSERT statement conflicted with the CHECK constraint "CheckActiveCountConstraint". The conflict occurred in database "TestSchema", table "dbo.CheckConstraint".
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);

SELECT * FROM CheckConstraint;
-- Id   Name         RecordStatus
-- ---- ------------ ------------
-- 1    No Problems  2
-- 1    No Problems  2
-- 1    No Problems  2
-- 1    No Problems  1
-- 2    Oh no!       1
-- 2    Oh no!       2

ALTER TABLE CheckConstraint
  DROP CONSTRAINT CheckActiveCountConstraint;

DROP FUNCTION CheckActiveCount;
DROP TABLE CheckConstraint;
D. Patrick
fonte
Eu olhei para as restrições de verificação de nível de tabela, mas não vejo que existe alguma maneira de passar os valores sendo inseridos ou atualizados para a função, você sabe como?
np-hard
Ok, postei um script de amostra que ajudará você a provar do que estou falando. Eu testei e funciona. Se você olhar as duas linhas comentadas, verá a mensagem que recebo. Nota bene, em minha implementação, eu apenas garanto que você não pode adicionar um segundo item com o mesmo Id que está ativo se já houver um ativo. Você pode modificar a lógica de forma que se houver um ativo, você não pode adicionar nenhum item com o mesmo id. Com esse padrão, as possibilidades são praticamente infinitas.
D. Patrick
Eu prefiro a mesma lógica em um gatilho. "uma consulta em uma função escalar ... pode criar grandes problemas se sua restrição CHECK depender de uma consulta e se mais de uma linha for afetada por qualquer atualização. O que acontece é que a restrição é verificada uma vez para cada linha antes que a instrução seja concluída . Isso significa que a atomicidade da instrução foi interrompida e a função será exposta ao banco de dados em um estado inconsistente. Os resultados são imprevisíveis e imprecisos. " Consulte: blogs.conchango.com/davidportas/archive/2007/02/19/…
um
Isso é apenas parcialmente verdadeiro um dia quando. O banco de dados se comporta de forma consistente e previsível. A restrição de verificação será executada após a linha ser adicionada à tabela e antes da transação ser confirmada pelo dbms e você pode contar com isso. Esse blog estava falando sobre um problema bastante único em que você precisa executar a restrição em um conjunto de inserções em vez de apenas uma inserção por vez. ashish está pedindo uma restrição em um inserto por vez e essa restrição funcionará de forma precisa, previsível e consistente. Lamento se isso soou conciso; Eu estava ficando sem personagens.
D. Patrick
3
Isso funciona muito bem para inserções, mas não parece funcionar para atualizações. EG Adicionar isso após as outras inserções funciona aqui quando eu não esperava. INSERT INTO CheckConstraint VALUES (1, 'Sem problemasA', 2); update CheckConstraint set Recordstatus = 1 onde name = 'No ProblemsA'
dwidel
148

Veja, o índice filtrado . Da documentação (grifo meu):

Um índice filtrado é um índice não clusterizado otimizado especialmente adequado para cobrir consultas que selecionam a partir de um subconjunto de dados bem definido. Ele usa um predicado de filtro para indexar uma parte das linhas na tabela. Um índice filtrado bem projetado pode melhorar o desempenho da consulta, bem como reduzir os custos de manutenção e armazenamento do índice em comparação com os índices de tabela completa.

E aqui está um exemplo combinando um índice exclusivo com um predicado de filtro:

create unique index MyIndex
on MyTable(ID)
where RecordStatus = 1;

Isso essencialmente impõe exclusividade de IDquando RecordStatusé 1.

Após a criação desse índice, uma violação de exclusividade levantará um erro:

Msg 2601, Nível 14, Estado 1, Linha 13
Não é possível inserir uma linha de chave duplicada no objeto 'dbo.MyTable' com índice exclusivo 'MyIndex'. O valor da chave duplicada é (9999).

Observação: o índice filtrado foi introduzido no SQL Server 2008. Para versões anteriores do SQL Server, consulte esta resposta .

cânone
fonte
Observe que o SQL Server exige ansi_paddingíndices filtrados, portanto, certifique-se de que essa opção esteja ativada executando SET ANSI_PADDING ONantes de criar um índice filtrado.
naXa
10

Você pode mover os registros excluídos para uma tabela que não tem a restrição e talvez usar uma visão com UNION das duas tabelas para preservar a aparência de uma única tabela.

Carl Manaster
fonte
2
Isso é realmente muito inteligente Carl. Não é uma resposta à pergunta em si, mas é uma boa solução. Se a tabela tiver muitas linhas, isso também pode acelerar a procura de um registro ativo, porque você pode olhar a tabela de registro ativo. Isso também aceleraria a restrição porque a restrição exclusiva usa um índice em oposição à restrição de verificação que escrevi abaixo, que deve executar uma contagem. Eu gosto disso.
D. Patrick
3

Você pode fazer isso de uma maneira realmente hacky ...

Crie uma visão baseada em esquema em sua tabela.

CREATE VIEW Qualquer SELECT * FROM Table WHERE RecordStatus = 1

Agora crie uma restrição única na visão com os campos que você deseja.

Uma observação sobre as visualizações associadas ao esquema, porém, se você alterar as tabelas subjacentes, terá que recriar a visualização. Muitas pegadinhas por causa disso.

Min
fonte
Esta é uma sugestão muito boa, e não tão "hacky". Aqui estão mais informações sobre essa alternativa de índice filtrado .
Scott Whitlock
É uma má idéia. A questão não é.
FabianoLothor 01 de
Usei uma visão associada a esquema uma vez e nunca repeti o erro. Eles podem ser uma dor real de se trabalhar. Não é que você precise recriar a visualização se alterar a tabela subjacente - você potencialmente terá que fazer isso para todas as visualizações, pelo menos no servidor SQL. É que você não pode alterar a tabela sem primeiro eliminar a visualização, o que talvez não seja possível fazer sem primeiro eliminar as referências a ela. Além do mais, o armazenamento pode ser problemático - seja por causa do espaço, seja pelo custo que adiciona para inserir e atualizar.
MattW
1

Porque, você vai permitir duplicatas, uma restrição única não funcionará. Você pode criar uma restrição de verificação para a coluna RecordStatus e um procedimento armazenado para INSERT que verifica os registros ativos existentes antes de inserir IDs duplicados.

Ichiban
fonte
1

Se você não pode usar NULL como um RecordStatus como Bill sugeriu, você poderia combinar sua ideia com um índice baseado em função. Crie uma função que retorne NULL se o RecordStatus não for um dos valores que você deseja considerar em sua restrição (e o RecordStatus caso contrário) e crie um índice sobre isso.

Isso terá a vantagem de não ser necessário examinar explicitamente outras linhas da tabela em sua restrição, o que pode causar problemas de desempenho.

Devo dizer que não conheço o servidor SQL, mas usei com sucesso essa abordagem no Oracle.

Vagabundo
fonte
boa ideia, mas não há nenhuma função indexada no sql server, mas obrigado pela resposta
np-hard