Project: YugabyteDB (App Definition and Development / Database)
File: type-timestamp.md

title: The plain timestamp and timestamptz data types [YSQL] headerTitle: The plain timestamp and timestamptz data types linkTitle: Plain timestamp and timestamptz description: The semantics of the plain timestamp and timestamptz data types. [YSQL] menu: v2.18: identifier: type-timestamp parent: date-time-data-types-semantics weight: 30 type: docs {{< tip title="Understanding this section depends on understanding the 'Timezones' section." >}} To understand the timestamptz data type, and converting its values to/from plain timestamp values, you need to understand what the section explains. {{< /tip >}} The plain timestamp data type and the timestamptz data type are cousins. But there are critical differences: Both a plain timestamp datum and a timestamptz datum have the identical internal representation. You can picture it as the real number of seconds (with microsecond precision) from a reference moment (12:00 on 1-Jan-1970, UTC). The extract(epoch from t) function, where t is either a plain timestamp value or a timestamptz value, returns this number. Moreover, the result is independent of the session's current TimeZone setting for both of these data types. (See the subsection below.) The difference is in the metadata that describes the datum: each knows which kind it is. And the difference is significant when a datum is recorded or read back. You need a clear understanding of the differences so that you can make the appropriate choice between these two data types according to the use case. {{< tip title="You need a very good reason to prefer plain 'timestamp' to 'timestamptz'." >}} You should definitely consider timestamptz to be your default choice for a persistent representation. And you should be able to write down a clear account of your reasoning when you decide that plain timestamp is the better choice in the present application design context. For example, you might choose plain timestamp to send the representation of a moment of interest to a client (or to receive it back from a client) where the scenario in which the value is used has pre-defined the reigning timezone as a defining property for the ongoing scenario. Here, you'd convert from/to the ultimate timestamptz value to/from plain timestamp at the timezone of interest and then you'd convert this to/from text, using the specified representation format, to send to, or receive from, the client. {{< /tip >}} Plain timestamp values represent the date and the time-of-day of some moment in a regime. There is no timezone-sensitivity, neither when a plain timestamp value is created, nor when it is read. Such a value therefore represents a moment at some unspecified locationjust as a clockwork wall-clock that shows the date does. You can picture a timestamp value as the number of microseconds to the present moment from the start of some epoch. Because there's no timezone-sensitivity, there's no sensitivity to Daylight Savings regimes either: every day runs from midnight (inclusive) through the next midnight (exclusive). (PostgresSQL, and therefore YSQL, don't support .) However the same quirk that allows '24:00:00' as a time value allows this as the time-of-day component when you specify a timestamp value. Try this: ```plpgsql select ('2021-02-14 00:00:00.000000'::timestamp)::text as "starting midnight", ('2021-02-14 23:59:59.999999'::timestamp)::text as "just before ending midnight", ('2021-02-14

as "ending midnight"; ``` This is the result: ```output starting midnight | just before ending midnight | ending midnight +--+ 2021-02-14 00:00:00 | 2021-02-14 23:59:59.999999 | 2021-02-15 00:00:00 ``` The fact that this is allowed is less of an annoyance than it is with time values because, now that the date is part of the picture, the obvious convention can be used to render a time-of-day component given as '24:00:00' on one day as '00:00:00' on the next day. This convention has a knock on effect on the result of subtracting one timestamp value from another timestamp value. Try this: ```plpgsql select ('2021-02-14 24:00:00'::timestamp - '2021-02-14 00:00:00'::timestamp)::text as "the interval"; ``` This is the result: ```output the interval -- 1 day ``` A timestamptz value represents an time. Such a value can therefore be deterministically expressed as the in the UTC timezoneor, more carefully stated, as the local time at an offset of '0 seconds'::interval with respect to the UTC Time Standard. Though the representations of an actual plain timestamp value and an actual timestamptz value are the same, the semantics, and therefore the interpretation, is different for a timestamptz value than for a plain timestamp value because of the difference in the metadata of the values. When a timestamptz value is assigned, the UTC offset must also be specified. Once a timestamptz value has been recorded (for example, in a table column with that data type), the representation doesn't remember what the offset was at the moment of recording. That information is entirely consumed when the incoming value is normalized to UTC. When a timestamptz value is converted to a text value for display (either implicitly using the ::text typecast or explicitly using the tochar() built-in function), the conversion normalizes the date-time component so that it shows the local time with respect to the UTC offset_ that the current timezone specifies. The UTC offset may be specified implicitly (using the session's current TimeZone setting) or explicitlyeither within the text of the timestamptz literal, or using the at time zone operator. Further, the specification of the offset may be explicit, as an interval value, or implicit using the name of a timezone. When a timezone observes Daylight Savings Time, it's name denotes different offsets during the Standard Time period and the Summer Time period. The rules for this, and examples that show all of the possible ways to assign a timestamptz value, are given in the section and its subsections. Create a test table with a timestamptz column, insert one row, and view the result using, successively, two different values for the session's current timezone setting. ```plpgsql drop table if exists t cascade; create table t(k int primary key, v timestamptz not null); -- This setting has no effect on the inserted

set timezone = 'Europe/Paris'; insert into t(k, v) values(1, '2021-02-14 13:30:35+03:00'::timestamptz); set timezone = 'America/Los_Angeles'; select v::text as "timestamptz value" from t where k = 1; set timezone = 'Asia/Shanghai'; select v::text as "timestamptz value" from t where k = 1; ``` This is the result of the first query: ```output 2021-02-14 02:30:35-08 ``` And this is the result of the second query: ```output 2021-02-14 18:30:35+08 ``` This outcome needs careful interpretation. It turns out that, using ordinary SQL, there is no direct way to inspect what is actually held by the internal representation as an easily-readable date-time value. You can of course, apply the at time zone 'UTC' operator to the value of interest. But this implies understanding what you see in the light of a rule that you must accept. And this section aims to demonstrate that the rule in question is correct, given that you know already what value is internally representedin other words, you'd be "proving" that you understand correctly by assuming that you have! The better way is to use the extract(epoch from timestamptzvalue) function. But even this requires an act of faith (or lots of empirical testing): you must be convinced that the result of extract() here is not sensitive to the current TimeZone_ setting. The shows that you can indeed rely on this. Usually, you "see" the value represented only indirectly. In the present casefor example as the ::text typecast of the value. And the evaluation of this typecast is sensitive to the current TimeZone setting. You can readily understand that the three values '2021-02-14 13:30:35+03:00', ' 2021-02-14 02:30:35-08', and '2021-02-14 18:30:35+08' all represent the very same actual absolute time. This is like the way that (10 + 1) and (14 - 3) both represent the same value. However, the apparent redundancy brought by the possibility of many different ::text typecasts of the same underlying value is useful for human readability in a scenario like this: We'll talk next Tuesday at 08:00 my time (i.e. UTC-8)in other words 17:00 your time (i.e. UTC+1). The meeting partners both have a background knowledge of their timezone. But the important fact for each, for the day of the meeting, is what time to set the reminder on their clock (which setting is done only in terms of the local time of day): respectively 08:00 and 17:00. Notice that when a timezone respects Daylight Savings Time, this is taken account of just like it is in the example above. Consider this scenario: Rickie, who lives in Los Angeles, has constraints set by her familyand she controls the meeting. She can manage only eight o'clock in the morning. It's unimportant to her whether Daylight Savings Time is in force or not because her local constraining events (like when school starts) are all fixed in local timeand only eight in the morning local time works for her. She needs to fix two Tuesday meetings that happen to straddle the "spring forward" moment in Los Angelesand then to see each listed as at eight o'clock in her online calendar. Vincent, who lives in Amsterdam, needs to see when these meetings will take place in his online calendar. Simulate a very specialized calendar application that deals with just this single scenario. (Never mind that it's unrealistic. It makes the desired teaching point.) ```plpgsql -- This is done when the app is

drop table if exists meetings cascade; create table meetings(k int primary key, t timestamptz); deallocate all; prepare cr_mtg(int, text, int, text) as insert into meetings (k, t) values ($1, $2::timestamptz), ($3, $4::timestamptz); prepare qry_mtg as select k as "Mtg", to_char(t, 'Dy hh24-mi on dd-Mon-yyyy TZ ["with offset" TZH:TZM]') as "When" from meetings order by k; ``` Now simulate Rickie creating the meetings. She has the timezone America/LosAngeles_ set in her calendar preferences: ```plpgsql set timezone = 'America/Los_Angeles'; execute cr_mtg( 1, '2021-03-09 08:00:00', 2, '2021-03-16 08:00:00'); execute qry_mtg; ``` This is Rickie's result: ```output Mtg | When --+ 1 | Tue 08-00 on 09-Mar-2021 PST [with offset -08:00] 2 | Tue 08-00 on 16-Mar-2021 PDT [with offset -07:00] ``` Rickie gets a gentle reminder that the meetings do happen to straddle her "spring forward" moment. But she doesn't really care. She confirms that each meeting will happen at eight. Now simulate Vincent attending the meetings. He has the timezone Europe/Amsterdam set in his calendar preferences: ```plpgsql set timezone = 'Europe/Amsterdam'; execute qry_mtg; ``` This is Vincent's result: ```output Mtg | When --+ 1 | Tue 17-00 on 09-Mar-2021 CET [with offset +01:00] 2 | Tue 16-00 on 16-Mar-2021 CET [with offset +01:00] ``` Because Europe's "spring forward" moment is two weeks after it is in the US, Vincent sees that the second meeting is one hour earlier than the first while the timezone specification is unchanged. If he doesn't know that the US is out of step on Daylight Savings Time, he might think that Rickie has simply done this on a whim. But this hardly matters: he knows when he has to attend each meeting. Imagine trying to write the logic that brings the correct, and useful, functionality that the code above demonstrates if you used the bare timestamp data type. The code would be voluminous, obscure, and very likely to be buggy. In contrast, the timestamptz data type brought the required functionality with no application code except to set the timezone specifically for each user. Another way to "see" a timestamptz value is to compare it with what you reason it will be. Try this: ```plpgsql drop table if exists t cascade; create table t(k int primary key, v timestamptz not null); set timezone = 'Europe/Helsinki'; -- The timezone of the inserted value is set implicitly. insert into t(k, v) values(1, '2021-02-14 13:30:35'::timestamptz); select ( (select v from t where k = 1) = '2021-02-14 11:30:35 UTC'::timestamptz )::text; ``` The result is true. The US recognizes Daylight Savings Time. It starts, in 2021, in the 'America/LosAngeles' zone, on 14-Mar at 02:00. Watch what a clock that automatically adjusts according to Daylight Savings Time start/end moments (like on a smartphone) does. It goes from '01:59:59' to '03:00:00'_. Try this: ```plpgsql set timezone = 'America/Los_Angeles'; select '2021-03-14 01:30:00 America/Los_Angeles'::timestamptz as "before", '2021-03-14 02:30:00 America/Los_Angeles'::timestamptz as "wierd", '2021-03-14 03:30:00 America/Los_Angeles'::timestamptz as "after"; ``` This is the result: ```output before | wierd | after ++ 2021-03-14 01:30:00-08 | 2021-03-14 03:30:00-07 | 2021-03-14 03:30:00-07 ``` <a name="just-after-fall-back"></a>The value in the column with the alias "weird" is weird because '2021-03-14 02:30:00' doesn't exist. The design could have made the attempt to set this cause an error. But it was decided that it be forgiving. Daylight Savings Time in the America/LosAngeles timezone ends, in 2021, on 7-Nov at

