用$dateToParts提取年月再分组是最直接方案,需确保created_at为BSON Date类型,字符串需先用$dateFromString转换,并注意UTC时区偏移;推荐拼接"YYYY-MM"字符串键以便排序和导出。

用 $dateToParts 提取年月再分组,是最直接的方案
MongoDB 本身不支持类似 SQL 的 DATE_FORMAT(created_at, '%Y-%m'),但 $dateToParts 能把日期字段拆成 year 和 month 字段,配合 $group 就能精准统计每月新增用户数。注意:该操作要求 created_at 是 BSON Date 类型,字符串日期(如 "2024-03-15")会返回空对象,导致分组丢失数据。
- 确保字段是 Date 类型:可用
{$type: "date"}过滤验证,或用$dateFromString转换字符串 -
$dateToParts默认按 UTC 解析,若业务用本地时区(如东八区),需先用$add加上 8 小时偏移再拆分 - 输出字段名固定为
year、month、day等,不能自定义,后续$group必须引用这些键名
聚合管道写法:先拆解再组合年月字段
不能只靠 year 和 month 两个字段做 $group,否则结果会是二维结构(如 {year: 2024, month: 3}),不方便排序或导出。更实用的做法是用 $concat 拼成 "2024-03" 这样的字符串键。
[
{
$project: {
yearMonth: {
$concat: [
{$toString: "$dateParts.year"},
"-",
{$toString: {$cond: [{ $lt: ["$dateParts.month", 10] }, {$concat: ["0", {$toString: "$dateParts.month"}]}, {$toString: "$dateParts.month"}]}}
]
}
}
},
{
$group: {
_id: "$yearMonth",
count: { $sum: 1 }
}
},
{ $sort: { _id: 1 } }
]
其中 $cond 是为了补零(3 → "03"),避免 "2024-1" 和 "2024-10" 在字符串排序时错序。
常见错误:$dateToParts 返回空或报错
最常遇到的是管道中 $dateToParts 输出全空字段,典型表现是 $group 后只剩一个 _id: null 分组,count 等于全部文档数。这几乎一定是输入值不是 Date 类型。
- 检查原始字段:
db.users.findOne({ created_at: { $type: "string" } })—— 如果有结果,说明混入了字符串 - 安全转换写法:
{ $dateToParts: { date: { $dateFromString: { dateString: "$created_at" } } } },但要注意$dateFromString对格式敏感,默认只认 ISO 格式("2024-03-15T08:30:00Z"),中文格式如"2024年3月15日"会失败 - 聚合阶段顺序不能颠倒:必须先
$dateToParts再$project引用其输出,反过来会报undefined错误
性能与索引注意事项
$dateToParts 是内存计算操作,无法利用索引加速。如果集合很大(千万级+),且查询高频固定时间范围(比如“近12个月”),建议在应用层预计算并存到新字段(如 created_year_month: "2024-03"),再对该字段建索引。
- 临时提速技巧:加
$match前置过滤,例如{ created_at: { $gte: ISODate("2023-01-01") } },减少进入$dateToParts的文档量 - 聚合结果默认无索引,导出后做二次分析(如环比计算)得靠应用代码,MongoDB 不提供窗口函数等后续处理能力
- 使用
explain("executionStats")查看实际扫描文档数,确认$match是否生效
created_at 都是干净的 Date 类型——线上数据库里总藏着几条被前端传错、被脚本硬编码、被迁移工具转丢精度的“假日期”。


















