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?
Edit: ceci est adapté à SQL Server avant de connaître MySQL
Il y a 4 5 façons de le faire: 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
La meilleure façon est un index filtré. Cela utilise DRI pour maintenir l'unicité.
Colonne calculée avec unicité (voir la réponse de Jack Douglas) (ajoutée par la modification 2)
Une vue indexée/matérialisée qui ressemble à un index filtré utilisant DRI
Déclencheur (selon les autres réponses)
Vérifiez la contrainte avec un UDF. Ce n'est pas sûr pour l'isolement simultané et instantané. Voir nDeuxTroisQuatre
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é le SO plusieurs fois:
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 orientée vers SQL Server.
Appliquer une contrainte CHECK à la table qui appelle un UDF et s'assure de son retour la valeur 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 index bitmap ou filtré sur la colonne isDefault
(en fonction de votre plate-forme) pour vous assurer que cette UDF s'exécute très rapidement.
Mises en garde
PostalCodes
, 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 suggestion de gbn d'un index unique filtré au lieu d'une contrainte de vérification.
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;
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.
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
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 à la table DefaultFlag
. 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é.
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, de sorte que 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;