Laravel使用subWeek()查询近7天记录返回8天数据问题排查

当前日期为 2/24/2022
在报表页面中,我从查询参数读取interval值,控制器代码如下:
$inputs = Request::all(); $interval = 'week'; // <<------------ 默认值 if(array_key_exists('interval', $inputs)){ $interval = $inputs['interval']; } switch ($interval) { case 'day': $q = BabyLog::where('updated_at', '>', now()->today()); break; case 'week': $q = BabyLog::where('updated_at', '>', now()->subWeek()); break; case 'month': $q = BabyLog::where('updated_at', '>', now()->subMonth()); break; case 'year': $q = BabyLog::where('updated_at', '>', now()->subYear()); break; default: $q = BabyLog::orderBy('updated_at', 'desc'); break; } $logs = $q->where('babyId',$baby->id)->orderBy('updated_at', 'desc')->get()->groupBy(function ($log) { return $log->updated_at->format('Y-m-d'); });
执行dd($logs);返回结果如下:
Illuminate\Database\Eloquent\Collection {#344 ▼ #items: array:8 [▼ "2022-02-24" => Illuminate\Database\Eloquent\Collection {#352 ▶} "2022-02-23" => Illuminate\Database\Eloquent\Collection {#353 ▶} "2022-02-22" => Illuminate\Database\Eloquent\Collection {#351 ▶} "2022-02-21" => Illuminate\Database\Eloquent\Collection {#349 ▶} "2022-02-20" => Illuminate\Database\Eloquent\Collection {#350 ▶} "2022-02-19" => Illuminate\Database\Eloquent\Collection {#348 ▶} "2022-02-18" => Illuminate\Database\Eloquent\Collection {#346 ▶} "2022-02-17" => Illuminate\Database\Eloquent\Collection {#345 ▶} ] }
我的需求是仅在图表中展示最近7天的记录,现有以下疑问:
- 为什么结果列表中会包含
2022-02-17的数据? - 上述代码存在什么错误?
- 调用
subWeek()是否总会返回8天的数据? - 是否需要对
subWeek()的结果减1天来修正?
补充说明:调用
subMonth()、subYear()时并未出现该问题,不需要额外做减1天处理。
问题解答
为什么结果包含
2022-02-17的数据?
你的查询条件写的是updated_at > now()->subWeek()。当前日期是2022-02-24,假设你执行代码的时间是2月24日15:30,now()->subWeek()算出来的时间就是2月17日15:30。这个条件会捞取所有2月17日15:30之后生成的记录,自然会包含2月17日15:30到24点之间的数据,按日期分组后就会出现2022-02-17的分组。上述代码存在什么错误?
核心问题是时间边界没有对齐自然日:你要按天分组展示最近N天的统计,却用了精确到时分秒的动态时间作为查询边界,没有把边界锚定到对应日期的0点,导致边界时间之前所属自然日的部分数据被纳入查询范围。
另外还有个冗余问题:default分支里提前写了orderBy('updated_at', 'desc'),后面查询构造时又重复调用了一次orderBy,属于多余代码。调用
subWeek()是否总会返回8天的数据?
不会。返回多少天的数据完全取决于你执行代码的时间点:如果你刚好在某天0点整执行这段代码,now()->subWeek()刚好落在7天前的0点,此时查询结果就只会有7个日期分组;只要执行时间晚于当天0点,就会把7天前0点到边界时刻的那部分数据查出来,凑出8个日期分组。是否需要对
subWeek()的结果减1天来修正?
完全不需要,减1天是错误的修复方式,会导致你少查一天数据。
正确的修复方式是把时间边界对齐到自然日的起始点:如果要展示包含当天在内的最近7天数据,把周维度的查询条件改成BabyLog::where('updated_at', '>=', now()->subDays(6)->startOfDay())即可,这个条件会捞取6天前0点到当前时刻的所有数据,刚好覆盖7个自然日,不会多查出更早日期的记录。
你提到subMonth()、subYear()没出现这个问题只是数据巧合——刚好这两个方法算出的边界时间点之前,对应上个月/上一年的那个时间点没有产生数据而已,逻辑本身是有问题的,只要对应时间点有数据,一样会出现多一个日期分组的情况。
内容的提问来源于stack exchange,提问作者code-8

