You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

优化Django ORM关联查询以高效生成学生出勤报表

优化Django出勤报表生成效率方案

问题背景

现有Django模型结构如下:

class Student(models.Model):
    full_name = models.CharField(max_length=100)
    std_class = models.CharField(max_length=50)

class Teacher(models.Model):
    full_name = models.CharField(max_length=100, unique=True)

class Attendance(models.Model):
    att_date = models.DateField(default=date.today)
    teacher = models.ManyToManyField(Teacher)
    subject_name = models.CharField(max_length=50)
    std_class = models.CharField(max_length=50)
    total_students = models.IntegerField(default=0)

class Entry(models.Model):
    attendance = models.ForeignKey(Attendance, on_delete=models.CASCADE)
    student = models.ForeignKey(Student, on_delete=models.CASCADE) 

需求为指定班级生成出勤报表:每行对应一个学生,每列对应一次出勤记录(含日期、教师、科目),存在Entry记录标记为P(出勤),否则标记为A(缺勤)。原实现代码功能正常但效率极低。

原代码的性能瓶颈

原代码存在两个核心性能问题:

  1. 循环嵌套冗余:外层遍历出勤记录,内层先遍历所有学生标记A,再遍历该次出勤的学生标记P,时间复杂度为O(M*N + M*K)(M为出勤记录数,N为班级学生数,K为单条出勤的学生数),数据量大时耗时剧增。
  2. N+1查询爆炸:每次遍历Attendance实例时单独查询Entry,模板中遍历item.teacher.all也会触发额外SQL查询,导致数据库请求次数呈指数级增长。

优化方案

1. 批量预取关联数据,消除N+1查询

通过prefetch_related和select_related一次性拉取所有需要的数据,把数据库查询次数压缩到最少:

from django.db.models import Prefetch

# 获取指定班级当月的所有出勤记录,预取关联的教师和出勤条目(含学生)
attendance_list = Attendance.objects.filter(
    std_class=class_full_name,
    att_date__month=curr_month
).order_by('att_date').prefetch_related(
    'teacher',
    Prefetch('entry_set', queryset=Entry.objects.select_related('student'))
)

# 获取班级所有学生,构建双向映射方便后续快速查找
class_students = Student.objects.filter(std_class=class_full_name)
student_id_to_name = {s.id: s.full_name for s in class_students}
student_name_to_id = {s.full_name: s.id for s in class_students}

2. 重构数据构建逻辑,减少循环嵌套

先初始化每个学生的出勤状态列表(默认全A),再通过预取的entry_set直接标记P,避免重复遍历操作:

# 初始化报表字典:学生姓名 -> 出勤状态列表(初始长度等于出勤记录数,全为'A')
report_dict = {name: ['A'] * len(attendance_list) for name in student_name_to_id.keys()}

# 遍历每条出勤记录,记录其在列表中的索引
for idx, attendance in enumerate(attendance_list):
    # 从预取的entry_set中快速提取本次出勤的学生id集合
    present_student_ids = {entry.student.id for entry in attendance.entry_set.all()}
    # 遍历所有学生,匹配到出勤的直接标记为'P'
    for name, student_id in student_name_to_id.items():
        if student_id in present_student_ids:
            report_dict[name][idx] = 'P'

3. 模板无额外修改(已自动优化)

由于已经通过prefetch_related('teacher')预取了教师数据,模板中遍历item.teacher.all不会再触发额外SQL查询,保持原有模板代码即可:

<tr>
    <th>Student Name</th>

    {% if attendance_list %}
        {% for item in attendance_list %}
            <th>{{ item.att_date }} <br>

                {% for teacher_name in item.teacher.all %}
                    {{ teacher_name }}<br/>
                {% endfor %}

                {{ item.subject_name }}
            </th>
        {% endfor %}
    {% endif %}
</tr>

优化效果

  • 数据库查询次数从1 + M + M*T(T为单条出勤的教师数)减少到2次(一次查Attendance及关联数据,一次查班级学生)。
  • 时间复杂度降低到O(M + N + K)(K为所有出勤的学生总数),远低于原代码的嵌套循环复杂度,数据量大时性能提升显著。

内容的提问来源于stack exchange,提问作者Vilashkumar Daate

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 04:54:52