What is the best way to add a specified interval to a timestamp with time zone, if I don't want to do the calculation in the time zone of the server. This is particularly important around daylight savings transitions.
e.g.
consider the evening that we "spring forward". (Here in Toronto, I think it was 2016-03-13 at 2am).
If I take a time stamp:
2016-03-13 00:00:00-05
and add '1 day'
to it, in Canada/Eastern, I would expect to get 2016-03-14 00:00:00-04
-> 1 day later, but actually only 23 hours
But if I add 1 day to it in Saskatchewan (a place that doesn't use DST), I would want it to add 24 hours, so that I'd end up with
2016-03-13 01:00:00-04
.
If I have columns / variables
t1 timestamp with time zone;
t2 timestamp with time zone;
step interval;
zoneid text; --represents the time zone
I essentially want to say
t2 = t1 + step; --in a time zone of my choosing
Postgres documentation seems to indicate that timestamp with time zone is internally stored in UTC time, which seems to indicate that a timestamptz column has no reckoning of a time zone in it. The SQL standard indicates that datetime + interval operation should maintain the time zone of the first operand.
t2 = (t1 AT TIME ZONE zoneid + step) AT TIME ZONE zoneid;
doesn't seem to work because the first cast turns t1 into a timezone-less timestamp and thus can't reckon DST transitions
t2 = t1 + step;
doesn't seem to work as it does the operation in the time zone of my SQL server
set the postgres time zone before the operation and change it back after?
A better illustration:
CREATE TABLE timestamps (t1 timestamp with time zone, timelocation text);
SET Timezone 'America/Toronto';
INSERT INTO timestamps(t1, timelocation) VALUES('2016-03-13 00:00:00 America/Toronto', 'America/Toronto');
INSERT INTO timestamps(t1, timelocation) VALUES('2016-03-13 00:00:00 America/Regina', 'America/Regina');
SELECT t1, timelocation FROM timestamps; -- shows times formatted in Toronto time. OK
"2016-03-13 00:00:00-05";"America/Toronto"
"2016-03-13 01:00:00-05";"America/Regina"
SELECT t1 + '1 day', timelocation FROM timestamps; -- Toronto timestamp has advanced by 23 hours. OK. Regina time stamp has also advanced by 23 hours. NOT OK.
"2016-03-14 00:00:00-04";"America/Toronto"
"2016-03-14 01:00:00-04";"America/Regina"
How to get around this?
a) Cast the timestamptz to a timestamp tz in the appropriate time zone?
SELECT t1 AT TIME ZONE timelocation + '1 day', timelocation FROM timestamps; --OK. Though my results are timestamps without time zone now.
"2016-03-14 00:00:00";"America/Toronto"
"2016-03-14 00:00:00";"America/Regina"
SELECT t1 AT TIME ZONE timelocation + '4 hours', timelocation FROM timestamps; -- NOT OK. I want the Toronto time to be 5am
"2016-03-13 04:00:00";"America/Toronto"
"2016-03-13 04:00:00";"America/Regina"
b) Change timezone of postgres and proceed.
SET TIMEZONE = 'America/Regina';
SELECT t1 + '1 day', timelocation FROM timestamps; -- Now the Regina time stamp is correct, but toronto time stamp is incorrect (should be 22:00-06)
"2016-03-13 23:00:00-06";"America/Toronto"
"2016-03-14 00:00:00-06";"America/Regina"
SET TIMEZONE = 'America/Toronto';
SELECT t1 + '1 day', timelocation FROM timestamps; -- toronto is correct, regina is not, as before
"2016-03-14 00:00:00-04";"America/Toronto"
"2016-03-14 01:00:00-04";"America/Regina"
This solution will only work if I continually switch the postgres timezone before every operation time interval operation.
It is a combination of two properties that causes your problem:
timestamp with time zone
is stored in UTC and does not contain any time zone information. A better name for it would be “UTC timestamp”.
Addition of timestamp with time zone
and interval
is always performed in the current time zone, i.e. the one set with the configuration parameter TimeZone
.
Since what you really need to store is a timestamp and the time zone in which it is valid, you should store a combination of timestamp without time zone
and a text
representing the time zone.
As you correctly noticed, you would have to switch the current time zone to perform interval addition over the daylight savings time shift correctly (otherwise PostgreSQL does not know how long 1 day
is).
But you don't have to do that by hand, you can use a PL/pgSQL function to do it for you:
CREATE OR REPLACE FUNCTION add_in_timezone(
ts timestamp without time zone,
tz text,
delta interval
) RETURNS timestamp without time zone
LANGUAGE plpgsql IMMUTABLE AS
$$DECLARE
result timestamp without time zone;
oldtz text := current_setting('TimeZone');
BEGIN
PERFORM set_config('TimeZone', tz, true);
result := (ts AT TIME ZONE tz) + delta;
PERFORM set_config('TimeZone', oldtz, true);
RETURN result;
END;$$;
That would give you the following, where the result is to be understood in the same time zone as the argument:
test=> SELECT add_in_timezone('2016-03-13 00:00:00', 'America/Toronto', '1 day');
add_in_timezone
---------------------
2016-03-14 00:00:00
(1 row)
test=> SELECT add_in_timezone('2016-03-13 00:00:00', 'America/Regina', '1 day');
add_in_timezone
---------------------
2016-03-14 00:00:00
(1 row)
test=> SELECT add_in_timezone('2016-03-13 00:00:00', 'America/Toronto', '4 hours');
add_in_timezone
---------------------
2016-03-13 05:00:00
(1 row)
test=> SELECT add_in_timezone('2016-03-13 00:00:00', 'America/Regina', '4 hours');
add_in_timezone
---------------------
2016-03-13 04:00:00
(1 row)
You could consider creating a combined type
CREATE TYPE timestampattz AS (
ts timestamp without time zone,
zone text
);
and define operators and casts on it, but that's probably a major project that exceeds what you want for this.
There even is a PostgreSQL extension timestampandtz that does exactly that; maybe that's just what you need (I didn't look what the semantics for addition are).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With