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


31

例如,使用与此类似的表:

create table foo(bar int identity, chk char(1) check (chk in('Y', 'N')));

该标志是否实现为char(1),a bit或任何形式都没有关系。我只希望能够强制执行只能在单行上设置的约束。


灵感来自这个问题被限制到MySQL
杰克·道格拉斯

2
问题的措辞暗示使用表必须是错误的答案。但是有时(大多数时候?)添加另一个表是一个好主意。并且添加表完全与数据库无关。
Mike Sherrill'Cat Recall'11

Answers:



16

SQL Server 2000、2005:

您可以利用以下事实:唯一索引中仅允许一个null:

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);

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


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);

该索引最多只会索引单个行。

了解了这个索引事实后,您还可以稍微不同地实现bit列:

create table foo(bar integer, chk char(1) check (chk ='Y') UNIQUE);

这里列的可能值chkYNULL。最多只能有一行具有该值Y.


chk需要not null约束吗?
杰克·道格拉斯

@jack:not null如果您不希望为null,则可以添加约束(问题规格对我来说还不清楚)。在任何情况下,只有一行可以具有值“ Y”。
Vincent Malgrat

+1我明白您的意思-没错,没必要(但是也许更整洁,尤其是与组合使用时default)?
杰克·道格拉斯,

2
@jack:您的发言使我意识到,如果您接受该列可以是Ynull,则可以使用一种更简单的方法,请参见我的更新。
Vincent Malgrat

1
选项2具有额外的好处,索引将很小 null s的跳过-在一些清晰的成本也许
杰克·道格拉斯

13

我认为这是正确构造数据库表的一种情况。更具体地说,如果您有一个具有多个地址的人员,并且希望将其设为默认地址,我认为您应该将默认地址的addressID存储在人员表中,而不是在地址表中包含默认列:

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

Address
--------
AddressID
Street
City, State, Zip, etc.

您可以将DefaultAddressID设置为可为空,但是通过这种结构,您可以强制执行约束。


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

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

你可以认为这是一种改进,添加一个触发器来改变falsetrue在插入/更新为缺乏检查约束的解决方法-国际海事组织它不是一个虽然改善。

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

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

不幸的是,至少有了MyISAM和InnoDB,我得到了

ERROR 1167 (42000): The used storage engine can't index column 'chk'

- 编辑

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


这可以回答我对RolandoMySQLDBA的回答的评论:我们可以使用DRI的MySQL解决方案吗?
GBN

这是一个有点难看的,虽然因为nullfalsetrue-我不知道是否有东西整洁...
杰克·道格拉斯

@Jack-+1可以很好地尝试MySQL中的纯DRI方法。
RolandoMySQLDBA 2011年

我建议在这里避免使用false,因为唯一约束只会允许提供这样一个false值。如果null表示false,则应在整个过程中始终使用它-如果可以进行其他验证(例如JSR-303 / hibernate-validator),则可以强制避免false。
史蒂夫·钱伯斯

1
MySQL / MariaDB的最新版本实现了虚拟列,我相信可以在dba.stackexchange.com/a/144847/94908
MattW

10

SQL Server:

怎么做:

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

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

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

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

如何不这样做:

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

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"

- 编辑

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

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"

6

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

数据库中的应用程序设置

如果数据库中有一个应用程序设置表,则可能会有一个条目引用您希望被视为“特殊”记录的ID。然后,您只需从设置表中查找ID是什么,就不必为要设置的一项仅填写一列。


这是一个很好的建议:它更符合规范化的设计,可与任何数据库平台一起使用,并且最容易实现。
Nick Chammas

+1,但请注意,“一整列”可能不会使用任何物理空间,具体取决于您的RDBMS :)
Jack Douglas

6

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

1)撤消桌上的“作家”特权。创建CRUD程序,以确保在事务边界处强制执行约束。

2)6NF:放下CHAR(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)
);

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

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


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 ;

要确保您的表是干净的并且存储过程正在运行,假定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
MySQL是否没有基于DRI的解决方案?只有代码?我很好奇,因为我开始越来越多地使用MySQL ...
gbn

4

在SQL Server 2000及更高版本中,您可以使用索引视图来实现复杂的(或多表)约束,例如您所要求的约束。
Oracle对于具有延迟检查约束的物化视图也有类似的实现。 在这里

查看我的帖子。


