Aiven Blog

Exploring how PostgreSQL 18 conquered time with temporal constraints

Alexander Fridriksson

|RSS Feed

Alexander does product marketing for databases at Aiven. Throughout his career he's worked across marketing, sales, analytics, data engineering and more.

Do you like working with time in your code?

If yes, you’re likely one of the lucky ones who are blissfully unaware of how deep the rabbit hole goes.

If you don’t like it, I have good news for you! Postgres can make working with time more enjoyable!

The newly released temporal constraints let you easily maintain referential integrity across temporal relationships.

That might seem simple, but it's kind of a big deal. Let’s explore it through an example.

As always, you can create a free Aiven for PostgreSQL instance with the Aiven CLI to try it out for yourself.

Loading code...

Making a reservation at King Crab

For our example, let’s imagine you are building the booking system for a restaurant called King Crab.

Before we create our tables, we’ll need to add the btree_gist extension, as temporal constraints rely on GiST (Generalized Search Tree) indexes rather than traditional B-tree indexes to handle ranges.

Loading code...

While B-tree indexes are great for searching across one-dimensional data (table_id and created_at), GiST indexes allow us to search across multidimensional data (table_id and available_period).

This will make more sense once we create our tables.

Creating our tables

Our booking system needs two tables:

  • restaurant_capacity, which lists all available tables and their available times.
  • bookings, which have the actual customer bookings.

Create restaurant_capacity

Loading code...

To understand what is happening here, let’s break it down into three parts:

First, for the available_period we’re using the tstzrange data type, which represents a range of timestamps in one row. This makes it multi-dimensional and different from the one-dimensional created_at timestamp, which has one row per timestamp.

Secondly, we’re making a composite primary key based on table_id and available_period. Because Postgres indexes primary keys by default, this means we’re combining a one-dimensional field with a multi-dimensional field in the index. This is why we needed the btree_gist extension.

Lastly, the important addition of WITHOUT OVERLAPS ensures that for each table, available_period cannot overlap.

Remember when I said that this seems simple, but it's kind of a big deal?

Well, this simple combination of tstzrange and WITHOUT OVERLAPS just saved you from having to write a ton of application code with plenty of edge cases and tests to cover them.

Now, regardless of which programming language or library you use, you can be sure that it's impossible to have overlapping periods, because the database won’t allow it.

Create bookings

Loading code...

While some might be opposed to putting business logic like this in the database, there are clear advantages. Especially how easy it is to maintain referential integrity across temporal relationships, by just adding a PERIOD clause to your foreign key.

This constraint ensures that the time period of the booking (booked_period) must be completely contained by the referenced capacity period (available_period). The reference must have a referent for its entire duration.

It's not all sunshine and rainbows though, there are some limitations. The most important being that for the ON DELETE and ON UPDATE clauses, the RESTRICT, CASCADE, SET NULL, and SET DEFAULT actions are not supported for temporal foreign keys.

Now that we’ve created our tables, let's see how it works in practice.

Inserting data to see how it works

To make it simple, let’s assume our King Crab restaurant has two tables and three availability periods: the entire evening, the early shift and the late shift.

Insert restaurant_capacity

Loading code...

Notice that table 2 has a break between 20:00 and 20:30, where the staff is changing shifts. You can insert as many time periods as you like as long as they don’t overlap, but for our purposes, we are happy with these three.

Insert bookings

Let’s also insert three bookings: two successful and one that fails.

Loading code...

For our failed query we get this error message:

Loading code...

This confirms that if you attempted to book table 2 during the gap, such as from 20:00 to 20:45, the temporal foreign key constraint would reject the insert. The key thing to note again is that this eliminates the need for complex application logic or custom triggers to enforce this temporal integrity.

Efficiently querying temporal data

Temporal constraints also make powerful queries easier by using specialized operators and functions to query based on time ranges.

Using the containment operator

The containment operator (@>) checks if a range contains a specific point in time. This is ideal for performing efficient point-in-time lookups because it is index-supported.

This query finds all reservations active at exactly 6:00 PM UTC on 2025-11-20:

Loading code...
booking_idcustomer_namebooked_period
1J. Smith["2025-11-20 18:00:00+00","2025-11-20 19:30:00+00")
2A. Miller["2025-11-20 17:00:00+00","2025-11-20 18:30:00+00")

Using range boundaries

The lower() and upper() functions allow you to extract the start and end points of any range, such as this query, which finds the exact start and end time for all capacity periods:

Loading code...
table_idstart_timeend_time
12025-11-20 17:00:00.000000 +00:002025-11-20 23:00:00.000000 +00:00
22025-11-20 17:00:00.000000 +00:002025-11-20 20:00:00.000000 +00:00
22025-11-20 20:30:00.000000 +00:002025-11-20 23:00:00.000000 +00:00

Summary

Postgres 18's temporal constraints using WITHOUT OVERLAPS for primary keys and PERIOD for foreign keys helps you conquer the annoyances of working with time by bringing temporal data integrity directly into your schema definition.

This makes it easier to build robust applications by enforcing temporal rules consistently, removing the headache of writing complex application code. You can now maintain complete historical information and complex relationships right where they belong: the database.

Postgres 18 comes with a lot more exciting features, and you can leverage these exciting new features and explore what else Postgres 18 has to offer with Aiven today.


Stay updated with Aiven

Subscribe for the latest news and insights on open source, Aiven offerings, and more.

Related resources