Como implementar um sinalizador 'padrão' que só pode ser definido em uma única linha

31

Por exemplo, com uma tabela semelhante a esta:

create table foo(bar int identity, chk char(1) check (chk in('Y', 'N')));

Não importa se o sinalizador é implementado como a char(1), a bitou o que for. Eu só quero poder impor a restrição de que ele só pode ser definido em uma única linha.

Jack Douglas
fonte
inspirado por esta questão que se limita ao MySQL
Jack Douglas
2
A maneira como a pergunta é formulada sugere que o uso de uma tabela deve ser a resposta errada. Mas às vezes (na maioria das vezes?) Adicionar outra tabela é uma boa ideia. E adicionar uma tabela é completamente independente do banco de dados.
Mike Sherrill 'Cat Recall'

Respostas:

31

SQL Server 2008 - índice exclusivo filtrado

CREATE UNIQUE INDEX IX_Foo_chk ON dbo.Foo(chk) WHERE chk = 'Y'
Mark Storey-Smith
fonte
16

SQL Server 2000, 2005:

Você pode aproveitar o fato de que apenas um nulo é permitido em um índice exclusivo:

create table t( id int identity, 
                chk1 char(1) not null default 'N' check(chk1 in('Y', 'N')), 
                chk2 as case chk1 when 'Y' then null else id end );
create unique index u_chk on t(chk2);

para 2000, você pode precisar SET ARITHABORT ON(graças a @gbn por esta informação)

Jack Douglas
fonte
14

Oráculo:

Como o Oracle não indexa entradas nas quais todas as colunas indexadas são nulas, você pode usar um índice exclusivo baseado em função:

create table foo(bar integer, chk char(1) not null check (chk in('Y', 'N')));
create unique index idx on foo(case when chk='Y' then 'Y' end);

Esse índice apenas indexará uma única linha no máximo.

Conhecendo esse fato do índice, você também pode implementar a coluna de bits de maneira um pouco diferente:

create table foo(bar integer, chk char(1) check (chk ='Y') UNIQUE);

Aqui os valores possíveis para a coluna chkserão Ye NULL. Somente uma linha no máximo pode ter o valorY.

Vincent Malgrat
fonte
chk precisa de uma not nullrestrição?
19411 Jack Douglas
@ Jack: Você pode adicionar uma not nullrestrição se não quiser valores nulos (não ficou claro para mim a partir das especificações da pergunta). Somente uma linha pode ter o valor 'Y' em qualquer caso.
Vincent Malgrat
+1 Entendo o que você quer dizer - você está certo, não é necessário (mas talvez seja um pouco mais limpo, especialmente se combinado com a default)?
19411 Jack Douglas
2
@ Jack: sua observação me fez perceber que uma possibilidade ainda mais simples está disponível se você aceitar que a coluna pode ser uma You outra null, veja minha atualização.
Vincent Malgrat
1
Opção 2 tem a vantagem do índice será pequena como nulls são ignorados - ao custo de alguma clareza, talvez
Jack Douglas
13

Eu acho que este é um caso de estruturar suas tabelas de banco de dados corretamente. Para torná-lo mais concreto, se você tem uma pessoa com vários endereços e deseja que seja o padrão, acho que você deve armazenar o ID do endereço do endereço padrão na tabela de pessoas, e não ter uma coluna padrão na tabela de endereços:

Person
-------
PersonID
Name
etc.
DefaultAddressID (fk to addressID)

Address
--------
AddressID
Street
City, State, Zip, etc.

Você pode tornar o DefaultAddressID anulável, mas dessa maneira a estrutura impõe sua restrição.

Decker97
fonte
12

MySQL:

create table foo(bar serial, chk boolean unique);
insert into foo(chk) values(null);
insert into foo(chk) values(null);
insert into foo(chk) values(false);
insert into foo(chk) values(true);

select * from foo;
+-----+------+
| bar | chk  |
+-----+------+
|   1 | NULL |
|   2 | NULL |
|   3 |    0 |
|   4 |    1 |
+-----+------+

insert into foo(chk) values(true);
ERROR 1062 (23000): Duplicate entry '1' for key 2
insert into foo(chk) values(false);
ERROR 1062 (23000): Duplicate entry '0' for key 2

As restrições de verificação são ignoradas no MySQL, portanto, temos que considerar nullou falsecomo falsas e trueverdadeiras. No máximo 1 linha pode terchk=true

Você pode considerar-se uma melhoria para adicionar um gatilho para a mudança falseem trueem insert / update como uma solução para a ausência de uma restrição de verificação - IMO não é uma melhoria embora.

Eu esperava poder usar um char (0) porque

