做数据分析的时候,按天看数据往往太碎,按月看又太粗,按周汇总刚好是一个折中的粒度。但一周从哪天开始、跨年的周怎么算、不同数据库的周函数结果为什么对不上,这些问题经常让人踩坑。本文以MySQL和PostgreSQL为例,把按周汇总的几种实现方式和边界问题一次讲清楚。

MySQL中按周汇总的几种写法
MySQL提供了WEEK()、YEARWEEK()和DATE_FORMAT()三种常用方式来获取日期所属的周。最直接的是WEEK(date),它返回日期在当年中的第几周,取值范围是0到53。
SELECT WEEK('2024-01-10') AS week_no; -- 返回2
SELECT YEARWEEK('2024-01-10') AS yw; -- 返回202402,带年份更安全
SELECT DATE_FORMAT('2024-01-10', '%x-%v') AS week_str; -- 返回2024-02
这里要特别提醒一点:WEEK()函数带第二个参数mode,不同的mode决定了周从周日还是周一开始、以及跨年那几天算哪一周。比如WEEK('2024-01-01')默认返回0,因为这一天属于上一年的最后一周;而WEEK('2024-01-01', 3)返回1,因为它把这天算作2024年的第一周。mode取值从0到7,最常用的是0(周日开头,美国习惯)和3(周一开头,符合ISO 8601标准,国内业务一般用这个)。
实际写聚合查询时,强烈建议用YEARWEEK(date, 3)而不是单独的WEEK(date, 3)。原因是跨年数据如果只按周号分组,2023年第52周和2024年第52周会被错误地合并到一起。下面是一个完整的按周统计订单量的例子:
SELECT
YEARWEEK(order_date, 3) AS year_week,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY YEARWEEK(order_date, 3)
ORDER BY year_week;
如果想让输出更友好,比如显示成“2024年第15周”或者直接展示这一周的起始日期,可以把周号转换回日期再格式化:STR_TO_DATE(CONCAT(YEARWEEK(order_date, 3), ' Monday'), '%X%V %W')就能拿到该周的周一日期。
PostgreSQL中用DATE_PART和EXTRACT按周汇总
PostgreSQL走的是另一套路线,主要靠DATE_PART()和EXTRACT()函数,两者功能完全等价,只是写法上一个像函数调用,一个像关键字语法。
SELECT DATE_PART('week', DATE '2024-01-10'); -- 返回2,双精度类型
SELECT EXTRACT(WEEK FROM DATE '2024-01-10'); -- 返回2,数值类型
PostgreSQL的week字段遵循ISO 8601标准:一周从周一开始,第一周是包含当年第一个周四的那一周。这意味着1月1日不一定属于第一周,可能属于上一年的第52周或53周。这一点和MySQL默认行为不同,从MySQL迁移到PostgreSQL时经常发现周数对不上,多数就是mode设置的问题。把MySQL端统一改成YEARWEEK(date, 3)后,两边结果基本就能对齐了。
同样地,PostgreSQL也存在跨年分组的坑,解决办法是用to_char()直接拼出带年份的周标识,或者用date_trunc('week', date)把日期截断到所在周的周一,然后按这个周一日期分组,这是最推荐的方式,因为分组键本身就是个日期,排序、展示、做报表都方便:
SELECT
date_trunc('week', order_date)::date AS week_start,
COUNT(*) AS order_count,
ROUND(SUM(amount)::numeric, 2) AS total_amount
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY week_start
ORDER BY week_start;
date_trunc的方式还有一个隐藏好处:生成的分组键天然连续,不会出现某一周因为没数据而在结果集中“消失”的感知问题,配合外部的日期维表做左连接就能补全空白周。
按周汇总的常见坑与处理技巧
第一个坑是周起始日不一致导致的统计偏差。假设业务定义周从周一开始,但使用了默认的WEEK(date),那么周日产生的数据会被算到下一周去,整体趋势图会发生错位。对策很简单:MySQL统一带mode参数,PostgreSQL默认就是ISO标准,两边核对清楚即可。另外时区问题也不能忽视,WEEK()和DATE_PART()都基于本地时区解释时间戳,如果服务器时区和业务时区不一致,跨周时刻附近的数据会被划错周,可以先用AT TIME ZONE转换时区再提取周数。
第二个坑是补全无数据的周。直接GROUP BY只会返回有数据的周,画折线图时中间会断档。标准做法是建立一张日期维表或用递归CTE生成连续的周序列,再左连接业务数据:
WITH RECURSIVE weeks AS (
SELECT DATE '2024-01-01' AS week_start
UNION ALL
SELECT week_start + INTERVAL '1 week'
FROM weeks
WHERE week_start < DATE '2024-12-30'
)
SELECT
w.week_start,
COALESCE(COUNT(o.id), 0) AS order_count
FROM weeks w
LEFT JOIN orders o
ON date_trunc('week', o.order_date)::date = w.week_start
GROUP BY w.week_start
ORDER BY w.week_start;
第三个坑是性能。对日期列套函数再分组会导致索引失效,数据量大时全表扫描会很慢。优化思路有两个:一是给表加一个预计算的周标识列(比如week_start_date),写入时算好并建立索引;二是如果查询周期固定,可以借助物化视图定期刷新。这两种方式虽然牺牲了一点存储,但换来的是数量级的查询提速,在报表场景里非常划算。
总结一下,按周汇总的核心在于两点:统一周的定义(起始日和跨年规则),以及让分组键自带年份信息或直接使用周起始日期。掌握了YEARWEEK、DATE_PART和date_trunc这三个工具,再配合维表补全和预计算优化,基本上能应对所有按周出报表的场景。
SQL按周汇总WEEK函数DATE_PART函数修改时间:2026-09-11 21:26:37