正确做法是用数据库生成日期序列加左连接统计,或PHP侧生成完整日期范围后合并结果;因for循环查库性能差、易漏数据、不支持空日期补零。

如何用 ThinkPHP5.1 实现两个日期间每日统计?
直接结论:别用 for 循环手动遍历日期再查库,性能差、易漏数据、不支持空日期补零。正确做法是用数据库生成日期序列 + 左连接统计,或 PHP 侧生成完整日期范围后合并结果。
为什么不能用 Db::query() 配合 for 循环查每一天?
常见错误是写个 for ($d = $start; $d ,然后每次查一次数据库。问题很实在:
- 100 天就发 100 次 SQL,哪怕加了索引也扛不住并发
-
created_at字段如果没精确到日(比如存的是'2024-03-15 14:22:03'),WHERE created_at = '2024-03-15'会漏掉当天所有记录 - 某天没数据,结果里就直接跳过,前端画图断层
- MySQL 8.0+ 支持 CTE,但 TP5.1 的
Db类不原生支持递归 CTE,硬写raw容易出错
推荐方案:PHP 生成日期数组 + 一次性查出有效数据再合并
兼顾兼容性、可读性和可控性。核心是两步:先拿到完整日期范围,再用 IN 或时间范围查出有数据的天,最后用 array_fill_keys 补零。
示例代码片段(假设统计 order 表每天创建订单数):
立即学习“PHP免费学习笔记(深入)”;
$start = '2024-03-01';
$end = '2024-03-10';
$dates = [];
for ($d = $start; $d <= $end; $d = date('Y-m-d', strtotime("$d +1 day"))) {
$dates[$d] = 0;
}
<p>// 一次性查出该区间内所有有数据的日期统计
$list = Db::name('order')
->field('DATE(created_at) as day, COUNT(*) as cnt')
->whereBetweenTime('created_at', $start, $end)
->group('day')
->select();</p><p>foreach ($list as $row) {
$dates[$row['day']] = (int)$row['cnt'];
}</p><p>// $dates 现在是 ['2024-03-01' => 5, '2024-03-02' => 0, ...]</p>注意点:
-
whereBetweenTime是 TP5.1 原生方法,自动处理时间字段的范围匹配(比whereTime更稳) - 务必用
DATE(created_at)聚合,否则分组可能按完整时间戳算,导致同一天被拆成多行 - 如果
created_at是 int 时间戳类型,得改用FROM_UNIXTIME(created_at, '%Y-%m-%d')
进阶:用 MySQL 生成日期序列(适合数据量大且 MySQL ≥ 5.7)
当日期跨度大(比如 365 天)、并发高、且你确定能控制 SQL 时,可用变量或 UNION ALL 构造日期表。TP5.1 不支持直接写 CTE,但可以用 Db::query() 执行原生 SQL:
SELECT d.day, COALESCE(t.cnt, 0) AS cnt
FROM (
SELECT DATE_SUB('2024-03-10', INTERVAL (a.a + (10 * b.a)) DAY) AS day
FROM (SELECT 0 AS a UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS a
CROSS JOIN (SELECT 0 AS a UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS b
WHERE DATE_SUB('2024-03-10', INTERVAL (a.a + (10 * b.a)) DAY) >= '2024-03-01'
) AS d
LEFT JOIN (
SELECT DATE(created_at) AS day, COUNT(*) AS cnt
FROM think_order
WHERE created_at BETWEEN '2024-03-01' AND '2024-03-10 23:59:59'
GROUP BY DATE(created_at)
) AS t ON d.day = t.day
ORDER BY d.day;
这个写法的关键在于:UNION ALL 构造的数字表最多支撑 100 天(10×10),超过要加第三层;DATE_SUB 方向别反;BETWEEN 右边界必须带时间,否则丢掉当天最后一秒的数据。
实际项目里,日期跨度不大时,PHP 侧生成更稳妥;真要上 SQL 方案,建议封装成视图或存储过程,别在 TP 里拼太长的原生 SQL。



















