PostgreSQL timezones

Description

The TimeZone configuration parameter can be set in the file postgresql.conf , or in any of the other standard ways described in Chapter 19 .

There are also some special ways to set it:

The SQL command SET TIME ZONE sets the time zone for the session.

This is an alternative spelling of SET TIMEZONE TO with a more SQL-spec-compatible syntax.

The PGTZ environment variable is used by libpq clients to send a SET TIME ZONE command to the server upon connection.

Dont do this

Don’t use CURRENT_TIME

Don’t use the CURRENT_TIME function. Use whichever of these is appropriate:

  • CURRENT_TIMESTAMP or now() if you want a timestamp with time zone,

  • LOCALTIMESTAMP if you want a timestamp without time zone,

  • CURRENT_DATE if you want a date,

  • LOCALTIME if you want a time

Be Aware of Timezones by Haki Benita

Timezone are always a source of confusion and pitfalls.

PostgreSQL does a fair job with timezones, but you still have to pay attention to some things.

A common mistake I see countless times is truncating timestamps without specifying the time zone.

Say we want to find out how many sales were made each day:

SELECT created_at::date, COUNT(*)
FROM sale
GROUP BY 1

Without explicitly setting the time zone, you might get different results, depending on the time zone set by the client application:

db=# SELECT now()::date;
    now
------------
 2019-11-08

db=# SET TIME ZONE 'australia/perth';
SET

db=# SELECT now()::date;
    now
------------
 2019-11-09

If you are not sure what time zone you are working with, you might be doing it wrong.

When truncating a timestamp, convert to the desired time zone first:

SELECT (timestamp at time zone 'asia/tel_aviv')::date, COUNT(*)
FROM sale
GROUP BY 1;

Setting the time zone is usually the responsibility of the client application .

For example, to get the time zone used by psql:

db=# SHOW timezone;
 TimeZone
----------
 Israel
(1 row)
db=# SELECT now();
              now
-------------------------------
 2019-11-09 11:41:45.233529+02
(1 row)

To set the time zone in psql:

db=# SET timezone TO 'UTC';
SET
db=# SELECT now();
              now
-------------------------------
 2019-11-09 09:41:55.904474+00
(1 row)

Another important thing to keep in mind is that the time zone of your server can be different than the time zone of your local machine. so if you run queries in you local machine they might yield different results in production.

To avoid mistakes, always explicitly set a time zone .

Avoid Transformations on Indexed Fields by Haki Benita

Using functions on an indexed field might prevent the database from using the index on the field:

db=# (
  SELECT *
  FROM sale
  WHERE created at time ZONE 'asia/tel_aviv' > '2019-10-01'
);
                             QUERY PLAN
----------------------------------------------------------------------------------
Seq Scan on sale (cost=0.00..510225.35 rows=4523386 width=276)
 Filter:timezone('asia/tel_aviv', created)>'2019-10-01 00:00:00'::timestamp without time zone

The field created is indexed, but because we transformed it with timezone, the index was not used .

One way to utilize the index in this case is to apply the transformation on the right-hand side instead:

db=# (
  SELECT *
  FROM sale
  WHERE created > '2019-10-01' AT TIME ZONE 'asia/tel_aviv'
);
                             QUERY PLAN
----------------------------------------------------------------------------------
Index Scan using sale_created_ix on sale  (cost=0.43..4.51 rows=1 width=276)
    Index Cond: (created > '2019-10-01 00:00:00'::timestamp with time zone)

Another common use-case involving dates is filtering a specific period:

db=# (
  SELECT *
  FROM sale
  WHERE created + INTERVAL '1 day' > '2019-10-01'
);
                             QUERY PLAN
----------------------------------------------------------------------------------
Seq Scan on sale  (cost=0.00..510225.35 rows=4523386 width=276)
 Filter: ((created + '1 day'::interval) > '2019-10-01 00:00:00+03'::timestamp with time zone)

Like before, the interval function on the field created prevented the database from utilizing the index.

To make the database use the index, apply the transformation on the right-hand side instead of the field:

db=# (
  SELECT *
  FROM sale
  WHERE created > '2019-10-01'::date - INTERVAL '1 day'
);
                             QUERY PLAN
----------------------------------------------------------------------------------
 Index Scan using sale_created_ix on sale  (cost=0.43..4.51 rows=1 width=276)
   Index Cond: (created > '2019-10-01 00:00:00'::timestamp without time zone)

Use BETWEEN Only For Inclusive Ranges

A common mistake I see very often is when filtering a date range using BETWEEN:

SELECT *
FROM sales
WHERE created BETWEEN '2019-01-01' AND '2020-01-01';

Intuitively, you might think this query is fetching all the sales in 2019, but in fact, it’s fetching all the sales made in 2019 and the first day of 2020.

BETWEEN is inclusive , so the query above is equivalent to this query:

SELECT *
FROM sales
WHERE created >= '2019-01-01'
AND created <= '2020-01-01';

To filter results in 2019 you can either write this:

SELECT *
FROM sales
WHERE created BETWEEN '2019-01-01' AND '2019-12-31';

Or better yet:

SELECT *
FROM sales
WHERE created >= '2019-01-01'
AND created < '2020-01-01';

Using BETWEEN incorrectly might produce overlapping results, for example, counting sales twice in two different periods.

PostgreSQL Data Types: Date, Timestamp, and Time Zones by Tapoueh

The first question we need to answer here is about using timestamps with or without time zones from our applications.

The answer is simple: always use timestamps WITH time zones .

select
    pg_column_size(timestamp without time zone 'now'),
    pg_column_size(timestamp with time zone 'now');
pg_column_size|pg_column_size|
             8|             8|

PostgreSQL defaults to using bigint internally to store timestamps, and the on-disk and in-memory format are the same with or without time zone support.

Here’s their whole type definition in the PostgreSQL source code (in src/include/datatype/timestamp.h):

typedef int64 Timestamp;
typedef int64 TimestampTz;

From the PostgreSQL documentation for timestamps, here’s how it works:

For timestamp with time zone, the internally stored value is always in UTC (Universal Coordinated Time, traditionally known as Greenwich Mean Time, GMT).

An input value that has an explicit time zone specified is converted to UTC using the appropriate offset for that time zone.

If no time zone is stated in the input string, then it is assumed to be in the time zone indicated by the system’s TimeZone parameter, and is converted to UTC using the offset for the timezone zone.

tz.sql

 1begin;
 2
 3drop table if exists tstz;
 4
 5create table tstz(ts timestamp, tstz timestamptz);
 6
 7set timezone to 'Europe/Paris';
 8select now();
 9insert into tstz values(now(), now());
10
11set timezone to 'Pacific/Tahiti';
12select now();
13insert into tstz values(now(), now());
14
15set timezone to 'Europe/Paris';
16table tstz;
17
18set timezone to 'Pacific/Tahiti';
19table tstz;
20
21commit;

In this script, we play with the client’s setting timezone and change from a French value to another French value, as Tahiti is an island in the Pacific that is part of France.

Here’s the full output as seen when running this script, when launched with:

psql -d db_intranet -U intranet -a -f tz.sql.rst:
 1begin;
 2BEGIN
 3drop table if exists tstz;
 4DROP TABLE
 5create table tstz(ts timestamp, tstz timestamptz);
 6CREATE TABLE
 7set timezone to 'Europe/Paris';
 8SET
 9select now();
10              now              
11-------------------------------
12 2020-04-30 18:35:12.909262+02
13(1 ligne)
14
15insert into tstz values(now(), now());
16INSERT 0 1
17set timezone to 'Pacific/Tahiti';
18SET
19select now();
20              now              
21-------------------------------
22 2020-04-30 06:35:12.909262-10
23(1 ligne)
24
25insert into tstz values(now(), now());
26INSERT 0 1
27set timezone to 'Europe/Paris';
28SET
29table tstz;
30             ts             |             tstz              
31----------------------------+-------------------------------
32 2020-04-30 18:35:12.909262 | 2020-04-30 18:35:12.909262+02
33 2020-04-30 06:35:12.909262 | 2020-04-30 18:35:12.909262+02
34(2 lignes)
35
36set timezone to 'Pacific/Tahiti';
37SET
38table tstz;
39             ts             |             tstz              
40----------------------------+-------------------------------
41 2020-04-30 18:35:12.909262 | 2020-04-30 06:35:12.909262-10
42 2020-04-30 06:35:12.909262 | 2020-04-30 06:35:12.909262-10
43(2 lignes)
44
45commit;
46COMMIT

First, we see that the now() function always returns the same timestamp within a single transaction. If you want to see the clock running while in a transaction, use the clock_timestamp() function instead.

Then, we see that when we change the timezone client setting, PostgreSQL outputs timestamps as expected, in the selected timezone.

If you manage an application with users in different time zones and you want to display time in their own local preferred time zone, then you can set timezone in your application code before doing any timestamp related processing, and have PostgreSQL do all the hard work for you.

Finally, when selecting back from the tstz table, we see that the column tstz realizes that both the inserted values actually are the same point in time, but seen from different places in the world, whereas the ts column makes it impossible to compare the entries and realize they actually happened at exactly the same time.

As said before, even when using timestamps with time zone, PostgreSQL will not store the time zone in use at input time, so there’s no way from our tstz table to know that the entries are at the same time but just from different places.

The opening of this section links to The Long, Painful History of Time , and if you didn’t read it yet, maybe now is a good time.

Several options are available to input timestamp values in PostgreSQL. The easiest is to use the ISO format, so if your application’s code allows that you’re all set.

In the following example we leave the time zone out, as usually, it’s handled by the timezone session parameter, as seen above.

If you need to, of course, you can input the time zone in the timestamp values directly:

select timestamptz '2017-01-08 04:05:06',
       timestamptz '2017-01-08 04:05:06+02';

At insert or update time, use the same literal strings without the type decoration: PostgreSQL already knows the type of the target column, and it uses that to parse the values literal in the DML statement.

Some application use-cases only need the date.

Then use the date data type in PostgreSQL. It is of course then possible to compare a date and a timestamp with time zone in your SQL queries, and even to append a time offset on top of your date to construct a timestamp.

Table pg_timezone_names

select * from pg_timezone_names order by name asc;
name                            |abbrev|utc_offset|is_dst|
Africa/Abidjan                  |GMT   |  00:00:00|false |
Africa/Accra                    |GMT   |  00:00:00|false |
Africa/Addis_Ababa              |EAT   |  03:00:00|false |
Africa/Algiers                  |CET   |  01:00:00|false |
Africa/Asmara                   |EAT   |  03:00:00|false |
Africa/Asmera                   |EAT   |  03:00:00|false |
Africa/Bamako                   |GMT   |  00:00:00|false |
Africa/Bangui                   |WAT   |  01:00:00|false |
Africa/Banjul                   |GMT   |  00:00:00|false |
Africa/Bissau                   |GMT   |  00:00:00|false |

Setting the timezone parameter (please use UTC timezone)

show timezone;

show timezone;
TimeZone    |
UTC|

select clock_timestamp();

select clock_timestamp();
clock_timestamp    |
2020-04-30 16:41:01|

SELECT now()

SELECT now() AT TIME ZONE current_setting('TimeZone');
SELECT now() AT TIME ZONE 'Europe/Paris';
SELECT now() AT TIME ZONE 'UTC';

SELECT now() AT TIME ZONE current_setting(‘TimeZone’);

SELECT now() AT TIME ZONE current_setting('TimeZone');

SELECT now() AT TIME ZONE ‘Europe/Paris’;

SELECT now() AT TIME ZONE 'Europe/Paris';

SELECT now() AT TIME ZONE ‘UTC’;

SELECT now() AT TIME ZONE 'UTC';