应使用 date_trunc 或等效函数将日期归入自然月再聚合,而非仅用 min/max;postgresql用 date_trunc('month',d) + interval '1 month' - '1 day' 得月末,mysql用 last_day(),须注意时区与null过滤。

如何用 DATE_TRUNC 或 EXTRACT 提取年月并分组
统计每月首尾数据,本质不是“找某天”,而是把日期归到所属月份再聚合。PostgreSQL 和 BigQuery 支持 DATE_TRUNC('month', date_col),直接截断到当月第一天;MySQL 用 DATE_SUB(date_col, INTERVAL DAYOFMONTH(date_col)-1 DAY) 或 STR_TO_DATE(CONCAT(YEAR(date_col), '-', MONTH(date_col), '-01'), '%Y-%m-%d');SQLite 用 strftime('%Y-%m-01', date_col)。别用 GROUP BY YEAR(date_col), MONTH(date_col) —— 这样无法直接拿到首尾日期值,还得二次计算。
为什么不能只靠 MIN(date_col) 和 MAX(date_col)
在按月分组后,MIN(date_col) 确实是该月最早记录的日期,MAX(date_col) 是最晚记录的日期,但它们不一定是当月第一天或最后一天(比如某月只有 5 号和 18 号有数据)。如果业务要求的是「自然月的起止日」(如 2024-03-01 和 2024-03-31),就得显式构造:
- 首日 =
DATE_TRUNC('month', date_col)(PostgreSQL) - 末日 =
DATE_TRUNC('month', date_col) + INTERVAL '1 month' - INTERVAL '1 day' - MySQL 对应写法:首日用
LAST_DAY(date_col) - INTERVAL DAY(LAST_DAY(date_col)) - 1 DAY,末日直接用LAST_DAY(date_col)
一个能同时返回首日、末日、当月数据量的查询示例
SELECT
DATE_TRUNC('month', created_at) AS month_start,
(DATE_TRUNC('month', created_at) + INTERVAL '1 month' - INTERVAL '1 day') AS month_end,
COUNT(*) AS record_count
FROM orders
WHERE created_at >= '2024-01-01'
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY month_start;
注意:month_start 和 month_end 是计算字段,不是原始数据里的值;若需关联其他表或过滤某月完整区间(比如只取 3 月 1 日到 31 日之间的记录),得在 WHERE 里用 created_at BETWEEN ... AND ...,不能依赖 GROUP BY 后的结果反推范围。
容易被忽略的时区与空值问题
DATE_TRUNC 和 LAST_DAY 都受当前会话时区影响。如果 created_at 是 TIMESTAMP WITH TIME ZONE,而你在 UTC 时区执行,但业务按北京时间算月度,结果可能错位一天。另外,NULL 值会让 MIN/MAX 返回 NULL,且 GROUP BY DATE_TRUNC(...) 会把 NULL 单独成一组——务必在 WHERE 中加 created_at IS NOT NULL 过滤。