também é bastante bom quando você precisa de uma coluna que pode ter apenas dois valores: uma coluna definida como CHAR (0) NULL ocupa apenas um bit e pode usar apenas os valores NULL e ''

Infelizmente, com o MyISAM e o InnoDB, pelo menos, recebo

ERROR 1167 (42000): The used storage engine can't index column 'chk'

--editar

afinal, essa não é uma boa solução, já que no MySQL booleané sinônimotinyint(1) e, portanto, permite valores não nulos que 0 ou 1. É possível que bitseja uma escolha melhor

Jack Douglas
fonte
Isso poderia responder meu comentário à resposta de RolandoMySQLDBA: podemos ter soluções MySQL com DRI?
GBN
É um pouco feio, porque embora o null, false, true- eu me pergunto se há algo mais puro ...
Jack Douglas
@ Jack - +1 para uma boa tentativa de abordagem pura de DRI no MySQL.
RolandoMySQLDBA
Eu recomendaria evitar o uso de false aqui, pois a restrição exclusiva permitiria apenas que um valor falso desse fosse fornecido. Se null representar false, ele deverá ser utilizado de forma consistente - a prevenção de false poderá ser aplicada se uma validação adicional estiver disponível (por exemplo, JSR-303 / hibernate-validator).
Steve Chambers
1
Versões recentes do MySQL / MariaDB implementam colunas virtuais que, acredito, permitem uma solução um pouco mais elegante descrita abaixo em dba.stackexchange.com/a/144847/94908
MattW.
10

Servidor SQL:

Como fazer isso:

  1. A melhor maneira é um índice filtrado. Usa o DRI
    SQL Server 2008 ou superior

  2. Coluna computada com exclusividade. Usa DRI
    Veja a resposta de Jack Douglas. SQL Server 2005 e anterior

  3. Uma exibição indexada / materializada que é como um índice filtrado. Usa DRI
    Todas as versões.

  4. Desencadear. Usa código, não DRI.
    Todas versões

Como não fazer:

  1. Verifique a restrição com um UDF. Isso não é seguro para o isolamento de simultaneidade e instantâneo.
    Veja um dois três quatro
gbn
fonte
10

PostgreSQL:

create table foo(bar serial, chk char(1) unique check(chk='Y'));
insert into foo default values;
insert into foo default values;
insert into foo(chk) values('Y');

select * from foo;
 bar | chk
-----+-----
   1 |
   2 |
   3 | Y

insert into foo(chk) values('Y');
ERROR:  duplicate key value violates unique constraint "foo_chk_key"

--editar

ou (muito melhor), use um índice parcial exclusivo :

create table foo(bar serial, chk boolean not null default false);
create unique index foo_i on foo(chk) where chk;
insert into foo default values;
insert into foo default values;
insert into foo(chk) values(true);

select * from foo;
 bar | chk
-----+-----
   1 | f
   2 | f
   3 | t
(3 rows)

insert into foo(chk) values(true);
ERROR:  duplicate key value violates unique constraint "foo_i"
Jack Douglas
fonte
6

Esse tipo de problema é outro motivo pelo qual perguntei a esta pergunta:

Configurações de aplicativo no banco de dados

Se você tiver uma tabela de configuração de aplicativo em seu banco de dados, poderá ter uma entrada que faça referência ao ID do registro que você deseja que seja considerado 'especial'. Depois, basta pesquisar qual é o ID da sua tabela de configurações, dessa forma, você não precisa de uma coluna inteira para definir apenas um item.

CenterOrbit
fonte
Essa é uma ótima sugestão: está mais de acordo com o design normalizado, funciona com qualquer plataforma de banco de dados e é mais fácil de implementar.
Nick Chammas
+1 mas nota que "uma coluna inteira" pode não usar qualquer espaço físico dependendo do seu RDBMS :)
Jack Douglas
6

Possíveis abordagens usando tecnologias amplamente implementadas:

1) Revogue privilégios de 'escritor' na mesa. Crie procedimentos CRUD que garantam que a restrição seja imposta nos limites da transação.

2) 6NF: solte a CHAR(1)coluna. Adicione uma tabela de referência restrita para garantir que sua cardinalidade não possa exceder uma:

alter table foo ADD UNIQUE (bar);

create table foo_Y
(
 x CHAR(1) DEFAULT 'x' NOT NULL UNIQUE CHECK (x = 'x'), 
 bar int references foo (bar)
);

Altere a semântica do aplicativo para que o 'padrão' considerado seja a linha na nova tabela. Possivelmente use visualizações para encapsular essa lógica.

