Data Modeling

A temporal uniqueness constraint says that two records may share an identifier only when their periods of validity do not overlap within the relevant business scope. It is the rule you need when an identifier, code, badge, room, contract number, price list entry, or assignment can be reused over time but must not point to two active meanings at once.

What temporal uniqueness means

Ordinary uniqueness is timeless. If customer_id must be unique, then two rows with the same customer_id are invalid, regardless of when they were true.

Temporal uniqueness adds a time interval to the rule. It allows the same identifier to appear in multiple rows when each row describes a different period. The identifier is unique at any point in time, not necessarily unique across the entire table.

The practical question is: can two records with the same identifier both be true for the same moment? If the answer is no, you need a temporal uniqueness constraint.

This is common in historical and operational models:

  • An employee badge number can be reassigned after a worker leaves.
  • A hotel room can be assigned to only one reservation for the same night.
  • A product code can refer to one active product definition at a time.
  • A customer can have one active account manager during a given period.
  • A pricing rule can be superseded by a later pricing rule with the same natural key.

The important distinction is that the time period is part of the meaning of the row, not just audit metadata about when the row was loaded.

A small hypothetical dataset

Consider a hypothetical company that reuses physical badge numbers. A badge number identifies who may enter the building during a period. The company wants to keep history, so it stores one row per badge assignment.

The intended rule is simple: the same badge number cannot be assigned to two people during overlapping periods at the same site. Reuse is allowed after the previous assignment ends.

For this example, assume each row has these attributes:

  • site_id: the building where the badge is valid.
  • badge_number: the identifier printed on the badge.
  • person_id: the person assigned to the badge.
  • valid_from: the first instant when the assignment is true.
  • valid_to: the first instant when the assignment is no longer true.

That final wording matters. In this model, valid_from is inclusive and valid_to is exclusive. An assignment from January 1 to February 1 is active on January 31, but not active at the instant February 1 begins.

Example records and counterexamples

The following records show safe reuse and unsafe reuse. The examples are deliberately small so the rule is visible without engine-specific syntax.

Rows A and B are valid together because the first assignment ends exactly when the second begins. Rows C and D are invalid together because they overlap for part of February. Row E shows that scope matters: the same badge number may be allowed at another site if site_id is part of the uniqueness scope.

Row site_id badge_number person_id valid_from valid_to Diagnosis
A NYC 104 17 2026-01-01 2026-02-01 Valid starting assignment
B NYC 104 29 2026-02-01 2026-03-01 Valid reuse; adjacent to Row A under half-open intervals
C NYC 205 31 2026-02-01 2026-03-01 Valid by itself
D NYC 205 44 2026-02-15 2026-04-01 Invalid with Row C; same site and badge overlap
E LON 205 58 2026-02-15 2026-04-01 Potentially valid if site_id is part of the scope

The overlap rule in plain English

Two time intervals overlap when each interval starts before the other interval ends. In plain English: if both records would be considered active at the same moment, they overlap.

Using half-open intervals, the overlap test is:

Record 1 starts before Record 2 ends, and Record 2 starts before Record 1 ends.

If that is true, the two periods overlap. If the identifier and scope are also the same, the pair violates the temporal uniqueness constraint.

The temporal uniqueness rule for the badge example can be stated as:

For any two different badge assignment rows, if they have the same site_id and badge_number, their validity periods must not overlap.

This formulation is more precise than saying the badge number is unique. Badge number alone is not unique across history. Badge number plus time is constrained so that it is unique at each point in time.

Rule of thumb

If two rows with the same scoped identifier would both answer yes to the question, "is this identifier active at this moment?", they violate temporal uniqueness.

Why ordinary unique keys are not enough

A normal candidate key identifies a row by values that must not repeat. For example, a table might declare that order_id is unique, or that tenant_id plus external_user_id is unique.

Temporal uniqueness is different because the invalid condition is not simple equality across columns. Two rows can have the same identifier and still be valid if their periods do not overlap. Two rows can have different valid_from values and still be invalid if the intervals overlap.

For example, these two rows are not duplicates in the ordinary sense:

  • Badge 104 assigned to Person 17 from February 1 to March 1.
  • Badge 104 assigned to Person 29 from February 15 to April 1.

They have different people and different start dates. But they are both true for February 15 through February 28, so the badge number has two meanings during that period. That is the modeling error.

A temporal uniqueness constraint protects the business meaning of an identifier over time. It does not merely remove duplicate rows.

Define the scope before the time rule

Most mistakes with temporal uniqueness come from choosing the wrong scope. The scope is the set of attributes within which the identifier must be unique over time.

In the badge example, badge_number alone may be too broad if different buildings reuse the same numbering system. The correct scope might be site_id plus badge_number. In a multi-tenant application, the scope might be tenant_id plus external_id. In a product catalog, it might be market_id plus sku plus sales_channel.

Ask three questions before writing the rule:

  1. What identifier is being reused? Name the business identifier directly, such as badge_number, room_number, plan_code, or account_manager_assignment.
  2. Where must it be unique? Decide whether uniqueness applies globally, per tenant, per site, per product, per market, or per relationship type.
  3. What does the time period mean? Confirm whether the interval describes business validity, contract effectivity, operational assignment, reporting period, or load history.

If the scope is too narrow, invalid overlaps slip through. If the scope is too broad, valid reuse is rejected.

