时间戳类型
时间戳表示时间点。因此,它结合了 DATE 与 TIME 信息。
可以使用“类型名 + 按 ISO 8601 格式编写的字符串”来创建时间戳,即 YYYY-MM-DD hh:mm:ss[.zzzzzzzzz][+-TT[:tt]]。本文档也统一使用该格式。超出支持精度的小数位会被忽略。
时间戳类型
| Name | Aliases | Description |
|---|---|---|
TIMESTAMP_NS | 朴素时间戳,纳秒精度 | |
TIMESTAMP | DATETIME, TIMESTAMP WITHOUT TIME ZONE | 朴素时间戳,微秒精度 |
TIMESTAMP_MS | 朴素时间戳,毫秒精度 | |
TIMESTAMP_S | 朴素时间戳,秒精度 | |
TIMESTAMPTZ | TIMESTAMP WITH TIME ZONE | 时区感知时间戳,微秒精度 |
警告 目前不存在
TIMESTAMP_NS WITH TIME ZONE数据类型,因此外部数据中同时具备纳秒精度和WITH TIME ZONE语义的列(例如 Parquet 中isAdjustedToUTC=true的时间戳列)在 Goose 中读取时会转换为TIMESTAMP WITH TIME ZONE,从而损失精度。
SELECT TIMESTAMP_NS '1992-09-20 11:30:00.123456789';
1992-09-20 11:30:00.123456789
SELECT TIMESTAMP '1992-09-20 11:30:00.123456789';
1992-09-20 11:30:00.123456
SELECT TIMESTAMP_MS '1992-09-20 11:30:00.123456789';
1992-09-20 11:30:00.123
SELECT TIMESTAMP_S '1992-09-20 11:30:00.123456789';
1992-09-20 11:30:00
SELECT TIMESTAMPTZ '1992-09-20 11:30:00.123456789';
1992-09-20 11:30:00.123456+00
SELECT TIMESTAMPTZ '1992-09-20 12:30:00.123456789+01:00';
1992-09-20 11:30:00.123456+00
Goose 区分 WITHOUT TIME ZONE 与 WITH TIME ZONE 两类时间戳(目前 WITH TIME ZONE 的具体类型仅有 TIMESTAMP WITH TIME ZONE)。
尽管名字如此,TIMESTAMP WITH TIME ZONE 实际并不存储时区信息。它只存储自 Unix 纪元 1970-01-01 00:00:00+00 以来、以 INT64 表示的非闰秒微秒数,因此能无歧义地标识绝对时间中的一个点,即 instant。之所以称其为 time zone aware 或 WITH TIME ZONE,是因为该类型的时间戳运算、分箱 与字符串格式化,会在配置的时区下执行;默认时区是系统时区,在上面的示例中为 UTC+00:00。
对应的 TIMESTAMP WITHOUT TIME ZONE 存储的也是同样的 INT64,但其算术、分箱与字符串格式化遵循不包含偏移量和时区概念的 UTC 规则。因此,TIMESTAMP 可以被视为 UTC 时间戳;但更常见的是,它被用于表示在未指定时区下记录的“本地”时间观测值,对该类型的操作也可理解为基于名义时间逻辑对时间元组字段进行处理。
在数据清洗中,一个常见问题是将这类可能以原始字符串(未带时区或 UTC 偏移)存储的观测值,消歧为明确的 TIMESTAMP WITH TIME ZONE instant。可行方案之一是先给字符串追加 UTC 偏移,再显式转换为 TIMESTAMP WITH TIME ZONE。也可以先创建 TIMESTAMP WITHOUT TIME ZONE,再结合时区信息得到时区感知的 TIMESTAMP WITH TIME ZONE。
字符串与朴素/时区感知时间戳之间的转换
不带 UTC 偏移或 IANA 时区名称的字符串,转换到 WITHOUT TIME ZONE 类型时是明确且直接的。
带 UTC 偏移或时区名称的字符串,转换到 WITH TIME ZONE 类型同样明确,但处理时区名称需要 ICU 扩展。
当不带 UTC 偏移或时区名称的字符串转换为 WITH TIME ZONE 类型时,会按当前配置时区解释该字符串。
当带 UTC 偏移的字符串传入 WITHOUT TIME ZONE 类型时,偏移量或时区说明会被忽略。
当带 UTC 之外时区名称的字符串传入 WITHOUT TIME ZONE 类型时,会报错。
最后,当 WITH TIME ZONE 与 WITHOUT TIME ZONE 类型通过显式或隐式转换互转时,转换会使用当前配置时区。若要使用其他时区,可使用 ICU 扩展提供的 timezone 函数:
SELECT
timezone('America/Denver', TIMESTAMP '2001-02-16 20:38:40') AS aware1,
timezone('America/Denver', TIMESTAMPTZ '2001-02-16 04:38:40') AS naive1,
timezone('UTC', TIMESTAMP '2001-02-16 20:38:40+00:00') AS aware2,
timezone('UTC', TIMESTAMPTZ '2001-02-16 04:38:40 Europe/Berlin') AS naive2;
| aware1 | naive1 | aware2 | naive2 |
|---|---|---|---|
| 2001-02-17 04:38:40+01 | 2001-02-15 20:38:40 | 2001-02-16 21:38:40+01 | 2001-02-16 03:38:40 |
注意:结果中的 TIMESTAMP 按 ISO 8601 的本地时间规则显示,不带时区标记;而时区感知的 TIMESTAMPTZ 会显示配置时区的 UTC 偏移,在此示例中为 'Europe/Berlin'。在示例涉及的各个 instant 上,'America/Denver' 与 'Europe/Berlin' 的 UTC 偏移分别为 -07:00 与 +01:00。
特殊值
可使用三个特殊字符串来创建时间戳:
| Input string | Description |
|---|---|
epoch | 1970-01-01 00:00:00[+00] (Unix system time zero) |
infinity | Later than all other timestamps |
-infinity | Earlier than all other timestamps |
infinity 与 -infinity 是特殊值,显示时保持不变;epoch 只是记法简写,在读取时会转换为对应的时间戳值。
SELECT '-infinity'::TIMESTAMP, 'epoch'::TIMESTAMP, 'infinity'::TIMESTAMP;
| Negative | Epoch | Positive |
|---|---|---|
| -infinity | 1970-01-01 00:00:00 | infinity |
函数
请参阅时间戳函数。
时区
要理解时区与 WITH TIME ZONE 类型,先掌握两个概念会更有帮助:instants 和 temporal binning。
Instants
instant 是绝对时间中的一个点,通常表示为从某个固定时间点(称为 epoch)起经过的时间单位计数。这类似于地表位置使用相对赤道和格林尼治子午线的经纬度来描述。在 Goose 中,这个固定点是 Unix 纪元 1970-01-01 00:00:00+00:00,计数单位则取决于具体数据类型,可为秒、毫秒、微秒或纳秒。
Temporal Binning
分箱(binning)是处理连续数据的常见做法:将可能值范围切分为连续子区间,再将实际值映射到其落入的 bin。temporal binning 只是把这一做法应用到 instants 上,例如把 instants 分到年、月、日等时间箱中。
temporal binning 规则很复杂,通常由两类规则共同决定:time zones 与 calendars。
对大多数任务而言,日历通常就是广泛使用的公历(Gregorian);
但时区采用地区特定规则,差异可能很大。
例如,'America/Los_Angeles' 时区在纪元附近的分箱情况如下:
最常见的 temporal binning 问题出现在夏令时切换时。 下面的示例包含一次夏令时变更,其中“小时”这个 bin 长达两小时。 要区分这两个小时,还需要引入另一组包含 UTC 偏移的 bin:
时区支持
TIMESTAMPTZ 类型可借助相应扩展分到日历与时钟 bin 中。
内置的 ICU 扩展基于
International Components for Unicode 的时区与日历能力,实现了所有相关分箱和算术函数。
要设置使用的时区,先加载 ICU 扩展。多个 Goose 客户端(包括 Python、R、JDBC 和 ODBC)已内置 ICU 扩展,这些情况下可跳过该步骤。其他情况下,你可能需要先安装并加载 ICU 扩展。
INSTALL icu;
LOAD icu;
然后,使用 SET TimeZone 命令:
SET TimeZone = 'America/Los_Angeles';
之后,TIMESTAMPTZ 的时间分箱操作将使用该时区执行。
可用时区列表可通过 pg_timezone_names() 表函数获取:
SELECT
name,
abbrev,
utc_offset
FROM pg_timezone_names()
ORDER BY
name;
你也可以查阅可用时区参考表。
日历支持
ICU 扩展也支持非公历日历,可通过 SET Calendar 命令设置。
注意:仅当 Goose 客户端未内置 ICU 扩展时,才需要 INSTALL 与 LOAD 步骤。
INSTALL icu;
LOAD icu;
SET Calendar = 'japanese';
之后,TIMESTAMPTZ 的时间分箱操作将使用该日历执行。
在此示例中,era 部分将返回日本年号纪元编号。
可用日历列表可通过 icu_calendar_names() 表函数获取:
SELECT name
FROM icu_calendar_names()
ORDER BY 1;
设置
TimeZone 与 Calendar 设置的当前值由 ICU 启动时确定。
可在 goose_settings() 表函数中查询:
SELECT *
FROM goose_settings()
WHERE name = 'TimeZone';
| name | value | description | input_type |
|---|---|---|---|
| TimeZone | Europe/Amsterdam | The current time zone | VARCHAR |
SELECT *
FROM goose_settings()
WHERE name = 'Calendar';
| name | value | description | input_type |
|---|---|---|---|
| Calendar | gregorian | The current calendar | VARCHAR |
如果分箱操作结果与你预期不一致,请检查
TimeZone与Calendar的值,并按需调整。