插入新记录时如何根据列值忽略多列上的唯一索引

How to ignore unique Index on multiple columns based on column value while inserting new record

我们有一个包含三列的 table,StudentIdSubjectIdActive(以及其他一些列,但与此问题无关)。

Active 列指示记录是否处于活动状态(如果有人从 UI 中删除记录,我们将此 Active 列设置为零)

StudentIdSubjectId列的索引定义如下:

CREATE UNIQUE NONCLUSTERED INDEX [UQ_StudentSubject_SubjectId_StudentId]  
ON [dbo].[StudentSubject]
(
    [StudentId] ASC,
    [SubjectId] ASC

)WITH(
    PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB =  OFF,     
    IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS =  ON,      
    ALLOW_PAGE_LOCKS = ON
) ON [PRIMARY]

GO

在我们的应用程序中,如果我们尝试使用此 SubjectIdstudentId 的组合插入另一条记录,则插入失败并且 Java 抛出以下错误:

Caused by: com.microsoft.sqlserver.jdbc.SQLServerException: Cannot insert duplicate key row in object 'dbo.StudentSubject' with unique index 'UQ_StudentSubject_SubjectId_StudentId'. The duplicate key value is (113460, 182).

组合为 113460、182 的记录的 Active 为零,因此我们正在尝试插入新记录而不是将 Active 标志设置为 1。

如果我们插入一条新记录,现有的 subjectId 和 studentId 组合的 Active 列为零,那么有什么方法可以在插入时忽略此索引?

编辑 很抱歉造成混淆,它是一个索引而不是一个约束。

下面是过滤索引的示例,它将忽略活动值为 0 的行:

CREATE UNIQUE NONCLUSTERED INDEX UQ_StudentSubject_SubjectId_StudentId  
ON dbo.StudentSubject
(
    [StudentId],
    [SubjectId]
) WHERE Active <> 0;

您可能不想将 Active 添加到索引中,但如果您希望拥有两个(或更多)版本的 (studentID, subjectID),只是 Active 的值不同,那么您已经设计得很好自己到那个角落。如果您需要不止一行具有 StudentID 和 SubjectID 的特定组合,您将不得不更改设计。对不起。

一个选项(如果可用)是通过时间戳添加一行 "unique-ifier"。

create table StudentSubject(
    StudentID   int not null,
    SubjectID   int not null,
    ChangeDate  datetime2 default GetDate(),
    Active      int default 1,
    ...,
    constraint  PK_StudentSubject primary key( StudentID, SubjectID, ChangeDate )
);

现在您有了一个无限扩展的解决方案 -- 任何特定的学生和主题配对都可以有任意数量的活动设置。

StudentID SubjectID ChangeDate Active ...
      100        42 2015-01-01      1 ... -- first entry
      100        42 2015-01-03      0 ... -- nope, deactivate this entry
      100        42 2015-01-05      1 ... -- changed my mind, reactivate
      100        42 2015-01-07      0 ... -- horoscope indicates bad idea
      100        42 2015-01-09      1 ... -- wait, I'm not superstitious, go for it!

要获取当前的活动设置,只需获取具有最新 ChangeDate 值的记录:

select  ss.StudentID, ss.SubjectID, ss.ChangeDate, ss.Active
from    StudentSubject  ss
where   ss.StudentID = @Stud
    and ss.SubjectID = @Subj
    and ss.ChangeDate =(
        select  Max( ss1.ChangeDate )
        from    StudentSubject  ss1
        where   ss1.StudentID = ss.StudentID
            and ss1.SubjectID = ss.SubjectID );

不要让子查询让你担心。注意它只使用聚簇索引。

这里有一些额外的好处。假设你被问到,"What was the Active value of student 100 and subject 42 as of 2015-01-04?" 好吧,你只需将这一行添加到查询的末尾,作为子查询的一部分:

            and ss1.ChangeDate <= @AsOfDate );

并将@AsOfDate 设置为您要查看的日期。

这为您提供了一个 "lookback" 查询来查看数据在任何特定时间的样子。事实上,将 @AsOfDate 设置为 GetDate() 将 return 当前状态,因此您可以对 "current" 和 "lookback" 查询使用相同的查询。

另一个可能的好处:假设您提前知道 Active 值会发生变化。今天是 2015 年 1 月 10 日,该行计划在 2015 年 1 月 15 日停用。继续并插入具有预定日期的行:

      100        42 2015-01-15      0 ...

"current" 查询将继续将 Active 值显示为 1 直到 15 日,届时该值将显示为 0。

仅显示当前版本的视图使应用程序可以使用该数据,因此它甚至不必了解结构。视图上的触发器将使应用程序可以在一个对象上查询和执行 DML,为您提供应用程序和数据之间的抽象层。