3) Solte a CHAR(1)coluna. Adicione uma seqcoluna inteira. Coloque uma restrição exclusiva seq. Altere a semântica do aplicativo para que o "padrão" considerado seja a linha em que o seqvalor é um ou o seqvalor em maior / menor valor ou similar. Possivelmente use visualizações para encapsular essa lógica.

um dia quando
fonte
5

Para quem usa o MySQL, aqui está um Procedimento Armazenado apropriado:

DELIMITER $$
DROP PROCEDURE IF EXISTS SetDefaultForZip;
CREATE PROCEDURE SetDefaultForZip (NEWID INT)
BEGIN
    DECLARE FOUND_TRUE,OLDID INT;

    SELECT COUNT(1) INTO FOUND_TRUE FROM PostalCode WHERE isDefault = TRUE;
    IF FOUND_TRUE = 1 THEN
        SELECT ID INTO OLDID FROM PostalCode WHERE isDefault = TRUE;
        IF NEWID <> OLDID THEN
            UPDATE PostalCode SET isDefault = FALSE WHERE ID = OLDID;
            UPDATE PostalCode SET isDefault = TRUE  WHERE ID = NEWID;
        END IF;
    ELSE
        UPDATE PostalCode SET isDefault = TRUE WHERE ID = NEWID;
    END IF;
END;
$$
DELIMITER ;

Para garantir que sua tabela esteja limpa e o procedimento armazenado esteja funcionando, supondo que o ID 200 seja o padrão, execute estas etapas:

ALTER TABLE PostalCode DROP INDEX isDefault_ndx;
UPDATE PostalCodes SET isDefault = FALSE;
ALTER TABLE PostalCode ADD INDEX isDefault_ndx (isDefault);
CALL SetDefaultForZip(200);
SELECT ID FROM PostalCodes WHERE isDefault = TRUE;

Aqui está um gatilho que também ajuda:

DELIMITER $$
CREATE TRIGGER postalcodes_bu BEFORE UPDATE ON PostalCodes FOR EACH ROW
BEGIN
    DECLARE FOUND_TRUE,OLDID INT;
    IF NEW.isDefault = TRUE THEN
        SELECT COUNT(1) INTO FOUND_TRUE FROM PostalCode WHERE isDefault = TRUE;
        IF FOUND_TRUE = 1 THEN
            SELECT ID INTO OLDID FROM PostalCode WHERE isDefault = TRUE;
            UPDATE PostalCodes SET isDefault = FALSE WHERE ID = OLDID;
        END IF;
    END IF;
END;
$$
DELIMITER ;

Para garantir que sua tabela esteja limpa e o gatilho esteja funcionando, supondo que o ID 200 seja o padrão, execute estas etapas:

DROP TRIGGER postalcodes_bu;
ALTER TABLE PostalCode DROP INDEX isDefault_ndx;
UPDATE PostalCodes SET isDefault = FALSE;
ALTER TABLE PostalCode ADD INDEX isDefault_ndx (isDefault);
DELIMITER $$
CREATE TRIGGER postalcodes_bu BEFORE UPDATE ON PostalCodes FOR EACH ROW
BEGIN
    DECLARE FOUND_TRUE,OLDID INT;
    IF NEW.isDefault = TRUE THEN
        SELECT COUNT(1) INTO FOUND_TRUE FROM PostalCode WHERE isDefault = TRUE;
        IF FOUND_TRUE = 1 THEN
            SELECT ID INTO OLDID FROM PostalCode WHERE isDefault = TRUE;
            UPDATE PostalCodes SET isDefault = FALSE WHERE ID = OLDID;
        END IF;
    END IF;
END;
$$
DELIMITER ;
UPDATE PostalCodes SET isDefault = TRUE WHERE ID = 200;
SELECT ID FROM PostalCodes WHERE isDefault = TRUE;

De uma chance !!!

RolandoMySQLDBA
fonte
3
Não existe uma solução baseada em DRI para o MySQL? Somente código? Estou curioso porque eu estou começando a usar o MySQL mais e mais ...
GBN
4

No SQL Server 2000 e superior, você pode usar as Exibições indexadas para implementar restrições complexas (ou com várias tabelas) como a que você está solicitando.
Além disso, a Oracle possui uma implementação semelhante para visualizações materializadas com restrições de verificação diferida.

Veja meu post aqui .

spaghettidba
fonte
Você poderia fornecer um pouco mais de "carne" nesta resposta, como um pequeno trecho de código? No momento, são apenas algumas idéias gerais e um link.
Nick Chammas
Seria um pouco difícil dar um exemplo aqui. Se você clicar no link, encontrará a "carne" que está procurando.
Spaghettidba
3

