如何在字段顺序无关紧要的情况下实现唯一性

时间:2012-03-14 16:56:59

标签: sql sql-server relational-database primary-key foreign-key-relationship

我认为以下示例将最好地解释这种情况。 假设我们有以下表结构:

-------------------------------------
Member1   int      NOT NULL (FK)(PK)
Member2   int      NOT NULL (FK)(PK)
-------------------------------------
Statust   char(1)  NOT NULL

以下是该表的表格内容:

Member1    Member2    Status
----------------------------
  100        105        A

我的问题是如何实现唯一性,以便以下INSERT语句将失败基于表中已有的那一行。

INSERT status_table (Member1,Member2,Status) VALUES(105,100,'D');

基本上,我正在尝试模拟两个成员之间的关系。无论我们有(100,105)还是(105,100),状态字段都是相同的。

我知道我可以使用before_insert和before_update触发器来检查表中的内容。但我想知道是否有更好的方法来做到这一点......我的数据库模型应该是不同的......

8 个答案:

答案 0 :(得分:6)

如果您可以确保所有应用程序/用户以最小到最大的顺序存储成员的ID(Member1中的最小成员ID和Member2中的最大成员ID),那么您可以简单地添加检查约束:

ALTER TABLE Status_table
  ADD CONSTRAINT Status_table_Prevent_double_pairs
    CHECK (Member1 < Member2)

如果您不想这样做,或者您希望存储额外的信息(因为您正在存储(仅举例)“会员100受邀(喜欢,被杀,无论如何)会员150”< / em>而不是相反),那么你可以使用@ Tegiri的方法,稍微修改一下(乘以两个足够大的整数会产生溢出问题):

CREATE TABLE Status_table
( Member1 INT NOT NULL
, Member2 INT NOT NULL
, Status CHAR(1) NOT NULL
, MemberOne  AS CASE WHEN Member1 < Member2 THEN Member1 ELSE Member2 END
          --- a computed column
, MemberTwo  AS CASE WHEN Member1 < Member2 THEN Member2 ELSE Member1 END
          --- and another one
, PRIMARY KEY (Member1, Member2)
, UNIQUE (MemberOne, MemberTwo)
, ...                                    --- FOREIGN KEY details, etc 
) ;

答案 1 :(得分:3)

数据库模型失败,因为你有两个实体{Member1,Member2},通过说无论哪个是哪个,你说的是同一个实体{Member}。换句话说,你在两个地方有一个事实,一个是关系数据库设计的主要罪过。

高级解决方案是更好地模拟关系的本质。一个例子可能是两个人的婚姻。而不是“新娘和新郎结婚”,并且首先列出哪些,你会有“婚姻#xyz介于(包含)参与者A和B之间”。因此,表Marriage与主键,表MarriageMember与外键的Marriage,外键到“Person”,以及两列的主键。让你有两个以上的成员,如果你在海因莱恩的故事中,这可能很有用。

如果您坚持使用现有模式(并非我们所有模式),我需要提交数据,例如,首先列出的最低值,以便始终正确排序。您可以将两列上的校验和作为计算列进行操作,但这并不能绝对保证唯一性。但是,唉,在一天结束时,你的模型似乎是出于你的目的的轻微缺陷。


附加物

根据以下评论,如果您要为某个特定成员所关联的成员建模,那么您的“成员与其他成员”有关。这里,Member1是“主要”成员,而Member2是“此”成员所关联的其他成员。 (这是两个成员列之间所需要的区别。)因此,如果关系是双向的,那么您需要两个条目,以涵盖“成员A与成员B相关”“B成员与A成员有关”。当然,这将通过{Member1,Member2}上的主键强制执行,因为Status似乎无关紧要(只有一个关系,而不是基于状态的多个关系)。

答案 2 :(得分:3)

这是另一种看待这种情况的方法。实际上,您可以强制执行规则,即相互关系始终由两行(A,B)(B,A)的存在来表示,而不是仅存在一行。

