如何实现只能在单行上设置的“默认”标志

Jac*_*las 32 constraint database-agnostic referential-integrity

例如,有一个类似这样的表:

create table foo(bar int identity, chk char(1) check (chk in('Y', 'N')));
Run Code Online (Sandbox Code Playgroud)

标志是否实现为 a char(1)、 abit或其他无关紧要。我只是希望能够强制执行它只能在单行上设置的约束。

Mar*_*ith 32

SQL Server 2008 - 筛选的唯一索引

CREATE UNIQUE INDEX IX_Foo_chk ON dbo.Foo(chk) WHERE chk = 'Y'
Run Code Online (Sandbox Code Playgroud)


Jac*_*las 16

SQL Server 2000、2005:

您可以利用唯一索引中只允许有一个空值这一事实:

create table t( id int identity, 
                chk1 char(1) not null default 'N' check(chk1 in('Y', 'N')), 
                chk2 as case chk1 when 'Y' then null else id end );
create unique index u_chk on t(chk2);
Run Code Online (Sandbox Code Playgroud)

对于 2000 年,您可能需要SET ARITHABORT ON(感谢 @gbn 提供此信息)


Vin*_*rat 14

甲骨文:

由于 Oracle 不会索引所有索引列都为空的条目,因此您可以使用基于函数的唯一索引:

create table foo(bar integer, chk char(1) not null check (chk in('Y', 'N')));
create unique index idx on foo(case when chk='Y' then 'Y' end);
Run Code Online (Sandbox Code Playgroud)

该索引最多只能索引一行。

知道这个索引事实,你还可以稍微不同地实现位列:

create table foo(bar integer, chk char(1) check (chk ='Y') UNIQUE);
Run Code Online (Sandbox Code Playgroud)

这里列的可能值chkYand NULL。最多只有一行可以有值Y.

  • @jack:您的评论让我意识到,如果您接受该列可以是 `Y` 或 `null`,还有一种更简单的可能性,请参阅我的更新。 (2认同)

小智 13

我认为这是正确构建数据库表的一种情况。更具体地说,如果你有一个人有多个地址,并且你想要一个作为默认地址,我认为你应该将默认地址的 addressID 存储在 person 表中,而不是在地址表中有一个默认列:

Person
-------
PersonID
Name
etc.
DefaultAddressID (fk to addressID)

Address
--------
AddressID
Street
City, State, Zip, etc.
Run Code Online (Sandbox Code Playgroud)

您可以使 DefaultAddressID 可以为空,但这种结构会强制执行您的约束。


Jac*_*las 12

MySQL:

create table foo(bar serial, chk boolean unique);
insert into foo(chk) values(null);
insert into foo(chk) values(null);
insert into foo(chk) values(false);
insert into foo(chk) values(true);

select * from foo;
+-----+------+
| bar | chk  |
+-----+------+
|   1 | NULL |
|   2 | NULL |
|   3 |    0 |
|   4 |    1 |
+-----+------+

insert into foo(chk) values(true);
ERROR 1062 (23000): Duplicate entry '1' for key 2
insert into foo(chk) values(false);
ERROR 1062 (23000): Duplicate entry '0' for key 2
Run Code Online (Sandbox Code Playgroud)

MySQL 中忽略检查约束,因此我们必须将nullorfalse视为 false 和truetrue。最多 1 行可以有chk=true

您可能认为在插入/更新时添加触发器更改false为一种改进,true作为缺少检查约束的解决方法 - IMO 虽然这不是改进。

我希望能够使用 char(0) 因为它

当您需要一个只能取两个值的列时也非常好:定义为 CHAR(0) NULL 的列只占用一位,并且只能取值 NULL 和 ''

不幸的是,至少使用 MyISAM 和 InnoDB,我得到

ERROR 1167 (42000): The used storage engine can't index column 'chk'
Run Code Online (Sandbox Code Playgroud)

- 编辑

这毕竟不是一个好的解决方案,因为在 MySQL 上,它boolean是 的同义词tinyint(1),因此允许非空值大于 0 或 1。这可能bit是更好的选择


gbn*_*gbn 10

SQL 服务器:

怎么做:

  1. 最好的方法是过滤索引。使用 DRI
    SQL Server 2008+

  2. 具有唯一性的计算列。使用 DRI
    参见 Jack Douglas 的回答。SQL Server 2005 及更早版本

  3. 索引/物化视图,类似于过滤索引。使用 DRI
    所有版本。

  4. 扳机。使用代码,而不是 DRI。
    所有版本

如何不这样做:

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


Jac*_*las 10

PostgreSQL:

create table foo(bar serial, chk char(1) unique check(chk='Y'));
insert into foo default values;
insert into foo default values;
insert into foo(chk) values('Y');

select * from foo;
 bar | chk
-----+-----
   1 |
   2 |
   3 | Y

insert into foo(chk) values('Y');
ERROR:  duplicate key value violates unique constraint "foo_chk_key"
Run Code Online (Sandbox Code Playgroud)

- 编辑

或者(更好),使用唯一的部分索引

create table foo(bar serial, chk boolean not null default false);
create unique index foo_i on foo(chk) where chk;
insert into foo default values;
insert into foo default values;
insert into foo(chk) values(true);

select * from foo;
 bar | chk
-----+-----
   1 | f
   2 | f
   3 | t
(3 rows)

insert into foo(chk) values(true);
ERROR:  duplicate key value violates unique constraint "foo_i"
Run Code Online (Sandbox Code Playgroud)


Cen*_*bit 6

这种问题是我问这个问题的另一个原因:

数据库中的应用程序设置

如果您的数据库中有一个应用程序设置表,您可以有一个条目来引用您希望被视为“特殊”的记录的 ID。然后,您只需从您的设置表中查找 ID,这样您就不需要一整列来设置一个项目。


one*_*hen 6

使用广泛实施的技术的可能方法:

1) 撤销表上的“作者”权限。创建 CRUD 过程以确保在事务边界强制执行约束。

2) 6NF:删除CHAR(1)列。添加受约束的引用表以确保其基数不能超过 1:

alter table foo ADD UNIQUE (bar);

create table foo_Y
(
 x CHAR(1) DEFAULT 'x' NOT NULL UNIQUE CHECK (x = 'x'), 
 bar int references foo (bar)
);
Run Code Online (Sandbox Code Playgroud)

更改应用程序语义,以便考虑的“默认”是新表中的行。可能使用视图来封装这个逻辑。

3) 删除CHAR(1)列。添加seq整数列。对 施加唯一约束seq。更改应用程序语义,以便考虑的“默认”是seq值为 1 或seq值为最大/最小值或类似值的行。可能使用视图来封装这个逻辑。


Rol*_*DBA 5

对于那些使用 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 ;
Run Code Online (Sandbox Code Playgroud)

为了确保您的表干净并且存储过程正常工作,假设 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;
Run Code Online (Sandbox Code Playgroud)

这是一个也有帮助的触发器:

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 ;
Run Code Online (Sandbox Code Playgroud)

为确保您的表干净且触发器正常工作,假设 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;
Run Code Online (Sandbox Code Playgroud)

试一试 !!!

  • MySQL 没有基于 DRI 的解决方案吗?只有代码?我很好奇,因为我开始越来越多地使用 MySQL... (3认同)