Re: date_trunc() in a specific time zone

Поиск
Список
Период
Сортировка
От Tom Lane
Тема Re: date_trunc() in a specific time zone
Дата
Msg-id 24863.1542151230@sss.pgh.pa.us
обсуждение исходный текст
Ответ на Re: date_trunc() in a specific time zone  (Tom Lane <tgl@sss.pgh.pa.us>)
Ответы Re: date_trunc() in a specific time zone  (Tom Lane <tgl@sss.pgh.pa.us>)
Список pgsql-hackers
I wrote:
> BTW, I'd been hoping that we could avoid rotate-to-local-and-back
> in Vik's desired case, but after further thought I suspect the only
> real optimization that's possible compared to writing it out with
> two AT TIME ZONE constructs is to do the zone name lookup just once.
> As an example, truncating to a day-or-larger boundary could result in
> shifting to a different UTC offset than you started with, due to crossing
> a DST boundary.

Here's a v2 that transposes the code to C so that we can get that
optimization.  I've not tried to time it, but it should actually be
a bit faster than standard date_trunc plus one AT TIME ZONE rotation,
never mind two of them.

            regards, tom lane

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 1678c8c..adffa7d 100644
*** a/doc/src/sgml/func.sgml
--- b/doc/src/sgml/func.sgml
*************** SELECT regexp_match('abc01234xyz', '(?:(
*** 7186,7191 ****
--- 7186,7200 ----
         </row>

         <row>
+         <entry><literal><function>date_trunc(<type>text</type>, <type>timestamp with time zone</type>,
<type>text</type>)</function></literal></entry>
+         <entry><type>timestamp with time zone</type></entry>
+         <entry>Truncate to specified precision in the specified time zone; see also <xref
linkend="functions-datetime-trunc"/>
+         </entry>
+         <entry><literal>date_trunc('day', timestamptz '2001-02-16 20:38:40+00', 'Australia/Sydney')</literal></entry>
+         <entry><literal>2001-02-16 13:00:00+00</literal></entry>
+        </row>
+
+        <row>
          <entry><literal><function>date_trunc(<type>text</type>, <type>interval</type>)</function></literal></entry>
          <entry><type>interval</type></entry>
          <entry>Truncate to specified precision; see also <xref linkend="functions-datetime-trunc"/>
*************** SELECT date_part('hour', INTERVAL '4 hou
*** 8078,8084 ****

     <para>
  <synopsis>
! date_trunc('<replaceable>field</replaceable>', <replaceable>source</replaceable>)
  </synopsis>
      <replaceable>source</replaceable> is a value expression of type
      <type>timestamp</type> or <type>interval</type>.
--- 8087,8093 ----

     <para>
  <synopsis>
! date_trunc(<replaceable>field</replaceable>, <replaceable>source</replaceable> [,
<replaceable>time_zone</replaceable>])
  </synopsis>
      <replaceable>source</replaceable> is a value expression of type
      <type>timestamp</type> or <type>interval</type>.
*************** date_trunc('<replaceable>field</replacea
*** 8112,8124 ****
     </para>

     <para>
!     Examples:
  <screen>
  SELECT date_trunc('hour', TIMESTAMP '2001-02-16 20:38:40');
  <lineannotation>Result: </lineannotation><computeroutput>2001-02-16 20:00:00</computeroutput>

  SELECT date_trunc('year', TIMESTAMP '2001-02-16 20:38:40');
  <lineannotation>Result: </lineannotation><computeroutput>2001-01-01 00:00:00</computeroutput>
  </screen>
     </para>
    </sect2>
--- 8121,8158 ----
     </para>

     <para>
!     If the optional <replaceable>time_zone</replaceable> argument is
!     present, the <replaceable>source</replaceable> value is truncated in the
!     specified time zone; for example, truncation to <literal>day</literal>
!     produces a value that is midnight in that zone.  The time zone name can
!     be specified in any of the ways described in
!     <xref linkend="datatype-timezones"/>.
!    </para>
!
!    <para>
!     When the <replaceable>time_zone</replaceable> argument is
!     present, the <replaceable>source</replaceable> and result are always of
!     type <type>timestamp with time zone</type>, whereas the two-argument
!     form of <function>date_trunc</function> is available for timestamps with
!     or without time zone.  The two-argument form truncates <type>timestamp
!     with time zone</type> values using the current
!     <xref linkend="guc-timezone"/> setting.
!    </para>
!
!    <para>
!     Examples (assuming the local time zone is <literal>America/New_York</literal>):
  <screen>
  SELECT date_trunc('hour', TIMESTAMP '2001-02-16 20:38:40');
  <lineannotation>Result: </lineannotation><computeroutput>2001-02-16 20:00:00</computeroutput>

  SELECT date_trunc('year', TIMESTAMP '2001-02-16 20:38:40');
  <lineannotation>Result: </lineannotation><computeroutput>2001-01-01 00:00:00</computeroutput>
+
+ SELECT date_trunc('day', TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40+00');
+ <lineannotation>Result: </lineannotation><computeroutput>2001-02-16 00:00:00-05</computeroutput>
+
+ SELECT date_trunc('day', TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40+00', 'Australia/Sydney');
+ <lineannotation>Result: </lineannotation><computeroutput>2001-02-16 08:00:00-05</computeroutput>
  </screen>
     </para>
    </sect2>
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 449164a..e5f8b51 100644
*** a/src/backend/utils/adt/timestamp.c
--- b/src/backend/utils/adt/timestamp.c
*************** timestamp_trunc(PG_FUNCTION_ARGS)
*** 3925,3938 ****
      PG_RETURN_TIMESTAMP(result);
  }

! /* timestamptz_trunc()
!  * Truncate timestamp to specified units.
   */
! Datum
! timestamptz_trunc(PG_FUNCTION_ARGS)
  {
-     text       *units = PG_GETARG_TEXT_PP(0);
-     TimestampTz timestamp = PG_GETARG_TIMESTAMPTZ(1);
      TimestampTz result;
      int            tz;
      int            type,
--- 3925,3939 ----
      PG_RETURN_TIMESTAMP(result);
  }

! /*
!  * Common code for timestamptz_trunc() and timestamptz_trunc_zone().
!  *
!  * tzp identifies the zone to truncate with respect to.  We assume
!  * infinite timestamps have already been rejected.
   */
! static TimestampTz
! timestamptz_trunc_internal(text *units, TimestampTz timestamp, pg_tz *tzp)
  {
      TimestampTz result;
      int            tz;
      int            type,
*************** timestamptz_trunc(PG_FUNCTION_ARGS)
*** 3943,3951 ****
      struct pg_tm tt,
                 *tm = &tt;

-     if (TIMESTAMP_NOT_FINITE(timestamp))
-         PG_RETURN_TIMESTAMPTZ(timestamp);
-
      lowunits = downcase_truncate_identifier(VARDATA_ANY(units),
                                              VARSIZE_ANY_EXHDR(units),
                                              false);
--- 3944,3949 ----
*************** timestamptz_trunc(PG_FUNCTION_ARGS)
*** 3954,3960 ****

      if (type == UNITS)
      {
!         if (timestamp2tm(timestamp, &tz, tm, &fsec, NULL, NULL) != 0)
              ereport(ERROR,
                      (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
                       errmsg("timestamp out of range")));
--- 3952,3958 ----

      if (type == UNITS)
      {
!         if (timestamp2tm(timestamp, &tz, tm, &fsec, NULL, tzp) != 0)
              ereport(ERROR,
                      (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
                       errmsg("timestamp out of range")));
*************** timestamptz_trunc(PG_FUNCTION_ARGS)
*** 4055,4061 ****
          }

          if (redotz)
!             tz = DetermineTimeZoneOffset(tm, session_timezone);

          if (tm2timestamp(tm, fsec, &tz, &result) != 0)
              ereport(ERROR,
--- 4053,4059 ----
          }

          if (redotz)
!             tz = DetermineTimeZoneOffset(tm, tzp);

          if (tm2timestamp(tm, fsec, &tz, &result) != 0)
              ereport(ERROR,
*************** timestamptz_trunc(PG_FUNCTION_ARGS)
*** 4071,4076 ****
--- 4069,4151 ----
          result = 0;
      }

+     return result;
+ }
+
+ /* timestamptz_trunc()
+  * Truncate timestamptz to specified units.
+  */
+ Datum
+ timestamptz_trunc(PG_FUNCTION_ARGS)
+ {
+     text       *units = PG_GETARG_TEXT_PP(0);
+     TimestampTz timestamp = PG_GETARG_TIMESTAMPTZ(1);
+     TimestampTz result;
+
+     if (TIMESTAMP_NOT_FINITE(timestamp))
+         PG_RETURN_TIMESTAMPTZ(timestamp);
+
+     result = timestamptz_trunc_internal(units, timestamp, session_timezone);
+
+     PG_RETURN_TIMESTAMPTZ(result);
+ }
+
+ /* timestamptz_trunc_zone()
+  * Truncate timestamptz to specified units in specified timezone.
+  */
+ Datum
+ timestamptz_trunc_zone(PG_FUNCTION_ARGS)
+ {
+     text       *units = PG_GETARG_TEXT_PP(0);
+     TimestampTz timestamp = PG_GETARG_TIMESTAMPTZ(1);
+     text       *zone = PG_GETARG_TEXT_PP(2);
+     TimestampTz result;
+     char        tzname[TZ_STRLEN_MAX + 1];
+     char       *lowzone;
+     int            type,
+                 val;
+     pg_tz       *tzp;
+
+     /*
+      * timestamptz_zone() doesn't look up the zone for infinite inputs, so we
+      * don't do so here either.
+      */
+     if (TIMESTAMP_NOT_FINITE(timestamp))
+         PG_RETURN_TIMESTAMP(timestamp);
+
+     /*
+      * Look up the requested timezone (see notes in timestamptz_zone()).
+      */
+     text_to_cstring_buffer(zone, tzname, sizeof(tzname));
+
+     /* DecodeTimezoneAbbrev requires lowercase input */
+     lowzone = downcase_truncate_identifier(tzname,
+                                            strlen(tzname),
+                                            false);
+
+     type = DecodeTimezoneAbbrev(0, lowzone, &val, &tzp);
+
+     if (type == TZ || type == DTZ)
+     {
+         /* fixed-offset abbreviation, get a pg_tz descriptor for that */
+         tzp = pg_tzset_offset(-val);
+     }
+     else if (type == DYNTZ)
+     {
+         /* dynamic-offset abbreviation, use its referenced timezone */
+     }
+     else
+     {
+         /* try it as a full zone name */
+         tzp = pg_tzset(tzname);
+         if (!tzp)
+             ereport(ERROR,
+                     (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                      errmsg("time zone \"%s\" not recognized", tzname)));
+     }
+
+     result = timestamptz_trunc_internal(units, timestamp, tzp);
+
      PG_RETURN_TIMESTAMPTZ(result);
  }

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4026018..9264a2e 100644
*** a/src/include/catalog/pg_proc.dat
--- b/src/include/catalog/pg_proc.dat
***************
*** 2280,2285 ****
--- 2280,2289 ----
    descr => 'truncate timestamp with time zone to specified units',
    proname => 'date_trunc', provolatile => 's', prorettype => 'timestamptz',
    proargtypes => 'text timestamptz', prosrc => 'timestamptz_trunc' },
+ { oid => '1284',
+   descr => 'truncate timestamp with time zone to specified units in specified time zone',
+   proname => 'date_trunc', provolatile => 's', prorettype => 'timestamptz',
+   proargtypes => 'text timestamptz text', prosrc => 'timestamptz_trunc_zone' },
  { oid => '1218', descr => 'truncate interval to specified units',
    proname => 'date_trunc', prorettype => 'interval',
    proargtypes => 'text interval', prosrc => 'interval_trunc' },
diff --git a/src/test/regress/expected/timestamptz.out b/src/test/regress/expected/timestamptz.out
index 2340f30..8a4c719 100644
*** a/src/test/regress/expected/timestamptz.out
--- b/src/test/regress/expected/timestamptz.out
*************** SELECT '' AS date_trunc_week, date_trunc
*** 649,654 ****
--- 649,672 ----
                   | Mon Feb 23 00:00:00 2004 PST
  (1 row)

+ SELECT '' AS date_trunc_at_tz, date_trunc('day', timestamp with time zone '2001-02-16 20:38:40+00',
'Australia/Sydney')as sydney_trunc;  -- zone name 
+  date_trunc_at_tz |         sydney_trunc
+ ------------------+------------------------------
+                   | Fri Feb 16 05:00:00 2001 PST
+ (1 row)
+
+ SELECT '' AS date_trunc_at_tz, date_trunc('day', timestamp with time zone '2001-02-16 20:38:40+00', 'GMT') as
gmt_trunc; -- fixed-offset abbreviation 
+  date_trunc_at_tz |          gmt_trunc
+ ------------------+------------------------------
+                   | Thu Feb 15 16:00:00 2001 PST
+ (1 row)
+
+ SELECT '' AS date_trunc_at_tz, date_trunc('day', timestamp with time zone '2001-02-16 20:38:40+00', 'VET') as
vet_trunc; -- variable-offset abbreviation 
+  date_trunc_at_tz |          vet_trunc
+ ------------------+------------------------------
+                   | Thu Feb 15 20:00:00 2001 PST
+ (1 row)
+
  -- Test casting within a BETWEEN qualifier
  SELECT '' AS "54", d1 - timestamp with time zone '1997-01-02' AS diff
    FROM TIMESTAMPTZ_TBL
diff --git a/src/test/regress/sql/timestamptz.sql b/src/test/regress/sql/timestamptz.sql
index f17d153..c3bd46c 100644
*** a/src/test/regress/sql/timestamptz.sql
--- b/src/test/regress/sql/timestamptz.sql
*************** SELECT '' AS "54", d1 - timestamp with t
*** 193,198 ****
--- 193,202 ----

  SELECT '' AS date_trunc_week, date_trunc( 'week', timestamp with time zone '2004-02-29 15:44:17.71393' ) AS
week_trunc;

+ SELECT '' AS date_trunc_at_tz, date_trunc('day', timestamp with time zone '2001-02-16 20:38:40+00',
'Australia/Sydney')as sydney_trunc;  -- zone name 
+ SELECT '' AS date_trunc_at_tz, date_trunc('day', timestamp with time zone '2001-02-16 20:38:40+00', 'GMT') as
gmt_trunc; -- fixed-offset abbreviation 
+ SELECT '' AS date_trunc_at_tz, date_trunc('day', timestamp with time zone '2001-02-16 20:38:40+00', 'VET') as
vet_trunc; -- variable-offset abbreviation 
+
  -- Test casting within a BETWEEN qualifier
  SELECT '' AS "54", d1 - timestamp with time zone '1997-01-02' AS diff
    FROM TIMESTAMPTZ_TBL

В списке pgsql-hackers по дате отправления:

Предыдущее
От: Tom Lane
Дата:
Сообщение: Re: add the source of param misconfigurations to error messages
Следующее
От: Andres Freund
Дата:
Сообщение: Re: TupleTableSlot abstraction