CREATE TABLE MutualRelationship
 (Member1 INT NOT NULL,
  Member2 INT NOT NULL,
  Status CHAR(1),
 PRIMARY KEY (Member1, Member2),
 UNIQUE (Member1, Member2, Status),
 FOREIGN KEY (Member2, Member1, Status) REFERENCES MutualRelationship (Member1, Member2, Status));

INSERT INTO MutualRelationship (Member1, Member2, Status)
VALUES
(100,105,'A'),
(105,100,'A');

答案 3 :(得分:2)

避免触发器的一种方法是尝试在Member1和Member2上使用UNIQUE计算列:

create table test (Member1 int not null, Member2 int not null, Status char(1)
, bc as abs(binary_checksum(Member1))+abs(binary_checksum(Member2)) PERSISTED UNIQUE)

INSERT INTO test values(123, 456, 'A'); --succeeds
INSERT INTO test values(123, 789, 'B'); --succeeds
INSERT INTO test values(456, 123, 'D'); --fails with the following error:
--Msg 2627, Level 14, State 1, Line 1
--Violation of UNIQUE KEY constraint 'UQ__test__3213B1084A8F946C'. Cannot insert duplicate key in object 'dbo.test'

答案 4 :(得分:2)

此处摘录了“SQL设计模式”一书中的“对称函数”,您可能会发现这些相关内容。

考虑一个盒子的库存数据库

table Boxes (
   length integer,
   width  integer,
   height integer
)

然而,现实世界中的盒子尺寸通常不以任何特定顺序给出。选择长度,宽度和高度的尺寸基本上是任意的。如果我们想根据尺寸识别盒子怎么办?例如,我们希望能够告诉长度= 1,宽度= 2和高度= 3的方框与长度= 3,宽度= 1和高度= 2的方框相同。那么,如何声明一个独特的尺寸约束呢?更具体地说,我们不允许任何两个具有相同尺寸的盒子。

分析思维会毫不费力地认识到问题的核心是列排序。长度,宽度和高度列的值可以互换,以形成另一个合法的记录!因此,为什么我们不引入3个伪列,比如说A,B和C

A ≤ B ≤ C

然后,对A,B,C的唯一约束应满足我们的要求!它可以实现为基于函数的唯一索引,只要我们可以在长度,宽​​度,高度方面分析表达A,B,C。一块蛋糕:A是最大的长度,宽度,高度; C是最少的,但我们如何表达B?嗯,答案很容易写

B = least (greatest (length,width),
           greatest (width,height),
           greatest (height,length) )
虽然很难解释。

像往常一样,数学观点澄清了很多。考虑三次方程式

如果我们知道根x1,x2,x3那么,可以考虑三次多项式,因此我们有

将两个方程结合起来,我们用根x1,x2,x3表示系数a,b,c

图4.1:多项式y=(x-x1)(x-x2)(x-x3)的图形形状完全由根x1,x2和x3定义。交换它们不会影响任何事情。

函数-x1-x2-x3, x1x2+x2x3+x3x1, -x1x2x3是对称的。置换x1,x2,x3对值a,b,c没有影响。换句话说,三次方程的根之间的顺序是无关紧要的:正式地说,我们说的是一组根,而不是一个根1的列表。这正是我们在Boxes示例中想要的效果。根据长度,宽度,高度重写的对称函数是

length+width+height
length*width+width*height+height*length
length*width*height

通过利用对称函数的否定也是对称的这一事实,简化了这些表达式。

我们的最后一个解决方案与之前的解决方案非常相似,其中最大的运算符扮演乘法的角色,而最小的运算符则作为加法。甚至可以提出一个解决方案,它是两者之间的混合

least(length,width,height)
least(length+width,width+height,height+length)
length+width+height

读者可以检查这三个函数是否再次对称2。 最后一步是在正式SQL中记录我们的解决方案

table Boxes (
   length integer,
   width  integer,
   height integer
);

create unique index b_idx on Boxes(
   length + width + height,
   length * width + width * height + height * length,
   length * width * height
);

