跳到主要内容

时间戳问题

带时区时间戳的提升转换

在 SQL 中处理时区有时会非常容易混淆。 例如,按日期范围过滤时,你可能会写出如下查询:

SET timezone = 'America/Los_Angeles';

CREATE TABLE times AS
FROM range('2025-08-30'::TIMESTAMPTZ, '2025-08-31'::TIMESTAMPTZ, INTERVAL 1 HOUR) tbl(t);

FROM times WHERE t <= '2025-08-30';
┌──────────────────────────┐
│ t │
│ timestamp with time zone │
├──────────────────────────┤
│ 2025-08-30 00:00:00-07 │
└──────────────────────────┘

但如果切换到其他时区,查询结果也会变化:

SET timezone = 'HST';
FROM times WHERE t <= '2025-08-30';
┌──────────────────────────┐
│ t │
│ timestamp with time zone │
├──────────────────────────┤
│ 2025-08-29 21:00:00-10 │
│ 2025-08-29 22:00:00-10 │
│ 2025-08-29 23:00:00-10 │
│ 2025-08-30 00:00:00-10 │
└──────────────────────────┘

甚至更糟:

SET timezone = 'America/New_York';
FROM times WHERE t <= '2025-08-30';
┌──────────────────────────┐
│ t │
│ timestamp with time zone │
├──────────────────────────┤
│ 0 rows │
└──────────────────────────┘

这些令人困惑的结果源于 SQL 从 DATETIMESTAMP WITH TIME ZONE 的转换规则。 该转换会把日期提升为当前时区的午夜时刻。

一般来说,除非你确实需要用当前时区做展示(或进行其他时间分桶), 否则建议时序数据使用普通 TIMESTAMP。 这样可避免此类困惑,且算术运算通常更快。

时区性能

Goose 使用 International Components for Unicode(ICU)时间库提供时区支持。 该库有多个优势,包括支持 2037 年之后的夏令时。 (注意:Pandas 在该年份之后会给出错误结果。)

使用 ICU 的代价是性能不算高。 一种常见优化方式是为目标时间范围预先构建日历表。 例如,如果业务要按小时粒度建模到 2100 年的供需数据,可这样创建日历表:

SET timezone = 'Europe/Amsterdam';

CREATE OR REPLACE TABLE hourly AS
SELECT
ts,
year::SMALLINT AS year,
month::TINYINT AS month,
day::TINYINT AS day,
hour::TINYINT AS hour,
FROM (
SELECT ts, unnest(date_part(['year', 'month', 'day', 'hour',], ts))
FROM generate_series(
'2020-01-01'::DATE::TIMESTAMPTZ,
'2100-01-01'::DATE::TIMESTAMPTZ,
INTERVAL 1 HOUR) tbl(ts)
) parts;

然后你可以把这张约 70 万行的表与任意时间戳列 join, 快速获得目标时区下的时间分桶值。 内部 cast 不是必须,但能让表更小, 因为 date_part 默认对所有部分返回 64 位整数。

注意,我们可以一次调用 date_part 就提取全部部分。 这种“部件列表”写法比逐个提取更快, 因为底层分桶计算本来就会算出所有部分, 按列表取值可避免重复调用较慢的 ICU 函数。

另外,这里也利用了上一节 DATE 的 cast 规则, 把日历范围限制在模型域内。

半开区间

在 SQL 里做时序分析时,另一个容易忽视的问题是 BETWEEN。 时序分析几乎总是使用半开分桶区间, 以避免边界重叠。 但 BETWEEN 实际上是闭区间:

x BETWEEN begin AND end
-- expands to
begin <= x AND x <= end
-- not
begin <= x AND x < end

为避免这个问题,请显式写比较边界,而不要直接用 BETWEEN