【问题标题】:How to optimize this method and avoid foreach loop如何优化此方法并避免 foreach 循环
【发布时间】:2020-06-15 13:54:39
【问题描述】:

我的 Laravel 应用程序中有以下模型:课程、活动、学生、注册、出勤。

  • 一个课程有许多事件。
  • 当学生注册课程时,会生成一个 Enrollment,因此课程有Many Enrolments。
  • 一个学生有很多出勤记录。
  • 出勤记录属于活动和学生。

我想检索至少有一名学生的出勤记录少于课程中预期事件总数的课程列表。例如,如果一门课程有 10 个活动,我需要获取所有课程,其中至少有一名学生的出勤记录少于这些活动的 10 个预期出勤记录。

作为临时解决方案,我在课程模型中使用了嵌套的 foreach 循环:

public function getEventsWithExpectedAttendanceAttribute()
{
    return $this->events()->where(function($query) {
        $query->where('exempt_attendance', '!=', true);
        $query->where('exempt_attendance', '!=', 1);
        $query->orWhereNull('exempt_attendance');
    })->where('start', '<', Carbon::now(env('COURSES_TIMEZONE'))->toDateTimeString())->get();
}

public function getMissingAttendanceAttribute()
{
    $eventsWithMissingAttendanceCount = 0;

    // loop through every event supposed to have attendance
    foreach ($this->events_with_expected_attendance as $event)
    {
        // loop through every student
        foreach ($this->enrollments as $enrollment)
        {
            // if the student has no attendance record for this event
            if (Attendance::where('student_id', $enrollment->student_id)->where('event_id', $event->id)->count() == 0)
            {
    // count one and break loop
            $eventsWithMissingAttendanceCount++;
            break;
            }
        }
    }

    return $eventsWithMissingAttendanceCount;
}

但这真的不漂亮,而且在性能方面也非常低效。但是,到目前为止,我一直无法找到更好的解决方案。任何帮助将不胜感激!

更新:这里是数据库架构

CREATE TABLE `courses` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`));

CREATE TABLE `events` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `course_id` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`));

CREATE TABLE `enrollments` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `student_id` int(10) unsigned NOT NULL,
  `course_id` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`));

CREATE TABLE `students` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`));

CREATE TABLE `attendances` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `student_id` int(10) unsigned NOT NULL,
  `event_id` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`));

【问题讨论】:

  • 如果是我,我会从 sql 开始。如果这听起来是一个合理的想法,请删除上面的内容并改为查看 meta.stackoverflow.com/questions/333952/…
  • 我在这里同意草莓。这应该可以在sql中解决。您可以添加表格方案,让想要回答的人更容易。
  • 我明白了,也许 sql 确实是一个更好的选择。我用信息更新了我的问题,以重现类似于我的数据库的简化模式。

标签: php mysql laravel optimization eloquent


【解决方案1】:

您的方法具有二次 n^2 性能,因为查询数乘以事件数和出席数。

减少查询数量的第一步可能是从最内层循环中提取此查询

Attendance::where('student_id', $enrollment->student_id)->where('event_id', $event->id)->count() == 0

并提前获取出勤率

public function getMissingAttendanceAttribute()
{
    $eventsWithMissingAttendanceCount = 0;

    $eventsIDs = $this->events_with_expected_attendance->pluck('id');
    $studentIDs = $this->enrollments->pluck('student_id');

    // Collection of attendances
    $attendances = Attendance::whereIn('student_id', $studentIDs)
        ->whereIn('event_id', $eventsIDs)
        ->get();


    // loop through every event supposed to have attendance
    foreach ($this->events_with_expected_attendance as $event)
    {
        // loop through every student
        foreach ($this->enrollments as $enrollment)
        {
            $hasNotAttended = $attendances->where('student_id', $enrollment->student_id)
                ->where('event_id', $event->id)
                ->isEmpty();

            if ($hasNotAttended) {
                $eventsWithMissingAttendanceCount++;
                break;
            }
        }
    }

    return $eventsWithMissingAttendanceCount;
}

这应该会提高性能,因为查询的数量不会随着事件和注册的数量而扩展,而是保持不变。

例如,您可以将$hasNotAttended 提取到注册模型的方法中。

public function getMissingAttendanceAttribute()
{
    $eventsWithMissingAttendanceCount = 0;

    $eventsIDs = $this->events_with_expected_attendance->pluck('id');
    $studentIDs = $this->enrollments->pluck('student_id');

    // Collection of attendances
    $attendances = Attendance::whereIn('student_id', $studentIDs)
        ->whereIn('event_id', $eventsIDs)
        ->get();


    // loop through every event supposed to have attendance
    foreach ($this->events_with_expected_attendance as $event)
    {
        // loop through every student
        foreach ($this->enrollments as $enrollment)
        {
            // pass preloaded attendances.
            // alternatively you could preload this relationship then no need to pass $attendances
            if ($enrollment->hasNotAttended($event->id, $attendances)) {
                $eventsWithMissingAttendanceCount++;
                break;
            }
        }
    }

    return $eventsWithMissingAttendanceCount;
}

然后您可以通过提取以下内容来扩展$this-&gt;enrollments 集合https://laravel.com/docs/6.x/collections#extending-collections

    if ($enrollment->hasNotAttended($event->id, $attendances)) {
        $eventsWithMissingAttendanceCount++;
        break;
    }

所以,它会被简化为

public function getMissingAttendanceAttribute()
{
    $eventsWithMissingAttendanceCount = 0;

    $eventsIDs = $this->events_with_expected_attendance->pluck('id');
    $studentIDs = $this->enrollments->pluck('student_id');

    // Collection of attendances
    $attendances = Attendance::whereIn('student_id', $studentIDs)
        ->whereIn('event_id', $eventsIDs)
        ->get();


    // loop through every event supposed to have attendance
    foreach ($this->events_with_expected_attendance as $event)
    {
        if ($this->enrollments->hasNotAttended($event->id, $attendances)) {
            $eventsWithMissingAttendanceCount++;
        }
    }

    return $eventsWithMissingAttendanceCount;
}

【讨论】:

  • 很好的解决方案!感谢您的帮助和详细的解释。我了解您的方法的好处,并相应地更新了我的代码。事实上,您的解决方案大大减少了数据库查询的数量。这正是我想要的。
【解决方案2】:

enrollments(例如)是一个多:多映射表。您拥有的架构定义非常低效。这提供了一些提高该领域性能的提示:http://mysql.rjweb.org/doc.php/index_cookbook_mysql#many_to_many_mapping_table

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-11-17
    • 1970-01-01
    • 2018-12-27
    • 2023-02-08
    • 2018-07-31
    • 2012-06-12
    • 1970-01-01
    • 2021-08-24
    相关资源
    最近更新 更多