From fcd210d496dad7d9f68c9399113368ccf2f009b9 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Tue, 20 Feb 2024 14:35:12 -0500 Subject: [PATCH] Doc: improve explanation of type interval, especially extract(). The explanation of interval's behavior in datatype.sgml wasn't wrong exactly, but it was unclear, partly because it buried the lede about there being three internal fields. Rearrange and wordsmith for more clarity. The discussion of extract() claimed that input of type date was handled by casting, but actually there's been a separate SQL function taking date for a very long time. Also, it was mostly silent about how interval inputs are handled, but there are several field types for which it seems useful to be specific. Improve discussion of justify_days()/justify_hours() too. In passing, remove vertical space in some groups of examples, as there was little consistency about whether to have such space or not. (I only did this within the datetime functions section; there are some related inconsistencies elsewhere.) Per discussion of bug #18348 from Michael Bondarenko. There may be some code changes coming out of that discussion too, but we likely won't back-patch them. This docs-only patch seems useful to back-patch, though I only carried it back to v13 because it didn't apply easily in v12. Discussion: https://postgr.es/m/18348-b097a3587dfde8a4@postgresql.org --- doc/src/sgml/datatype.sgml | 71 ++++++++++++++---------- doc/src/sgml/func.sgml | 111 ++++++++++++++++++------------------- 2 files changed, 96 insertions(+), 86 deletions(-) diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml index 66510ee031..73e51b0b11 100644 --- a/doc/src/sgml/datatype.sgml +++ b/doc/src/sgml/datatype.sgml @@ -2869,10 +2869,31 @@ P years-months- - Field values can have fractional parts: for example, '1.5 + Internally, interval values are stored as three integral + fields: months, days, and microseconds. These fields are kept + separate because the number of days in a month varies, while a day + can have 23 or 25 hours if a daylight savings time transition is + involved. An interval input string that uses other units is + normalized into this format, and then reconstructed in a standardized + way for output, for example: + + +SELECT '2 years 15 months 100 weeks 99 hours 123456789 milliseconds'::interval; + interval +--------------------------------------- + 3 years 3 mons 700 days 133:17:36.789 + + + Here weeks, which are understood as 7 days, have been + kept separate, while the smaller and larger time units were + combined and normalized. + + + + Input field values can have fractional parts, for example '1.5 weeks' or '01:02:03.45'. However, - because interval internally stores only three integer units (months, - days, microseconds), fractional units must be spilled to smaller + because interval internally stores only integral fields, + fractional values must be converted into smaller units. Fractional parts of units greater than months are rounded to be an integer number of months, e.g. '1.5 years' becomes '1 year 6 mons'. Fractional parts of @@ -2922,33 +2943,6 @@ P years-months- - - Internally interval values are stored as months, days, - and microseconds. This is done because the number of days in a month - varies, and a day can have 23 or 25 hours if a daylight savings - time adjustment is involved. The months and days fields are integers - while the microseconds field can store fractional seconds. Because intervals are - usually created from constant strings or timestamp subtraction, - this storage method works well in most cases, but can cause unexpected - results: - - -SELECT EXTRACT(hours from '80 minutes'::interval); - date_part ------------ - 1 - -SELECT EXTRACT(days from '80 hours'::interval); - date_part ------------ - 0 - - - Functions justify_days and - justify_hours are available for adjusting days - and hours that overflow their normal ranges. - - @@ -2960,6 +2954,23 @@ SELECT EXTRACT(days from '80 hours'::interval); formatting + + As previously explained, PostgreSQL + stores interval values as months, days, and + microseconds. For output, the months field is converted to years and + months by dividing by 12. The days field is shown as-is. The + microseconds field is converted to hours, minutes, seconds, and + fractional seconds. Thus months, minutes, and seconds will never be + shown as exceeding the ranges 0–11, 0–59, and 0–59 + respectively, while the displayed years, days, and hours fields can + be quite large. (The justify_days + and justify_hours + functions can be used if it is desirable to transpose large days or + hours values into the next higher field.) + + The output format of the interval type can be set to one of the four styles sql_standard, postgres, diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index cf3de80394..830566028c 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -9166,7 +9166,8 @@ SELECT regexp_match('abc01234xyz', '(?:(.*?)(\d+)(.*)){1,1}'); Subtract timestamps (converting 24-hour intervals into days, - similarly to justify_hours()) + similarly to justify_hours()) timestamp '2001-09-29 03:00' - timestamp '2001-07-27 12:00' @@ -9579,35 +9580,35 @@ SELECT regexp_match('abc01234xyz', '(?:(.*?)(\d+)(.*)){1,1}'); - + justify_days justify_days ( interval ) interval - Adjust interval so 30-day time periods are represented as months + Adjust interval, converting 30-day time periods to months - justify_days(interval '35 days') - 1 mon 5 days + justify_days(interval '1 year 65 days') + 1 year 2 mons 5 days - + justify_hours justify_hours ( interval ) interval - Adjust interval so 24-hour time periods are represented as days + Adjust interval, converting 24-hour time periods to days - justify_hours(interval '27 hours') - 1 day 03:00:00 + justify_hours(interval '50 hours 10 minutes') + 2 days 02:10:00 @@ -10040,13 +10041,19 @@ EXTRACT(field FROM source) The extract function retrieves subfields such as year or hour from date/time values. source must be a value expression of - type timestamp, time, or interval. - (Expressions of type date are - cast to timestamp and can therefore be used as - well.) field is an identifier or + type timestamp, date, time, + or interval. (Timestamps and times can be with or + without time zone.) + field is an identifier or string that selects what field to extract from the source value. + Not all fields are valid for every input data type; for example, fields + smaller than a day cannot be extracted from a date, while + fields of a day or more cannot be extracted from a time. The extract function returns values of type numeric. + + + The following are valid field names: @@ -10055,7 +10062,8 @@ EXTRACT(field FROM source) century - The century + The century; for interval values, the year field + divided by 100 @@ -10063,17 +10071,13 @@ SELECT EXTRACT(CENTURY FROM TIMESTAMP '2000-12-16 12:21:13'); Result: 20 SELECT EXTRACT(CENTURY FROM TIMESTAMP '2001-02-16 20:38:40'); Result: 21 +SELECT EXTRACT(CENTURY FROM DATE '0001-01-01 AD'); +Result: 1 +SELECT EXTRACT(CENTURY FROM DATE '0001-12-31 BC'); +Result: -1 +SELECT EXTRACT(CENTURY FROM INTERVAL '2001 years'); +Result: 20 - - - The first century starts at 0001-01-01 00:00:00 AD, although - they did not know it at the time. This definition applies to all - Gregorian calendar countries. There is no century number 0, - you go from -1 century to 1 century. - - If you disagree with this, please write your complaint to: - Pope, Cathedral Saint-Peter of Roma, Vatican. - @@ -10081,14 +10085,13 @@ SELECT EXTRACT(CENTURY FROM TIMESTAMP '2001-02-16 20:38:40'); day - For timestamp values, the day (of the month) field - (1–31) ; for interval values, the number of days + The day of the month (1–31); for interval + values, the number of days SELECT EXTRACT(DAY FROM TIMESTAMP '2001-02-16 20:38:40'); Result: 16 - SELECT EXTRACT(DAY FROM INTERVAL '40 days 1 minute'); Result: 40 @@ -10162,10 +10165,8 @@ SELECT EXTRACT(DOY FROM TIMESTAMP '2001-02-16 20:38:40'); SELECT EXTRACT(EPOCH FROM TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40.12-08'); Result: 982384720.120000 - SELECT EXTRACT(EPOCH FROM TIMESTAMP '2001-02-16 20:38:40.12'); Result: 982355920.120000 - SELECT EXTRACT(EPOCH FROM INTERVAL '5 days 3 hours'); Result: 442800.000000 @@ -10193,7 +10194,8 @@ SELECT to_timestamp(982384720.12); hour - The hour field (0–23) + The hour field (0–23 in timestamps, unrestricted in + intervals) @@ -10228,7 +10230,7 @@ SELECT EXTRACT(ISODOW FROM TIMESTAMP '2001-02-18 20:38:40'); The ISO 8601 week-numbering year that the date - falls in (not applicable to intervals) + falls in @@ -10245,9 +10247,6 @@ SELECT EXTRACT(ISOYEAR FROM DATE '2006-01-02'); different from the Gregorian year. See the week field for more information. - - This field is not available in PostgreSQL releases prior to 8.3. - @@ -10256,7 +10255,7 @@ SELECT EXTRACT(ISOYEAR FROM DATE '2006-01-02'); The Julian Date corresponding to the - date or timestamp (not applicable to intervals). Timestamps + date or timestamp. Timestamps that are not local midnight result in a fractional value. See for more information. @@ -10289,12 +10288,15 @@ SELECT EXTRACT(MICROSECONDS FROM TIME '17:12:28.5'); millennium - The millennium + The millennium; for interval values, the year field + divided by 1000 SELECT EXTRACT(MILLENNIUM FROM TIMESTAMP '2001-02-16 20:38:40'); Result: 3 +SELECT EXTRACT(MILLENNIUM FROM INTERVAL '2001 years'); +Result: 2 @@ -10337,18 +10339,16 @@ SELECT EXTRACT(MINUTE FROM TIMESTAMP '2001-02-16 20:38:40'); month - For timestamp values, the number of the month - within the year (1–12) ; for interval values, - the number of months, modulo 12 (0–11) + The number of the month within the year (1–12); + for interval values, the number of months modulo 12 + (0–11) SELECT EXTRACT(MONTH FROM TIMESTAMP '2001-02-16 20:38:40'); Result: 2 - SELECT EXTRACT(MONTH FROM INTERVAL '2 years 3 months'); Result: 3 - SELECT EXTRACT(MONTH FROM INTERVAL '2 years 13 months'); Result: 1 @@ -10379,7 +10379,6 @@ SELECT EXTRACT(QUARTER FROM TIMESTAMP '2001-02-16 20:38:40'); SELECT EXTRACT(SECOND FROM TIMESTAMP '2001-02-16 20:38:40'); Result: 40.000000 - SELECT EXTRACT(SECOND FROM TIME '17:12:28.5'); Result: 28.500000 @@ -10461,6 +10460,20 @@ SELECT EXTRACT(YEAR FROM TIMESTAMP '2001-02-16 20:38:40'); + + When processing an interval value, + the extract function produces field values that + match the interpretation used by the interval output function. This + can produce surprising results if one starts with a non-normalized + interval representation, for example: + +SELECT INTERVAL '80 minutes'; +Result: 01:20:00 +SELECT EXTRACT(MINUTES FROM INTERVAL '80 minutes'); +Result: 20 + + + When the input value is +/-Infinity, extract returns @@ -10502,7 +10515,6 @@ date_part('field', source) SELECT date_part('day', TIMESTAMP '2001-02-16 20:38:40'); Result: 16 - SELECT date_part('hour', INTERVAL '4 hours 3 minutes'); Result: 4 @@ -10580,16 +10592,12 @@ date_trunc(field, source [ SELECT date_trunc('hour', TIMESTAMP '2001-02-16 20:38:40'); Result: 2001-02-16 20:00:00 - SELECT date_trunc('year', TIMESTAMP '2001-02-16 20:38:40'); Result: 2001-01-01 00:00:00 - SELECT date_trunc('day', TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40+00'); Result: 2001-02-16 00:00:00-05 - SELECT date_trunc('day', TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40+00', 'Australia/Sydney'); Result: 2001-02-16 08:00:00-05 - SELECT date_trunc('hour', INTERVAL '3 days 02:47:33'); Result: 3 days 02:00:00 @@ -10628,7 +10636,6 @@ date_bin(stride, source, < SELECT date_bin('15 minutes', TIMESTAMP '2020-02-11 15:44:17', TIMESTAMP '2001-01-01'); Result: 2020-02-11 15:30:00 - SELECT date_bin('15 minutes', TIMESTAMP '2020-02-11 15:44:17', TIMESTAMP '2001-01-01 00:02:30'); Result: 2020-02-11 15:32:30 @@ -10814,16 +10821,12 @@ SELECT date_bin('15 minutes', TIMESTAMP '2020-02-11 15:44:17', TIMESTAMP '2001-0 SELECT TIMESTAMP '2001-02-16 20:38:40' AT TIME ZONE 'America/Denver'; Result: 2001-02-16 19:38:40-08 - SELECT TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40-05' AT TIME ZONE 'America/Denver'; Result: 2001-02-16 18:38:40 - SELECT TIMESTAMP '2001-02-16 20:38:40' AT TIME ZONE 'Asia/Tokyo' AT TIME ZONE 'America/Chicago'; Result: 2001-02-16 05:38:40 - SELECT TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40-05' AT LOCAL; Result: 2001-02-16 17:38:40 - SELECT TIME WITH TIME ZONE '20:38:40-05' AT LOCAL; Result: 17:38:40 @@ -10928,16 +10931,12 @@ LOCALTIMESTAMP(precision) SELECT CURRENT_TIME; Result: 14:39:53.662522-05 - SELECT CURRENT_DATE; Result: 2019-12-23 - SELECT CURRENT_TIMESTAMP; Result: 2019-12-23 14:39:53.662522-05 - SELECT CURRENT_TIMESTAMP(2); Result: 2019-12-23 14:39:53.66-05 - SELECT LOCALTIMESTAMP; Result: 2019-12-23 14:39:53.662522