您能否在此答案中提供更多“内容”,例如一小段代码?现在,这只是几个一般性想法和一个链接。
Nick Chammas

在这里举一个例子有点困难。如果单击链接,您将找到所需的“肉”。
spaghettidba

3

标准过渡SQL-92,广泛实施,例如SQL Server 2000及更高版本:

从表中撤消“作者”特权。为WHERE chk = 'Y'和创建两个视图,WHERE chk = 'N'分别包括WITH CHECK OPTION。对于WHERE chk = 'Y'视图,包括一个搜索条件,使其基数不能超过一个。在视图上授予“作者”特权。

视图的示例代码:

CREATE VIEW foo_chk_N
AS
SELECT *
  FROM foo AS f1
 WHERE chk = 'N' 
WITH CHECK OPTION

CREATE VIEW foo_chk_Y
AS
SELECT *
  FROM foo AS f1
 WHERE chk = 'Y' 
       AND 1 >= (
                 SELECT COUNT(*)
                   FROM foo AS f2
                  WHERE f2.chk = 'Y'
                )
WITH CHECK OPTION

即使您的RDBMS支持此功能,它也会疯狂地进行序列化,因此,如果您有多个用户,则可能会遇到问题
Jack Douglas

如果多个用户同时进行修改,则他们必须排队(序列化)-有时可以,但通常不行(请考虑使用大量OLTP或进行长事务)。
杰克·道格拉斯,

3
感谢您的澄清。我必须说,如果多个用户经常设置唯一的默认行,那么设计选择(同一表中的标志列)是可疑的。
2011年

3

这是一个使用虚拟列的MySQL和MariaDB解决方案,更加优雅。它要求MySQL> = 5.7.6或MariaDB> = 5.2:

MariaDB [db]> create table foo(bar varchar(255), chk boolean);

MariaDB [db]> describe foo;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| bar   | varchar(255) | YES  |     | NULL    |       |
| chk   | tinyint(1)   | YES  |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

如果您不想强制执行唯一约束,则创建一个虚拟列为NULL:

MariaDB [db]> ALTER table foo ADD checked_bar varchar(255) as (IF(chk, bar, null)) PERSISTENT UNIQUE;

(对于MySQL,请使用STORED代替PERSISTENT。)

MariaDB [db]> insert into foo(bar, chk) values('a', false);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> insert into foo(bar, chk) values('a', false);
Query OK, 1 row affected (0.01 sec)

MariaDB [salt_dev]> insert into foo(bar, chk) values('a', false);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> insert into foo(bar, chk) values('a', true);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> insert into foo(bar, chk) values('a', true);
ERROR 1062 (23000): Duplicate entry 'a' for key 'checked_bar'

MariaDB [db]> insert into foo(bar, chk) values('b', true);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> select * from foo;
+------+------+-------------+
| bar  | chk  | checked_bar |
+------+------+-------------+
| a    |    0 | NULL        |
| a    |    0 | NULL        |
| a    |    0 | NULL        |
| a    |    1 | a           |
| b    |    1 | b           |
+------+------+-------------+

1

标准FULL SQL-92:在CHECK约束中使用子查询,但未广泛实现,例如在ANSI-92查询模式下在Access2000(ACE2007,Jet 4.0等)及更高版本中受支持。

示例代码:CHECKAccess中的note 约束始终是表级别的。由于问题中的CREATE TABLE语句使用行级CHECK约束,因此需要通过添加逗号来对其稍作修改:

create table foo(bar int identity, chk char(1), check (chk in('Y', 'N')));

ALTER TABLE foo ADD 
   CHECK (1 >= (
                SELECT COUNT(*) 
                  FROM foo AS f2 
                 WHERE f2.chk = 'Y'
               ));

1
在我使用过的任何RDBMS中都不好... 警告到处都是
杰克·道格拉斯,

0

我只浏览了答案,所以我可能错过了类似的答案。想法是使用生成的列,该列要么是pk,要么是不存在的常数作为pk的值

create table foo 
(  bar int not null primary key
,  chk char(1) check (chk in('Y', 'N'))
,  some_name generated always as ( case when chk = 'N' 
                                        then bar 
                                        else -1 
                                   end )
, unique (somename)
);

AFAIK在SQL2003中有效(因为您正在寻找不可知的解决方案)。DB2允许它,但不确定有多少其他供应商接受它。

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.