TDengine 夏令时(DST)实战指南:夏令时对写入、查询与展示的影响及最佳实践
TDengine 夏令时DST实战指南夏令时对写入、查询与展示的影响及最佳实践【免费下载链接】TDengineHigh-performance, scalable time-series database designed for Industrial IoT (IIoT) scenarios项目地址: https://gitcode.com/GitHub_Trending/tde/TDengine时序业务常常跨越使用夏令时Daylight Saving TimeDST的地区欧洲、北美、澳大利亚等地区的设备在同一台数据库中写入当地时间时春季与秋季各有一次时钟跳变直接威胁数据的可写性与可查性。本文以Europe/Berlin为例系统说明 TDengine 中夏令时对写入、查询与展示的具体影响并结合开源仓库中时间解析的源码实现解释其底层行为最后给出使用明确时间偏移量的最佳实践方案。读完本文你可以准确判断哪些写法会踩坑、如何在 REST API 与 Explorer 中安全地处理本地时间。一、基础概念时区、标准时间与夏令时1.1 时区与 IANA 时区数据库时区是地球上使用相同标准时间的区域。由于地球自转为保证各地时间与当地日出日落协调全球被划分为多个时区。IANAInternet Assigned Numbers Authority时区数据库也称为 tz database提供全球时区信息的标准参考是现代各类系统和软件处理时区相关操作的基础。IANA 使用区域/城市格式如Europe/Berlin来明确标识时区。TDengine 在不同组件中均支持使用 IANA 时区除 Windowstaos.cfg时区设置外。1.2 标准时间与当地时间标准时间是根据地球上某个固定经线确定的时间为各个时区提供统一参考点格林尼治标准时间GMT历史上使用的参考时间位于 0° 经线。协调世界时UTC现代的时间标准类似于 GMT但更加精确。标准时间与时区的关系基准标准时间如 UTC是时区设定的基准点偏移量不同时区通过相对于标准时间的偏移量定义如 UTC1 表示比 UTC 快 1 小时区域划分全球被划分为多个时区每个时区使用一个或多个标准时间。相对于标准时间每个地区根据其所在时区设定当地时间时区偏移当地时间等于标准时间加上该时区的偏移量如 UTC2 表示比 UTC 时间快 2 小时夏令时DST某些地区在特定时间段调整当地时间例如将时钟拨快一小时。1.3 夏令时DST及其在 IANA 数据库中的表达夏令时是一种通过将时间提前一小时以充分利用日光、节约能源的制度通常在春季开始、秋季结束。具体开始和结束时间因地区而异。以下均以柏林时间为例说明。按该规则可见柏林当地时间 2024 年 03 月 31 日 02:00:00 到 03:00:00不含 03:00:00之间的时间不存在跳变柏林当地时间 2024 年 10 月 27 日 02:00:00 到 03:00:00不含 03:00:00之间的时间出现了两次。IANA 时区数据库与 DST 的关系记录规则数据库详细记录各地的夏令时规则包括开始与结束的日期、时间自动调整许多操作系统和软件利用 IANA 数据库自动处理夏令时调整历史变更数据库还追踪历史上的夏令时变化以确保准确性。1.4 时间戳与当地时间转换的方向性这是理解后文所有行为的钥匙时间戳 → 当地时间是确定的。例如1729990654 为柏林时间夏令时2024-10-27 02:57:341729994254 为柏林时间冬令时2024-10-27 02:57:34这两个本地时间除时间偏移量外完全一样。当地时间 → 时间戳不指定时间偏移量时是不确定的夏令时跳过的时间不存在无法转换成时间戳如柏林时间2024-03-31 02:34:56不存在所以无法转换为时间戳夏令时结束时时间重复无法确定是哪一个时间戳如2024-10-27 02:57:34不指定偏移量时无法确定是 1729990654 还是 1729994254。指定时间偏移量才能确定时间戳如2024-10-27 02:57:34 CEST(02:00)指定了夏令时状态对应时间戳 1729990654。二、RFC 3339 时间格式RFC 3339 是一种互联网时间格式标准基于 ISO 8601 标准更具体地规定了一些格式细节基本格式YYYY-MM-DDTHH:MM:SSZ时区表示Z表示协调世界时UTC偏移量格式例如02:00表示与 UTC 的时差。通过明确的时区偏移RFC 3339 格式可以在全球范围内准确解析和比较时间。其优势在于标准化提供统一格式方便跨系统数据交换、清晰性明确时区信息避免时间误解。TDengine 在 REST API 和 Explorer UI 中均使用 RFC 3339 格式进行展示。在 SQL 语句中也可以使用 RFC 3339 格式写入时间戳数据INSERT INTO t1 values(2024-10-27T01:59:59.000Z, 0); SELECT * from t1 where ts 2024-10-27T01:59:59.000Z;从源码实现看带T分隔符且带时区后缀的时间字符串由 ttime.c 中的parseTimeWithTz处理它先按%Y-%m-%dT%H:%M:%S解析日期时间主体再区分Z直接按 UTC 计算与/-HH:MM解析出tzOffset后以*time tzOffset * factor修正两条路径。也就是说带显式偏移的 RFC 3339 时间在解析阶段就消除了任何时区二义性这正是后文推荐它作为写入/查询写法的原因。三、未定义行为Undefined Behavior未定义行为是指特定代码或操作没有明确规定的结果也不会对该结果作出兼容性保证——TDengine 可能在某个版本后对当前行为作出修改而不通知用户。因此在 TDengine 中用户不可依赖当前未定义的行为进行判断或应用。下文中夏令时开始时刻的不存在时间与夏令时结束时刻的重复时间在写入和查询中的各种异常表现均属于未定义行为。四、夏令时在 TDengine 中的写入与查询下表展示夏令时对写入与查询的影响TIMESTAMP 为 64 位整数原始时间戳UTC 为时间戳对应的 UTC 时间Europe/Berlin 为该时区对应的 RFC 3339 时间Local 为不含时区的当地时间4.1 夏令时开始3 月 31 日 02:00时钟往后跳一小时浅绿色是夏令时开始前一小时的时间戳深绿色是夏令时开始后一小时的时间戳红色为向 TDengine 数据库插入了不存在的当地时间使用INSERT INTO t1 values(2024-03-31 02:59:59,..)插入2024-03-31 02:00:00到2024-03-31 02:59:59的数据会被自动调整为 -1000在 TDengine 中属于未定义行为当前该值与数据库精度precision有关毫秒数据库为 -1000微秒数据库为 -1000000纳秒数据库为 -1000000000因为那一时刻在本地时间中不存在。4.2 夏令时结束10 月 27 日 03:00时钟往前跳一小时浅蓝色表示时钟跳变前一小时的时间戳深蓝色表示时钟跳变后一小时内的时间戳其无时区的当地时间与上一小时一致紫色表示时钟跳变一小时后的时间戳。4.3 规律总结当地时间变化由于夏令时调整导致当地时间变化某些时间段出现重复或缺失UTC 时间不变UTC 时间保持不变确保时间的一致性和顺序性RFC 3339格式时间显示了时间偏移量的变化夏令时开始后变为 02:00结束后变为 01:00。4.4 条件查询在跳变/重复时区的表现均为未定义行为夏令时开始时跳过的时间区间[03-31 02:00:00, 03-31 03:00:00)不存在用其查询行为不确定。不存在的本地时间戳被转换为-1000SELECT ts FROM t1 WHERE ts BETWEEN 2024-03-31 02:00:00 AND 2024-03-31 02:59:59; ts | -1000 | Query OK, 1 row(s) in set (0.003635s)当不存在的时间戳与存在的时间戳共同使用时结果同样不符合预期。以下为起始本地时间不存在SELECT ts, TO_ISO8601(ts,Z) FROM t1 WHERE ts BETWEEN 2024-03-31 02:00:00 AND 2024-03-31 03:59:59; ts | TO_ISO8601(ts,Z) | -1000 | 1969-12-31T23:59:59.000Z | 1711843200000 | 2024-03-31T00:00:00.000Z | 1711846799000 | 2024-03-31T00:59:59.000Z | 1711846800000 | 2024-03-31T01:00:00.000Z | 1711846801000 | 2024-03-31T01:00:01.000Z | Query OK, 5 row(s) in set (0.003339s)以下语句中第一个 SQL 查询截止时间不存在第二个截止时间存在第一个查询结果不符合预期SELECT ts, TO_ISO8601(ts,Z) FROM t1 WHERE ts BETWEEN 2024-03-31 01:00:00 AND 2024-03-31 02:00:00; Query OK, 0 row(s) in set (0.000930s) SELECT ts, TO_ISO8601(ts,Z) FROM t1 WHERE ts BETWEEN 2024-03-31 01:00:00 AND 2024-03-31 01:59:59; ts | TO_ISO8601(ts,Z) | 1711843200000 | 2024-03-31T00:00:00.000Z | 1711846799000 | 2024-03-31T00:59:59.000Z | Query OK, 2 row(s) in set (0.001227s)夏令时结束时跳变的时间区间[10-27 02:00:00, 10-27 03:00:00)不含10-27 03:00:00重复了两次用该区间内的时间戳查询同样属于未定义行为。查询[2024-10-27 02:00:00, 2024-10-27 03:00:00]之间结果包含了两次重复的时间戳以及2024-10-27 03:00:00这个时间点的数据SELECT ts, TO_ISO8601(ts,Z), TO_CHAR(ts, YYYY-MM-DD HH:mi:ss) FROM t1 WHERE ts BETWEEN 2024-10-27 02:00:00 AND 2024-10-27 03:00:00; ts | TO_ISO8601(ts,Z) | TO_CHAR(ts, YYYY-MM-DD HH:mi:ss) | 1729987200000 | 2024-10-27T00:00:00.000Z | 2024-10-27 02:00:00 | 1729990799000 | 2024-10-27T00:59:59.000Z | 2024-10-27 02:59:59 | 1729990800000 | 2024-10-27T01:00:00.000Z | 2024-10-27 02:00:00 | 1729994399000 | 2024-10-27T01:59:59.000Z | 2024-10-27 02:59:59 | 1729994400000 | 2024-10-27T02:00:00.000Z | 2024-10-27 03:00:00 | Query OK, 5 row(s) in set (0.001370s)但查询[2024-10-27 02:00:00.000, 2024-10-27 02:57:00.999]区间只能查询到第一个2024-10-27 02:00:00时间点的数据SELECT ts, TO_ISO8601(ts,Z), TO_CHAR(ts, YYYY-MM-DD HH:mi:ss) FROM t1 WHERE ts 2024-10-27 02:00:00 AND ts 2024-10-27 02:57:00.999; ts | TO_ISO8601(ts,Z) | TO_CHAR(ts, YYYY-MM-DD HH:mi:ss) | 1729987200000 | 2024-10-27T00:00:00.000Z | 2024-10-27 02:00:00 | Query OK, 1 row(s) in set (0.004480s)而查询[2024-10-27 02:00:01, 2024-10-27 02:57:35]却能查到 3 条数据包含一条 02:59:59 的当地时间数据SELECT ts, TO_ISO8601(ts,Z), TO_CHAR(ts, YYYY-MM-DD HH:mi:ss) FROM t1 WHERE ts 2024-10-27 02:00:00 AND ts 2024-10-27 02:57:35; ts | TO_ISO8601(ts,Z) | TO_CHAR(ts, YYYY-MM-DD HH:mi:ss) | 2024-10-27 02:00:00.000 | 2024-10-27T00:00:00.000Z | 2024-10-27 02:00:00 | 2024-10-27 02:59:59.000 | 2024-10-27T00:59:59.000Z | 2024-10-27 02:59:59 | 2024-10-27 02:00:00.000 | 2024-10-27T01:00:00.000Z | 2024-10-27 02:00:00 | Query OK, 3 row(s) in set (0.004428s)4.5 源码透视当地时间到时间戳的转换如何实现上述不存在的当地时间被转为 -1000的行为可以从时间解析源码中找到依据。不带时区后缀的本地时间字符串由 ttime.c 中的parseLocaltimeDst处理int32_t parseLocaltimeDst(char* timestr, int32_t len, int64_t* utime, int32_t timePrec, char delim, timezone_t tz) { *utime 0; struct tm tm {0}; tm.tm_isdst -1; /* 交由 mktime 依据 IANA 规则自行推断 DST 状态 */ ... int64_t seconds taosMktime(tm, tz); ... *utime TSDB_TICK_PER_SECOND(timePrec) * seconds fraction; ... }要点有二其一tm.tm_isdst -1表示让底层mktime依据 IANA 时区规则推断夏令时状态——这就是带时区语义的解析入口其二timezone_t tz由tzalloc从 IANA 时区名如Europe/Berlin加载而来见 ttime.c 与taosValidateTimezone时区字符串非法时会返回[0x26B2] Invalid timezone一类错误。对于春季跳变区间内不存在的本地时间mktime只能按某种方式强行归一化出一个时间戳当前表现为负值偏移这正是文档所描述的未定义行为来源而带显式偏移的 RFC 3339 字符串则走parseTimeWithTz路径偏移量在解析期即被确定不经过 DST 推断因此结果稳定。五、总结与建议5.1 总结仅针对使用当地时间带来的影响作说明使用 UNIX 时间戳或 RFC 3339 无影响写入无法写入夏令时跳变时不存在的时间数据写入夏令时跳变时重复的时间是未定义行为。查询查询条件指定夏令时开始时跳变的时间其查询结果为未定义行为查询条件指定夏令时结束时重复的时间其查询结果为未定义行为。显示带时区显示不受影响显示当地时间是准确的但夏令时结束时重复的时间无法区分用户应谨慎使用不带时区的时间进行展示和应用。5.2 建议为避免夏令时给查询和写入造成不必要的影响在 TDengine 中建议使用明确的时间偏移量进行写入和查询。1使用 UNIX 时间戳使用 UNIX 时间戳可完全避免时区问题。TIMESTAMPUTCEurope/BerlinLocal17118467990002024-03-31T00:59:59.000Z2024-03-31T01:59:59.00001:002024-03-31 01:59:5917118468000002024-03-31T01:00:00.000Z2024-03-31T03:00:00.00002:002024-03-31 03:00:00INSERT INTO t1 values(1711846799000, 1)(1711846800000, 2); Insert OK, 2 row(s) affected (0.001434s) select * from t1 where ts between 1711846799000 and 1711846800000; ts | v1 | 1711846799000 | 1 | 1711846800000 | 2 | Query OK, 2 row(s) in set (0.003503s)2使用 RFC 3339 时间格式带时区偏移量的 RFC 3339 时间格式可以有效避免夏令时的不确定性。TIMESTAMPUTCEurope/BerlinLocal17299872000002024-10-27T00:00:00.000Z2024-10-27T02:00:00.00002:002024-10-27 02:00:0017299907990002024-10-27T00:59:59.000Z2024-10-27T02:59:59.00002:002024-10-27 02:59:5917299908000002024-10-27T01:00:00.000Z2024-10-27T02:00:00.00001:002024-10-27 02:00:0017299943990002024-10-27T01:59:59.000Z2024-10-27T02:59:59.00001:002024-10-27 02:59:59INSERT INTO t1 values (2024-10-27T02:00:00.00002:00, 1) (2024-10-27T02:59:59.00002:00, 2) (2024-10-27T02:00:00.00001:00, 3) (2024-10-27T02:59:59.00001:00, 4); Insert OK, 4 row(s) affected (0.001514s) SELECT *, TO_ISO8601(ts,Z), TO_CHAR(ts, YYYY-MM-DD HH:mi:ss) FROM t1 WHERE ts 2024-10-27T02:00:00.00002:00 AND ts 2024-10-27T02:59:59.00001:00; ts | v1 | TO_ISO8601(ts,Z) | TO_CHAR(ts, YYYY-MM-DD HH:mi:ss) | 1729987200000 | 1 | 2024-10-27T00:00:00.000Z | 2024-10-27 02:00:00 | 1729990799000 | 2 | 2024-10-27T00:59:59.000Z | 2024-10-27 02:59:59 | 1729990800000 | 3 | 2024-10-27T01:00:00.000Z | 2024-10-27 02:00:00 | 1729994399000 | 4 | 2024-10-27T01:59:59.000Z | 2024-10-27 02:59:59 | Query OK, 4 row(s) in set (0.004275s) SELECT *, TO_ISO8601(ts,Z), TO_CHAR(ts, YYYY-MM-DD HH:mi:ss) FROM t1 WHERE ts 2024-10-27T02:00:00.00002:00 AND ts 2024-10-27T02:59:59.00002:00; ts | v1 | TO_ISO8601(ts,Z) | TO_CHAR(ts, YYYY-MM-DD HH:mi:ss) | 1729987200000 | 1 | 2024-10-27T00:00:00.000Z | 2024-10-27 02:00:00 | 1729990799000 | 2 | 2024-10-27T00:59:59.000Z | 2024-10-27 02:59:59 | Query OK, 2 row(s) in set (0.004275s)可以看到把02:00:00同时写两遍时02:00与01:00分别精确落入重叠小时的上下两段按02:00上界查询也只命中第一段结果与时间语义完全一致。3查询时注意时区设定在查询和显示时如果需要本地时间务必考虑夏令时的影响。taosAdapter使用 REST API 时支持设置 IANA 时区结果使用 RFC 3339 格式返回$ curl -uroot:taosdata localhost:6041/rest/sql?tzEurope/Berlin \ -d select ts from tz1.t1 {code:0,column_meta:[[ts,TIMESTAMP,8]],data:[[1970-01-01T00:59:59.00001:00],[2024-03-31T01:00:00.00001:00],[2024-03-31T01:59:59.00001:00],[2024-03-31T03:00:00.00002:00],[2024-03-31T03:00:01.00002:00],[2024-10-27T02:00:00.00002:00],[2024-10-27T02:59:59.00002:00],[2024-10-27T02:00:00.00001:00],[2024-10-27T02:59:59.00001:00],[2024-10-27T03:00:00.00001:00]],rows:10}注意返回数据中2024-10-27 02:00:00出现两次分别带02:00与01:00偏移——这正是重复小时可通过带时区显示加以区分的直观体现。Explorer使用 Explorer 页面进行 SQL 查询时用户可配置客户端时区以 RFC 3339 格式显示。六、延伸阅读时区配置IANA 名称、POSIX 固定偏移、五层时区优先级、SET TIMEZONE、TO_ISO8601/TIMETRUNCATE等时间函数与自然时间单位详见 时区与自然时间单位时间字符串到 UTC 时间戳的完整解析逻辑位于 source/common/src/ttime.c其中taosParseTime、parseTimeWithTz、parseLocaltimeDst分别对应带偏移量与不带偏移量的解析路径可结合本文 4.5 节对照阅读。核心结论TDengine 内部始终以 UTC 时间戳int64存储时间时区只作用于时间字符串 ↔ UTC的转换环节。只要坚持写入与查询都使用 UNIX 时间戳或带显式偏移量的 RFC 3339 时间即可完全规避夏令时带来的跳变与重复问题展示层则应优先使用带时区RFC 3339格式避免使用不含时区的裸本地时间。【免费下载链接】TDengineHigh-performance, scalable time-series database designed for Industrial IoT (IIoT) scenarios项目地址: https://gitcode.com/GitHub_Trending/tde/TDengine创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →