英文:
Display timestamptz in a user's timezone
问题
I have a timestamptz
column. The end users of my application would like to view these timestamps in their own timezone, with the offset included. For example,
Given a row containing 2023-08-10T12:00:00.000+00
When the user queries from time zone 'US/Eastern'
Then the output they should see is 2023-08-10T08:00:00.000-04
If I execute this in a SQL console, it gives me the exact result I want,
SET LOCAL TIME ZONE 'US/Eastern';
SELECT to_char('2023-08-10T12:00:00.000+00'::TIMESTAMPTZ, 'YYYY-MM-DD"T"HH24:MI:SS.MSOF');
2023-08-10T08:00:00.000000-04
However, I do not know how to use this from within my Spring Boot application.
- According to my benchmarks, if I try to do this in code, it comes with a ~30% performance penalty, which is significant in this case, so I would like to do it entirely in SQL if possible.
- I cannot execute two separate statements separated by
;
in aPreparedStatement
- I do not want to change the default time zone for the database, since there may be multiple concurrent requests.
- For the same reason, I do not want to change the default time zone for the JVM.
So, an answer to either of these questions would be a huge help:
- In jdbc, is there a way to set the time zone for only a single query, without affecting other concurrent queries?
- In PostgreSQL, is there a way I can express the above SQL in a single line (no semi colons)?
英文:
I'm working on a Spring Boot application which queries a PostgreSQL database.
I have a timestamptz
column. The end users of my application would like to view these timestamps in their own timezone, with the offset included. For example,
Given a row containing 2023-08-10T12:00:00.000+00
When the user queries from time zone 'US/Eastern'
Then the output they should see is 2023-08-10T08:00:00.000-04
Sounds simple enough, but I'm having trouble figuring out how to handle this given that there will be multiple concurrent requests coming from different time zones.
If I execute this in a SQL console, it gives me the exact result I want,
SET LOCAL TIME ZONE 'US/Eastern';
SELECT to_char('2023-08-10T12:00:00.000+00'::TIMESTAMPTZ, 'YYYY-MM-DD"T"HH24:MI:SS.MSOF');
2023-08-10T08:00:00.000000-04
However, I do not know how to use this from within my Spring Boot application.
- According to my benchmarks, if I try to do this in code, it comes with a ~30% performance penalty, which is significant in this case, so I would like to do it entirely in SQL if possible.
- I cannot execute two separate statements separated by
;
in aPreparedStatement
- I do not want to change the default time zone for the database, since there may be multiple concurrent requests.
- For the same reason, I do not want to change the default time zone for the JVM.
So, an answer to either of these questions would be a huge help:
- In jdbc, is there a way to set the time zone for only a single query, without affecting other concurrent queries?
- In PostgreSQL, is there a way I can express the above SQL in a single line (no semi colons)?
答案1
得分: 1
这是您要翻译的部分:
这个您发布的函数依赖于当前会话的timezone
设置,如果不恰好是UTC的话,就会显示不正确的偏移。
另外,它比必要的复杂。请尝试以下代码:
CREATE OR REPLACE FUNCTION display_at_zone(tstz timestamptz, display_zone text)
RETURNS text
LANGUAGE plpgsql STABLE PARALLEL SAFE AS -- 不是IMMUTABLE!
$func$
DECLARE
at_zone timestamp := tstz AT TIME ZONE display_zone;
BEGIN
RETURN to_char(at_zone, 'YYYY-MM-DD"T"HH24:MI:SS.MS')
|| to_char(at_zone - tstz AT TIME ZONE 'UTC', 'HH:MI'); -- 修正!
END
$func$;
在我的测试中,这个更简单的等价函数比您的函数快大约两倍。
请注意,我使用了at_zone - tstz AT TIME ZONE 'UTC'
而不是at_zone - tstz
,以获得正确的偏移,独立于当前的timezone
设置。
将其设置为STABLE
,而不是IMMUTABLE
,因为to_char()
只是STABLE
。因此,它可以在外部查询中进行“内联”。参见:
将其设置为PARALLEL SAFE
(因为它是安全的),这样它就不会妨碍并行化。参见:
在PL/pgSQL中,不要使用比所需更多的赋值,因为这些赋值相对昂贵。
或者,使用纯SQL更短的代码:
CREATE OR REPLACE FUNCTION display_at_zone_sql(tstz timestamptz, display_zone text)
RETURNS text
LANGUAGE sql STABLE PARALLEL SAFE AS
$func$
SELECT to_char(tstz AT TIME ZONE display_zone, 'YYYY-MM-DD"T"HH24:MI:SS.MS')
|| to_char(tstz AT TIME ZONE display_zone
- tstz AT TIME ZONE 'UTC', 'HH:MI');
$func$;
PL/pgSQL和SQL函数具有稍微不同的性能特性。这个SQL函数可以内联,并且在嵌套在更大的查询中时明显更快。参见:
也许,直接将其转换为text
就足够了?与您的格式略有不同:
CREATE OR REPLACE FUNCTION display_at_zone_sql3(tstz timestamptz, display_zone text)
RETURNS text
LANGUAGE sql STABLE PARALLEL SAFE AS
$func$
SELECT (tstz AT TIME ZONE display_zone)::text -- 是否足够?
|| to_char(tstz AT TIME ZONE display_zone
- tstz AT TIME ZONE 'UTC', 'HH:MI');
$func$;
更快,而且更短。
英文:
The function you posted depends on the timezone
setting of the current session, and shows an incorrect offset if that doesn't happen to be UTC.
Also, it's more convoluted than it needs to be. Try instead:
CREATE OR REPLACE FUNCTION display_at_zone(tstz timestamptz, display_zone text)
RETURNS text
LANGUAGE plpgsql STABLE PARALLEL SAFE AS -- not IMMUTABLE!
$func$
DECLARE
at_zone timestamp := tstz AT TIME ZONE display_zone;
BEGIN
RETURN to_char(at_zone, 'YYYY-MM-DD"T"HH24:MI:SS.MS')
|| to_char(at_zone - tstz AT TIME ZONE 'UTC', 'HH:MI'); -- FIX!
END
$func$;
This simpler equivalent is about twice as fast as your function in my tests.
Note how I work with at_zone - tstz AT TIME ZONE 'UTC'
instead of at_zone - tstz
to get the correct offset, independent of the current timezone
setting.
Make it STABLE
, not IMMUTABLE
, because to_char()
is only STABLE
. So it can be "inlined" in an outer query. See:
Make it PARALLEL SAFE
(because it is) so it does not stand in the way of parallelization. See:
Don't use more assignments than needed in PL/pgSQL, where those are comparatively expensive.
Or shorter, yet, with plain SQL:
CREATE OR REPLACE FUNCTION display_at_zone_sql(tstz timestamptz, display_zone text)
RETURNS text
LANGUAGE sql STABLE PARALLEL SAFE AS
$func$
SELECT to_char(tstz AT TIME ZONE display_zone, 'YYYY-MM-DD"T"HH24:MI:SS.MS')
|| to_char(tstz AT TIME ZONE display_zone
- tstz AT TIME ZONE 'UTC', 'HH:MI');
$func$;
PL/pgSQL and SQL functions have slightly different performance characteristics. This SQL function can be inlined and is noticeably faster when nested in a bigger query. See:
Maybe, a plain cast to text
is good enough? Differs slightly from your format:
CREATE OR REPLACE FUNCTION display_at_zone_sql3(tstz timestamptz, display_zone text)
RETURNS text
LANGUAGE sql STABLE PARALLEL SAFE AS
$func$
SELECT (tstz AT TIME ZONE display_zone)::text -- does the job?
|| to_char(tstz AT TIME ZONE display_zone
- tstz AT TIME ZONE 'UTC', 'HH:MI');
$func$;
Faster, yet.
答案2
得分: 0
只需使用at time zone
子句将服务器的timestamptz转换为用户的时区,这里有一个示例:
select '2023-08-10T12:00:00.000+00' original_timestamptz
,'2023-08-10T12:00:00.000+00' at time zone 'US/Eastern' converted,
split_part(to_char('2023-08-10T12:00:00.000+00' at time zone 'US/Eastern',
'YYYY-MM-DD"T"HH24:MI:SS.MSOF'),'+',1)||
(select split_part(utc_offset::text,':',1) from pg_timezone_names where name = 'US/Eastern')
with_offset;
由于表达式timestamp with time zone AT TIME ZONE zone
返回timestamp without time zone
,因此需要"手动"将偏移量添加到所需的结果中,如官方文档中所示:official documentation。
英文:
Just use the clause at time zone
to convert server's timestamptz to user's time zone, here an example:
select '2023-08-10T12:00:00.000+00' original_timestamptz
,'2023-08-10T12:00:00.000+00' at time zone 'US/Eastern' converted,
split_part(to_char('2023-08-10T12:00:00.000+00' at time zone 'US/Eastern',
'YYYY-MM-DD"T"HH24:MI:SS.MSOF'),'+',1)||
(select split_part(utc_offset::text,':',1) from pg_timezone_names where name = 'US/Eastern')
with_offset;
It's necessary to "manually" add the offset to the desired result since the expression timestamp with time zone AT TIME ZONE zone
returns timestamp without time zone
as can be seen in official documentation
答案3
得分: 0
以下是翻译好的部分:
创建或替换函数 display_at_zone(
tstz TIMESTAMPTZ,
display_zone VARCHAR
) 返回 VARCHAR
AS
$$
DECLARE
at_zone TIMESTAMP;
utc_offset INTERVAL;
hours INT;
minutes INT;
string_no_zone VARCHAR;
BEGIN
at_zone = tstz AT TIME ZONE display_zone;
utc_offset = at_zone - tstz;
hours = EXTRACT(HOURS FROM utc_offset);
minutes = ABS(EXTRACT(MINUTES FROM utc_offset));
string_no_zone = TO_CHAR(at_zone, 'YYYY-MM-DD"T"HH24:MI:SS.MS');
RETURN string_no_zone || TO_CHAR(hours, 'SG00:') || TO_CHAR(minutes, 'FM00');
END
$$ IMMUTABLE LANGUAGE plpgsql;
另一个选项是在函数中执行我的原始建议,但这样的性能更差。
BEGIN
PERFORM set_config('timezone', display_zone, true /* local */);
RETURN to_char(tstz, 'YYYY-MM-DD"T"HH24:MI:SS.MSOF');
END
英文:
Turns out this is quite complicated. Here is a function I've come up with which meets the original requirements by combining all of Pepe's ideas from his answer and comments:
CREATE OR REPLACE FUNCTION display_at_zone(
tstz TIMESTAMPTZ,
display_zone VARCHAR
) RETURNS VARCHAR
AS
$$
DECLARE
at_zone TIMESTAMP;
utc_offset INTERVAL;
hours INT;
minutes INT;
string_no_zone VARCHAR;
BEGIN
at_zone = tstz AT TIME ZONE display_zone;
utc_offset = at_zone - tstz;
hours = EXTRACT(HOURS FROM utc_offset);
minutes = ABS(EXTRACT(MINUTES FROM utc_offset));
string_no_zone = TO_CHAR(at_zone, 'YYYY-MM-DD"T"HH24:MI:SS.MS');
RETURN string_no_zone || TO_CHAR(hours, 'SG00:') || TO_CHAR(minutes, 'FM00');
END
$$ IMMUTABLE LANGUAGE plpgsql;
This has (at least) two problems:
- Compared to doing the exact same operation using
java.time
, the performance is worse, not better. - Does not work if the
display_timezone
parameter is an offset, but this can be easily worked around.
Another option is to do my original suggestion in a function, but the performance of this is even worse.
BEGIN
PERFORM set_config('timezone', display_zone, true /* local */);
RETURN to_char(tstz, 'YYYY-MM-DD"T"HH24:MI:SS.MSOF');
END
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论