Watch what a clock that automatically adjusts according to Daylight Savings Time start/end does now. It falls back from '01:59:59' to '01:00:00'. This means that, for example, '01:30:00' on 7-Nov is ambiguous. If you ring your room-mate latish on Saturday evening 6-Nov to say that you'll won't be back home until the small hours, probably about one-thirty, they won't know what you mean because the clock will read this time twice_. It's easiest to see this in reverse. Try this: ```plpgsql set timezone = 'America/Los_Angeles'; select to_char('2021-11-07 08:30:00 UTC'::timestamptz, 'hh24:mi:ss TZ (OF)') as "1st 1:30", to_char('2021-11-07 09:30:00 UTC'::timestamptz, 'hh24:mi:ss TZ (OF)') as "2nd 1:30"; ``` This is the result: ```output 1st 1:30 | 2nd 1:30 --+-- 01:30:00 PDT (-07) | 01:30:00 PST (-08) ``` So you have to tell them that you'll be back in an elaborate way: either at about one-thirty PDT (or about one-thirty PST) or equivalently at about one-thirty before the fall back moment (or one-thirty after the fall back moment) or at about one-thirty UTC-7 (or about one-thirty UTC-8) or even at about eight-thirty UTC (or nine-thirty UTC). Confused? Your room-mate soon will be! This strange (but logically unavoidable) consequence of observing Daylight Savings Time means that you have to be careful in the other direction too. Try this: ```plpgsql select tochar(('2021-11-07 01:30:00 America/LosAngeles'::timestamptz) at time zone 'UTC', 'hh24:mi:ss') as "Ambiguous"; ``` This is the result: ```output Ambiguous -- 09:30:00 ``` PostgreSQL (and therefore YSQL) resolve the ambiguity by convention: the later moment is chosen. Express what you mean explicitly to avoid the ambiguity: ```plpgsql set timezone = 'UTC'; select to_char('2021-11-07 01:30:00 -07:00'::timestamptz, 'hh24:mi:ss TZ') as "Before fallback", to_char('2021-11-07 01:30:00 -08:00'::timestamptz, 'hh24:mi:ss TZ') as "After fallback"; ``` This is the result: ```output Before fallback | After fallback --+- 08:30:00 UTC | 09:30:00 UTC ``` Or, according to the display that best suits your purpose, you might do this instead: ```plpgsql set timezone = 'America/Los_Angeles'; select to_char('2021-11-07 01:30:00 -07:00'::timestamptz, 'hh24:mi:ss TZ') as "Before fallback", to_char('2021-11-07 01:30:00 -08:00'::timestamptz, 'hh24:mi:ss TZ') as "After fallback"; ``` This is the now result: ```output Before fallback | After fallback --+- 01:30:00 PDT | 01:30:00 PST ``` {{< tip title="The mapping 'abbrev' to 'utcoffset' in the 'pgtimezone_names' view isn't unique." >}} Try this: ```plpgsql select name, abbrev, lpad(utc_offset::text, 9) as "UTC offset" from pgtimezonenames where abbrev in ('PST', 'PDT') order by utc_offset; ``` This is the result: ```output name | abbrev | UTC offset -+--+ Canada/Yukon | PDT | -07:00:00 ... America/Los_Angeles | PDT | -07:00:00 ... Canada/Pacific | PDT | -07:00:00 Asia/Manila | PST | 08:00:00 ``` Some results were elided. The blank lines were added manually to improve the readability. However, the [name, abbrev] tuple does uniquely identify a utcoffset_ value. You might be tempted to write PDT and PST in the example above in place of -07:00 and -08:00. That would work, in this specific use, but it won't work reliably in general because the abbreviations are looked up internally in pgtimezoneabbrevs.abbrev. But such abbreviations are never looked up in pgtimezonenames.abbrev. However, some abbreviations are found only in pgtimezonenames.abbrev. This can confuse the application programmer and lead, in some unfortunate circumstances, even to wrong

