3

In PostgreSQL, I want to make a computed column, where end_datetime = start_datetime + minute_duration adding timestamps

I keep getting error, how can I fix?

ERROR: generation expression is not immutable SQL state: 42P17

Tried two options below:

CREATE TABLE appt ( 
  appt_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  minute_duration INTEGER NOT NULL,
  start_datetime TIMESTAMPTZ NOT NULL,
  end_datetime TIMESTAMPTZ GENERATED ALWAYS AS (start_datetime + (minute_duration || ' minutes')::INTERVAL) STORED
 );


 CREATE TABLE appt ( 
  appt_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  minute_duration INTEGER NOT NULL,
  start_datetime TIMESTAMPTZ NOT NULL,
  end_datetime TIMESTAMPTZ GENERATED ALWAYS AS (start_datetime + make_interval(mins => minute_duration)) STORED
);

The only other option would be trigger or computed on minute_duration, but trying to refrain from this method in follow up question PostgreSQL Meeting Table, Computed Column on TimeDuration or Trigger on EndDatetime

New contributor
mattsmith5 is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
3
  • Can you please explain why you think you need to store a value that can be easily computed during run time?
    – mustaccio
    Commented 14 hours ago
  • @mustaccio indexing is the obvious example, but denormalization is often done for it's own sake I reckon. Commented 14 hours ago
  • Turns out to be a x-posted duplicate of the earlier question on SO stackoverflow.com/q/79729171/939860 - where I answered before any of the additional answers here. (And I would still go with that answer.) To the OP: please don't post the same question across multiple SE sites, especially not without disclosing. Commented 2 hours ago

3 Answers 3

3

You could do something like this - but it involves "cheating".

First, you create a function as follows (all the code below is available on the fiddle here):

CREATE OR REPLACE FUNCTION Date_Add(tz1 TIMESTAMPTZ, mindur INT)
RETURNS TIMESTAMPTZ
AS 
$$ 
  SELECT $1 + ($2 || ' MINUTES')::INTERVAL
$$
LANGUAGE SQL IMMUTABLE;

There is a risk associated with this - you could end up with errors with edge cases where you go over day/month/year boundaries. If you always work in the same timezone, you should be fine! You have been warned! :-)

So now, we come to your table.

CREATE TABLE apt 
( 
  apt_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  mindur INTEGER NOT NULL,
  sd     TIMESTAMPTZ NOT NULL,
  ed     TIMESTAMPTZ GENERATED ALWAYS AS (Date_Add(sd, mindur)) STORED
);

Not sure why you want a UUID here - best to use UUID v7 (PG 18) - or you could use a ULID extension (similar to v7) and - see here, plus there are v7 extensions available for earlier versions of PostgreSQL.

And populate:

INSERT INTO apt (mindur, sd) VALUES 
(120,  '2025-08-08 14:00'::TIMESTAMPTZ),
(1440, '2025-08-08 15:00'::TIMESTAMPTZ);  -- 1440 = 24hr

and to check - SELECT * FROM apt;

Result:

mindur              sd                  ed
   120  2025-08-08 14:00:00+00  2025-08-08 16:00:00+00
  1440  2025-08-08 15:00:00+00  2025-08-09 15:00:00+00

Et voilà!

3
  • I am might be using a different time zone for different global customers, but this is an interesting solution, thank you for all this explanation!
    – mattsmith5
    Commented 9 hours ago
  • 1) I might just use minute_duration as the computed column then, and leave everything else as TIMESTAMPTZ , 2) or add a trigger insert/update for peer_appt_end_Datetime, would these two solutions work without any issues?
    – mattsmith5
    Commented 9 hours ago
  • i posted another question here, dba.stackexchange.com/questions/347447/…
    – mattsmith5
    Commented 8 hours ago
4

@PeterVandivier rightly points out that the immutability comes from TIMESTAMPTZ, but it also comes from the interval. In general, intervals are treated as mutable (it appears, even with a TIMESTAMP without time zone).

The problem comes from the fact that some intervals are time-zone dependent, in particular days: you get days of 23 and 25 hours around the Daylight Saving Time changes.

Here, you're working with minutes, so you can work around this by making everything immutable:

