Fix submission list API performance: N+1 queries, missing indexes, and in-memory pagination
- Add select_related("user") in update_score() to eliminate N+1 on rating.user.role
- Return QuerySet directly from list_submissions so @paginate works at DB level
- Defer html/css/js fields in list queries to reduce data transfer
- Add db_index on flag field for filtered queries
- Add select_related("task", "user") to get_submission endpoint
- Replace manual list() serialization with resolve_* methods on SubmissionOut
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,9 @@ def list_submissions(request, filters: SubmissionFilter = Query(...)):
|
|||||||
"""
|
"""
|
||||||
获取提交列表,支持按任务和用户过滤
|
获取提交列表,支持按任务和用户过滤
|
||||||
"""
|
"""
|
||||||
submissions = Submission.objects.select_related("task", "user")
|
submissions = Submission.objects.select_related("task", "user").defer(
|
||||||
|
"html", "css", "js"
|
||||||
|
)
|
||||||
|
|
||||||
if filters.task_id:
|
if filters.task_id:
|
||||||
task = get_object_or_404(Task, id=filters.task_id)
|
task = get_object_or_404(Task, id=filters.task_id)
|
||||||
@@ -76,12 +78,7 @@ def list_submissions(request, filters: SubmissionFilter = Query(...)):
|
|||||||
)
|
)
|
||||||
submissions = submissions.annotate(my_score=user_rating_subquery)
|
submissions = submissions.annotate(my_score=user_rating_subquery)
|
||||||
|
|
||||||
def get_submission_data(submission):
|
return submissions
|
||||||
"""从 submission 对象构建 SubmissionOut 数据"""
|
|
||||||
my_score = getattr(submission, "my_score", None) or 0
|
|
||||||
return SubmissionOut.list(submission, {submission.id: my_score})
|
|
||||||
|
|
||||||
return [get_submission_data(submission) for submission in submissions]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{submission_id}", response=SubmissionOut)
|
@router.get("/{submission_id}", response=SubmissionOut)
|
||||||
@@ -90,10 +87,11 @@ def get_submission(request, submission_id: UUID):
|
|||||||
"""
|
"""
|
||||||
获取单个提交的详细信息
|
获取单个提交的详细信息
|
||||||
"""
|
"""
|
||||||
submission = get_object_or_404(Submission, id=submission_id)
|
submission = get_object_or_404(
|
||||||
|
Submission.objects.select_related("task", "user"), id=submission_id
|
||||||
|
)
|
||||||
rating = (
|
rating = (
|
||||||
Rating.objects.select_related("user", "submission")
|
Rating.objects.filter(user=request.user, submission=submission)
|
||||||
.filter(user=request.user, submission=submission)
|
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
return SubmissionOut.get(submission, rating)
|
return SubmissionOut.get(submission, rating)
|
||||||
|
|||||||
18
submission/migrations/0005_add_flag_index.py
Normal file
18
submission/migrations/0005_add_flag_index.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.1 on 2026-03-09 10:24
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('submission', '0004_submission_flag'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='submission',
|
||||||
|
name='flag',
|
||||||
|
field=models.CharField(blank=True, choices=[('red', '值得展示'), ('blue', '需要讲解'), ('green', '优秀作品'), ('yellow', '需要改进')], db_index=True, default=None, max_length=10, null=True, verbose_name='标记'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -44,6 +44,7 @@ class Submission(TimeStampedModel):
|
|||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
default=None,
|
default=None,
|
||||||
|
db_index=True,
|
||||||
verbose_name="标记",
|
verbose_name="标记",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ class Submission(TimeStampedModel):
|
|||||||
"""
|
"""
|
||||||
更新当前Submission的分数
|
更新当前Submission的分数
|
||||||
"""
|
"""
|
||||||
ratings = self.ratings.all()
|
ratings = list(self.ratings.select_related("user").all())
|
||||||
|
|
||||||
super_score = 0.0
|
super_score = 0.0
|
||||||
admin_score = 0.0
|
admin_score = 0.0
|
||||||
@@ -77,13 +78,13 @@ class Submission(TimeStampedModel):
|
|||||||
else:
|
else:
|
||||||
normal_score += rating.score
|
normal_score += rating.score
|
||||||
|
|
||||||
if ratings.exists():
|
if ratings:
|
||||||
total_score = super_score * 0.5 + admin_score * 0.3 + normal_score * 0.2
|
total_score = super_score * 0.5 + admin_score * 0.3 + normal_score * 0.2
|
||||||
self.score = total_score / ratings.count()
|
self.score = total_score / len(ratings)
|
||||||
else:
|
else:
|
||||||
self.score = 0.0
|
self.score = 0.0
|
||||||
|
|
||||||
self.save()
|
self.save(update_fields=["score"])
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class SubmissionOut(Schema):
|
|||||||
task_title: str
|
task_title: str
|
||||||
task_type: Literal["tutorial", "challenge"]
|
task_type: Literal["tutorial", "challenge"]
|
||||||
score: float
|
score: float
|
||||||
my_score: int
|
my_score: int = 0
|
||||||
html: Optional[str] = None
|
html: Optional[str] = None
|
||||||
css: Optional[str] = None
|
css: Optional[str] = None
|
||||||
js: Optional[str] = None
|
js: Optional[str] = None
|
||||||
@@ -30,22 +30,40 @@ class SubmissionOut(Schema):
|
|||||||
modified: str
|
modified: str
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def list(submission, rating_dict):
|
def resolve_userid(obj):
|
||||||
return {
|
return obj.user.id
|
||||||
"id": submission.id,
|
|
||||||
"userid": submission.user.id,
|
@staticmethod
|
||||||
"username": submission.user.username,
|
def resolve_username(obj):
|
||||||
"task_id": submission.task.id,
|
return obj.user.username
|
||||||
"task_display": submission.task.display,
|
|
||||||
"task_title": submission.task.title,
|
@staticmethod
|
||||||
"task_type": submission.task.task_type,
|
def resolve_task_id(obj):
|
||||||
"score": submission.score,
|
return obj.task.id
|
||||||
"my_score": rating_dict.get(submission.id, 0),
|
|
||||||
"conversation_id": submission.conversation_id,
|
@staticmethod
|
||||||
"flag": submission.flag,
|
def resolve_task_display(obj):
|
||||||
"created": submission.created.isoformat(),
|
return obj.task.display
|
||||||
"modified": submission.modified.isoformat(),
|
|
||||||
}
|
@staticmethod
|
||||||
|
def resolve_task_title(obj):
|
||||||
|
return obj.task.title
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_task_type(obj):
|
||||||
|
return obj.task.task_type
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_my_score(obj):
|
||||||
|
return getattr(obj, "my_score", None) or 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_created(obj):
|
||||||
|
return obj.created.isoformat()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_modified(obj):
|
||||||
|
return obj.modified.isoformat()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get(submission, rating):
|
def get(submission, rating):
|
||||||
|
|||||||
Reference in New Issue
Block a user