StarRocks WEEK 函数完全指南:周数计算的 8 种模式与底层实现解析
【免费下载链接】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
WEEK()是 StarRocks 日期时间函数家族中用于计算给定日期所在周序号的函数,行为与 MySQL 的WEEK函数保持一致。在电商大促分析、财务周报、库存周维度聚合等“按周切片”的场景中,它是将日期列快速转换为周编号(INT)的核心工具。读完本文,你将掌握WEEK()的完整语法、8 种mode模式的取舍逻辑、边界日期(跨年周)的判定规则,以及该函数在 StarRocks BE 端 C++ 实现中的底层计算原理,从而在 SQL 中正确使用周维度统计,避免"周一对不上"的经典坑。
函数定位与版本支持
WEEK()返回指定日期在一年中的周序号(week number),自 StarRocksv2.3起支持。它接受DATETIME或DATE类型的日期参数,并可通过可选的mode参数控制“一周从周日还是周一开始”以及“周序号范围是 0–53 还是 1–53”,行为完全对齐 MySQL 的WEEK(date[, mode])语义。
在实际业务中,它与同族的日期函数形成互补关系:
WEEK():返回周序号(本文主题);WEEKOFYEAR():等价于WEEK(date, 3),即默认从周一开始、范围 1–53 的 ISO 风格周号;YEARWEEK():返回形如YYYYWW的"年+周"组合值;DATE_TRUNC(date, week)/DATE_TRUNC(datetime, week):将日期截断到所在周的起始时刻,常用于周粒度聚合的分组键。
语法与参数说明
INT WEEK(DATETIME|DATE date, INT mode)参数明细:
| 参数 | 类型 | 必选 | 说明 |
|---|---|---|---|
date | DATETIME / DATE | 是 | 待计算周序号的日期;传入时间戳时仅取日期部分参与周计算 |
mode | INT | 否 | 周计算逻辑开关,取值 0–7,默认0,缺省时按 mode 0 处理 |
mode参数同时控制两个维度:一周的第一天(Sunday 或 Monday),以及周序号范围(0–53 或 1–53),并进一步决定“第 1 周”的判定条件(是该年内第一个含周日的周,还是该年内第一个含 4 天及以上的周,或是第一个含周一的周)。完整规则如下表:
| Mode | 一周的第一天 | 周序号范围 | 第 1 周是第一个满足以下条件的周 |
|---|---|---|---|
| 0 | Sunday | 0-53 | 该年内包含一个周日 |
| 1 | Monday | 0-53 | 该年内包含 4 天及以上 |
| 2 | Sunday | 1-53 | 该年内包含一个周日 |
| 3 | Monday | 1-53 | 该年内包含 4 天及以上 |
| 4 | Sunday | 0-53 | 该年内包含 4 天及以上 |
| 5 | Monday | 0-53 | 该年内包含一个周一 |
| 6 | Sunday | 1-53 | 该年内包含 4 天及以上 |
| 7 | Monday | 1-53 | 该年内包含一个周一 |
从这张表可以提炼出 8 种模式的三种"基因"组合:周起始日(Sunday/Monday)、返回值范围(0–53/1–53)、第 1 周判定(含周日 / 含周一 / 含 4 天以上)。其中:
- ISO 8601 标准周对应
mode = 3:周一为一周起点,范围 1–53,第 1 周必须包含该年的 4 天以上(即包含该年的第一个周四); mode = 1与mode = 3的区别仅在于是否允许返回 0(mode 1 返回 0–53,mode 3 返回 1–53),这正是WEEKOFYEAR()与WEEK(date, 1)的差异所在。
返回值与边界行为
- 返回类型为
INT,取值范围 0–53,具体区间由mode决定; - 当
date取值非法(如无法解析的日期)或输入为空(NULL)时,函数返回NULL; - 跨年日期会因模式不同出现“回退到上一年最后一周”的情况,此时返回值可能是 52/53 或 0,详见下文示例。
实战示例:8 种模式对同一日期的结果差异
以下示例统一使用日历上的周一2007-01-01(2007 年 1 月 1 日),完整演示不同模式下同一日期的不同周号:
mode = 0,返回 0
一周从周日开始,2007-01-01是周一,不属于该年第一个"含周日的周"的第 1 周,因此返回0:
mysql> SELECT WEEK('2007-01-01', 0); +-----------------------+ | week('2007-01-01', 0) | +-----------------------+ | 0 | +-----------------------+ 1 row in set (0.02 sec)mode = 1,返回 1
一周从周一开始,2007-01-01恰为周一,直接命中第 1 周:
mysql> SELECT WEEK('2007-01-01', 1); +-----------------------+ | week('2007-01-01', 1) | +-----------------------+ | 1 | +-----------------------+ 1 row in set (0.02 sec)mode = 2,返回 53
一周从周日开始,但2007-01-01是周一、不是周日;由于 mode 2 的范围是 1–53 且不允许返回 0,该日期被归属到上一年(2006 年)的最后一周,即第 53 周:
mysql> SELECT WEEK('2007-01-01', 2); +-----------------------+ | week('2007-01-01', 2) | +-----------------------+ | 53 | +-----------------------+ 1 row in set (0.01 sec)这三个示例清晰展示了“0 与 53 其实是同一周在 0–53 / 1–53 两种范围下的不同呈现”:2007-01-01属于 2006 年的最后一周,在 0–53 模式下记作0(表示"尚未进入 2007 年第 1 周"),在 1–53 模式下记作53(上一年的最后一周)。
更多典型日期验证
结合源码测试用例(time_functions_test.cpp)中的数据,可以进一步验证各模式的行为:
| 日期 | mode=0 | mode=1 | mode=2 | mode=3 | mode=4 | mode=5 | mode=6 | mode=7 |
|---|---|---|---|---|---|---|---|---|
| 2007-01-01(周一) | 0 | 1 | 53 | 1 | 0 | 1 | 52 | 1 |
| 2017-05-01 | 18 | 18 | 18 | 18 | 18 | 18 | 18 | 18 |
| 2020-09-23 | 38 | 39 | 38 | 39 | 38 | 38 | 39 | 39 |
| 2015-10-11(周日) | 41 | 41 | 41 | 41 | 41 | 41 | 41 | 41 |
上表中 2007-01-01 各 mode 的结果来自本文示例与测试数据推断;2017-05-01、2020-09-23、2015-10-11 的 mode 3/2/1/0/5/7/4/6 结果直接取自 time_functions_test.cpp 中的
weeks[]数组({1, 18, 39, 41, 49, 18, 5, 36} 分别对应 mode 3,2,1,0,5,7,4,6)。可见:
- 2015-10-11 是周日,8 种模式结果全部一致(41),说明不落在跨年边界附近时,大多数模式结果趋同;
- 2020-09-23 在 mode 0/2/4/5 下为 38、在 mode 1/3/6/7 下为 39,体现了周一/周日起点对周归属的实质性影响。
在 SQL 中的典型用法
-- 统计每日销售额,并输出所在周序号(默认 mode 0) SELECT WEEK(sale_date) AS week_no, SUM(amount) AS total_amount FROM sales GROUP BY WEEK(sale_date) ORDER BY week_no; -- 使用 ISO 标准周(周一为一周起点) SELECT WEEK(sale_date, 3) AS iso_week_no, SUM(amount) AS total_amount FROM sales GROUP BY WEEK(sale_date, 3);底层实现:从 SQL 到 C++ 内核
WEEK()并非单独的一个 C++ 函数,它在 StarRocks BE 端由week_of_year_with_default_mode(缺省 mode)与week_of_year_with_mode(显式 mode)两个向量化函数承载,二者定义于 time_functions.cpp,并最终统一调用核心计算函数TimeFunctions::compute_week()与模式归一化函数TimeFunctions::week_mode()。
1. 模式归一化:week_mode()
源码 time_functions.cpp 将用户传入的 mode 折叠为三个位标志的组合:
uint TimeFunctions::week_mode(uint mode) { uint week_format = (mode & 7); if (!(week_format & WEEK_MONDAY_FIRST)) week_format ^= WEEK_FIRST_WEEKDAY; return week_format; }三个位标志定义于 time_functions.h:
| 位标志 | 值 | 含义 |
|---|---|---|
WEEK_MONDAY_FIRST | 1 | 周一为一周第一天(否则为周日) |
WEEK_YEAR | 2 | 使用“周年”(week year)语义,涉及跨年归属 |
WEEK_FIRST_WEEKDAY | 4 | 第 1 周判定依赖一周第一天(周日/周一)是否出现在该年内 |
其中if (!(week_format & WEEK_MONDAY_FIRST)) week_format ^= WEEK_FIRST_WEEKDAY;是一条关键变换:当一周从周日开始时,自动翻转WEEK_FIRST_WEEKDAY位,从而把"含周日"与"含周一"两种第 1 周判定统一到同一套算法中。
2. 周计算核心:compute_week()
TimeFunctions::compute_week(year, month, day, week_behaviour, *to_year)(time_functions.cpp)实现了与 MySQL 一致的周号算法,核心逻辑包括:
- 日序号(day number)换算:将年/月/日换算为自某基准日以来的累计天数
daynr,并据此计算该日是周几; - 年初边界判断:若日期落在 1 月且
day <= 7 - weekday,说明该日期位于"跨年周"内,需要根据WEEK_YEAR与WEEK_FIRST_WEEKDAY决定其归属年份——要么归入上一年最后一周(周号可能为 52/53 或 0),要么仍算作新年第 1 周; - 周号累加:以第 1 周为基准,用
(daynr - 第一周起始日) / 7的偏移量累加得到周号; - 年末边界修正:若
week_year && days >= 52 * 7,说明日期可能落入 53 周区间,需再次校验是否真正属于第 53 周; - 输出年份回写:通过
*to_year输出实际归属的年份(可能为前一年),供YEARWEEK()等组合函数复用。
配套的compute_weekday(long daynr, bool sunday_first_day_of_week)(time_functions.cpp)负责基于daynr计算星期几,sunday_first_day_of_week参数即为"周日起始"与"周一起始"的开关。
3. 向量化执行与测试验证
BE 端通过DEFINE_TIME_UNARY_FN/DEFINE_TIME_BINARY_FN宏将上述实现包装为可直接作用于列(Column)的向量化函数:
week_of_year_with_default_mode:单参数(日期列),内部以week_mode(0)调用compute_week,对应 SQL 中的WEEK(date);week_of_year_with_mode:双参数(日期列 + mode 列),对应WEEK(date, mode),因此 mode 也可以来自表字段而非仅限常量。
上述两种入口的对应测试分别位于 time_functions_test.cpp(weekWithDefaultModeTest)与 time_functions_test.cpp(weekWithModeTest)。后者逐行验证了 2007-01-01、2017-05-01、2020-09-23、2015-10-11、2014-12-11、2001-05-03、2005-02-03、2003-09-03 在 mode 3/2/1/0/5/7/4/6 下的周号,可作为实现正确性的直接佐证。
周边相关函数与选择建议
在周维度分析中,建议按如下方式选择函数:
| 需求 | 推荐写法 | 说明 |
|---|---|---|
| 对齐 MySQL 默认行为 | WEEK(date) | 等价WEEK(date, 0),周日起始、0–53 |
| 符合 ISO 8601 标准周 | WEEK(date, 3) | 周一起始、1–53,财务/跨国业务常用 |
| 直接取 ISO 周号 | WEEKOFYEAR(date) | 等价WEEK(date, 3) |
| 需要"年+周"拼接键 | YEARWEEK(date) | 返回YYYYWW,避免跨年周与年份错位 |
| 周粒度聚合分组键 | DATE_TRUNC(date, week) | 截断到所在周起始日(周一 00:00:00) |
需要特别注意:不要将WEEK(date, 0)(默认)的结果直接与 ISO 周号混用。默认模式以周日为起点,而大多数"周报"约定周一为起点,两种口径下同一日期可能相差 1 周;跨年日期在 1–53 模式下还会回退到 52/53,务必结合YEARWEEK()或分组时同时输出年份字段,避免"2026 年第 0 周"这类歧义数据进入报表。
总结
WEEK(date[, mode])自 v2.3 起可用,行为与 MySQLWEEK()完全一致,返回INT型周序号,非法/空日期返回NULL;- 8 种 mode 由"周起始日(周日/周一)+范围(0–53/1–53)+第 1 周判定(含周日/含周一/含 4 天以上)"三个维度组合而成,
mode=3即 ISO 标准周; - 跨年边界日期的行为最为关键:同一
2007-01-01在不同 mode 下分别返回 0、1、53,深刻理解这一点才能避免周统计口径错误; - 实现层面,BE 端由
week_of_year_with_default_mode/week_of_year_with_mode调用week_mode()与compute_week()完成计算,相关逻辑与测试用例见 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),仅供参考