Como forçar um registro a ter um valor verdadeiro para uma coluna booleana e todos os outros um valor falso?

20

Quero impor que apenas um registro em uma tabela seja considerado o valor "padrão" para outras consultas ou exibições que possam acessar essa tabela.

Basicamente, quero garantir que essa consulta sempre retorne exatamente uma linha:

SELECT ID, Zip 
FROM PostalCodes 
WHERE isDefault=True

Como eu faria isso no SQL?

emaynard
fonte
1
"Quero garantir que essa consulta sempre retorne exatamente uma linha" - sempre? E quando PostalCodesestá vazio? Se uma linha já tiver uma propriedade, deve ser impedida de ser definida como falsa, a menos que outra linha (se houver) seja definida como verdadeira na mesma instrução SQL? Zero linhas podem ter a propriedade entre os limites da transação? A última linha da tabela deve ser forçada a ter a propriedade e ser impedida de ser excluída? A experiência me diz que "garantir exatamente uma linha" tende a significar algo diferente na realidade, geralmente simplesmente "no máximo uma linha".
onedaywhen

Respostas:

8

Edit: isso é voltado para o SQL Server antes de conhecermos o MySQL

Existem 4 maneiras de fazer isso: na ordem do mais desejado para o menos desejado

Nós não sabemos o que RDBMS isso se, embora para que nem todos possam se aplicar

  1. A melhor maneira é um índice filtrado. Isso usa o DRI para manter a exclusividade.

  2. Coluna computada com exclusividade (consulte a resposta de Jack Douglas) (adicionada pela edição 2)

  3. Uma exibição indexada / materializada que é como um índice filtrado usando DRI

  4. Disparador (conforme outras respostas)

  5. 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

Observe que o requisito para "um padrão" está na tabela, não no procedimento armazenado. O procedimento armazenado terá os mesmos problemas de simultaneidade que uma restrição de verificação com um udf

Nota: solicitado em SO muitas vezes:

gbn
fonte
gbn - Parece que o índice filtrado / solução DRI é SQL 2008, apenas por isso parece que vai pular para a opção 2. tentando
emaynard
10

Nota: Esta resposta foi dada antes de ficar claro que o solicitante queria algo específico do MySQL. Esta resposta é tendenciosa para o SQL Server.

Aplique uma restrição CHECK à tabela que chama um UDF e verifique se seu valor de retorno é <= 1. O UDF pode apenas contar as linhas na tabela WHERE isDefault = TRUE. Isso garantirá que não haja mais de uma linha padrão na tabela.

Você deve adicionar um bitmap ou índice filtrado na isDefaultcoluna (dependendo do seu platforom) para garantir que esse UDF seja executado rapidamente.

Ressalvas

  • As restrições de verificação têm certas limitações . Ou seja, eles são chamados para todas as linhas modificadas, mesmo que essas linhas ainda não tenham sido confirmadas em uma transação. Portanto, se você iniciar uma transação, defina uma nova linha como padrão e, em seguida, desmarque a linha antiga, sua restrição de verificação reclamará. Isso ocorre porque ele será executado depois que você tornar a nova linha o padrão, descobrir que agora existem dois padrões e falhar. A solução é garantir que você desmarque a linha padrão primeiro antes de definir um novo padrão, mesmo se você estiver executando este trabalho em uma transação.
  • Como gbn apontou , as UDFs podem retornar resultados inconsistentes se você estiver usando o isolamento de instantâneo no SQL Server. No entanto, um UDF embutido não se depara com esse problema e, como um UDF que apenas conta é passível de embutir, isso não é um problema aqui.
  • A força do poder de processamento de um mecanismo de banco de dados está na capacidade de trabalhar em conjuntos de dados de uma só vez. Com uma restrição de verificação apoiada em UDF, o mecanismo ficará limitado à aplicação serial dessa UDF a todas as linhas modificadas. No seu caso de uso, é improvável que você execute atualizações em massa na PostalCodestabela, no entanto, isso continua sendo uma preocupação de desempenho para as situações em que é provável uma atividade baseada em conjunto.

Dadas todas essas advertências, recomendo usar a sugestão de gbn de um índice exclusivo filtrado em vez de uma restrição de verificação.

Nick Chammas
fonte
4

Aqui está um procedimento armazenado (MySQL Dialect):

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;

Em vez de um procedimento armazenado, que tal um gatilho?

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;
RolandoMySQLDBA
fonte
3

Você pode usar um gatilho para aplicar as regras. Quando uma instrução UPDATE ou INSERT define isDefault como True, o SQL no gatilho pode definir todas as outras linhas como False.

Você terá que considerar outras situações ao aplicar isso. Por exemplo, o que deve acontecer se mais de uma linha e UPDATE ou INSERT definiremDefault como True? Quais regras serão aplicadas para evitar violar a regra?

Além disso, é possível a condição em que não há padrão? O que acontecerá quando uma atualização definir o isDefault de True para False.