Transição padrão SQL-92, amplamente implementada, por exemplo, SQL Server 2000 e superior:

Revogue privilégios de 'escritor' da tabela. Crie duas visualizações para WHERE chk = 'Y'e WHERE chk = 'N'respectivamente, incluindo WITH CHECK OPTION. Para a WHERE chk = 'Y'visualização, inclua uma condição de pesquisa no sentido de que sua cardinalidade não pode exceder uma. Conceda privilégios de 'escritor' nas visualizações.

Código de exemplo para as visualizações:

CREATE VIEW foo_chk_N
AS
SELECT *
  FROM foo AS f1
 WHERE chk = 'N' 
WITH CHECK OPTION

CREATE VIEW foo_chk_Y
AS
SELECT *
  FROM foo AS f1
 WHERE chk = 'Y' 
       AND 1 >= (
                 SELECT COUNT(*)
                   FROM foo AS f2
                  WHERE f2.chk = 'Y'
                )
WITH CHECK OPTION
um dia quando
fonte
mesmo que o seu RDBMS suporta isso, ele irá seriar como tão louco se você tiver mais de um usuário, você pode ter um problema
Jack Douglas
se vários usuários estiverem modificando simultaneamente, eles terão que ficar na fila (serializar) - às vezes isso é bom, geralmente não é (pense em OLTP pesado ou em transações longas).
Jack Douglas
3
Agradeço por ter esclarecido. Devo dizer que, se vários usuários estão definindo frequentemente a única linha padrão, a opção de design (coluna de flag na mesma tabela) é questionável.
onedaywhen
3

Aqui está uma solução para MySQL e MariaDB usando colunas virtuais um pouco mais elegantes. Requer MySQL> = 5.7.6 ou MariaDB> = 5.2:

MariaDB [db]> create table foo(bar varchar(255), chk boolean);

MariaDB [db]> describe foo;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| bar   | varchar(255) | YES  |     | NULL    |       |
| chk   | tinyint(1)   | YES  |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

Crie uma coluna virtual que é NULL se você não deseja aplicar a restrição exclusiva:

MariaDB [db]> ALTER table foo ADD checked_bar varchar(255) as (IF(chk, bar, null)) PERSISTENT UNIQUE;

(Para MySQL, use em STOREDvez de PERSISTENT.)

MariaDB [db]> insert into foo(bar, chk) values('a', false);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> insert into foo(bar, chk) values('a', false);
Query OK, 1 row affected (0.01 sec)

MariaDB [salt_dev]> insert into foo(bar, chk) values('a', false);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> insert into foo(bar, chk) values('a', true);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> insert into foo(bar, chk) values('a', true);
ERROR 1062 (23000): Duplicate entry 'a' for key 'checked_bar'

MariaDB [db]> insert into foo(bar, chk) values('b', true);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> select * from foo;
+------+------+-------------+
| bar  | chk  | checked_bar |
+------+------+-------------+
| a    |    0 | NULL        |
| a    |    0 | NULL        |
| a    |    0 | NULL        |
| a    |    1 | a           |
| b    |    1 | b           |
+------+------+-------------+
MattW.
fonte
1

Padrão SQL COMPLETO-92: use uma subconsulta em uma CHECKrestrição, não amplamente implementada, por exemplo, suportada no Access2000 (ACE2007, Jet 4.0, qualquer que seja) e acima quando estiver no modo de consulta ANSI-92 .

Código de exemplo: as CHECKrestrições de nota no Access são sempre no nível da tabela. Como a CREATE TABLEdeclaração na pergunta usa uma CHECKrestrição no nível da linha , ela precisa ser ligeiramente alterada adicionando uma vírgula:

create table foo(bar int identity, chk char(1), check (chk in('Y', 'N')));

ALTER TABLE foo ADD 
   CHECK (1 >= (
                SELECT COUNT(*) 
                  FROM foo AS f2 
                 WHERE f2.chk = 'Y'
               ));
um dia quando
fonte
1
não é bom em nenhum RDBMS que eu usei ... advertências são muitas
Jack Douglas
0

Eu só dei uma olhada nas respostas, então poderia ter perdido uma resposta semelhante. A idéia é usar uma coluna gerada que seja o pk ou uma constante que não exista como um valor para o pk

create table foo 
(  bar int not null primary key
,  chk char(1) check (chk in('Y', 'N'))
,  some_name generated always as ( case when chk = 'N' 
                                        then bar 
                                        else -1 
                                   end )
, unique (somename)
);

AFAIK, isso é válido no SQL2003 (desde que você esteja procurando uma solução independente). O DB2 permite, sem ter certeza de quantos outros fornecedores o aceitam.

Lennart
fonte