From a63d94843af00912f5445069c4ab2f01d679197d Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 9 Jul 2025 22:05:17 -0500 Subject: [PATCH 1/4] Optimize slow query --- .../api/endpoints/organization_releases.py | 41 ++++++++++--------- src/sentry/models/release.py | 32 +++++++++++++++ .../endpoints/test_organization_releases.py | 2 +- 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/src/sentry/api/endpoints/organization_releases.py b/src/sentry/api/endpoints/organization_releases.py index b6b0ba83052eef..14eaf7539a2cb6 100644 --- a/src/sentry/api/endpoints/organization_releases.py +++ b/src/sentry/api/endpoints/organization_releases.py @@ -30,7 +30,12 @@ from sentry.models.activity import Activity from sentry.models.orgauthtoken import is_org_auth_token_auth, update_org_auth_token_last_used from sentry.models.project import Project -from sentry.models.release import Release, ReleaseStatus +from sentry.models.release import ( + Release, + ReleaseStatus, + filter_releases_by_environments, + filter_releases_by_projects, +) from sentry.models.releases.exceptions import ReleaseCommitError from sentry.models.releases.release_project import ReleaseProject from sentry.models.releases.util import SemverFilter @@ -307,7 +312,11 @@ def get(self, request: Request, organization) -> Response: # health data in the last 24 hours. debounce_update_release_health_data(organization, filter_params["project_id"]) - queryset = Release.objects.filter(organization=organization) + queryset = Release.objects.filter(organization_id=organization.id) + queryset = filter_releases_by_environments( + queryset, [e.id for e in filter_params.get("environment_objects", [])] + ) + queryset = queryset.annotate(date=F("date_added")) if status_filter: try: @@ -320,9 +329,6 @@ def get(self, request: Request, organization) -> Response: else: queryset = queryset.filter(status=status_int) - queryset = queryset.annotate(date=F("date_added")) - - queryset = add_environment_to_queryset(queryset, filter_params) if query: try: queryset = _filter_releases_by_query(queryset, organization, query, filter_params) @@ -333,12 +339,11 @@ def get(self, request: Request, organization) -> Response: ) select_extra = {} - - queryset = queryset.distinct() if flatten: + queryset = queryset.filter(projects__id__in=filter_params["project_id"]) select_extra["_for_project_id"] = "sentry_release_project.project_id" - - queryset = queryset.filter(projects__id__in=filter_params["project_id"]) + else: + queryset = filter_releases_by_projects(queryset, filter_params["project_id"]) if sort == "date": queryset = queryset.order_by("-date") @@ -664,20 +669,16 @@ def get(self, request: Request, organization) -> Response: except NoProjects: return Response([]) + queryset = Release.objects.filter(organization_id=organization.id) + queryset = add_date_filter_to_queryset(queryset, filter_params) + queryset = filter_releases_by_projects(queryset, filter_params["project_id"]) + queryset = filter_releases_by_environments( + queryset, [e.id for e in filter_params.get("environment_objects", [])] + ) queryset = ( - Release.objects.filter( - organization=organization, projects__id__in=filter_params["project_id"] - ) - .annotate( - date=F("date_added"), - ) - .values("version", "date") - .order_by("-date") - .distinct() + queryset.annotate(date=F("date_added")).values("version", "date").order_by("-date") ) - queryset = add_date_filter_to_queryset(queryset, filter_params) - queryset = add_environment_to_queryset(queryset, filter_params) if query: try: queryset = _filter_releases_by_query(queryset, organization, query, filter_params) diff --git a/src/sentry/models/release.py b/src/sentry/models/release.py index 0cb7e77a3a0a3a..3e443f6f66d743 100644 --- a/src/sentry/models/release.py +++ b/src/sentry/models/release.py @@ -13,6 +13,7 @@ from django.utils import timezone from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ +from django_stubs_ext import QuerySetAny from sentry_relay.exceptions import RelayError from sentry_relay.processing import parse_release @@ -32,6 +33,7 @@ from sentry.db.models.manager.base import BaseManager from sentry.models.artifactbundle import ArtifactBundle from sentry.models.commitauthor import CommitAuthor +from sentry.models.releaseprojectenvironment import ReleaseProjectEnvironment from sentry.models.releases.constants import ( DB_VERSION_LENGTH, ERR_RELEASE_HEALTH_DATA, @@ -818,3 +820,33 @@ def get_previous_release(release: Release) -> Release | None: .order_by("-sort") .first() ) + + +def filter_releases_by_projects(queryset: QuerySetAny, project_ids: list[int]): + """Return releases belonging to a project.""" + if not project_ids: + return queryset + + return queryset.filter( + Exists( + ReleaseProject.objects.filter( + release=OuterRef("pk"), + project_id__in=project_ids, + ) + ) + ) + + +def filter_releases_by_environments(queryset: QuerySetAny, environment_ids: list[int]): + """Return a release queryset filtered by environments.""" + if not environment_ids: + return queryset + + return queryset.filter( + Exists( + ReleaseProjectEnvironment.objects.filter( + release=OuterRef("pk"), + environment_id__in=environment_ids, + ) + ) + ) diff --git a/tests/sentry/api/endpoints/test_organization_releases.py b/tests/sentry/api/endpoints/test_organization_releases.py index a752753b7210bb..49aeca6282642f 100644 --- a/tests/sentry/api/endpoints/test_organization_releases.py +++ b/tests/sentry/api/endpoints/test_organization_releases.py @@ -325,7 +325,7 @@ def test_release_filter(self): release = Release.objects.create( organization_id=org.id, version="foobar", - date_added=datetime(2013, 8, 13, 3, 8, 24, 880386, tzinfo=UTC), + date_added=datetime(2013, 8, 13, 3, 8, 24, 880387, tzinfo=UTC), ) release.add_project(project) From 530e5230df220dc41fcf648cc080bda43ac7ec87 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Thu, 10 Jul 2025 08:54:59 -0500 Subject: [PATCH 2/4] Scope ReleaseProjectEnvironment filter by project ids --- src/sentry/api/endpoints/organization_releases.py | 8 ++++++-- src/sentry/models/release.py | 7 ++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/sentry/api/endpoints/organization_releases.py b/src/sentry/api/endpoints/organization_releases.py index 14eaf7539a2cb6..0a440c389278c0 100644 --- a/src/sentry/api/endpoints/organization_releases.py +++ b/src/sentry/api/endpoints/organization_releases.py @@ -314,7 +314,9 @@ def get(self, request: Request, organization) -> Response: queryset = Release.objects.filter(organization_id=organization.id) queryset = filter_releases_by_environments( - queryset, [e.id for e in filter_params.get("environment_objects", [])] + queryset, + filter_params["project_id"], + [e.id for e in filter_params.get("environment_objects", [])], ) queryset = queryset.annotate(date=F("date_added")) @@ -673,7 +675,9 @@ def get(self, request: Request, organization) -> Response: queryset = add_date_filter_to_queryset(queryset, filter_params) queryset = filter_releases_by_projects(queryset, filter_params["project_id"]) queryset = filter_releases_by_environments( - queryset, [e.id for e in filter_params.get("environment_objects", [])] + queryset, + filter_params["project_id"], + [e.id for e in filter_params.get("environment_objects", [])], ) queryset = ( queryset.annotate(date=F("date_added")).values("version", "date").order_by("-date") diff --git a/src/sentry/models/release.py b/src/sentry/models/release.py index 3e443f6f66d743..b8268de62fd453 100644 --- a/src/sentry/models/release.py +++ b/src/sentry/models/release.py @@ -837,7 +837,11 @@ def filter_releases_by_projects(queryset: QuerySetAny, project_ids: list[int]): ) -def filter_releases_by_environments(queryset: QuerySetAny, environment_ids: list[int]): +def filter_releases_by_environments( + queryset: QuerySetAny, + project_ids: list[int], + environment_ids: list[int], +): """Return a release queryset filtered by environments.""" if not environment_ids: return queryset @@ -847,6 +851,7 @@ def filter_releases_by_environments(queryset: QuerySetAny, environment_ids: list ReleaseProjectEnvironment.objects.filter( release=OuterRef("pk"), environment_id__in=environment_ids, + project_id__in=project_ids, ) ) ) From 156f2962d42e33464923e6c065c521c859585d61 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Thu, 10 Jul 2025 09:15:50 -0500 Subject: [PATCH 3/4] Add distinct --- src/sentry/api/endpoints/organization_releases.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sentry/api/endpoints/organization_releases.py b/src/sentry/api/endpoints/organization_releases.py index 0a440c389278c0..147d731b214aea 100644 --- a/src/sentry/api/endpoints/organization_releases.py +++ b/src/sentry/api/endpoints/organization_releases.py @@ -342,7 +342,9 @@ def get(self, request: Request, organization) -> Response: select_extra = {} if flatten: - queryset = queryset.filter(projects__id__in=filter_params["project_id"]) + # NOTE: Do a deeper dive on this. What is this _for_project_id value? Was the author's + # intent that distinct would return multiple unique rows per project per release? + queryset = queryset.filter(projects__id__in=filter_params["project_id"]).distinct() select_extra["_for_project_id"] = "sentry_release_project.project_id" else: queryset = filter_releases_by_projects(queryset, filter_params["project_id"]) From 9d114de20d25f820d324d2151da09cf0e44e4e44 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Thu, 10 Jul 2025 09:36:02 -0500 Subject: [PATCH 4/4] Distinct first because cursor complained --- src/sentry/api/endpoints/organization_releases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/api/endpoints/organization_releases.py b/src/sentry/api/endpoints/organization_releases.py index 147d731b214aea..de98ae3948d53c 100644 --- a/src/sentry/api/endpoints/organization_releases.py +++ b/src/sentry/api/endpoints/organization_releases.py @@ -344,7 +344,7 @@ def get(self, request: Request, organization) -> Response: if flatten: # NOTE: Do a deeper dive on this. What is this _for_project_id value? Was the author's # intent that distinct would return multiple unique rows per project per release? - queryset = queryset.filter(projects__id__in=filter_params["project_id"]).distinct() + queryset = queryset.distinct().filter(projects__id__in=filter_params["project_id"]) select_extra["_for_project_id"] = "sentry_release_project.project_id" else: queryset = filter_releases_by_projects(queryset, filter_params["project_id"])