The complex rules in this space are explained in the section . Yugabyte recommends that you program defensively to avoid these pitfalls and follow the approach described in the section . {{< /tip >}} The code blocks above, and especially those in the section , are just that: examples that show the functional benefit that the timestamptz data type brings. The outcomes that are shown accord with intuition. But, so that you can write reliable application code, you must also understand the rules that explain, and let you reliably predict, these beneficial outcomes. The demonstration uses the table function. The overall shape of this is very similar to that of the table function presented in the "sensitivity of the conversion between timestamptz and plain timestamp to the UTC offset" section. The demonstration does two things: It shows that the result produced by the extract(epoch from timestamp-[tz]-value) function is insensitive to the session's TimeZone_ settingfor both data types. It tests the correctness of tersest statement the underlying semantic rules for conversion. The demonstration that follows is designed like this: A table function, timestamptzvsplaintimestamp()_, is used to enable a convenient "running commentary" visualization. Two constant values, one with data type plain timestamp and one with data type timestamptz are initialized so that the internal representations (as opposed to the metadata) are identical. Look: ```output tsplain constant timestamp not null := maketimestamp (yyyy, mm, dd, hh, mi, ss); tswithtz constant timestamptz not null := make_timestamptz(yyyy, mm, dd, hh, mi, ss, 'UTC'); ``` Each uses the same constant int values, yyyy, mm, dd, hh, mi, and ss, to define the identical date-and-time part for each of the two moments. The fact that UTC is used for the timezone argument of the maketimestamptz() invocation ensures the required identity of the internal representations of the two momentsactually, both as plain timestamp_ values. The extract(epoch from ... ) function is used to get the numbers of seconds, as constant double precision values, from the start of the epoch for the two moment values. An assert statement confirms that these two numbers of seconds are identical. A constant array, timezones, is populated by this query: ```plpgsql select name from pgtimezonenames where name like 'Etc/GMT%' and utcoffset <> makeinterval() order by utc_offset; ``` The query produces timezones that are listed on the page. This is a convenient way to define a set of UTC offset values, independently of when during the winter or summer you execute the query, that span the range from -12:00 to +14:00 in steps of one hour. A foreach loop is run thus: ```output foreach z in array timezones loop ``` At each loop iteration: The session's TimeZone setting is set to the value that the iterand, z, specifies. The extract(epoch from ... ) function is used again on the reference plain timestamp and timestamptz values (tsplain, and tswithtz) at the present TimeZone setting. An assert statement shows that these newly-extracted values are always identical to the initially extracted valuesin other words that they are unaffected by that setting. Notice that the initially extracted values were obtained at whatever the session's TimeZone_ setting happens to be on entry to the

Using the utcoffset() user-defined function (it looks up the UTC offset for the timezone z in the pgtimezonenames_ catalog view) these values are obtained: ```output t1 double precision := extract(epoch from ts_plain); t2 double precision := extract(epoch from tswithtz); tzoftimezone interval := utc_offset(z); tzdisplay interval := tochar(tswithtz, 'TZH:TZM'); tsdisplay timestamp := tochar(tswithtz, 'yyyy-mm-dd hh24:mi:ss'); delta interval := tsdisplay - tsplain; ``` These assert statements are executed: ```output assert (t1 = tsplainepoch), 'Assert #1 failed'; assert (t2 = tswithtz_epoch), 'Assert #2 failed'; assert (tzdisplay = tzof_timezone), 'Assert #3 failed'; assert (tz_display = delta), 'Assert #4 failed'; ``` Running commentary output is generated thus: ```output reportline(z, tsplain, tswithtz); ``` Finally, after the loop completes and before exiting, the session's TimeZone setting is restored to the value that it had on entry to the function. (It's always good practice to do this for any settings that your programs need, temporarily, to change.) The timestamptzvsplaintimestamp() table function uses two helper functions. The function utcoffset() gets the UTC offset, as an interval value, for the specified timezone thus: ```plpgsql drop function if exists utc_offset(text) cascade; create function utcoffset(tzname in text) returns interval language plpgsql as $body$ declare i constant interval not null := ( select a.utc_offset from pgtimezonenames a where a.name = tz_name ); begin return i; end; $body$; ``` The function reportline() formats the name of the session's current TimeZone setting and the reference constant timestamp and constant timestamptz values (tsplain and tswithtz) for maximally easily readable output. Critical comment: It's this formatting action that demonstrates the sensitivity of the display of a timestamptz value to the value of the reigning UTC

```plpgsql drop function if exists report_line(text, timestamp, timestamptz) cascade; create function reportline(z in text, tsplain in timestamp, tswithtz in timestamptz) returns text language plpgsql as $body$ declare t constant text not null := rpad(z, 15)||' '|| rpad(tochar(tsplain, 'Dy hh24:mi' ), 9)||' '|| rpad(tochar(tswith_tz, 'Dy hh24:mi TZH:TZM'), 16); begin return t; end; $body$; ``` <a name="timestamptz-vs-plain-timestamp"></a>Create and execute the timestamptzvsplaintimestamp()_ table function thus: ```plpgsql drop function if exists timestamptzvsplain_timestamp() cascade; create function timestamptzvsplain_timestamp() returns table(t text) language plpgsql as $body$ declare set_timezone constant text not null := $$set timezone = '%s'$$; tzonentry constant text not null := current_setting('timezone'); yyyy constant int not null := 2000; mm constant int not null := 1; dd constant int not null := 1; hh constant int not null := 11; mi constant int not null := 0; ss constant int not null := 0; tsplain constant timestamp not null := maketimestamp (yyyy, mm, dd, hh, mi, ss); tswithtz constant timestamptz not null := make_timestamptz(yyyy, mm, dd, hh, mi, ss, 'UTC'); tsplainepoch constant double precision not null := extract(epoch from ts_plain); tswithtzepoch constant double precision not null := extract(epoch from tswith_tz); begin assert (tswithtzepoch = tsplainepoch), 'Assert "tswithtzepoch = tsplainepoch" failed'; t := rpad('Timezone', 15)||' '||rpad('tsplain', 9)||' '||rpad('tswith_tz', 16); return next; t := rpad('-', 15, '-') ||' '||rpad('-', 9, '-') ||' '||rpad('-', 16, '-'); return next; declare z text not null := ''; timezones constant text[] not null := ( select arrayagg(name order by utcoffset) from pgtimezonenames where name like 'Etc/GMT%' and utcoffset <> makeinterval() ); begin z := 'UTC'; execute format(set_timezone, z); t := reportline(z, tsplain, tswithtz); return next; t := ''; return next; foreach z in array timezones loop execute format(set_timezone, z); declare t1 constant double precision not null := extract(epoch from ts_plain); t2 constant double precision not null := extract(epoch from tswithtz); tzoftimezone constant interval not null := utc_offset(z); tzdisplay constant interval not null := tochar(tswithtz, 'TZH:TZM'); tsdisplay constant timestamp not null := tochar(tswithtz, 'yyyy-mm-dd hh24:mi:ss'); delta constant interval not null := tsdisplay - tsplain; begin assert (t1 = tsplainepoch), 'Assert #1 failed'; assert (t2 = tswithtz_epoch), 'Assert #2 failed'; assert (tzdisplay = tzof_timezone), 'Assert #3 failed'; assert (tz_display = delta), 'Assert #4 failed'; end; t := reportline(z, tsplain, tswithtz); return next; end loop; end; execute format(settimezone, tzon_entry); end; $body$; select t from timestamptzvsplain_timestamp(); ``` This is the result: ```output Timezone tsplain tswith_tz UTC Sat 11:00 Sat 11:00 +00:00 Etc/GMT+12 Sat 11:00 Fri 23:00 -12:00 Etc/GMT+11 Sat 11:00 Sat 00:00 -11:00 Etc/GMT+10 Sat 11:00 Sat 01:00 -10:00 Etc/GMT+9 Sat 11:00 Sat 02:00 -09:00 Etc/GMT+8 Sat 11:00 Sat 03:00 -08:00 Etc/GMT+7 Sat 11:00 Sat 04:00 -07:00 Etc/GMT+6 Sat 11:00 Sat 05:00 -06:00 Etc/GMT+5 Sat 11:00 Sat 06:00 -05:00 Etc/GMT+4 Sat 11:00 Sat 07:00 -04:00 Etc/GMT+3 Sat 11:00 Sat 08:00 -03:00 Etc/GMT+2 Sat 11:00 Sat 09:00 -02:00 Etc/GMT+1 Sat 11:00 Sat 10:00 -01:00 Etc/GMT-1 Sat 11:00 Sat 12:00 +01:00 Etc/GMT-2 Sat 11:00 Sat 13:00 +02:00 Etc/GMT-3 Sat 11:00 Sat 14:00 +03:00 Etc/GMT-4 Sat 11:00 Sat 15:00 +04:00 Etc/GMT-5 Sat 11:00 Sat 16:00 +05:00 Etc/GMT-6 Sat 11:00 Sat 17:00 +06:00 Etc/GMT-7 Sat 11:00 Sat 18:00 +07:00 Etc/GMT-8 Sat 11:00 Sat 19:00 +08:00 Etc/GMT-9 Sat 11:00 Sat 20:00 +09:00 Etc/GMT-10 Sat 11:00 Sat 21:00 +10:00 Etc/GMT-11 Sat 11:00 Sat 22:00 +11:00 Etc/GMT-12 Sat 11:00 Sat 23:00 +12:00 Etc/GMT-13 Sat 11:00 Sun 00:00 +13:00 Etc/GMT-14 Sat 11:00 Sun 01:00 +14:00 ``` The execution finishes without error, confirming that the four tested assertions hold. The assert statements confirm that the extract(epoch from timestamptzvalue) result is unaffected by the session's timeZone_ setting. This is, of course, what your intuition tells you to expect. The output confirms that the display of