Depois de definir as regras, você pode construí-las no gatilho.

Em seguida, conforme indicado em outra resposta, você deseja aplicar uma restrição de verificação para ajudar a aplicar as regras.

bobs
fonte
3

A seguir, configure uma tabela e dados de exemplo:

IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[MyTable]') AND type in (N'U'))
DROP TABLE [dbo].[MyTable]
GO

CREATE TABLE dbo.MyTable
(
    [id] INT IDENTITY(1,1) PRIMARY KEY CLUSTERED
    , [IsDefault] BIT DEFAULT 0
)
GO

INSERT dbo.MyTable DEFAULT VALUES
GO 100

Se você criar um procedimento armazenado para definir o sinalizador IsDefault e remover permissões para alterar a tabela base, poderá aplicá-lo com a consulta abaixo. Exigiria uma varredura de tabela a cada vez.

DECLARE @Id INT
SET @Id = 10

UPDATE
    dbo.MyTable
SET
    IsDefault = CASE WHEN [id] = @Id THEN 1 ELSE 0 END 
GO

Dependendo do tamanho da tabela, um índice em IsDefault (DESC) pode resultar em uma ou ambas as consultas abaixo, evitando uma verificação completa.

DECLARE @Id INT
SET @Id = 10

UPDATE
    dbo.MyTable
SET
    IsDefault = CASE WHEN [id] = @Id THEN 1 ELSE 0 END
FROM
    dbo.MyTable
WHERE
    [id] = @Id
OR  IsDefault = 1

UPDATE
    dbo.MyTable
SET
    IsDefault = CASE WHEN [id] = @Id THEN 1 ELSE 0 END
FROM
    dbo.MyTable
WHERE
    [id] = @Id
OR  ([id] != @Id AND IsDefault = 1)

Se você não conseguir remover as permissões da tabela base, precisar impor a integridade por outros meios e estiver usando o SQL2008, poderá usar um índice exclusivo filtrado:

CREATE UNIQUE INDEX IX_MyTable_IsDefault  ON dbo.MyTable (IsDefault) WHERE IsDefault = 1
Mark Storey-Smith
fonte
1

Para o MySQL que não possui índices filtrados, você pode fazer algo como o seguinte. Ele explora o fato de o MySQL permitir vários NULLs em índices exclusivos e usa uma tabela dedicada e uma chave estrangeira para simular um tipo de dados que só pode ter NULL e 1 como valores.

Com esta solução, em vez de verdadeiro / falso, você usaria apenas 1 / NULL.

CREATE TABLE DefaultFlag (id TINYINT UNSIGNED NOT NULL PRIMARY KEY);
INSERT INTO DefaultFlag (id) VALUES (1);

CREATE TABLE PostalCodes (
    ID INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
    Zip VARCHAR (32) NOT NULL,
    isDefault TINYINT UNSIGNED NULL DEFAULT NULL,
    CONSTRAINT FOREIGN KEY (isDefault) REFERENCES DefaultFlag (id),
    UNIQUE KEY (isDefault)
);

INSERT INTO PostalCodes (Zip, isDefault) VALUES ('123', 1   );
/* Only one row can be default */
INSERT INTO PostalCodes (Zip, isDefault) VALUES ('abc', 1   );
/* ERROR 1062 (23000): Duplicate entry '1' for key 'isDefault' */

/* MySQL allows multiple NULLs in unique indexes */
INSERT INTO PostalCodes (Zip, isDefault) VALUES ('456', NULL);
INSERT INTO PostalCodes (Zip, isDefault) VALUES ('789', NULL);

/* only NULL and 1 are admitted in isDefault thanks to foreign key */
INSERT INTO PostalCodes (Zip, isDefault) VALUES ('789', 2   );
/* ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`test`.`PostalCodes`, CONSTRAINT `PostalCodes_ibfk_1` FOREIGN KEY (`isDefault`) REFERENCES `DefaultFlag` (`id`))*/

A única coisa que pode dar errado, eu acho, é se alguém adicionar uma linha à DefaultFlagtabela. Acho que isso não é uma grande preocupação, e você sempre pode corrigi-lo com permissões se realmente quiser estar seguro.

djfm
fonte
0
SELECT ID, Zip FROM PostalCodes WHERE isDefault=True 

Isso estava essencialmente lhe dando problemas, porque você colocou o campo no lugar errado e isso é tudo.

Tenha uma tabela 'Padrões', com PostalCode, que contém o ID que você está procurando. Em geral, também é bom nomear suas tabelas de acordo com um registro, para que o nome da sua tabela inicial seja melhor chamado 'PostalCode'. Os padrões serão uma tabela de linha única, até que você precise especializar seus padrões em alguma outra configuração (como históricos).

A consulta final que você deseja é a seguinte:

select Zip from PostalCode p, Defaults d where p.ID=d.PostalCode;
Konchog
fonte