Не имеет прямого отношения к ОП, но поскольку Адриан проявил интерес. Вот таблица, в которой SQL Server поддерживает целостность, гарантируя, что в любое время присутствует только одно допустимое значение. В этом случае я имею дело с таблицей current / history, но пример можно изменить для работы с будущими данными (хотя в этом случае у вас не может быть индексированного представления, и вам нужно написать слияние непосредственно , а не через триггеры).
В данном конкретном случае я имею дело с таблицей ссылок, по которой я хочу отслеживать историю. Во-первых, таблицы, которые мы связываем:
create table dbo.Clients (
ClientID int IDENTITY(1,1) not null,
Name varchar(50) not null,
/* Other columns */
constraint PK_Clients PRIMARY KEY (ClientID)
)
go
create table dbo.DataItems (
DataItemID int IDENTITY(1,1) not null,
Name varchar(50) not null,
/* Other columns */
constraint PK_DataItems PRIMARY KEY (DataItemID),
constraint UQ_DataItem_Names UNIQUE (Name)
)
go
Теперь, если бы мы строили нормальную таблицу, у нас было бы следующее ( Не запускайте эту ):
create table dbo.ClientAnswers (
ClientID int not null,
DataItemID int not null,
IntValue int not null,
Comment varchar(max) null,
constraint PK_ClientAnswers PRIMARY KEY (ClientID,DataItemID),
constraint FK_ClientAnswers_Clients FOREIGN KEY (ClientID) references dbo.Clients (ClientID),
constraint FK_ClientAnswers_DataItems FOREIGN KEY (DataItemID) references dbo.DataItems (DataItemID)
)
Но нам нужна таблица, которая может представлять полную историю. В частности, мы хотим спроектировать структуру так, чтобы перекрывающиеся периоды времени никогда не появлялись в базе данных. Мы всегда знаем, какая запись была действительной в любое конкретное время:
create table dbo.ClientAnswerHistories (
ClientID int not null,
DataItemID int not null,
IntValue int null,
Comment varchar(max) null,
/* Temporal columns */
Deleted bit not null,
ValidFrom datetime2 null,
ValidTo datetime2 null,
constraint UQ_ClientAnswerHistories_ValidFrom UNIQUE (ClientID,DataItemID,ValidFrom),
constraint UQ_ClientAnswerHistories_ValidTo UNIQUE (ClientID,DataItemID,ValidTo),
constraint CK_ClientAnswerHistories_NoTimeTravel CHECK (ValidFrom < ValidTo),
constraint FK_ClientAnswerHistories_Clients FOREIGN KEY (ClientID) references dbo.Clients (ClientID),
constraint FK_ClientAnswerHistories_DataItems FOREIGN KEY (DataItemID) references dbo.DataItems (DataItemID),
constraint FK_ClientAnswerHistories_Prev FOREIGN KEY (ClientID,DataItemID,ValidFrom)
references dbo.ClientAnswerHistories (ClientID,DataItemID,ValidTo),
constraint FK_ClientAnswerHistories_Next FOREIGN KEY (ClientID,DataItemID,ValidTo)
references dbo.ClientAnswerHistories (ClientID,DataItemID,ValidFrom),
constraint CK_ClientAnswerHistory_DeletionNull CHECK (
Deleted = 0 or
(
IntValue is null and
Comment is null
)),
constraint CK_ClientAnswerHistory_IntValueNotNull CHECK (Deleted=1 or IntValue is not null)
)
go
Это много ограничений. Единственный способ сохранить эту таблицу - использовать операторы слияния (см. Примеры ниже и попытайтесь выяснить причину этого сами). Теперь мы собираемся создать представление, которое имитирует таблицу ClientAnswers
, определенную выше:
create view dbo.ClientAnswers
with schemabinding
as
select
ClientID,
DataItemID,
ISNULL(IntValue,0) as IntValue,
Comment
from
dbo.ClientAnswerHistories
where
Deleted = 0 and
ValidTo is null
go
create unique clustered index PK_ClientAnswers on dbo.ClientAnswers (ClientID,DataItemID)
go
И у нас есть ограничение PK, которое мы изначально хотели. Мы также использовали ISNULL
для восстановления not null
-ности столбца IntValue
(хотя проверочные ограничения уже гарантируют это, SQL Server не может получить эту информацию). Если мы работаем с ORM, мы позволяем ему нацеливаться на ClientAnswers
, и история создается автоматически. Далее у нас может быть функция, которая позволяет нам оглянуться назад во времени:
create function dbo.ClientAnswers_At (
@At datetime2
)
returns table
with schemabinding
as
return (
select
ClientID,
DataItemID,
ISNULL(IntValue,0) as IntValue,
Comment
from
dbo.ClientAnswerHistories
where
Deleted = 0 and
(ValidFrom is null or ValidFrom <= @At) and
(ValidTo is null or ValidTo > @At)
)
go
И, наконец, нам нужны триггеры на ClientAnswers
, которые строят эту историю. Нам нужно использовать операторы слияния, поскольку нам нужно одновременно вставлять новые строки и обновлять предыдущую «действительную» строку, чтобы завершить дату ее новым значением ValidTo.
create trigger T_ClientAnswers_I
on dbo.ClientAnswers
instead of insert
as
set nocount on
;with Dup as (
select i.ClientID,i.DataItemID,i.IntValue,i.Comment,CASE WHEN cah.ClientID is not null THEN 1 ELSE 0 END as PrevDeleted,t.Dupl
from
inserted i
left join
dbo.ClientAnswerHistories cah
on
i.ClientID = cah.ClientID and
i.DataItemID = cah.DataItemID and
cah.ValidTo is null and
cah.Deleted = 1
cross join
(select 0 union all select 1) t(Dupl)
)
merge into dbo.ClientAnswerHistories cah
using Dup on cah.ClientID = Dup.ClientID and cah.DataItemID = Dup.DataItemID and cah.ValidTo is null and Dup.Dupl = 0 and Dup.PrevDeleted = 1
when matched then update set ValidTo = SYSDATETIME()
when not matched and Dup.Dupl=1 then insert (ClientID,DataItemID,IntValue,Comment,Deleted,ValidFrom)
values (Dup.ClientID,Dup.DataItemID,Dup.IntValue,Dup.Comment,0,CASE WHEN Dup.PrevDeleted=1 THEN SYSDATETIME() END);
go
create trigger T_ClientAnswers_U
on dbo.ClientAnswers
instead of update
as
set nocount on
;with Dup as (
select i.ClientID,i.DataItemID,i.IntValue,i.Comment,t.Dupl
from
inserted i
cross join
(select 0 union all select 1) t(Dupl)
)
merge into dbo.ClientAnswerHistories cah
using Dup on cah.ClientID = Dup.ClientID and cah.DataItemID = Dup.DataItemID and cah.ValidTo is null and Dup.Dupl = 0
when matched then update set ValidTo = SYSDATETIME()
when not matched then insert (ClientID,DataItemID,IntValue,Comment,Deleted,ValidFrom)
values (Dup.ClientID,Dup.DataItemID,Dup.IntValue,Dup.Comment,0,SYSDATETIME());
go
create trigger T_ClientAnswers_D
on dbo.ClientAnswers
instead of delete
as
set nocount on
;with Dup as (
select d.ClientID,d.DataItemID,t.Dupl
from
deleted d
cross join
(select 0 union all select 1) t(Dupl)
)
merge into dbo.ClientAnswerHistories cah
using Dup on cah.ClientID = Dup.ClientID and cah.DataItemID = Dup.DataItemID and cah.ValidTo is null and Dup.Dupl = 0
when matched then update set ValidTo = SYSDATETIME()
when not matched then insert (ClientID,DataItemID,Deleted,ValidFrom)
values (Dup.ClientID,Dup.DataItemID,1,SYSDATETIME());
go
Очевидно, я мог бы создать более простую таблицу (не таблицу соединений), но это мой стандартный пример перехода (хотя мне потребовалось некоторое время, чтобы восстановить его - я на некоторое время забыл операторы set nocount on
) , Но сила здесь в том, что базовая таблица ClientAnswerHistories
не может хранить диапазоны времени перекрытия для тех же значений ClientID
и DataItemID
.
Все становится сложнее, когда вам нужно иметь дело с временными внешними ключами.
Конечно, если вы не хотите никаких реальных пробелов, вы можете удалить столбец Deleted
(и связанные с ним проверки), сделать столбцы not null
действительно not null
, изменить триггер insert
, чтобы сделать просто вставьте и заставьте триггер delete
выдавать ошибку.