I'm designing a bitemporal database, where we have 1:N relations between bitemporal tables (we also have M:N relations, but they're just modeled with a connector table and two 1:N relations, so I consider them a special case of an 1:N relation).
To illustrate things, let's consider a simple case with two tables:
|===============| |==================|
| tblOrder | | tblOrderItem |
|============== | |==================|
| - OrderId | | - OrderItemId |
| - OrderNumber | | - FK_OrderId |
|===============| | - Amount |
|==================|
FK_OrderId
is a foreign key to tblOrder
.
To make this database model bitemporal, I've come up with the following design:
|===============| |==================| |====================|
| tblOrder | | tblOrderItem | | tblVersions |
|============== | |==================| |====================|
| - Id | | - Id | | - VersionId |
| - OrderId | | - OrderItemId | | - VersionDate |
| - OrderNumber | | - FK_OrderId | |====================|
| - VersionId | | - Amount |
| - IsDeleted | | - VersionId |
| - StartDate | | - IsDeleted |
| - EndDate | | - StartDate |
|===============| | - EndDate |
|==================|
VersionId
columns are foreign keys to the tblVersions
table. For every change in the database, an entry in the tblVersions
table is created. The current state of the data is then just the sum of all versions. This makes it possible to reconstruct previous states of the database (via a WHERE VersionDate < ...
clause). This is the transaction time dimension of the bitemporality.tblVersions table could also be avoided if we're just including the
VersionDate` column into the two data tables.StartDate
and EndDate
columns are the valid time dimensionality of the bitemporality. Yes, EndDate
is kind of redundant, we could model the tables with just StartTime
.Id
columns of the two tables are the new primary keys. Because we have multiple rows for the same entity (multiple versions, multiple date ranges in valid time), the ID of the entity cannot be the primary key of the table. The columns OrderId
and OrderItemId
are the IDs of the entity, but not anymore the primary key of the table. Instead of creating the new primary keys Id
, we could also have defined the primary key as (OrderId, VersionId, StartDate)
.IsDeleted = 1
. All other entries in the table (the inserts and updates) have IsDeleted = 0
.FK_OrderId
of tblOrderitem
references the column OrderId
of tblOrder
. This is not anymore a real foreign key (in the sense of a database constraint), since OrderId
is not anymore a primary key. But it still tells us which OrderItems are part of a certain Order.This seems to work well, we have created the necessary CRUD queries and are able to read and write bitemporal data.
What kind of constraints do I need for that to work consistently?
I'm not interested in how to implement the constraints (whether to implement them as database constraints like FOREIGN KEY
s or UNIQUE
constraints, or TRIGGER
s, or CHECK
s, whatever). I just need to know what types of constraints I need.
I figured out a bunch of constraints, which I'm gonna post as an answer. But maybe there are more?
(I'm using the abbreviation PIVT = 'point in valid time'. This denotes a certain point in time on the valid time dimension)
Here are the constraints I already thought of:
FK_VersionId
: Obviously, we need standard foreign key constraints on the VersionId
columns.(OrderId, VersionId, StartDate)
must be unique (and the same for tblOrderItem
).StartDate
and EndDate
do not overlap, and StartDate
is always earlier than EndDate
.IsDeleted = 1
, and if there is such a row, there must not be any version of the entity after that row.FK_OrderId
is set to a value that identifies an Order entity which exists at the given PIVT, which has been inserted in an earlier version and which has not been deleted (by setting IsDeleted = 1
).