Modeling choice Allows Rejects Common failure if wrong
badge_number is globally unique over time No reuse anywhere during overlap Same badge active in any two places at once Too strict if each site has its own badge numbering
site_id plus badge_number is unique over time Same badge number at different sites Same badge active twice at one site Too loose if badges are physically portable across sites
tenant_id plus external_id is unique over time Different tenants using the same external ID Same tenant mapping one external ID to two entities at once Cross-tenant contamination if tenant_id is omitted
product_id plus price_type is unique over time Different price types for one product Two active standard prices for the same product False conflicts if price type is not included

Choose boundary semantics once

A temporal uniqueness constraint needs a clear decision about interval boundaries. Without that decision, teams will disagree about whether adjacent records overlap.

A practical default is the half-open interval: valid_from is inclusive, valid_to is exclusive. That means one record can end at the same instant the next record begins without overlap.

For example:

  • Assignment A: January 1 through February 1.
  • Assignment B: February 1 through March 1.

With half-open intervals, these are adjacent, not overlapping. Assignment A is no longer active when Assignment B becomes active.

Closed intervals, where both start and end are inclusive, are harder to use for adjacent operational periods because the boundary instant belongs to both records. Date-only models can also create ambiguity if one team treats valid_to as the last active date while another treats it as the first inactive date.

Time zones should be handled deliberately. If source systems operate in different local times, define whether validity is stored in local business time, a standard time zone, or both. The goal is not to make every model global from day one; it is to prevent hidden conversions from changing overlap results.

Practical checkpoint

Document whether valid_to means "last active moment" or "first inactive moment." Many overlap bugs are boundary bugs, not data entry bugs.

Handle open-ended records explicitly

Many temporal tables use an open-ended valid_to value to mean the record is current until further notice. That is useful, but it must be part of the rule.

If valid_to is missing or marked as open-ended, treat the interval as continuing into the future for overlap purposes. A new record for the same scoped identifier cannot simply be added while the old current record remains open. The previous record must be ended at the same boundary where the new record begins, or the model must define a different business meaning.

A safe transition looks like this:

  • Old row: Badge 104 assigned to Person 17 from February 1 to April 15.
  • New row: Badge 104 assigned to Person 29 from April 15 onward.

An unsafe transition leaves both rows open or starts the new row before the old row ends.

Open-ended records are not a special exemption from temporal uniqueness. They are simply intervals whose end is not yet known.

Separate current state from historical truth

Temporal uniqueness becomes messy when one table tries to serve two different purposes without saying so: current operational lookup and historical record.

A current-state table usually needs ordinary uniqueness. There should be one current row for a scoped identifier. A history table needs temporal uniqueness, because it stores multiple rows for the same identifier across time.

Both models can be valid, but they answer different questions:

  • Current-state question: who has badge 104 right now?
  • Historical question: who had badge 104 on March 10?
  • Audit question: when did the data warehouse learn about this assignment?

Do not mix business validity time with load time. A row loaded today may describe a business assignment that began last month. Temporal uniqueness should usually be based on the business interval, not the ingestion timestamp, unless the table is explicitly modeling system-versioned history.

Modeling warning

Do not use load timestamps as a substitute for business validity unless the table is explicitly about when the system learned something.

How to diagnose temporal uniqueness violations

When a report, join, or dashboard shows duplicated facts, temporal uniqueness is often one of the hidden causes. The failure pattern is simple: a fact row joins to more than one supposedly valid dimension or assignment row for the same point in time.

To diagnose the issue, look for pairs of rows that share the scoped identifier and overlap in business time. Then classify the problem before changing data:

  • True business conflict: the source system allowed two active assignments that should not both exist.
  • Wrong scope: the rows overlap only because the model omitted tenant, site, channel, or relationship type.
  • Boundary mismatch: one process treats valid_to as inclusive while another treats it as exclusive.
  • Late correction: a backdated change created a historical overlap that must be reconciled.
  • Different time concepts: load time, event time, and business effective time were mixed in one comparison.

The fix depends on the classification. Some cases require source correction. Some require a better key. Some require a documented boundary convention.

Operator checklist for defining the constraint

Use this checklist when adding temporal uniqueness to a model or reviewing an existing historical table.

  1. Name the identifier. Be specific about the value being reused, such as badge_number or plan_code.
  2. Name the scope. Decide whether the rule applies globally or within tenant, site, market, product, channel, or another context.
  3. Name the time interval. Use business-valid time when the question is about what was true in the business.
  4. Pick boundary semantics. Prefer one documented convention, commonly inclusive start and exclusive end.
  5. Define open-ended rows. Decide how current-until-further-notice records participate in overlap checks.
  6. State the forbidden condition. Two different rows with the same scoped identifier must not have overlapping validity periods.
  7. Create counterexamples. Write at least one valid adjacent reuse, one invalid overlap, and one valid reuse in a different scope.
  8. Decide the response to violations. Mark whether violations should reject data, quarantine records, warn analysts, or require manual reconciliation.

The point is not to make the model elaborate. The point is to make the allowed reuse of identifiers unambiguous.

Key takeaways

  • Temporal uniqueness constraints allow identifier reuse across history while preventing two active meanings at the same time.
  • The core rule is no overlapping validity periods for the same scoped identifier.
  • Scope matters as much as time: include tenant, site, channel, market, or relationship type when the business rule requires it.
  • Boundary semantics must be documented. Inclusive start and exclusive end usually make adjacent records easier to manage.
  • Open-ended current records still participate in overlap checks; they are not exempt from the constraint.
  • Temporal uniqueness is a modeling rule about business validity, not a generic duplicate-removal rule.

Next step

Take one historical table in your environment and write three examples: a valid adjacent reuse, an invalid overlap, and a valid same-identifier record in a different scope. If you cannot classify all three, the temporal uniqueness rule is not yet precise enough.

Recommended next reads