最稳做法是用 DATE_FORMAT 或 DATE_TRUNC 转时间字段为区间标识再 GROUP BY;避免 WHERE 中套函数或先分组后筛选;需确保 created_at 类型正确且无非法值,否则记录静默丢失。

直接用 DATE_FORMAT 或 DATE_TRUNC 转时间字段为区间标识再 GROUP BY,是最稳、最可索引的做法。别先分组再筛时间,也别在 WHERE 里套函数。
MySQL 用 DATE_FORMAT 按小时/天/月分组
核心是把时间戳“截断”成统一格式的字符串,作为分组键。比如按小时聚合:
SELECT DATE_FORMAT(created_at, '%Y-%m-%d %H') AS hour_group, COUNT(*) FROM production_log WHERE created_at >= '2026-07-01' AND created_at < '2026-07-21' GROUP BY hour_group;
注意点:
-
created_at必须是DATETIME或TIMESTAMP类型,不是字符串;否则DATE_FORMAT无效且无法走索引 - 用
>=和替代 <code>BETWEEN,避免末尾毫秒被截断漏数据 - 如果要按自然月(如 2026-07-01 到 2026-07-31),用
'%Y-%m';但跨年时必须组合YEAR(created_at)和MONTH(created_at),否则 2025-12 和 2026-01 会被混进同一组
PostgreSQL 优先用 DATE_TRUNC
它语义清晰、时区可控,比拼函数更可靠:
SELECT DATE_TRUNC('hour', created_at) AS hour_group, COUNT(*)
FROM production_log
WHERE created_at >= '2026-07-01' AND created_at < '2026-07-21'
GROUP BY hour_group;常见陷阱:
-
DATE_TRUNC('week', created_at)默认从周一 00:00 开始,不依赖系统 locale;但若字段带时区(如TIMESTAMP WITH TIME ZONE),先做对齐:created_at AT TIME ZONE 'Asia/Shanghai' - 不要只用
EXTRACT(WEEK FROM created_at)单独分组——不同年份的第 1 周会撞在一起 - 想按每 30 分钟分组?
DATE_TRUNC('minute', created_at) + INTERVAL '30 min' * FLOOR(EXTRACT(MINUTE FROM created_at)/30),但性能不如预建辅助表
SQL Server 用 DATEPART + YEAR 防跨年错乱
单独 DATEPART(MONTH, created_at) 或 DATEPART(WEEK, created_at) 会跨年合并,必须配 YEAR:
SELECT YEAR(created_at) AS y, MONTH(created_at) AS m, COUNT(*) AS cnt FROM production_log WHERE created_at >= '2026-07-01' AND created_at < '2026-08-01' GROUP BY YEAR(created_at), MONTH(created_at);
关键细节:
- 别用
FORMAT(created_at, 'yyyy-MM')——它不可索引,大数据量时慢出明显延迟 - 周计算受
DATEFIRST影响,默认是周日(7),业务按周一算就先执行SET DATEFIRST 1 - 如果字段是
datetime2,精度高,DATEPART没问题;但如果是varchar存时间,先用TRY_CONVERT(datetime2, created_at)转,否则 NULL 值直接丢弃
没有数据的日子怎么补 0?
数据库本身不会自动补空日期,得靠生成日期序列再 LEFT JOIN。MySQL 示例(近 30 天):
SELECT d.date_str, COALESCE(t.cnt, 0) AS cnt
FROM (
SELECT DATE_SUB(CURDATE(), INTERVAL n DAY) AS date_str
FROM (
SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION ... UNION SELECT 29
) nums
) d
LEFT JOIN (
SELECT DATE(created_at) AS dt, COUNT(*) AS cnt
FROM production_log
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 29 DAY)
GROUP BY DATE(created_at)
) t ON d.date_str = t.dt;注意:
- 子查询生成的日期范围必须覆盖你要统计的完整区间,不能依赖原始表里存在的日期
- 用
COALESCE而不是IFNULL更跨数据库兼容 - 如果跨度大(比如一年),手写 365 行 UNION 不现实,应改用递归 CTE(PostgreSQL/SQL Server)或临时数字表
真正容易被忽略的是:所有这些分组逻辑都建立在 created_at 字段有有效值且类型正确的基础上。一旦它是 VARCHAR 或含非法时间(如 '0000-00-00'),整行记录就在 GROUP BY 后消失,连告警都没有。

