对称函数为一个漂亮的解决方案提供了基础。然而,在实践中,通常可以通过模式重新设计来解决问题。在盒子库存数据库示例中,我们甚至不需要架构重新设计:我们可以只需要更改插入无约束记录(length,width,height)的做法,并要求

length ≥ width ≥ height

答案 5 :(得分:1)

除了触发器之外,无法想出一种更好的方法来增强现有的唯一约束。 e.g。

CREATE TRIGGER dbo.StatusTable_PreventDualUniques
ON dbo.status_table
INSTEAD OF INSERT
AS
BEGIN
    SET NOCOUNT ON;

    IF EXISTS (
      SELECT 1 FROM inserted AS i 
        INNER JOIN dbo.status_table AS s
        ON i.Member1 = s.Member1 AND i.Member2 = s.Member2
        OR i.Member2 = s.Member1 AND i.Member1 = s.Member2
    )
    BEGIN
        RAISERROR('Duplicate detected', 11, 1);
    END
    ELSE
    BEGIN
        INSERT dbo.status_table(Member1, Member2, Status)
            SELECT Member1, Member2, Status
            FROM inserted;
    END
END

现在我不知道这只涉及单行插入。如果需要处理多行插入,逻辑可能会变得更复杂,因为您需要在insertedinserted与基表之间检查重复项。这也不能在默认隔离级别处理高并发性(例如,另一个事务在检查和插入之间插入重复的行)。但这应该是一个开始。

(您还需要一个UPDATE ...)

答案 6 :(得分:1)

@ypercube's solution的一个细微变化是创建一个索引视图并将唯一约束移动到视图。这是一个演示该方法的完整脚本:

/* the reference table (almost irrelevant for the tests,
   but added to make the environment closer to the one in the question) */
CREATE TABLE dbo.Members (
  ID int IDENTITY CONSTRAINT PK_Members PRIMARY KEY,
  Name varchar(50)
);

GO

/* the table to add the constraint on */
CREATE TABLE dbo.Data (
  Member1 int CONSTRAINT FK_Data_Member1 FOREIGN KEY REFERENCES dbo.Members (ID),
  Member2 int CONSTRAINT FK_Data_Member2 FOREIGN KEY REFERENCES dbo.Members (ID),
  Statust char(1),
  CONSTRAINT PK_Data PRIMARY KEY (Member1, Member2)
);

GO

/* the indexed view that the constraint will actually be applied to */
CREATE VIEW dbo.DataView
WITH SCHEMABINDING  /* required with indexed views */
AS
SELECT
  /* the column definitions are practically identical to ypercube's */
  Member1 = CASE WHEN Member1 > Member2 THEN Member2 ELSE Member1 END,
  Member2 = CASE WHEN Member1 > Member2 THEN Member1 ELSE Member2 END
FROM dbo.Data

GO

/* finally, the constraint itself */
CREATE UNIQUE CLUSTERED INDEX UQ_DataView ON dbo.DataView (Member1, Member2);

GO

/* preparing the stage: adding some data to the reference table */
INSERT INTO dbo.Members (Name)
SELECT 'Member A' UNION ALL
SELECT 'Member B' UNION ALL
SELECT 'Member C';

GO

/* the first two rows should and do insert into the target table without issues */
INSERT INTO dbo.Data (Member1, Member2, Statust) VALUES (3, 1, 'A');
INSERT INTO dbo.Data (Member1, Member2, Statust) VALUES (2, 3, 'A');

GO

/* and this one fails, which demonstrates the constraint in work */
INSERT INTO dbo.Data (Member1, Member2, Statust) VALUES (1, 3, 'B');

GO

/* cleaning up */
DROP VIEW dbo.DataView;
DROP TABLE dbo.Data;
DROP TABLE dbo.Members;

详细了解MSDN上的索引视图:

答案 7 :(得分:0)

不是试图让表本身强制执行这个特定的业务逻辑,而是将它封装在存储过程中会更好吗?您当然可以更灵活地在两个成员之间实施独特的关系。