1. 场景与目标
问题描述
在数据分析和报表场景中,月度聚合经常遇到“缺失月份”的情况,即某些月没有交易记录,查询结果会跳过这些月。这会导致前后月份对比失真,需要一个完整的月份序列来补全缺失数据。
本文章围绕 Laravel8 数据查询中如何补全缺失的月份?完整实现指南 展开,讲解从数据库层到 Laravel 层的落地实现思路与具体示例。
2. 月份序列设计与数据建模
为什么需要一个统一的月份集合
通过一个统一的月份序列,我们可以对原始数据进行左连接,把每个月都映射出来,未发生的月份用 0 或 null 表示,便于后的展示与对比。
常见做法是维护一个专门的日历表/时间维度表,字段通常包括 year、month、以及一个可选的 month_key(YYYY-MM)。

3. 数据库端实现:生成完整月份序列的两种方案
MySQL 8+:使用递归公用表表达式(CTE)生成月份
WITH RECURSIVE months AS (SELECT DATE_FORMAT('2020-01-01', '%Y-%m-01') AS month_startUNION ALLSELECT DATE_ADD(month_start, INTERVAL 1 MONTH)FROM monthsWHERE month_start < '2025-12-01'
)
SELECT DATE_FORMAT(month_start, '%Y-%m') AS ym,COALESCE(SUM(t.amount), 0) AS total
FROM months
LEFT JOIN your_table tON DATE_FORMAT(t.date, '%Y-%m') = DATE_FORMAT(month_start, '%Y-%m')
GROUP BY ym
ORDER BY ym;
通过 递归CTE,可以在一个查询中生成从起始月到结束月的完整月份集合,并与数据表进行 LEFT JOIN,COALESCE 处理缺失值,从而实现缺失月份的补全。
PostgreSQL:使用 generate_series 生成月份
SELECT to_char(m, 'YYYY-MM') AS ym,COALESCE(SUM(t.amount), 0) AS total
FROM generate_series(date '2020-01-01', date '2025-12-01', interval '1 month') AS m
LEFT JOIN your_table t ON date_trunc('month', t.date) = m
GROUP BY ym
ORDER BY ym;
解释:generate_series 是 PostgreSQL 的原生函数,生成整段时间序列,随后通过 LEFT JOIN 与现有数据表进行补全,确保每个月都有一条记录。
4. Laravel8 数据查询中的实现:两种落地思路
方案A:直接执行数据库层的完整月份查询
在 Laravel 8 中,可以将上面的 SQL 直接通过 DB facade 执行,获取经过月度填充的结果集,并可直接用于前端渲染。这样做的好处是逻辑集中、SQL 可维护性高。
$start = '2020-01-01';
$end = date('Y-m-d', strtotime('today'));$rows = DB::select("WITH RECURSIVE months AS (SELECT DATE_FORMAT('$start', '%Y-%m-01') AS month_startUNION ALLSELECT DATE_ADD(month_start, INTERVAL 1 MONTH)FROM monthsWHERE month_start < DATE_FORMAT('$end', '%Y-%m-01'))SELECT DATE_FORMAT(month_start, '%Y-%m') AS ym,COALESCE(SUM(t.amount), 0) AS totalFROM monthsLEFT JOIN your_table t ON DATE_FORMAT(t.date, '%Y-%m') = DATE_FORMAT(month_start, '%Y-%m')GROUP BY ymORDER BY ym
");
在这段代码中,绑定参数可避免 SQL 注入风险,LEFT JOIN 保留所有月份的记录,COALESCE 则实现缺失月份填充为 0。
方案B:在 PHP 层生成月份序列再做合并
当数据库版本不支持递归 CTE,或者你更愿意在应用层控制逻辑时,可以先在 PHP 侧生成月份集合,再从数据库取出对应月份的聚合数据,最后在应用层进行合并。Carbon 可以方便地生成月度序列,whereBetween、groupBy 结合即可实现补全。
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;$start = Carbon::parse('2020-01-01')->startOfMonth();
$end = Carbon::now()->startOfMonth();$months = [];
for ($d = $start->copy(); $d <= $end; $d->addMonth()) {$months[$d->format('Y-m')] = 0;
}$sums = DB::table('orders')->selectRaw("DATE_FORMAT(order_date, '%Y-%m') as ym, SUM(amount) as total")->whereBetween('order_date', [$start, $end])->groupBy('ym')->pluck('total','ym')->toArray();foreach ($sums as $ym => $total) {$months[$ym] = (int)$total;
}$final = [];
foreach ($months as $ym => $total) {$final[] = ['month' => $ym, 'total' => $total];
}
方案B 的要点在于:先生成月份集合,再从数据库取出对应月份的聚合并在应用层进行整合,灵活性更高,且兼容性更强。
5. 实现中的注意点与优化
字段设计与索引
为了让聚合查询高效,YYYY-MM 的月份字段应保持规范化,便于排序与对比;在日期字段上建立索引能显著提升按月聚合的性能。
另外,使用 COALESCE 在缺失月份上填充 0,能让前端渲染更直观;若数据量较大,可以考虑 物化视图、缓存层或分区表来提升性能。
缺失月份的数值填充策略
默认将缺失月份的聚合值设为 0,这使报表呈现更完整;你也可以将其设为 null,然后在前端或前端图表库中自行处理渲染。
性能与扩展性
当数据量较大时,递归 CTE 和大规模 LEFT JOIN 的成本可能较高。此时可以考虑将月份序列作为独立的日历表,建立索引、分区或物化视图,并对热点时间段进行缓存。


