最稳妥做法是GROUP BY DATE(created_at),适用于DATETIME/TIMESTAMP类型;字符串需先转类型,避免用YEAR/MONTH/DAY分组;PostgreSQL用::DATE或date_trunc();注意时区、索引失效和WHERE时间范围。

MySQL里用 DATE() 提取日期再分组
直接对时间字段用 GROUP BY DATE(created_at) 是最常用也最稳妥的做法。前提是 created_at 是 DATETIME 或 TIMESTAMP 类型,DATE() 会丢掉时分秒,只保留年月日。
- 如果字段是字符串(比如
'2024-05-20 14:23:11'),DATE()仍能自动解析,但性能略差;建议先转成DATETIME类型 - 别用
GROUP BY YEAR(created_at), MONTH(created_at), DAY(created_at)—— 这样写语义不清,且跨年时容易出错(比如 2023-12-31 和 2024-01-01 会被当成同一天) - 注意时区:如果数据库时区和业务时区不一致,
DATE()返回的是数据库本地时间,可能和你预期的“当天”不符
PostgreSQL 必须用 ::DATE 或 date_trunc()
PostgreSQL 不认 DATE() 函数,直接写 GROUP BY created_at::DATE 最简洁;如果还要兼容小时粒度,可以用 date_trunc('day', created_at)。
-
created_at::DATE是类型转换,速度快;date_trunc()更灵活,但多一层函数调用 - 如果
created_at是TEXT,得先用TO_TIMESTAMP()转换,否则会报错:ERROR: invalid input syntax for type timestamp - 索引失效风险:对字段做类型转换或函数操作后,普通 B-tree 索引大概率无法命中,查大表前先
EXPLAIN看执行计划
WHERE 条件别漏掉时间范围,否则统计结果失真
按天分组本身不带时间过滤,如果没加 WHERE created_at >= '2024-05-01' AND created_at ,可能把历史脏数据或未来测试数据一起算进去。
- 推荐用左闭右开区间(
而不是),避免重复计入边界日的 00:00:00 - 别用
BETWEEN—— 它包含上下界,对DATETIME字段容易漏掉最后一秒 - 如果业务按自然日统计(比如每天 00:00–23:59),确保 WHERE 条件和
DATE()/::DATE使用同一套时区逻辑
统计结果为空的日期怎么补全?
原生 SQL 没法自动补零,必须靠生成日期序列再 LEFT JOIN。MySQL 8.0+ 可用 CTE,PostgreSQL 用 GENERATE_SERIES()。
- MySQL 示例:
WITH RECURSIVE dates AS (SELECT '2024-05-01' AS day UNION ALL SELECT DATE_ADD(day, INTERVAL 1 DAY) FROM dates WHERE day - PostgreSQL 更简单:
SELECT gs::DATE, COALESCE(t.cnt, 0) FROM GENERATE_SERIES('2024-05-01'::DATE, '2024-05-31'::DATE, '1 day') gs LEFT JOIN (SELECT created_at::DATE d, COUNT(*) cnt FROM log GROUP BY created_at::DATE) t ON gs = t.d - 补全逻辑通常在应用层做更可控,SQL 层硬补容易拖慢查询,尤其跨度超过 30 天时
时区、空值、索引失效、日期补全——这几个点不提前确认,跑出来的数字看着对,实际可能全错。










