DATE_TRUNC('week', ...) 默认以周日为起点且受lc_time和时区影响,无法保证周一为始;应结合extract(isodow)手动计算周一日期,再转date并加6天得区间。

DATE_TRUNC 为什么必须用 'week' 而不是 'iw' 或 'isodow'?
PostgreSQL 的 DATE_TRUNC 不支持直接截取“自然周”(周一到周日)或“ISO 周”,DATE_TRUNC('week', ...) 默认按周日为起点截断,且受 timezone 和 lc_time 影响。如果业务要求“周一为每周开始”,不能只靠 DATE_TRUNC('week', ...) —— 它在不同 locale 下行为不一致,比如德语环境可能返回周一开头,而中文环境默认是周日。
- 用
DATE_TRUNC('week', ...)前先确认SHOW lc_time和当前时区,否则 GROUP BY 结果会错位 - 更稳的写法是:用
date_trunc('day', ts) - extract(isodow from ts)::int % 7 + 1算出周一日期,再转成 date 类型 - 别用
'iw'(ISO week year),它返回的是年份+周序号字符串,无法直接用于时间范围比较或排序
GROUP BY DATE_TRUNC 后怎么对齐「自然周区间」显示?
直接 GROUP BY DATE_TRUNC('week', login_time) 得到的是带时分秒的 timestamp(如 2024-06-02 00:00:00),但用户要的是「2024-06-02 至 2024-06-08」这样的可读区间。不处理的话,前端或报表工具容易误判起止日。
- 用
DATE_TRUNC('week', login_time)::date转成 date 类型,得到当周周日(默认)或周一(配合偏移后) - 加一列:
(DATE_TRUNC('week', login_time)::date + interval '6 days')::date表示结束日 - 注意:如果用了周一为起点的逻辑,起始日就是
monday_date,结束日是monday_date + 6,别漏掉 +6
MySQL / ClickHouse 用户别硬套 DATE_TRUNC
MySQL 没有 DATE_TRUNC 函数,ClickHouse 虽有但语法和语义不同——硬搬 PostgreSQL 写法会报错或结果偏差。
- MySQL 用
STR_TO_DATE(CONCAT(YEARWEEK(ts, 1), ' Monday'), '%x%v %W')算 ISO 周一,或更稳妥用DATE_SUB(ts, INTERVAL WEEKDAY(ts) DAY) - ClickHouse 的
toStartOfWeek(ts, mode)中mode=0是周日开头,mode=1是周一开头,必须显式传参,不传默认是 0 - 三者对「2024-01-01 是周一」这种边界日的周归属可能不同,建议用具体日期跑个 SELECT 验证
活跃用户数统计里,COUNT(DISTINCT uid) 为什么比预期少?
常见原因是 login_time 字段含时区信息(timestamptz),而 DATE_TRUNC 在不同时区下截断点不一致,导致同一天的登录被分到两个周组里;或者字段本身是字符串没转 time 类型就直接 trunc。
- 先检查
pg_typeof(login_time),如果是text或varchar,必须用TO_TIMESTAMP(...)或CAST(... AS timestamptz)转换 - 用
AT TIME ZONE 'Asia/Shanghai'统一时区再 trunc,例如:DATE_TRUNC('week', login_time AT TIME ZONE 'Asia/Shanghai') - 如果表数据量大,这个表达式无法走索引,考虑提前物化一列
week_start_date并建索引
事情说清了就结束。周维度分析最麻烦的从来不是函数怎么写,而是“哪天算一周开始”这件事,业务、DB、BI 工具三方没对齐,后面所有数字都得返工。

















