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
|
| Список | 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 по дате отправления: