时区
ClickHouse 如何转换 DateTime 字符串
DateTime 字符串值:
- 如果某一列定义了时区 (
DateTime64(9, ‘Asia/Tokyo’)) ,则该字符串值会被视为该时区中的时间戳。2026-01-01 13:00:00换算为UTC时间后将是2026-01-01 04:00:00。 - 如果某一列没有定义时区,则只使用服务器时区。注意:
session_timezone设置不起作用。因此,如果服务器时区为UTC,而会话时区为America/Los_Angeles,那么2026-01-01 13:00:00将按UTC时间写入。 - 从未定义时区的列中读取值时,会使用
session_timezone;如果未设置,则使用服务器时区。这就是为什么将时间戳作为字符串读取时会受到session_timezone的影响。这本身没有问题,但需要注意这一点。
跨时区写入时间戳
us-west 区域,本地时区为 UTC-8,我们需要写入一个本地时间戳 2026-01-01 02:00:00,它对应的 UTC 时间为 2026-01-01 10:00:00:
- 如果以字符串形式写入,就需要先将其转换为服务器时区或列时区。
- 如果以语言原生的时间结构写入,则要求驱动知道目标时区,但:
- 这并不总是可行的
- 驱动 API 在这方面的设计并不理想
- 唯一的办法是明确会执行哪些转换,以便应用进行补偿 (或者将 Unix 时间戳 作为数值写入)
Java 和 JDBC 的 timestamp API
- 使用
Timestamp类,它本质上是一个 Unix 时间戳。- 与
Calendar对象一起使用时,可以按照该日历的时区重新解释Timestamp。 Timestamp有一个不太直观的内部日历。
- 与
- 使用
LocalDateTime类,它很容易转换为任意时区,但没有方法允许你传入目标时区。 - 使用
ZonedDateTime类,在写入不带时区的DateTime时,它有助于进行时区转换 (因为我们知道应使用服务器时区) 。- 但将
ZonedDateTime写入定义了时区的列时,用户需要自行补偿驱动程序的转换。
- 但将
- 使用
Long写入 Unix 时间戳 的毫秒值。 - 使用
String在应用程序端完成所有转换 (这并不是很可移植) 。
日期
Date 和 Date32 类型用于存储日期。这两种类型都使用自纪元 (1970-01-01) 以来的天数。Date 仅使用正整数天数,因此其范围截止于 2149-06-06。Date32 支持负整数天数,以覆盖 1970-01-01 之前的日期,但它的范围更小 (从 1900-01-01 到 2100-01-01,其中 0 对应 1970-01-01) 。无论在哪个时区,ClickHouse 都会将 2026-01-01 视为 2026-01-01,并且列定义中没有时区参数。
使用 java.time.LocalDate
java.time.LocalDate。客户端使用该类存储 Date 和 Date32 列的值 (读取 LocalDate.ofEpochDay((long)readUnsignedShortLE())) 。
我们建议使用 java.time.LocalDate,因为它不受时区转换影响,并且属于现代时间 API。
使用 java.sql.Date
LocalDate 是在 Java 8 中引入的。在此之前,读写日期使用的是 java.sql.Date。从内部实现来看,这个类本质上是对某个瞬时值的封装 (即表示时间轴上绝对时间点的时间值) 。因此,toString() 返回的日期会因 JVM 所在时区不同而有所差异。这就要求驱动程序在构造值时格外谨慎,也要求用户清楚这一点。
基于日历的重新诠释
java.sql.ResultSet 提供了一个可接收 Calendar 参数的日期值获取方法,java.sql.PreparedStatement 中也有类似的方法。这样设计是为了让 JDBC 驱动按照指定时区重新诠释日期值。例如,DB 中的值是 2026-01-01,但应用程序希望将该日期视为 Tokyo 时区的午夜。这意味着返回的 java.sql.Date 对象会对应某个特定时刻;当它转换到本地时区时,由于时差,可能会变成不同的日期。对于 LocalDate,也可以通过 java.time.LocalDate#atStartOfDay(java.time.ZoneId) 实现同样的效果。
ClickHouse JDBC 驱动始终返回一个表示本地日期午夜的 java.sql.Date 对象。换句话说,如果日期是 2026-01-01,它表示的是 JVM 时区中的 2026-01-01 12:00 AM (其行为与 PostgreSQL 和 MariaDB 的 JDBC 驱动相同) 。
Time
’6:30’ 在任何地方读取都一样。
ClickHouse Time 类型
Time 和 Time64 是在 25.6 版本中引入的。在此之前,使用的是时间戳类型 DateTime 和 DateTime64 (将在本指南后文讨论) 。Time 以 32 位整数的秒数形式存储,取值范围为 [-999:59:59, 999:59:59]。Time64 编码为无符号 Decimal64,并会根据精度存储不同的时间单位。常见的精度选择有 3 (毫秒) 、6 (微秒) 和 9 (纳秒) 。精度取值范围为 [0, 9]。
Java 类型映射
Time 和 Time64,并将其存储为 LocalDateTime。这样做是为了支持负时间范围 (LocalTime 不支持) 。在这种情况下,日期部分为纪元日期 1970-01-01,因此负值会落在该日期之前。
对时间类型的主要支持通过 LocalTime (当值在一天以内时) 和 Duration 实现,以覆盖完整的取值范围。LocalDateTime 只能用于读取。
使用 java.sql.Time
java.sql.Time 的使用仅限于 LocalTime 的取值范围。在内部,java.sql.Time 会被转换为字符串字面量。可以通过在 PreparedStatement#setTime() 中传入 Calendar 参数来更改该值。
toTime 函数
toTime始终要求输入Date、DateTime或其他类似类型,不接受字符串。相关问题:https://github.com/ClickHouse/ClickHouse/issues/89896- 它是
toTimeWithFixedDate的别名。 - 存在一个与时区相关的问题:https://github.com/ClickHouse/ClickHouse/pull/90310
时间戳
1970-01-01 00:00:00 UTC 的秒数 (负数表示 Unix 时间之前的时间戳,正数表示之后的时间戳) 。如果观察者处于 UTC 时区,或者使用 UTC 而非本地时区,这种表示方式就很容易计算和处理。
ClickHouse 时间戳类型
DateTime (32 位整数,分辨率始终为秒) 和 DateTime64 (64 位整数,分辨率取决于定义) 。值始终以 UTC 时间戳存储。这意味着,当它们以数字形式表示时,不会进行任何时区转换。
字符串表示形式和时区行为
- 如果列定义中未指定时区,且写入时传入的是字符串,它会从服务器时区转换为 UTC timestamp 数值。从这类列中读取值时,则会将 UTC timestamp 转换为字面 timestamp,并使用服务器时区或会话时区 (对于 expression 中未显式定义时区的 timestamp 字面量,也采用类似的处理方式) 。
- 如果列定义中指定了时区,那么所有字符串转换都只会使用该时区。这与未指定时区时的逻辑不同,因此需要充分理解查询中每一列的数据是如何写入的。
- 如果以包含时区的格式将日期作为字符串传入,则需要使用转换函数。通常使用
parseDateTimeBestEffort。
JDBC 驱动如何处理时间戳
timestamp 值的大多数转换问题,因为它会以统一的格式将数据发送到服务器。不过,这种方法需要对 SQL 语句做一些小调整,但它也是将时间戳写入任意列的最简单、最直接的方法。
DateTime 和 DateTime64 在客户端会以 java.time.ZonedDateTime 的形式读取和存储,这有助于将这类值转换为任何其他时区 (时区信息会被保留) 。
toDateTime64 的常见陷阱
toDateTime64 使用的是服务器时区,无法识别源时区。
转换表
Date 列不能读取为 java.sql.Timestamp,因为它不包含时间部分。
驱动程序不会将整数值转换为任何日期/时间值。调用 pstmt.setLong("timestamp", 1772132359L) 会导致 1772132359 作为数字写入服务器,并被视为
UTC Unix 时间戳 (以秒为单位) 。
使用 PreparedStatement#setObject 写入值
PreparedStatement#setObject(column, value) 设置值时的转换方式:
应将该列的类型视为未知,具体向预处理语句传递什么值由应用程序决定。
使用 ResultSet#getObject 读取值
ResultSet#getObject(column, class) 读取时,值会如何转换:
使用基于 Calendar 的方法
PreparedStatement#setTime(param, value, calendar) 和 PreparedStatement#setDate(param, value, calendar) 存储,则应相应使用 ResultSet#getTime(column, calendar) 和 ResultSet#getDate(column, calendar)。