StarRocks from_unixtime 函数详解:UNIX 时间戳与日期时间格式的转换、时区处理与底层实现
【免费下载链接】starrocksThe world's fastest open query engine for sub-second analytics both on and off the data lakehouse. With the flexibility to support nearly any scenario, StarRocks provides best-in-class performance for multi-dimensional analytics, real-time analytics, and ad-hoc queries. A Linux Foundation project.项目地址: https://gitcode.com/GitHub_Trending/st/starrocks
from_unixtime是 StarRocks 中用于将 UNIX 时间戳(从 1970-01-01 00:00:00 UTC 起算的秒数)转换为人类可读的日期时间字符串的核心内置函数,广泛用于报表展示、日志解析、分区裁剪与 ETL 时间字段格式化等场景。本文以官方函数文档 from_unixtime.md 为骨架,结合 FE(前端)与 BE(后端)源码及测试用例,深入讲解其语法、参数边界、格式符、时区行为与底层实现原理,帮助你准确使用该函数并理解其在查询优化中的特殊地位。
一、函数功能与典型应用场景
from_unixtime将一个 BIGINT 类型的 UNIX 时间戳转换为指定格式的时间字符串,默认输出格式为yyyy-MM-dd HH:mm:ss。典型应用场景包括:
- 将表中存储的 epoch 秒(如事件日志的
event_ts列)在查询时格式化为可读时间; - 按天/小时维度做时间分桶(如
from_unixtime(ts, '%Y-%m-%d')); - 与
unix_timestamp()函数配合完成「时间 ↔ 时间戳」的双向转换。
其逆函数为unix_timestamp()(将日期时间转为时间戳),二者在 FE 的 FunctionSet.java 中作为一组时间类内置函数注册(还包括毫秒级变体from_unixtime_ms)。
二、语法与参数说明
VARCHAR from_unixtime(BIGINT unix_timestamp[, VARCHAR string_format])unix_timestamp
- 类型:BIGINT(在常量折叠场景下,FE 同时接受 INT 与 BIGINT 两种入参,见下文源码)。
- 取值范围:
0到253402243199。超出该范围时返回NULL。 - 对应时间范围:
1970-01-01 00:00:00到9999-12-30 11:59:59,具体边界会因会话时区而有所偏移(文档原话为 "varies because of timezone")。上界常量在 FE 中定义为TimeUtils.MAX_UNIX_TIMESTAMP = 253402243199L,见 TimeUtils.java。
string_format
- 类型:VARCHAR,可选参数,指定输出格式,缺省时使用默认格式
yyyy-MM-dd HH:mm:ss。 - 支持的格式符如下(其余格式符视为非法,返回
NULL):
%Y: Year e.g.: 2014, 1900 %m: Month e.g.: 12, 09 %d: Day e.g.: 11, 01 %H: Hour e.g.: 23, 01, 12 %i: Minute e.g.: 05, 11 %s: Second e.g.: 59, 01同时,文档明确说明该函数也支持 date_format 中定义的格式集合。date_format文档给出了更完整的格式符参考(同样适用于from_unixtime的格式化输出):
%a | Abbreviated weekday name (Sun to Sat) %b | Abbreviated month name (Jan to Dec) %c | Numeric month name (0-12) %D | Day of the month as a numeric value, followed by suffix in English %d | Day of the month as a numeric value (00-31) %e | Day of the month as a numeric value (0-31) %f | Microseconds %H | Hour (00-23) %h | Hour (01-12) %I | Hour (01-12) %i | Minutes (00-59) %j | Day of the year (001-366) %k | Hour (0-23) %l | Hour (1-12) %M | Month name in full %m | Month name as a numeric value (00-12) %p | AM or PM %r | Time in 12 hour (hh:mm:ss AM or PM) %S | Seconds (00-59) %s | Seconds (00-59) %T | Time in 24 hour format (hh:mm:ss) %U | Week (00-53) where Sunday is the first day of the week %u | Week (00-53) where Monday is the first day of the week %V | Week (01-53) where Sunday is the first day of the week. Used with %X. %v | Week (01-53) where Monday is the first day of the week. Used with %x. %W | Weekday name in full %w | Day of the week where Sunday=0 and Saturday=6 %X | Year for the week where Sunday is the first day of the week. 4-digital value. Used with %V. %x | Year for the week where Monday is the first day of the week. 4-digital value. Used with %v. %Y | Year. 4-digital value. %y | Year. 2-digital value. %% | Represent %.注意区分大小写:分钟是%i、秒是%s,而%S同样表示秒(%S与%s等价);小时 24 小时制为%H,12 小时制为%h/%I。
三、返回值
- 返回值类型为
VARCHAR。 - 当
string_format指定的是 DATE 格式(即只含年月日,不包含时间部分)时,返回的是 VARCHAR 类型的 DATE 值,例如2007-12-01。 - 当时间戳超出取值范围(小于 0 或大于
253402243199),或string_format为非法格式时,返回NULL。
四、使用示例
以下示例均来自官方文档原文,可直接在 MySQL 客户端中执行验证:
MySQL > select from_unixtime(1196440219); +---------------------------+ | from_unixtime(1196440219) | +---------------------------+ | 2007-12-01 00:30:19 | +---------------------------+ MySQL > select from_unixtime(1196440219, 'yyyy-MM-dd HH:mm:ss'); +--------------------------------------------------+ | from_unixtime(1196440219, 'yyyy-MM-dd HH:mm:ss') | +--------------------------------------------------+ | 2007-12-01 00:30:19 | +--------------------------------------------------+ MySQL > select from_unixtime(1196440219, '%Y-%m-%d'); +-----------------------------------------+ | from_unixtime(1196440219, '%Y-%m-%d') | +-----------------------------------------+ | 2007-12-01 | +-----------------------------------------+ MySQL > select from_unixtime(1196440219, '%Y-%m-%d %H:%i:%s'); +--------------------------------------------------+ | from_unixtime(1196440219, '%Y-%m-%d %H:%i:%s') | +--------------------------------------------------+ | 2007-12-01 00:30:19 | +--------------------------------------------------+由示例可见:
- 第二个示例中的
yyyy-MM-dd HH:mm:ss即默认输出格式; - 第三个示例仅保留日期部分,得到 VARCHAR 类型的 DATE 值;
- 第四个示例展示了
%H:%i:%s(时:分:秒)的典型组合,结果与默认格式一致。
实战扩展:基于时间戳做分组统计
-- 按天统计事件数量 SELECT from_unixtime(event_ts, '%Y-%m-%d') AS event_day, COUNT(*) FROM event_log GROUP BY from_unixtime(event_ts, '%Y-%m-%d'); -- 按小时统计,并过滤非法(越界)时间戳 SELECT from_unixtime(ts, '%Y-%m-%d %H:00:00'), COUNT(*) FROM fact_table WHERE ts BETWEEN 0 AND 253402243199 GROUP BY from_unixtime(ts, '%Y-%m-%d %H:00:00');五、时区行为与边界范围
from_unixtime的转换结果依赖 StarRocks 的会话时区(time_zone会话变量):同一个时间戳在不同时区下会得到不同的本地时间。官方文档特别指出,其可转换的时间范围上界9999-12-30 11:59:59会因时区而偏移。
从 BE 的底层单元测试 datetime_value_test.cpp 中可以直观看到时区的影响(默认时区为Asia/Shanghai,即 UTC+8):
TEST_F(DateTimeValueTest, from_unixtime) { char str[MAX_DTVALUE_STR_LEN]; DateTimeValue value; value.from_unixtime(570672000, TimezoneUtils::default_time_zone); value.to_string(str); ASSERT_STREQ("1988-02-01 08:00:00", str); value.from_unixtime(253402271999, TimezoneUtils::default_time_zone); value.to_string(str); ASSERT_STREQ("9999-12-31 23:59:59", str); value.from_unixtime(0, TimezoneUtils::default_time_zone); value.to_string(str); ASSERT_STREQ("1970-01-01 08:00:00", str); ASSERT_FALSE(value.from_unixtime(1586098092, "+20:00")); ASSERT_FALSE(value.from_unixtime(1586098092, "foo")); }该测试揭示三个关键点:
- 时区偏移:时间戳
0(UTC 的 1970-01-01 00:00:00)在Asia/Shanghai时区下输出为1970-01-01 08:00:00; - 上界能力:底层
DateTimeValue::from_unixtime可处理更大的秒值(253402271999输出9999-12-31 23:59:59),而 SQL 层from_unixtime函数的上界由253402243199约束,超出即返回NULL; - 非法时区处理:非法时区标识(如
+20:00、foo)会使转换失败返回false,上层据此返回NULL。
BE 表达式层的测试 time_functions_test.cpp 也验证了时区语义:24 * 60 * 60(即 1 天,86400 秒)在默认时区(+08:00)下格式化为1970-01-01 16:00:00:
TEST_F(TimeFunctionsTest, fromUnixToDatetimeWithFormat) { ... auto tc1 = Int32Column::create(); tc1->append(24 * 60 * 60); ... ColumnPtr result = TimeFunctions::from_unix_to_datetime_with_format_32(_utils->get_fn_ctx(), columns).value(); ASSERT_EQ("['1970-01-01 16:00:00', '1970-01-01 16:01:01', '1970-01-01 17:03:09']", result->debug_string()); ... }六、源码级原理:FE 常量折叠与 BE 的 cctz 实现
FE 侧:常量折叠与函数注册
在 FE 的 ScalarOperatorFunctions.java 中,from_unixtime被注册为可做常量折叠(constant folding)的内置函数,共有三组重载:
@ConstantFunction.List(list = { @ConstantFunction(name = "from_unixtime", argTypes = {INT}, returnType = VARCHAR, isMonotonic = true), @ConstantFunction(name = "from_unixtime", argTypes = {BIGINT}, returnType = VARCHAR, isMonotonic = true) }) public static ConstantOperator fromUnixTime(ConstantOperator unixTime) throws AnalysisException { long value = 0; if (unixTime.getType().isInt()) { value = unixTime.getInt(); } else { value = unixTime.getBigint(); } if (value < 0 || value > TimeUtils.MAX_UNIX_TIMESTAMP) { throw new AnalysisException( "unixtime should larger than zero and less than " + TimeUtils.MAX_UNIX_TIMESTAMP); } ConstantOperator dl = ConstantOperator.createDatetime( LocalDateTime.ofInstant(Instant.ofEpochSecond(value), TimeUtils.getTimeZone().toZoneId())); return ConstantOperator.createVarchar(dl.toString()); }关键实现细节:
- 参数类型:常量场景下同时接受
INT和BIGINT;非常量场景以文档语法中的 BIGINT 为准; - 范围校验:
value < 0 || value > TimeUtils.MAX_UNIX_TIMESTAMP(即253402243199L)时,在常量折叠路径会直接抛出AnalysisException——也就是说,如果 SQL 中的时间戳是字面量且越界,分析阶段就会报错,而不是等到执行期返回 NULL; - 单调性标记:三组注册均标注
isMonotonic = true,这使优化器可以基于其单调性做表达式重写(见下文第七节); - 时区来源:默认使用
TimeUtils.getTimeZone()(会话时区),第三组重载允许显式传入时区参数:
@ConstantFunction.List(list = { @ConstantFunction(name = "from_unixtime", argTypes = {INT, VARCHAR, VARCHAR}, returnType = VARCHAR, isMonotonic = true), @ConstantFunction(name = "from_unixtime", argTypes = {BIGINT, VARCHAR, VARCHAR}, returnType = VARCHAR, isMonotonic = true) }) public static ConstantOperator fromUnixTime(ConstantOperator unixTime, ConstantOperator fmtLiteral, ConstantOperator timezone) throws AnalysisException { ... ConstantOperator dl = ConstantOperator.createDatetime( LocalDateTime.ofInstant(Instant.ofEpochSecond(value), TimeUtils.getOrSystemTimeZone(timezone.getVarchar()).toZoneId())); return dateFormat(dl, fmtLiteral); }带格式符的版本最终复用dateFormat()完成格式化(ScalarOperatorFunctions.java),该函数对「unix 风格」格式(%Y等)走DateUtils.unixDatetimeFormatter,对 Java 风格格式则走DateTimeFormatter.ofPattern。此外还提供了毫秒级变体from_unixtime_ms(BIGINT),将毫秒除以 1000 后执行相同转换。
BE 侧:基于 cctz 的底层实现
BE 的 datetime_value.cpp 提供了三个重载的from_unixtime,最终统一收敛到带微秒参数的版本:
bool DateTimeValue::from_unixtime(int64_t timestamp, const std::string& timezone) { cctz::time_zone ctz; if (!TimezoneUtils::find_cctz_time_zone(timezone, ctz)) { return false; } return from_unixtime(timestamp, ctz); } bool DateTimeValue::from_unixtime(int64_t timestamp, const cctz::time_zone& ctz) { return from_unixtime(timestamp, 0, ctz); } bool DateTimeValue::from_unixtime(int64_t timestamp, int64_t microsecond, const cctz::time_zone& ctz) { static const cctz::time_point<cctz::sys_seconds> epoch = std::chrono::time_point_cast<cctz::sys_seconds>(std::chrono::system_clock::from_time_t(0)); cctz::time_point<cctz::sys_seconds> t = epoch + cctz::seconds(timestamp); const auto tp = cctz::convert(t, ctz); _neg = 0; _type = TIME_DATETIME; _year = tp.year(); _month = tp.month(); _day = tp.day(); _hour = tp.hour(); _minute = tp.minute(); _second = tp.second(); _microsecond = microsecond; return true; }实现要点:
- 时间运算基于 Google 的cctz库,
epoch + cctz::seconds(timestamp)构造绝对时间点,再通过cctz::convert(t, ctz)换算为指定时区的日历字段(年/月/日/时/分/秒); - 时区解析失败(非法时区标识)时返回
false,上层函数据此返回NULL; - 该底层方法同时也是 BE 中
now()、curdate()、curtime()、utc_timestamp()、convert_tz()等时间函数的公共基石,见 time_functions.cpp(如utc_timestamp使用"+00:00"固定 UTC 时区,now/curdate/curtime使用会话时区state->timezone_obj())。
七、查询优化:单调性重写与 hour(from_unixtime) 简化
from_unixtime在 FE 中注册为isMonotonic = true,这意味着它对时间戳是单调的(时间戳越大,输出时间越晚)。该属性被优化器用于两类重写:
MIN/MAX 单调重写:在涉及
from_unixtime的谓词下推与分区裁剪场景中,优化器可以借助单调性对MIN/MAX表达式进行等价改写,相关逻辑引用见 RewriteMinMaxByMonotonicFunctionRule.java 与 ListPartitionPruner.java。hour(from_unixtime(ts)) → hour_from_unixtime(ts):在 SimplifiedPredicateRule.java 中,优化器将
hour(from_unixtime(ts))这类嵌套调用直接简化为专用的hour_from_unixtime(ts)函数,省去「先转日期时间再取小时」的中间步骤:
// Simplify hour(from_unixtime(ts)) to hour_from_unixtime(ts) // Also simplify hour(to_datetime(ts)) and hour(to_datetime(ts, 0)) to hour_from_unixtime(ts) private static ScalarOperator simplifiedHourFromUnixTime(CallOperator call) { ... // Case 1: hour(from_unixtime(ts)) -> hour_from_unixtime(ts) ScalarOperator fromUnixTime = lookupChild(call, x -> x instanceof CallOperator && ((CallOperator) x).getFnName().equalsIgnoreCase(FunctionSet.FROM_UNIXTIME)); if (fromUnixTime != null) { ... return new CallOperator(FunctionSet.HOUR_FROM_UNIXTIME, call.getType(), fromUnixTime.getChildren(), fn); } ... }对应地,BE 侧实现了TimeFunctions::hour_from_unixtime(time_functions.cpp),并有完整的单元测试覆盖(time_functions_test.cpp)。因此在写「按小时分析」类查询时,使用hour(from_unixtime(ts))与hour_from_unixtime(ts)会被优化器统一处理为高效实现。
八、Trino 语法兼容
对于从 Trino/Presto 迁移的 SQL,StarRocks 的语法兼容层 Trino2SRFunctionCallTransformer.java 提供了from_unixtime的三种改写规则:
// to_unixtime -> unix_timestamp registerFunctionTransformer("to_unixtime", 1, "unix_timestamp", List.of(Expr.class)); // from_unixtime(unixtime) -> from_unixtime registerFunctionTransformer("from_unixtime", 1, "from_unixtime", List.of(Expr.class)); // from_unixtime(unixtime, zone) -> convert_tz(from_unixtime(unixtime), time_zone, zone) registerFunctionTransformer("from_unixtime", 2, new FunctionCallExpr("convert_tz", List.of( new FunctionCallExpr("from_unixtime", List.of( new PlaceholderExpr(1, Expr.class))), new VariableExpr("time_zone"), new PlaceholderExpr(2, Expr.class)))); // from_unixtime(unixtime, hours, minutes) -> hours_add(minutes_add(from_unixtime(unixtime), minutes), hours) registerFunctionTransformer("from_unixtime", 3, new FunctionCallExpr("hours_add", List.of( new FunctionCallExpr("minutes_add", List.of( new FunctionCallExpr("from_unixtime", List.of( new PlaceholderExpr(1, Expr.class))), new PlaceholderExpr(3, Expr.class))), new PlaceholderExpr(2, Expr.class))));即:Trino 的from_unixtime(ts)原样映射;from_unixtime(ts, zone)被改写为「先按会话时区转换、再convert_tz到目标时区」;from_unixtime(ts, hours, minutes)被改写为在转换结果上叠加小时与分钟偏移。相关改写有 FE 单测覆盖,见 TrinoFunctionTransformTest.java。
九、使用注意事项与最佳实践
- 入参类型:语法要求
BIGINT,若时间戳存储为字符串,请先显式CAST(... AS BIGINT),避免隐式转换带来的不确定性。 - 范围校验:时间戳必须落在
0到253402243199之间,否则返回NULL(常量字面量越界时,FE 分析阶段会直接报错unixtime should larger than zero and less than ...)。数据清洗时建议先用WHERE ts BETWEEN 0 AND 253402243199过滤脏数据。 - 格式符大小写:分钟用
%i、秒用%s/%S、24 小时制用%H。传错格式符(如用%M表示分钟)会导致返回NULL或输出不符合预期,因为不支持的格式符按文档约定返回NULL。 - 时区一致性:转换结果依赖会话时区
time_zone。跨时区数据分析时,应显式统一会话时区(如SET time_zone = 'Asia/Shanghai'),或借助convert_tz()做时区换算,避免不同客户端得到不同结果。 - 性能:
from_unixtime在 BE 中按行向量化执行,并针对常量格式做了from_unix_prepare/from_unix_close的局部状态预编译(见 time_functions_test.cpp),同一查询内格式串可复用;配合hour(from_unixtime(ts))的优化器简化,适合大规模扫描场景。 - 毫秒时间戳:若手头是毫秒级(13 位)或微秒级(16 位)时间戳,不能直接传给
from_unixtime,应先除以1000(毫秒)或1000000(微秒)转为秒;也可使用 FE 注册的毫秒级函数from_unixtime_ms(BIGINT)直接处理毫秒输入。
十、相关函数
unix_timestamp()/unix_timestamp(datetime):from_unixtime的逆函数,将日期时间转为 UNIX 时间戳;from_unixtime_ms(BIGINT):毫秒级时间戳的转换变体(注册于 FunctionSet.java,常量实现见 ScalarOperatorFunctions.java);date_format(DATETIME, format):对已有日期时间做格式化,from_unixtime的格式符与其完全一致(详见 date_format 文档);to_datetime(unixtime[, scale]):将时间戳直接转为 DATETIME 类型(支持 0/3/6 三种精度,分别对应秒/毫秒/微秒),适合需要保留 DATETIME 类型而非字符串的场景;convert_tz(datetime, from_tz, to_tz):时区换算,常用于跨时区场景下对from_unixtime结果的二次处理。
十一、小结
from_unixtime是 StarRocks 时间体系中最常用的转换函数之一:它把 epoch 秒换算为受会话时区控制的本地时间字符串,支持%Y/%m/%d/%H/%i/%s及date_format全套格式符,越界时间戳与非法格式均返回NULL。其实现横跨 FE 常量折叠(ScalarOperatorFunctions.java)与 BE 的 cctz 底层转换(datetime_value.cpp),并被优化器利用单调性做重写、被 Trino 兼容层做语法映射。掌握其参数边界、时区语义与格式符约定,即可在报表、ETL 与实时分析中稳定、高效地完成时间戳格式化。
【免费下载链接】starrocksThe world's fastest open query engine for sub-second analytics both on and off the data lakehouse. With the flexibility to support nearly any scenario, StarRocks provides best-in-class performance for multi-dimensional analytics, real-time analytics, and ad-hoc queries. A Linux Foundation project.项目地址: https://gitcode.com/GitHub_Trending/st/starrocks
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考