Answers:
编辑:这是针对SQL Server的,然后才知道MySQL
有4种 5种方式可以做到这一点:从最需要到最不想要的顺序
我们不知道这是否适用于RDBMS,尽管并非全部适用
最好的方法是过滤索引。这使用DRI维护唯一性。
计算列具有唯一性(请参阅杰克·道格拉斯的回答)(由编辑2补充)
索引/物化视图,类似于使用DRI的筛选索引
触发(根据其他答案)
请注意,表上是“默认值”的要求,而不是存储过程。存储过程将具有与使用udf的检查约束相同的并发问题
注意:问了很多遍:
注意:这个答案是在明确要求询问者想要MySQL特定内容之前给出的。这个答案偏向于SQL Server。
将CHECK约束应用于调用UDF的表,并确保其返回值小于等于1。UDF可以仅对表WHERE中的行进行计数isDefault = TRUE
。这将确保表中的默认行不超过1个。
您应该在列上添加位图或过滤索引isDefault
(取决于平台),以确保此UDF能够非常快速地运行。
注意事项
PostalCodes
表执行批量更新,但是对于那些可能基于集合的活动的情况,这仍然是性能问题。考虑到所有这些注意事项,我建议使用gbn建议的过滤唯一索引而不是检查约束。
这是一个存储过程(MySQL方言):
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 ;
要确保您的表是干净的并且存储过程正在运行,假设默认ID为200,请运行以下步骤:
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;
代替存储过程,触发器如何?
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 ;
要确保您的表是干净的并且触发器可以正常工作,请假定ID 200为默认值,请运行以下步骤:
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;
下面建立一个示例表和数据:
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
如果创建存储过程以设置IsDefault标志并删除更改基表的权限,则可以通过以下查询来强制执行此操作。每次都需要进行表扫描。
DECLARE @Id INT
SET @Id = 10
UPDATE
dbo.MyTable
SET
IsDefault = CASE WHEN [id] = @Id THEN 1 ELSE 0 END
GO
根据表的大小,IsDefault(DESC)上的索引可能会导致下面的一个或两个查询避免全面扫描。
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)
如果您不能从基表中删除权限,需要通过其他方式强制执行完整性并且正在使用SQL2008,则可以使用过滤后的唯一索引:
CREATE UNIQUE INDEX IX_MyTable_IsDefault ON dbo.MyTable (IsDefault) WHERE IsDefault = 1
对于没有过滤索引的MySQL,您可以执行以下操作。它利用了MySQL在唯一索引中允许多个NULL的事实,并使用专用表和外键来模拟只能具有NULL和1作为值的数据类型。
使用此解决方案,您可以只使用1 / NULL来代替true / false。
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`))*/
我认为唯一可能出错的是,如果有人在DefaultFlag
表中添加一行。我认为这不是一个大问题,如果您确实希望安全,可以随时使用权限进行修复。
SELECT ID, Zip FROM PostalCodes WHERE isDefault=True
这实际上给您带来了问题,因为您将字段放置在错误的位置,仅此而已。
有一个“默认值”表,其中包含PostalCode,其中包含您要查找的ID。通常,根据一条记录为表命名也很不错,因此,最好将初始表的名称称为“ PostalCode”。默认值将是一个单行表,直到您需要针对其他配置(例如历史记录)专门设置默认值为止。
您想要的最终查询如下:
select Zip from PostalCode p, Defaults d where p.ID=d.PostalCode;
PostalCodes
是空的呢?如果某行已经具有属性,那么除非在同一SQL语句中将另一行(如果存在)设置为true,否则将其设置为false?零行能否在事务边界之间具有属性?是否应该强制表的最后一行具有该属性并防止其被删除?经验告诉我,“保证精确地排成一排”往往意味着现实中的某些不同,通常只是“最多排成一排”。