Temporal Collections - Bitemporal vs Unitemporal
Understanding Time-Based Versioning
Temporal design decisions become expensive to reverse. Choosing between unitemporal and bitemporal is therefore not just a modelling preference; it is an audit, compliance, and operational choice.
The short version is:
- Unitemporal tracks valid time only.
- Bitemporal tracks valid time and system time.
Valid time answers when a fact is true in the real world. System time answers when the database knew that fact. If those two timelines can diverge in your domain, bitemporal is usually the safer choice.
The examples in this article assume the llamaverse (v2.3.1+) is deployed. The llamaverse sample data is freely available from github.com/cleverllamas/llamaverse — see the llamaverse article for full setup instructions.
Time Dimensions
| Dimension | What it means | Typical question |
|---|---|---|
| Valid time | When a fact is true in the modelled world | Who owned this llama on 2025-06-15? |
| System time | When the database stored a given version | What did we believe on 2025-07-01? |
This distinction is what enables powerful audit queries such as: "What did we believe then about facts valid at that date?"
Visual: Time Through a Clever Llama Lens
The key difference is simple: how many clocks your model needs to answer the business questions.
Unitemporal: valid-time only
Bitemporal: valid time plus system time
Model choice flow
Required Indexing
Temporal axes depend on dateTime range indexes. You must configure those indexes before axis creation succeeds.
| Axis field | Expected type | Used by |
|---|---|---|
| valid-start | dateTime | Unitemporal and bitemporal |
| valid-end | dateTime | Unitemporal and bitemporal |
| system-start | dateTime | Bitemporal |
| system-end | dateTime | Bitemporal |
Create Axes and Collections
Create the valid-time axis
xquery version "1.0-ml";
import module namespace temporal = "http://marklogic.com/xdmp/temporal"
at "/MarkLogic/temporal.xqy";
(: Create a valid-time axis backed by dateTime range indexes. :)
temporal:axis-create(
"valid",
cts:element-reference(xs:QName("valid-start"), ("type=dateTime")),
cts:element-reference(xs:QName("valid-end"), ("type=dateTime"))
)
Create the system-time axis
xquery version "1.0-ml";
import module namespace temporal = "http://marklogic.com/xdmp/temporal"
at "/MarkLogic/temporal.xqy";
(: Create a system-time axis for bitemporal collections. :)
temporal:axis-create(
"system",
cts:element-reference(xs:QName("system-start"), ("type=dateTime")),
cts:element-reference(xs:QName("system-end"), ("type=dateTime"))
)
Create unitemporal and bitemporal collections
xquery version "1.0-ml";
import module namespace temporal = "http://marklogic.com/xdmp/temporal"
at "/MarkLogic/temporal.xqy";
(: Unitemporal collection: valid axis only. :)
temporal:collection-create("llama-ownership", "valid"),
(: Bitemporal collection: valid axis + system axis. :)
temporal:collection-create("llama-health-audit", "valid", "system")
Unitemporal: Operational Simplicity
Unitemporal collections are often enough when your primary concern is "state as valid on date X" and you do not need to preserve the full history of when the database learned each correction.
xquery version "1.0-ml";
import module namespace temporal = "http://marklogic.com/xdmp/temporal"
at "/MarkLogic/temporal.xqy";
(: Insert a unitemporal ownership record. :)
temporal:document-insert(
"llama-ownership",
"/cleverllamas/llamaverse/temporal/ownership/llama-001.xml",
<ownership>
<llama-id>llama-001</llama-id>
<owner>Alice</owner>
<valid-start>2025-01-01T00:00:00Z</valid-start>
<valid-end>9999-12-31T23:59:59Z</valid-end>
</ownership>,
(xdmp:permission("cleverllamas-llamaverse-reader", "read")),
("temporal", "ownership")
)
Unitemporal strengths:
- Simpler mental model.
- Lower storage and operational complexity.
- Straightforward historical-as-of queries on valid time.
Unitemporal limitations:
- Cannot answer "what did we know then" independently of valid time.
- Harder to satisfy strict audit reconstruction requirements.
Bitemporal: Strong Auditability
Bitemporal collections add a system-time axis managed by MarkLogic. You supply valid-time boundaries; MarkLogic records system-time boundaries as versions are inserted and superseded.
xquery version "1.0-ml";
import module namespace temporal = "http://marklogic.com/xdmp/temporal"
at "/MarkLogic/temporal.xqy";
(: Insert a bitemporal health record. MarkLogic manages system time values. :)
temporal:document-insert(
"llama-health-audit",
"/cleverllamas/llamaverse/temporal/health/llama-001.xml",
<health-assessment>
<llama-id>llama-001</llama-id>
<status>Healthy</status>
<assessed-by>Dr Woolfe</assessed-by>
<valid-start>2025-03-01T00:00:00Z</valid-start>
<valid-end>9999-12-31T23:59:59Z</valid-end>
</health-assessment>,
(xdmp:permission("cleverllamas-llamaverse-reader", "read")),
("temporal", "health")
)
Bitemporal strengths:
- Reconstructs both real-world and database-knowledge timelines.
- Supports compliance, investigation, and legal replay scenarios.
- Makes late-arriving corrections explicit rather than destructive.
Bitemporal trade-offs:
- More complex query design.
- More retained versions and storage overhead.
- Higher governance discipline required.
Querying As-Of State
The core as-of pattern is a period query on the valid axis.
xquery version "1.0-ml";
(: Query a temporal collection as of a valid-time point. :)
let $point := xs:dateTime("2025-06-15T00:00:00Z")
return cts:search(
fn:collection("llama-ownership"),
cts:period-range-query(
"valid",
"AL_CONTAINS",
cts:period($point, $point)
)
)
For bitemporal models, you typically combine valid-time criteria with system-time criteria to answer dual-timeline questions. Design these queries early in a project so your axis naming and index strategy match audit requirements.
Choosing the Right Model
| Requirement | Better fit |
|---|---|
| Business-state history only | Unitemporal |
| Regulatory replay of knowledge state | Bitemporal |
| Simple operational reporting | Unitemporal |
| Legal/compliance-grade lineage | Bitemporal |
A practical default is to start with unitemporal only when you are certain system-time reconstruction will not be required. If uncertainty is high, bitemporal is often cheaper than a later migration.
Common Mistakes
| Mistake | Impact | Better approach |
|---|---|---|
| Choosing unitemporal for strict audit domains | Missing evidence of knowledge timeline | Use bitemporal from the start |
| Treating system time as user-managed | Inconsistent and untrustworthy lineage | Let MarkLogic manage system time |
| Delaying index planning | Axis creation and query failures later | Define range indexes first |
| Designing queries before axis conventions | Fragile, hard-to-maintain code | Standardise axis and field names early |
Need Some Help?
Looking for more information on this subject or any other topic related to MarkLogic? Contact Us (info@cleverllamas.com) to find out how we can assist you with consulting or training!
- Time Dimensions
- Visual: Time Through a Clever Llama Lens
- Unitemporal: valid-time only
- Bitemporal: valid time plus system time
- Model choice flow
- Required Indexing
- Create Axes and Collections
- Create the valid-time axis
- Create the system-time axis
- Create unitemporal and bitemporal collections
- Unitemporal: Operational Simplicity
- Bitemporal: Strong Auditability
- Choosing the Right Model
- Common Mistakes