Ваши инстинкты служат вам хорошо. Не используйте дату окончания. Это добавляет усложнение и источник возможных аномальных данных. Возьмите следующие последовательные записи:
ID <attr> StartDate EndDate
1 ... Jan 1 Jan 20
1 ... Jan 20 Jan 22
1 ... Feb 1 Jul 30
Там было изменение состояния записано на 1 января, который был в силе до следующего изменения состояния на 20 января Теперь у нас есть проблемы. Согласно EndDate этой версии, 22 января произошло другое изменение состояния, но следующая версия началась 1 февраля.
Это создает пробел в потоке времени, и мы не имеем указаний, где лежит проблема. Является ли EndDate от 22 января неправильным? Является ли StartDate 1 февраля неправильным? Или есть недостающая версия, которая соединяет два конца разрыва? Невозможно сказать.
ID <attr> StartDate EndDate
1 ... Jan 1 Jan 20
1 ... Jan 20 Feb 20
1 ... Feb 1 Jul 30
Теперь существует перекрытие состояний. Второе государство якобы продолжалось до 20 февраля, но третье государство заявляет, что оно началось 1 февраля. Но начало одного состояния логически означает конец предыдущего состояния. Опять же, мы понятия не имеем (просто посмотрев на данные), дата которых неверна.
Зная, что начало одного состояния также указывает конец предыдущего состояния, выглядит, что происходит, когда мы просто удаляем столбец EndDate.
ID <attr> EffDate
1 ... Jan 1
1 ... Jan 20
1 ... Feb 1
Теперь пробелы и перекрытия невозможны. Каждое состояние начинается с даты вступления в силу и заканчивается, когда начинается следующее состояние. Поскольку поле EffDate является частью PK, никакая запись не может иметь одинаковое значение EffDate для заданного значения ID.
Эта конструкция не используется с таблицей основных сущностей. Он реализуется как специальная форма второй нормальной формы, что я могу изменить в нормальной форме (vnf).
В таблице Employee будут отображаться поля, которые не меняются с течением времени, а некоторые из них выполняются. У вас могут также быть поля, которые меняются, но вы не хотите отслеживать эти изменения.
create table Employees(
ID int auto_generated primary key,
Hired date not null,
FName varchar not null,
LName varchar not null,
Sex enum -- M or F
BDay date,
Position enum not null,
PayRate currency,
DeptID int references Depts(ID)
);
Если мы хотим отслеживать изменения данных, мы можем добавить эффективное поле даты. Однако учтите, что данные, такие как дата найма и дата рождения, не будут изменяться с одной версии на другую. Таким образом, они зависят только от поля ID. Данные, которые меняются (Position, PayRate, DeptID), зависят от идентификатора и. Таблица больше не находится в 2nf.
Так нормировать:
create table Employees(
ID int auto_generated primary key,
Hired date not null,
FName varchar not null,
Sex enum -- M or F
BDay date
);
create table Employees_V(
ID int not null references Employees(ID),
EffDate date not null,
LName varchar not null,
Position enum not null,
PayRate currency,
DeptID int references Depts(ID),
constraint PK_Employees_V primary key(ID, EffDate)
);
Фамилию можно ожидать изменения в настоящее время, а затем, особенно среди женщин, работающих.
Одним из основных преимуществ этого метода является то, что внешние ключи не могут ссылаться на версии. Теперь все FK могут ссылаться на таблицу основных сущностей как обычно.
Запрос для получения «текущего» данные относительно прост:
select e.ID, e.Hired, e.FName, v.Lname, e.Sex, e.BDay, v.Position, v.PayRate, v.DeptID
from Employees e
join Employees)V v
on v.ID = e.ID
and v.EffDate =(
select Max(EffDate)
from Employees_V
where ID = v.ID
and EffDate <= GetDate())
where e.ID = 123;
Сравните запрашивая таблицу с начала/даты окончания.
select ID, Hired, FName, Lname, Sex, BDay, Position, PayRate, DeptID
from Employees
where ID = 123
and StartDate >= GetDate()
and EndDate < GetDate();
Предполагается, что значение EndDate для текущей версии является магическим значением, таким как 12/31/9999.
Этот второй запрос выглядит намного проще, чем первый. Даже если данные нормализованы, как показано выше, есть соединение, но без подзапроса. Похоже, он будет выполняться намного быстрее.
Я использовал эту технику около 8 лет, и мне никогда не приходилось ее менять из-за проблем с производительностью. Запрос vnf выполняется в в худшем случае менее чем на 10% медленнее, чем начальная/конечная версия. Таким образом, один минутный запрос займет около одной минуты 5 секунд. Однако при некоторых условиях запрос vnf будет выполняться быстрее.
Возьмите объекты, которые имеют много и много изменений (много тысяч версий). Запрос start/end выполняет сканирование индекса. Он начинается с самой ранней версии и должен проверять каждую версию последовательно, пока не найдет тот, у которого значение EndDate меньше заданной даты. Обычно это последняя версия. В запросе vnf подзапрос позволяет выполнять поиск индекса.
Так что не отвергайте этот дизайн, потому что считаете его медленным. Это не медленно. Особенно, если учесть, что для вставки новой версии требуется только одна инструкция INSERT. При работе с датами начала и окончания вставка новой версии требует UPDATE, а затем INSERT. Это два UPDATE и INSERT при вставке новой версии между двумя существующими версиями. Для удаления начальной/конечной версии требуется одно или два оператора UPDATE и один DELETE. Чтобы удалить версию vnf, просто удалите версию.
И если даты начала и окончания между версиями когда-либо выходят из строя, у вас есть пробел или совпадение, и удачи в поиске правильных значений.
Так что я возьму небольшой удар производительности, чтобы данные не могли выйти из строя и превратиться в аномальный. Это (vnf), как оказалось, действительно более простой дизайн.
Любая возможность использовать временную таблицу (https://msdn.microsoft.com/en-us/library/mt631669.aspx)? –
Этот вопрос, вероятно, лучше задать на [dba.se] (http://dba.stackexchange.com/). –
Спасибо всем за ваши ответы! – JackSparrow123