StarRocks 日期函数 date_sub / subdate 完全指南:语法、参数、返回值与底层实现解析
【免费下载链接】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
date_sub(别名subdate)是 StarRocks 中用于从日期时间值中减去指定时间间隔的核心函数,广泛应用于时间窗口过滤、滚动区间统计、增量数据回溯等场景。本文以官方文档 date_sub.md 为骨架,结合 StarRocks FE(前端)与 BE(后端)源码,系统讲解其语法、参数单位、边界行为、完整示例,以及从 SQL 解析、常量折叠到向量化执行的底层实现链路,帮助你准确、高效地使用这一日期时间函数族。
函数概述:date_sub 与 subdate 的关系
date_sub与subdate是完全等价的两个函数名,语义完全相同——都是从给定的日期或日期时间中减去指定长度的时间间隔。在 StarRocks 的函数注册表中,两者被同时注册:
- FunctionSet.java 中分别定义了
DATE_SUB = "date_sub"与SUBDATE = "subdate"两个函数名常量; - ScalarOperatorFunctions.java 通过
@ConstantFunction注解为subdate和date_sub注册了相同的函数签名(DATETIME, INT) -> DATETIME,且标记为isMonotonic = true(单调函数,详见下文"谓词化简"一节)。
因此,在实际 SQL 中你可以根据团队编码习惯任选其一,两者在功能、性能与返回值上没有任何差异。
语法与参数详解
函数签名
DATETIME DATE_SUB(DATETIME|DATE date, INTERVAL expr type)参数说明
| 参数 | 类型要求 | 含义 |
|---|---|---|
date | DATE 或 DATETIME | 合法的日期/日期时间表达式,是减法运算的基准时间 |
expr | INT | 要减去的时间间隔数值(可为负数,负数等价于加上该间隔) |
type | 时间单位关键字 | 时间间隔的单位,决定expr的语义 |
type支持的时间单位
| 单位关键字 | 说明 | 引入版本 |
|---|---|---|
YEAR | 年 | 全版本 |
QUARTER | 季度 | 全版本 |
MONTH | 月 | 全版本 |
DAY | 天 | 全版本 |
HOUR | 小时 | 全版本 |
MINUTE | 分钟 | 全版本 |
SECOND | 秒 | 全版本 |
MILLISECOND | 毫秒 | 3.1.7 及以后 |
MICROSECOND | 微秒 | 3.1.7 及以后 |
版本注意:
MILLISECOND与MICROSECOND两个单位自 StarRocks 3.1.7 起才可用,低于该版本会报语法错误。这与 BE 侧time_functions.cpp中按单位宏展开的millis_add/millis_sub、micros_add/micros_sub函数族一一对应,见下文"BE 端实现"。
语法细节
- 函数名大小写不敏感,
date_sub与DATE_SUB均合法; - 单位关键字同样不区分大小写,
INTERVAL 2 DAY与INTERVAL 2 day等价(示例中两种写法均有出现); date既可以是字符串字面量(如'2010-11-30 23:59:59'),也可以是DATE/DATETIME类型的列、表达式或函数返回值。
返回值与边界行为
date_sub始终返回DATETIME 类型的值,返回值保留原时间的小时、分钟、秒及小数部分。需要特别注意以下边界行为:
- 无效日期返回 NULL:如果输入的日期本身不存在,例如
2020-02-30(2 月没有 30 号),函数直接返回NULL,而不是执行"进位修正"。 - 类型不合法返回 NULL:若
date不是合法的 DATE 或 DATETIME 值,同样返回NULL。 - DATE 类型输入自动按 DATETIME 处理:当输入为纯日期(如
'2010-11-30')时,函数会将其视为2010-11-30 00:00:00参与运算,结果仍以 DATETIME 返回。这在下面的示例中可以看到:减去 2 小时后得到2010-11-29 22:00:00,体现了"借位到前一天"的日期回退逻辑。 - 跨月/跨年借位:按 DAY 等短单位做减法时,会自动进行借月、借年运算,正确处理闰年与大小月。
- 月份相关的月末溢出语义:按 MONTH/QUARTER/YEAR 做减法时,StarRocks 遵循"直接减月份字段,超出月末则返回该月最后一天"的常见日期库语义(即
2010-03-31减 1 个月得到2010-02-28)。
完整示例
以下是官方文档 date_sub.md 给出的全部示例,逐一展开说明:
-- 示例 1:减去 2 天,时间部分保持不变 select date_sub('2010-11-30 23:59:59', INTERVAL 2 DAY); +-------------------------------------------------+ | date_sub('2010-11-30 23:59:59', INTERVAL 2 DAY) | +-------------------------------------------------+ | 2010-11-28 23:59:59 | +-------------------------------------------------+ -- 示例 2:DATE 类型输入按 00:00:00 处理,减去 2 小时发生跨天借位 select date_sub('2010-11-30', INTERVAL 2 hour); +-----------------------------------------+ | date_sub('2010-11-30', INTERVAL 2 HOUR) | +-----------------------------------------+ | 2010-11-29 22:00:00 | +-----------------------------------------+ -- 示例 3:无效日期(2020 年 2 月没有 30 号)返回 NULL select date_sub('2010-02-30', INTERVAL 2 DAY); +----------------------------------------+ | date_sub('2010-02-30', INTERVAL 2 DAY) | +----------------------------------------+ | NULL | +----------------------------------------+ -- 示例 4:减去 2 个季度(共 6 个月) select date_sub('2010-11-30 23:59:59', INTERVAL 2 QUARTER); +-----------------------------------------------------+ | date_sub('2010-11-30 23:59:59', INTERVAL 2 QUARTER) | +-----------------------------------------------------+ | 2010-05-30 23:59:59 | +-----------------------------------------------------+ -- 示例 5:使用别名 subdate 减去 2 毫秒(3.1.7 及以后) select subdate('2010-11-30 23:59:59', INTERVAL 2 millisecond); +--------------------------------------------------------+ | subdate('2010-11-30 23:59:59', INTERVAL 2 MILLISECOND) | +--------------------------------------------------------+ | 2010-11-30 23:59:58.998000 | +--------------------------------------------------------+ -- 示例 6:减去 2 微秒,返回微秒精度(3.1.7 及以后) select date_sub('2010-11-30 23:59:59', INTERVAL 2 microsecond); +---------------------------------------------------------+ | date_sub('2010-11-30 23:59:59', INTERVAL 2 MICROSECOND) | +---------------------------------------------------------+ | 2010-11-30 23:59:58.999998 | +---------------------------------------------------------+补充示例:负的 expr
expr支持 INT 类型负数,INTERVAL -2 DAY等价于加 2 天。这一特性可用于在一条 SQL 中同时表达"回退"与"前推"语义:
select date_sub('2010-11-30 23:59:59', INTERVAL -2 DAY); -- 结果:2010-12-02 23:59:59与其他日期时间函数的对照
为便于选型,这里给出与date_sub同族或易混淆函数的对照(相关文档见 日期时间函数索引):
| 函数 | 语义 | 典型场景 |
|---|---|---|
date_sub/subdate | 减去指定时间间隔,单位覆盖年/季/月/日/时/分/秒/毫秒/微秒 | 通用时间回退计算 |
date_add/adddate | 加上指定时间间隔,与date_sub互为逆运算 | 过期时间、未来时间计算,文档见 date_add.md |
days_sub/days_add | 仅支持按天为单位的加减 | 性能敏感且只需按天计算的场景,文档见 days_sub.md |
timestampadd | 标准 SQL 风格:timestampadd(unit, interval, datetime) | 需要符合 ANSI SQL 语法的场景,文档见 timestampadd.md |
years_sub/months_sub等 | 单单位专用函数,如 years_sub.md、months_sub.md | 只针对单一时间单位、且希望函数签名更明确的场景 |
从实现上看,这一整族"按单位加减"的函数在 BE 端由同一套宏模板统一生成,见下节源码分析。
源码实现原理
FE 端:解析、重写与常量折叠
date_sub家族(date_add、adddate、date_sub、subdate、days_sub)在 StarRocks FE 的 SQL 解析阶段即被特殊处理。AstBuilder.java 中定义了DATE_FUNCTIONS列表,注释明确指出:这类日期函数在解析时会被重写为TimestampArithmeticExpr(一种专门的"时间戳算术表达式"节点),而非普通函数调用。这意味着:
- 语法上
date_sub(date, INTERVAL expr type)与date - INTERVAL expr type的算术写法被统一为同一类表达式; - 后续优化器可对这类表达式进行专门的下推、化简与常量折叠。
常量折叠:当date与expr均为常量时,FE 会在优化阶段直接算出结果。这在 ScalarOperatorFunctions.java 中通过@ConstantFunction(name = "date_sub", argTypes = {DATETIME, INT}, returnType = DATETIME, isMonotonic = true)注册——isMonotonic = true表明该函数关于时间参数是单调的。
谓词化简(monotonic 的应用):正因为date_sub被标记为单调函数,SimplifiedPredicateRule.java 将其纳入TIME_FNS与TIME_FN_NAMES映射表,用于谓词等价变换。例如源码注释中给出的化简规则(L442):
-- 优化器将嵌套的时间函数化简,减少一次计算 date_sub(date_add(x, 1), 2) -> date_sub(x, 1)这意味着在写 SQL 时,即使表达式被写成了嵌套形式,优化器也会自动合并化简,不会产生多余的计算开销。
BE 端:向量化执行与模板化实现
BE(后端)侧的实现位于 time_functions.cpp(约 L944-L999),其核心是模板宏驱动的函数族生成:
template <TimeUnit UNIT> TimestampValue timestamp_add(TimestampValue tsv, int count) { return tsv.add<UNIT>(count); } #define DEFINE_TIME_ADD_FN(FN, UNIT) ... { return timestamp_add<UNIT>(timestamp, value); } #define DEFINE_TIME_SUB_FN(FN, UNIT) ... { return timestamp_add<UNIT>(timestamp, -value); }关键设计点:
- 减法是加法的负数形式:
DEFINE_TIME_SUB_FN直接复用timestamp_add<UNIT>,将value取负后调用,即"减 N 个单位 = 加 -N 个单位"。这与文档中expr支持负 INT 的行为完全一致。 - 一个模板覆盖全部单位:通过
DEFINE_TIME_ADD_AND_SUB_FN(years, TimeUnit::YEAR)等宏调用,一次生成years_add/years_sub、quarters_add/quarters_sub、months_add/months_sub、weeks_add/weeks_sub、days_add/days_sub、hours_add/hours_sub、minutes_add/minutes_sub、seconds_add/seconds_sub、millis_add/millis_sub、micros_add/micros_sub共 20 个函数,覆盖了date_sub支持的全部type单位(含 3.1.7 起新增的毫秒、微秒)。 - 向量化执行:所有函数均返回
ColumnPtr,通过DEFINE_TIME_CALC_FN定义(TYPE_DATETIME, TYPE_INT) -> TYPE_DATETIME的二元向量化签名,BE 会以批处理方式同时对整列数据求值,这正是 StarRocks 面向 OLAP 大规模列式计算性能的基础。 - 单元测试保障:time_functions_test.cpp 中包含了
yearAddTest、quarterAddTest、millisAddTest、yearOverflowTest等测试用例,覆盖年/季度/毫秒加减及溢出场景,验证了各单位的计算正确性。
从 SQL 到结果的全链路
一条SELECT date_sub('2010-11-30 23:59:59', INTERVAL 2 DAY)的完整执行路径为:
- FE 解析:
AstBuilder识别date_sub属于DATE_FUNCTIONS家族,构建TimestampArithmeticExpr节点(见 AstBuilder.java); - FE 优化:若参数全为常量,触发常量折叠直接产出结果;若出现在谓词中,利用单调性进行等价化简(见 SimplifiedPredicateRule.java);
- BE 执行:生成计划下发到 BE,调用
time_functions.cpp中按TimeUnit模板实例化的days_sub等向量化函数,对整列批量求值(见 time_functions.cpp); - 返回结果:以 DATETIME 列形式返回给客户端。
典型使用场景
1. 时间窗口过滤(滚动近 N 天数据)
-- 查询最近 7 天产生的订单 SELECT order_id, order_time, amount FROM orders WHERE order_time >= date_sub(now(), INTERVAL 7 DAY);2. 环比/同比区间统计
-- 本周销售额与上周销售额对比 SELECT SUM(IF(ts >= date_sub(now(), INTERVAL 7 DAY), amount, 0)) AS this_week, SUM(IF(ts >= date_sub(now(), INTERVAL 14 DAY) AND ts < date_sub(now(), INTERVAL 7 DAY), amount, 0)) AS last_week FROM sales;3. 与聚合函数配合生成时间分桶
-- 按"过去 30 天内每 7 天为一桶"聚合 SELECT date_sub(event_day, INTERVAL MOD(DATEDIFF(now(), event_day), 7) DAY) AS bucket_start, COUNT(*) FROM user_events WHERE event_day >= date_sub(now(), INTERVAL 30 DAY) GROUP BY bucket_start;4. 物化视图 / 增量计算中的时间推进
date_sub也常与date_add成对出现在物化视图定义或 ETL 任务的时间游标计算中,例如MVMaintenance相关的时间推进逻辑中,通过date_sub(last_refresh_time, INTERVAL 1 DAY)回溯增量窗口起点。
注意事项与最佳实践
- 优先使用 DAY 等"字段无关"单位:对于只需要按天计算的场景,
days_sub在语义上与date_sub(..., INTERVAL n DAY)等价,且函数签名更聚焦,可读性更好。 - 注意月末与季度边界:当
date落在月末(如 3 月 31 日)时,按MONTH/QUARTER/YEAR减法会发生"月末收敛"(结果为 2 月 28/29 日)。若业务需要"同一天号",建议改用ADD_MONTHS语义或自行按DAY计算。 - 无效日期不会自动修正:传入
2020-02-30之类不存在的日期会直接得到NULL,在 ETL 场景中建议结合COALESCE或CASE WHEN处理,避免 NULL 污染下游统计。 - 毫秒/微秒单位依赖版本:使用
MILLISECOND、MICROSECOND前请确认集群版本 ≥ 3.1.7,否则语句无法通过语法校验。 - 负间隔是合法输入:
expr支持 INT 负数,可等价实现"加法",但为可读性考虑,正向推进建议直接使用date_add(相关文档见 date_add.md)。 - 类型精度:返回值为 DATETIME,含微秒精度;若业务只关心日期部分,可再配合
to_date或date_trunc收敛(参见 to_date.md)。
延伸阅读
- 官方英文文档:date_sub.md、中文文档:date_sub.md
- 逆运算:date_add.md、adddate.md
- 单单位专用函数:days_sub.md、years_sub.md、months_sub.md、quarters_sub.md
- SQL 标准风格:timestampadd.md、timestampdiff.md
- 日期时间函数总览:date-time-functions.mdx
- 源码参考:AstBuilder.java、ScalarOperatorFunctions.java、SimplifiedPredicateRule.java、time_functions.cpp、time_functions_test.cpp
【免费下载链接】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),仅供参考