应先将UTC时间转为业务时区再截取日期分组,避免跨时区数据错分;需确保字段为DATETIME/TIMESTAMP类型,优先建生成列索引;所有日期函数必须作用于已转换时区的时间值,顺序不可颠倒。

直接用原始时间字段 GROUP BY 会导致跨时区数据被错误切分——比如 UTC 时间 '2026-06-03 15:59:59' 和 '2026-06-03 16:00:00' 在北京时间(Asia/Shanghai)里分属不同自然日,但按原值分组会挤在同一天里。
先转时区再截日期,别在 GROUP BY 里硬套函数
很多同学写成 GROUP BY DATE(CONVERT_TZ(created_at, '+00:00', 'Asia/Shanghai')),语法虽通,但每次查询都实时计算,无法走索引,大表扫描慢得明显。更糟的是,如果 created_at 是 VARCHAR 类型,CONVERT_TZ 会静默返回 NULL,整组数据就丢了。
- 确保
created_at是DATETIME或TIMESTAMP类型;若为字符串,先用STR_TO_DATE(created_at, '%Y-%m-%d %H:%i:%s')转型 - 优先建生成列(MySQL 5.7+):
ALTER TABLE orders ADD COLUMN biz_date DATE AS (DATE(CONVERT_TZ(created_at, '+00:00', 'Asia/Shanghai'))) STORED,然后GROUP BY biz_date - PostgreSQL 用
timezone('Asia/Shanghai', created_at)替代CONVERT_TZ,函数名不同但逻辑一致
聚合前必须对齐粒度,否则 COUNT/SUM 会漏数
时区转换只是第一步,真正出错常发生在“粒度不一致”:比如想按自然日统计,却用了 DATE_FORMAT(created_at, '%Y-%m-%d') 直接处理未转换的 UTC 字段,结果把北京时间凌晨的数据全算进前一天。
- 所有日期提取函数(
DATE()、DATE_FORMAT()、DATE_TRUNC())必须作用于已转换时区的时间值,顺序不能颠倒 - MySQL 中
DATE()对TIMESTAMP类型会隐式转当前会话时区,但对DATETIME不会——务必显式调用CONVERT_TZ,别依赖会话设置 - 如果业务要求按“工作日”或“营业周”聚合,光转时区不够,还得关联营业日维表补零,否则周末没数据的那天直接不出行
窗口函数里混用时区和截取,容易 silently 失效
想算“过去7天每日订单均值”,有人写 AVG(COUNT(*)) OVER (ORDER BY DATE(CONVERT_TZ(created_at, ...)) ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)——这会报错,因为 COUNT(*) 是聚合函数,不能嵌套在窗口里;就算改成子查询,若没提前把时间规整到日级,ROWS BETWEEN 按物理行数算,根本不是时间范围。
- 先用 CTE 或子查询完成基础分组:
SELECT DATE(CONVERT_TZ(created_at, '+00:00', 'Asia/Shanghai')) AS d, COUNT(*) AS cnt FROM orders GROUP BY 1 - 再在此结果上开窗,
ORDER BY d,并用ROWS BETWEEN 6 PRECEDING AND CURRENT ROW(此时每行已是自然日) - MySQL 不支持
RANGE BETWEEN INTERVAL '6 days' PRECEDING,PostgreSQL/BigQuery 可以,但前提是ORDER BY字段是 timestamp 类型而非字符串
最易忽略的一点:数据库服务端时区设置(time_zone)和连接时区(SET time_zone = '+08:00')可能不一致,导致 NOW() 和 CONVERT_TZ 行为割裂。上线前务必用 SELECT @@global.time_zone, @@session.time_zone 确认二者是否统一。

















