如何强制一条记录的布尔列为真值,其他所有记录为假值?


20

我要强制将表中的仅一条记录视为其他可能访问该表的查询或视图的“默认”值。

基本上,我想保证此查询将始终仅返回一行:

SELECT ID, Zip 
FROM PostalCodes 
WHERE isDefault=True

我将如何在SQL中做到这一点?


1
“我想保证此查询将始终只返回一行” –总是吗?那什么时候PostalCodes是空的呢?如果某行已经具有属性,那么除非在同一SQL语句中将另一行(如果存在)设置为true,否则将其设置为false?零行能否在事务边界之间具有属性?是否应该强制表的最后一行具有该属性并防止其被删除?经验告诉我,“保证精确地排成一排”往往意味着现实中的某些不同,通常只是“最多排成一排”。
某天,2012年

Answers:


8

编辑:这是针对SQL Server的,然后才知道MySQL

4种 5种方式可以做到这一点:从最需要到最不想要的顺序

我们不知道这是否适用于RDBMS,尽管并非全部适用

  1. 最好的方法是过滤索引。这使用DRI维护唯一性。

  2. 计算列具有唯一性(请参阅杰克·道格拉斯的回答)(由编辑2补充)

  3. 索引/物化视图,类似于使用DRI的筛选索引

  4. 触发(根据其他答案)

  5. 使用UDF检查约束。这对于并发和快照隔离是不安全的。见一个 两个 三个 四个

请注意,表上是“默认值”的要求,而不是存储过程。存储过程将具有与使用udf的检查约束相同的并发问题

注意:问了很多遍:


GBN -看起来像过滤索引/ DRI的解决方案是SQL 2008只所以看起来我会跳到尝试选项2
emaynard

10

注意:这个答案是在明确要求询问者想要MySQL特定内容之前给出的。这个答案偏向于SQL Server。

CHECK约束应用于调用UDF的表,并确保其返回值小于等于1。UDF可以仅对表WHERE中的行进行计数isDefault = TRUE。这将确保表中的默认行不超过1个。

您应该在列上添加位图过滤索引isDefault(取决于平台),以确保此UDF能够非常快速地运行。

注意事项

  • 检查约束有一定的 局限性。即,即使尚未在事务中提交任何行,也将为每个被修改的行调用它们。因此,如果启动事务,将新行设置为默认行,然后取消设置旧行,则检查约束将失败。这是因为它将在您将新行设置为默认值之后执行,发现现在有两个默认值,然后失败。解决方案是确保即使在事务中进行此工作,也要确保在设置新默认值之前先取消设置默认行。
  • 正如gbn所 指出的,如果您在SQL Server中使用快照隔离,则UDF可能会返回不一致的结果。但是,内联UDF不会遇到此问题,并且由于只进行计数的UDF是可内联的,因此这里不是问题。
  • 数据库引擎处理能力的强项在于它能够一次处理数据集的能力。使用UDF支持的检查约束,引擎将只能将此UDF顺序地应用于修改的每一行。在您的用例中,不太可能对PostalCodes表执行批量更新,但是对于那些可能基于集合的活动的情况,这仍然是性能问题。

考虑到所有这些注意事项,我建议使用gbn建议的过滤唯一索引而不是检查约束。


4

这是一个存储过程(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;

3

您可以使用触发器来应用规则。当UPDATE或INSERT语句将isDefault设置为True时,触发器中的SQL可以将所有其他行设置为False。

应用此方法时,您必须考虑其他情况。例如,如果在其中多行并且UPDATE或INSERT将isDefault设置为True,该怎么办?为了避免违反规则,将应用哪些规则?

另外,是否有可能没有默认条件?当更新将isDefault从True设置为False时会发生什么。

定义规则后,您可以在触发器中构建它们。

然后,如另一个答案所示,您想应用检查约束来帮助强制执行规则。


3

下面建立一个示例表和数据:

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

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表中添加一行。我认为这不是一个大问题,如果您确实希望安全,可以随时使用权限进行修复。


0
SELECT ID, Zip FROM PostalCodes WHERE isDefault=True 

这实际上给您带来了问题,因为您将字段放置在错误的位置,仅此而已。

有一个“默认值”表,其中包含PostalCode,其中包含您要查找的ID。通常,根据一条记录为表命名也很不错,因此,最好将初始表的名称称为“ PostalCode”。默认值将是一个单行表,直到您需要针对其他配置(例如历史记录)专门设置默认值为止。

您想要的最终查询如下:

select Zip from PostalCode p, Defaults d where p.ID=d.PostalCode;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.