CREATE TABLE appt ( 
  appt_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  minute_duration INTEGER NOT NULL,
  start_datetime TIMESTAMPTZ NOT NULL,
  end_datetime TIMESTAMPTZ GENERATED ALWAYS AS
      ((start_datetime AT TIME ZONE 'UTC' + (minute_duration * '1 minute'::INTERVAL)) AT TIME ZONE 'UTC') STORED
); 

To explain this a bit more closely:

(start_datetime AT TIME ZONE 'UTC'
   + (minute_duration * '1 minute'::INTERVAL)
) AT TIME ZONE 'UTC'
  • start_datetime AT TIME ZONE 'UTC' converts your TIMESTAMPTZ into its UTC TIMESTAMP equivalent in an immutable way.

  • minute_duration * '1 minute'::INTERVAL multiplies the number of minutes (immutable) by a 1-minute interval (also immutable). This trick is necessary, because the (minute_duration || ' minutes')::INTERVAL doesn't seem to detect that the string will necessarily be a number and the word ' minutes', making it immutable (an arbitrary string as as '1 day 1 hour' would not necessarily be immutable).

  • Convert it back to a TIMESTAMPTZ with AT TIME ZONE 'UTC'.

Note that the end result will not necessarily be in UTC, but in whatever time zone you've chosen with your session (as you would expect from any TIMESTAMPTZ column).

Indeed, PostgreSQL's TIMESTAMP WITH TIME ZONE doesn't actually store any time zone information: it just displays and makes calculations with the time zone you're currently using.

4
  • 1) I might just use minute_duration as the computed column then, and leave everything else as TIMESTAMPTZ , 2) or add a trigger insert/update for peer_appt_end_Datetime, would these two solutions work without any issues?
    – mattsmith5
    Commented 9 hours ago
  • I am might be using a different time zone for different global customers, but this is an interesting solution, thank you for all this explanation!
    – mattsmith5
    Commented 8 hours ago
  • 1
    @mattsmith5 That's the thing, timezones for different customers don't matter. The "WITH TIME ZONE" is a bit misleading, but the timezone at which you store the timestamptz isn't actually stored in PostgreSQL: it's all UTC under the hood and displayed with the timezone set by the PostgreSQL client. If you want different something to handle different timezones for different customers, you must have an extra column for the timezone (I'd suggest the standard name "Continent/City")
    – Bruno
    Commented 7 hours ago
  • @mattsmith5 You'll always have the same issues, regardless which of the columns is computed and whether you compute it with a trigger or not. Note that timestamp ≈ Datetime and timestamptz ≈ Instant. Neither of them store timezone information (as a ZonedDatetime would, or like luxon.DateTime does).
    – Bergi
    Commented 4 hours ago
3

The volatility is coming from the start_datetime column. You must declare a timezone for your timestamp in some static way.

For example, the following expression is valid:

start_datetime at time zone 'utc' + make_interval(mins => minute_duration)

See also complete fiddle examples.

Further discussion on this thread.

You may also consider

  1. declaring end_datetime explicitly and enforcing the relationship with a check constraint
  end_datetime   timestamptz not null
    check (
      end_datetime = (start_datetime + duration)
    ) 
  1. making duration your computed column
  duration       interval
    generated always
    as (end_datetime - start_datetime)
    stored,

Sidebar: unfortunately, extraction of time zone data from timestamptz is not immutable (further reading) so you have to trust that a UTC timestamp is in fact present in the start_datetime column. IIRC timestamptz is always stored as UTC but I expect I may be missing possible failure cases for these examples.

3
  • That mailing list thread is a great find
    – Bruno
    Commented 12 hours ago
  • Thank you, this is very helpful explanation ! 1) I might just use minute_duration as the computed column then, and leave everything else as TIMESTAMPTZ , 2) or add a trigger insert/update for peer_appt_end_Datetime, would these two solutions work without any issues?
    – mattsmith5
    Commented 9 hours ago
  • The phrase "extraction of time zone data from timestamptz is not immutable" makes it sound like there is some time zone data in there - but there isn't any at all. The whole point is that due to the lack of time zone data, doing date math on timestamptz has to assume the timezone from the session context, which makes it volatile.
    – Bergi
    Commented 4 hours ago

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.