Question

Background:

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        |
                         |==================|

Explanations:

  • The 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.
  • The tblVersions table could also be avoided if we're just including theVersionDate` column into the two data tables.
  • The 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.
  • The 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).
  • If an entity is deleted, we just create a new version entry, as well as an entry in the entity table with IsDeleted = 1. All other entries in the table (the inserts and updates) have IsDeleted = 0.
  • The column 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.

Question:

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 KEYs or UNIQUE constraints, or TRIGGERs, or CHECKs, 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?

Was it helpful?

Solution

(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.
  • Bitemporal uniqueness: Also, the combination (OrderId, VersionId, StartDate) must be unique (and the same for tblOrderItem).
  • Valid time serialization: We need to check that for each entity and each version, there are no overlaps in valid time, i.e. the columns StartDate and EndDate do not overlap, and StartDate is always earlier than EndDate.
  • Deletion integrity: We need to make sure that for each entity and each PIVT, there is maximal one row with IsDeleted = 1, and if there is such a row, there must not be any version of the entity after that row.
  • Referential integrity: We need to check that for each OrderItem entity, each version and each PIVT, the value of 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).
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top