Comment forcer un enregistrement à avoir une valeur vraie pour une colonne booléenne et tous les autres une valeur fausse?

20

Je veux faire en sorte qu'un seul enregistrement dans une table soit considéré comme la valeur "par défaut" pour d'autres requêtes ou vues qui pourraient accéder à cette table.

Fondamentalement, je veux garantir que cette requête renvoie toujours exactement une ligne:

SELECT ID, Zip 
FROM PostalCodes 
WHERE isDefault=True

Comment pourrais-je faire cela en SQL?

emaynard
la source
1
"Je veux garantir que cette requête retournera toujours exactement une ligne" - toujours? Et quand PostalCodesest vide? Si une ligne a déjà une propriété, la propriété doit être empêchée d'être définie sur false à moins qu'une autre ligne (s'il en existe une) soit définie sur true dans la même instruction SQL. Les lignes nulles peuvent-elles avoir la propriété entre les limites de transaction? La dernière ligne du tableau doit-elle être obligée d'avoir la propriété et ne pas être supprimée? L'expérience me dit que «garantir exactement une rangée» a tendance à signifier quelque chose de différent en réalité, souvent simplement «au plus une rangée».
quand

Réponses:

8

Edit: ceci est adapté à SQL Server avant de connaître MySQL

Il y a 4 5 façons de procéder: par ordre du plus désiré au moins désiré

Nous ne savons pas à quoi sert le SGBDR si bien que tous ne s'appliquent pas

  1. La meilleure façon est un index filtré. Cela utilise DRI pour maintenir l'unicité.

  2. Colonne calculée avec unicité (voir la réponse de Jack Douglas) (ajoutée par la modification 2)

  3. Une vue indexée / matérialisée qui ressemble à un index filtré utilisant DRI

  4. Déclencheur (selon les autres réponses)

  5. Vérifiez la contrainte avec un UDF. Ce n'est pas sûr pour l'isolement simultané et instantané. Voir un deux trois quatre

Notez que l'exigence pour "un défaut" est sur la table, pas la procédure stockée. La procédure stockée aura les mêmes problèmes de concurrence qu'une contrainte de vérification avec un udf

Remarque: demandé plusieurs fois:

gbn
la source
gbn - il semble que la solution d'index / DRI filtré ne soit que SQL 2008, donc il semblerait que je vais passer à l'option 2.
emaynard
10

Remarque: Cette réponse a été donnée avant qu'il ne soit clair que le demandeur voulait quelque chose de spécifique à MySQL. Cette réponse est biaisée vers SQL Server.

Appliquez une contrainte CHECK à la table qui appelle un UDF et assurez-vous que sa valeur de retour est <= 1. L'UDF peut simplement compter les lignes de la table WHERE isDefault = TRUE. Cela garantira qu'il n'y a pas plus d'une ligne par défaut dans le tableau.

Vous devez ajouter un bitmap ou un index filtré sur la isDefaultcolonne (en fonction de votre plate-forme) pour vous assurer que cette UDF s'exécute très rapidement.

Avertissements

  • Les contraintes de vérification ont certaines limites . A savoir, ils sont invoqués pour chaque ligne qui est modifiée, même si ces lignes n'ont pas encore été validées dans une transaction. Donc, si vous démarrez une transaction, définissez une nouvelle ligne comme valeur par défaut, puis désactivez l'ancienne ligne, votre contrainte de vérification se plaindra. En effet, il s'exécutera une fois que vous aurez défini la nouvelle ligne par défaut, qu'il y aura désormais deux valeurs par défaut et échouera. La solution consiste alors à vous assurer de désactiver la ligne par défaut avant de définir une nouvelle valeur par défaut, même si vous effectuez ce travail dans une transaction.
  • Comme l'a souligné gbn , les FDU peuvent renvoyer des résultats incohérents si vous utilisez l'isolement de cliché dans SQL Server. Cependant, un UDF en ligne ne se heurte pas à ce problème, et puisqu'un UDF qui fait juste un compte est en ligne, ce n'est pas un problème ici.
  • La puissance de traitement d'un moteur de base de données réside dans sa capacité à travailler simultanément sur des ensembles de données. Avec une contrainte de vérification soutenue par UDF, le moteur se limitera à appliquer cet UDF en série à chaque ligne qui est modifiée. Dans votre cas d'utilisation, il est peu probable que vous effectuiez des mises à jour en masse de la PostalCodestable, mais cela reste un problème de performances pour les situations où une activité basée sur un ensemble est probable.

Compte tenu de toutes ces mises en garde, je recommande d'utiliser la suggestion de gbn d'un index unique filtré au lieu d'une contrainte de vérification.

Nick Chammas
la source
4

Voici une procédure stockée (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 ;

Pour vous assurer que votre table est propre et que la procédure stockée fonctionne, en supposant que l'ID 200 est la valeur par défaut, exécutez ces étapes:

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;

Au lieu d'une procédure stockée, que diriez-vous d'un déclencheur?

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 ;

Pour vous assurer que votre table est propre et que le déclencheur fonctionne, en supposant que l'ID 200 est la valeur par défaut, exécutez ces étapes:

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
la source
3

Vous pouvez utiliser un déclencheur pour appliquer les règles. Lorsqu'une instruction UPDATE ou INSERT définit isDefault sur True, le SQL dans le déclencheur peut définir toutes les autres lignes sur False.

Vous devrez prendre en compte d'autres situations lors de l'application de cette option. Par exemple, que doit-il se passer si plusieurs lignes et UPDATE ou INSERT définissent isDefault sur True? Quelles règles s'appliqueront pour éviter d'enfreindre la règle?

Aussi, est-ce possible la condition où il n'y a pas de défaut? Que se passe-t-il lorsqu'une mise à jour définit l'isDefault de True à False.

Une fois que vous avez défini les règles, vous pouvez les créer dans le déclencheur.

Ensuite, comme indiqué dans une autre réponse, vous souhaitez appliquer une contrainte de vérification pour aider à appliquer les règles.

bobs
la source
3

Ce qui suit configure un exemple de table et de données:

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

Si vous créez une procédure stockée pour définir l'indicateur IsDefault et supprimer les autorisations pour modifier la table de base, vous pouvez appliquer cela avec la requête ci-dessous. Cela nécessiterait une analyse de table à chaque fois.

DECLARE @Id INT
SET @Id = 10

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

Selon la taille de la table, un index sur IsDefault (DESC) peut entraîner une ou les deux requêtes ci-dessous en évitant une analyse complète.

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)

Si vous ne pouvez pas supprimer les autorisations de la table de base, devez appliquer l'intégrité par d'autres moyens et utilisez SQL2008, vous pouvez utiliser un index unique filtré:

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

Pour MySQL qui n'a pas d'index filtrés, vous pouvez faire quelque chose comme ceci. Il exploite le fait que MySQL autorise plusieurs NULL dans des index uniques, et utilise une table dédiée et une clé étrangère pour simuler un type de données qui ne peut avoir que NULL et 1 comme valeurs.

Avec cette solution, au lieu de true / false, vous utiliseriez simplement 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`))*/

La seule chose qui peut mal tourner, je pense, c'est que quelqu'un ajoute une ligne au DefaultFlagtableau. Je pense que ce n'est pas un gros problème, et vous pouvez toujours le réparer avec des autorisations si vous voulez vraiment être en sécurité.

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

Cela vous posait essentiellement des problèmes parce que vous aviez placé le champ au mauvais endroit et c'est tout.

Ayez une table 'Defaults', avec PostalCode dedans, qui contient l'identifiant que vous recherchez. En général, il est également bon de nommer vos tables en fonction d'un enregistrement, donc le nom de votre table initiale serait mieux appelé «PostalCode». Les valeurs par défaut seront une table à une seule ligne, jusqu'à ce que vous ayez besoin de spécialiser vos valeurs par défaut avec une autre configuration (comme les historiques).

La dernière requête que vous souhaitez est la suivante:

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