Skip to content

Conversation

@cp-at-mit
Copy link
Contributor

@cp-at-mit cp-at-mit commented Jan 5, 2026

What are the relevant tickets?

https://github.com/mitodl/hq/issues/8776

Description (What does it do?)

This PR addresses a lot of different N+1 issues. After resolving the N+1 issue that appears in the linked issue, other's began to emerge.

How can this be tested?

Tests should pass, N+1 issues should no longer be present.

@github-actions
Copy link

github-actions bot commented Jan 6, 2026

OpenAPI Changes

Show/hide ## Changes for v0.yaml:
## Changes for v0.yaml:


## Changes for v1.yaml:


## Changes for v2.yaml:


Unexpected changes? Ensure your branch is up-to-date with main (consider rebasing).

@cp-at-mit cp-at-mit marked this pull request as ready for review January 7, 2026 16:24
Copy link
Collaborator

@rhysyngsun rhysyngsun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, noted a few things, some are nits.

Comment on lines +246 to +249
hasattr(instance, "_prefetched_objects_cache")
and "courseruns" in instance._prefetched_objects_cache # noqa: SLF001
):
courseruns = instance._prefetched_objects_cache["courseruns"] # noqa: SLF001
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's always iffy to use internals like this, but the only things I can think of would just make this more convoluted so I think you hit the best approach given the limitations of the library.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I didn't feel great doing it.


return (
queryset.prefetch_related(
Prefetch("courseruns", queryset=enrollable_courseruns_qs)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The removal of this will cause unenrollable course runs to start being returned and I don't thnik we want to change that behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will the filter below handle removing unenrollable course runs from what is returned?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're talking about

.filter(courseruns__id__in=enrollable_courseruns_qs.values_list("id", flat=True))

the filter only applies to Course records. Then courseruns is preloaded in a separate query that has no knowledge of that filter().

So

prefetch_related("courseruns")

is always implicitly

prefetch_related(Prefetch("courseruns", queryset=CourseRun.objects.all()))

The behavior might be different if we were doing select_related("courseruns") here, but I'd honestly need to test it out to know exactly what django would automagically do in that case.

courseruns_qs = CourseRun.objects.unenrollable()
courseruns_qs = CourseRun.objects.unenrollable().prefetch_related("products")
return (
queryset.prefetch_related(Prefetch("courseruns", queryset=courseruns_qs))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, the removal of this will cause unenrollable courses to be returned.

def get_queryset(self):
return (
CourseRunEnrollment.objects.filter(user=self.request.user)
.select_related("run__course__page", "user", "run")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be good to move at least run_course_page and possibly run out of the select_related and into prefetch_related too, that's a lot of joins particularly for the page to be doing.


# Get direct topics from the page
# Get direct topics from the page (this should be prefetched)
direct_topics = page_instance.topics.all()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably also prefetch parent since that's another set of N+1s.

)

return queryset
.all()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I'm not sure how the pattern of putting in all() ended up being introduced, but you only need to use all() when you need the default queryset from an objects manager. In this usage, it just returns a copy of the query. It's basically glue-code django implemented so that a queryset can swap in for a manager in cases where all() is called on it.

Course.objects.all()  # needed here to get a queryset
Course.objects.filter(id_in=[1,2]) # would be a no-op here because we already have a queryset

Co-authored-by: Nathan Levesque <rhysyngsun@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants