Skip to content

Commit 63d682c

Browse files
Optimize the DB queries for engagement, finding, product and product_type views. Mostly counters fixes
1 parent 6ec9521 commit 63d682c

5 files changed

Lines changed: 172 additions & 151 deletions

File tree

dojo/engagement/views.py

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import re
66
import time
77
from datetime import datetime
8-
from functools import reduce
8+
from functools import partial, reduce
99
from pathlib import Path
1010
from tempfile import NamedTemporaryFile
1111
from time import strftime
@@ -16,7 +16,8 @@
1616
from django.contrib.auth.models import User
1717
from django.core.exceptions import PermissionDenied, ValidationError
1818
from django.db import DEFAULT_DB_ALIAS
19-
from django.db.models import Count, Q
19+
from django.db.models import Count, OuterRef, Q, Value
20+
from django.db.models.functions import Coalesce
2021
from django.db.models.query import Prefetch, QuerySet
2122
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect, QueryDict, StreamingHttpResponse
2223
from django.shortcuts import get_object_or_404, render
@@ -104,6 +105,7 @@
104105
add_error_message_to_response,
105106
add_success_message_to_response,
106107
async_delete,
108+
build_count_subquery,
107109
calculate_grade,
108110
generate_file_response_from_file_path,
109111
get_cal_event,
@@ -592,23 +594,27 @@ def post(self, request, eid, *args, **kwargs):
592594

593595

594596
def prefetch_for_view_tests(tests):
595-
prefetched = tests
596-
if isinstance(tests,
597-
QuerySet): # old code can arrive here with prods being a list because the query was already executed
598-
599-
prefetched = prefetched.select_related("lead")
600-
prefetched = prefetched.prefetch_related("tags", "test_type", "notes")
601-
prefetched = prefetched.annotate(count_findings_test_all=Count("finding__id", distinct=True))
602-
prefetched = prefetched.annotate(count_findings_test_active=Count("finding__id", filter=Q(finding__active=True), distinct=True))
603-
prefetched = prefetched.annotate(count_findings_test_active_verified=Count("finding__id", filter=Q(finding__active=True) & Q(finding__verified=True), distinct=True))
604-
prefetched = prefetched.annotate(count_findings_test_mitigated=Count("finding__id", filter=Q(finding__is_mitigated=True), distinct=True))
605-
prefetched = prefetched.annotate(count_findings_test_dups=Count("finding__id", filter=Q(finding__duplicate=True), distinct=True))
606-
prefetched = prefetched.annotate(total_reimport_count=Count("test_import__id", filter=Q(test_import__type=Test_Import.REIMPORT_TYPE), distinct=True))
607-
608-
else:
597+
# old code can arrive here with prods being a list because the query was already executed
598+
if not isinstance(tests, QuerySet):
609599
logger.warning("unable to prefetch because query was already executed")
610-
611-
return prefetched
600+
return tests
601+
602+
prefetched = tests.select_related("lead", "test_type").prefetch_related("tags", "notes")
603+
base_findings = Finding.objects.filter(test_id=OuterRef("pk"))
604+
count_subquery = partial(build_count_subquery, group_field="test_id")
605+
return prefetched.annotate(
606+
count_findings_test_all=Coalesce(count_subquery(base_findings), Value(0)),
607+
count_findings_test_active=Coalesce(count_subquery(base_findings.filter(active=True)), Value(0)),
608+
count_findings_test_active_verified=Coalesce(
609+
count_subquery(base_findings.filter(active=True, verified=True)), Value(0),
610+
),
611+
count_findings_test_mitigated=Coalesce(count_subquery(base_findings.filter(is_mitigated=True)), Value(0)),
612+
count_findings_test_dups=Coalesce(count_subquery(base_findings.filter(duplicate=True)), Value(0)),
613+
total_reimport_count=Coalesce(
614+
count_subquery(Test_Import.objects.filter(test_id=OuterRef("pk"), type=Test_Import.REIMPORT_TYPE)),
615+
Value(0),
616+
),
617+
)
612618

613619

614620
@user_is_authorized(Engagement, Permissions.Test_Add, "eid")

dojo/finding/views.py

Lines changed: 48 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import logging
77
import mimetypes
88
from collections import OrderedDict, defaultdict
9+
from functools import partial
910
from itertools import chain
1011
from pathlib import Path
1112

@@ -14,8 +15,8 @@
1415
from django.core import serializers
1516
from django.core.exceptions import PermissionDenied, ValidationError
1617
from django.db import models
17-
from django.db.models import Count, Q, QuerySet
18-
from django.db.models.functions import Length
18+
from django.db.models import OuterRef, QuerySet, Value
19+
from django.db.models.functions import Coalesce, Length
1920
from django.db.models.query import Prefetch
2021
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseRedirect, JsonResponse, StreamingHttpResponse
2122
from django.shortcuts import get_object_or_404, render
@@ -110,6 +111,7 @@
110111
add_field_errors_to_response,
111112
add_success_message_to_response,
112113
apply_cwe_to_template,
114+
build_count_subquery,
113115
calculate_grade,
114116
close_external_issue,
115117
do_false_positive_history,
@@ -132,87 +134,58 @@
132134

133135

134136
def prefetch_for_findings(findings, prefetch_type="all", *, exclude_untouched=True):
135-
prefetched_findings = findings
136-
if isinstance(
137-
findings, QuerySet,
138-
): # old code can arrive here with prods being a list because the query was already executed
139-
prefetched_findings = prefetched_findings.prefetch_related(
140-
"reviewers",
141-
)
142-
prefetched_findings = prefetched_findings.prefetch_related("reporter")
143-
prefetched_findings = prefetched_findings.prefetch_related(
144-
"jira_issue__jira_project__jira_instance",
145-
)
146-
prefetched_findings = prefetched_findings.prefetch_related("test__test_type")
147-
prefetched_findings = prefetched_findings.prefetch_related(
148-
"test__engagement__jira_project__jira_instance",
149-
)
150-
prefetched_findings = prefetched_findings.prefetch_related(
151-
"test__engagement__product__jira_project_set__jira_instance",
152-
)
153-
prefetched_findings = prefetched_findings.prefetch_related("found_by")
137+
# old code can arrive here with prods being a list because the query was already executed
138+
if not isinstance(findings, QuerySet):
139+
logger.debug("unable to prefetch because query was already executed")
140+
return findings
154141

155-
# for open/active findings the following 4 prefetches are not needed
156-
if prefetch_type != "open":
157-
prefetched_findings = prefetched_findings.prefetch_related(
158-
"risk_acceptance_set",
159-
)
160-
prefetched_findings = prefetched_findings.prefetch_related(
161-
"risk_acceptance_set__accepted_findings",
162-
)
163-
prefetched_findings = prefetched_findings.prefetch_related(
164-
"original_finding",
165-
)
166-
prefetched_findings = prefetched_findings.prefetch_related(
167-
"duplicate_finding",
168-
)
142+
prefetched_findings = findings.prefetch_related(
143+
"reviewers",
144+
"reporter",
145+
"jira_issue__jira_project__jira_instance",
146+
"test__test_type",
147+
"test__engagement__jira_project__jira_instance",
148+
"test__engagement__product__jira_project_set__jira_instance",
149+
"found_by",
150+
)
169151

170-
if exclude_untouched:
171-
# filter out noop reimport actions from finding status history
172-
prefetched_findings = prefetched_findings.prefetch_related(
173-
Prefetch(
174-
"test_import_finding_action_set",
175-
queryset=Test_Import_Finding_Action.objects.exclude(
176-
action=IMPORT_UNTOUCHED_FINDING,
177-
),
178-
),
179-
)
180-
else:
181-
prefetched_findings = prefetched_findings.prefetch_related(
182-
"test_import_finding_action_set",
183-
)
184-
"""
185-
we could try to prefetch only the latest note with SubQuery and OuterRef,
186-
but I'm getting that MySql doesn't support limits in subqueries.
187-
"""
188-
prefetched_findings = prefetched_findings.prefetch_related("notes")
189-
prefetched_findings = prefetched_findings.prefetch_related("tags")
190-
prefetched_findings = prefetched_findings.prefetch_related("endpoints")
191-
prefetched_findings = prefetched_findings.prefetch_related("status_finding")
192-
prefetched_findings = prefetched_findings.annotate(
193-
active_endpoint_count=Count(
194-
"status_finding__id", filter=Q(status_finding__mitigated=False),
195-
),
196-
)
197-
prefetched_findings = prefetched_findings.annotate(
198-
mitigated_endpoint_count=Count(
199-
"status_finding__id", filter=Q(status_finding__mitigated=True),
200-
),
201-
)
202-
prefetched_findings = prefetched_findings.prefetch_related("finding_group_set")
152+
# for open/active findings, the following 4 prefetches are not needed
153+
if prefetch_type != "open":
203154
prefetched_findings = prefetched_findings.prefetch_related(
204-
"test__engagement__product__members",
205-
)
206-
prefetched_findings = prefetched_findings.prefetch_related(
207-
"test__engagement__product__prod_type__members",
155+
"risk_acceptance_set",
156+
"risk_acceptance_set__accepted_findings",
157+
"original_finding",
158+
"duplicate_finding",
208159
)
160+
161+
if exclude_untouched:
162+
# filter out noop reimport actions from finding status history
209163
prefetched_findings = prefetched_findings.prefetch_related(
210-
"vulnerability_id_set",
164+
Prefetch(
165+
"test_import_finding_action_set",
166+
queryset=Test_Import_Finding_Action.objects.exclude(action=IMPORT_UNTOUCHED_FINDING),
167+
),
211168
)
212169
else:
213-
logger.debug("unable to prefetch because query was already executed")
170+
prefetched_findings = prefetched_findings.prefetch_related("test_import_finding_action_set")
171+
172+
prefetched_findings = prefetched_findings.prefetch_related(
173+
"notes",
174+
"tags",
175+
"endpoints",
176+
"status_finding",
177+
"finding_group_set",
178+
"test__engagement__product__members",
179+
"test__engagement__product__prod_type__members",
180+
"vulnerability_id_set",
181+
)
214182

215-
return prefetched_findings
183+
base_status = Endpoint_Status.objects.filter(finding_id=OuterRef("pk"))
184+
count_subquery = partial(build_count_subquery, group_field="finding_id")
185+
return prefetched_findings.annotate(
186+
active_endpoint_count=Coalesce(count_subquery(base_status.filter(mitigated=False)), Value(0)),
187+
mitigated_endpoint_count=Coalesce(count_subquery(base_status.filter(mitigated=True)), Value(0)),
188+
)
216189

217190

218191
def prefetch_for_similar_findings(findings):

dojo/product/views.py

Lines changed: 65 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import logging
55
from collections import OrderedDict
66
from datetime import date, datetime, timedelta
7+
from functools import partial
78
from math import ceil
89

910
from dateutil.relativedelta import relativedelta
@@ -12,8 +13,9 @@
1213
from django.contrib.postgres.aggregates import StringAgg
1314
from django.core.exceptions import PermissionDenied, ValidationError
1415
from django.db import DEFAULT_DB_ALIAS, connection
15-
from django.db.models import Count, F, Max, OuterRef, Prefetch, Q, Subquery, Sum
16+
from django.db.models import Count, DateField, F, OuterRef, Prefetch, Q, Subquery, Sum
1617
from django.db.models.expressions import Value
18+
from django.db.models.functions import Coalesce
1719
from django.db.models.query import QuerySet
1820
from django.http import Http404, HttpRequest, HttpResponseRedirect, JsonResponse
1921
from django.shortcuts import get_object_or_404, render
@@ -89,6 +91,7 @@
8991
Product_Type,
9092
System_Settings,
9193
Test,
94+
Test_Import,
9295
Test_Type,
9396
)
9497
from dojo.product.queries import (
@@ -113,6 +116,7 @@
113116
add_external_issue,
114117
add_field_errors_to_response,
115118
async_delete,
119+
build_count_subquery,
116120
calculate_finding_age,
117121
get_enabled_notifications_list,
118122
get_open_findings_burndown,
@@ -134,10 +138,15 @@ def product(request):
134138
# perform all stuff for filtering and pagination first, before annotation/prefetching
135139
# otherwise the paginator will perform all the annotations/prefetching already only to count the total number of records
136140
# see https://code.djangoproject.com/ticket/23771 and https://code.djangoproject.com/ticket/25375
141+
137142
name_words = prods.values_list("name", flat=True)
143+
base_findings = Finding.objects.filter(test__engagement__product_id=OuterRef("pk"), active=True)
138144
prods = prods.annotate(
139-
findings_count=Count("engagement__test__finding", filter=Q(engagement__test__finding__active=True)),
145+
findings_count=Coalesce(
146+
build_count_subquery(base_findings, group_field="test__engagement__product_id"), Value(0),
147+
),
140148
)
149+
141150
filter_string_matching = get_system_setting("filter_string_matching", False)
142151
filter_class = ProductFilterWithoutObjectLookups if filter_string_matching else ProductFilter
143152
prod_filter = filter_class(request.GET, queryset=prods, user=request.user)
@@ -157,48 +166,63 @@ def product(request):
157166

158167

159168
def prefetch_for_product(prods):
160-
prefetched_prods = prods
161-
if isinstance(prods,
162-
QuerySet): # old code can arrive here with prods being a list because the query was already executed
163-
164-
prefetched_prods = prefetched_prods.prefetch_related("team_manager")
165-
prefetched_prods = prefetched_prods.prefetch_related("product_manager")
166-
prefetched_prods = prefetched_prods.prefetch_related("technical_contact")
167-
168-
prefetched_prods = prefetched_prods.annotate(
169-
active_engagement_count=Count("engagement__id", filter=Q(engagement__active=True)))
170-
prefetched_prods = prefetched_prods.annotate(
171-
closed_engagement_count=Count("engagement__id", filter=Q(engagement__active=False)))
172-
prefetched_prods = prefetched_prods.annotate(last_engagement_date=Max("engagement__target_start"))
173-
prefetched_prods = prefetched_prods.annotate(active_finding_count=Count("engagement__test__finding__id",
174-
filter=Q(
175-
engagement__test__finding__active=True)))
176-
prefetched_prods = prefetched_prods.annotate(
177-
active_verified_finding_count=Count("engagement__test__finding__id",
178-
filter=Q(
179-
engagement__test__finding__active=True,
180-
engagement__test__finding__verified=True)))
181-
prefetched_prods = prefetched_prods.prefetch_related("jira_project_set__jira_instance")
182-
prefetched_prods = prefetched_prods.prefetch_related("members")
183-
prefetched_prods = prefetched_prods.prefetch_related("prod_type__members")
184-
active_endpoint_query = Endpoint.objects.filter(
185-
status_endpoint__mitigated=False,
186-
status_endpoint__false_positive=False,
187-
status_endpoint__out_of_scope=False,
188-
status_endpoint__risk_accepted=False,
189-
).distinct()
190-
prefetched_prods = prefetched_prods.prefetch_related(
191-
Prefetch("endpoint_set", queryset=active_endpoint_query, to_attr="active_endpoints"))
192-
prefetched_prods = prefetched_prods.prefetch_related("tags")
169+
# old code can arrive here with prods being a list because the query was already executed
170+
if not isinstance(prods, QuerySet):
171+
logger.debug("unable to prefetch because query was already executed")
172+
return prods
193173

194-
if get_system_setting("enable_github"):
195-
prefetched_prods = prefetched_prods.prefetch_related(
196-
Prefetch("github_pkey_set", queryset=GITHUB_PKey.objects.all().select_related("git_conf"),
197-
to_attr="github_confs"))
174+
prefetched_prods = prods.select_related("team_manager", "product_manager", "technical_contact").prefetch_related(
175+
"tags",
176+
"members",
177+
"prod_type__members",
178+
"jira_project_set__jira_instance",
179+
)
198180

199-
else:
200-
logger.debug("unable to prefetch because query was already executed")
181+
engagements = Engagement.objects.filter(product_id=OuterRef("pk"))
182+
count_subquery = partial(build_count_subquery, group_field="product_id")
183+
prefetched_prods = prefetched_prods.annotate(
184+
active_engagement_count=Coalesce(count_subquery(engagements.filter(active=True)), Value(0)),
185+
closed_engagement_count=Coalesce(count_subquery(engagements.filter(active=False)), Value(0)),
186+
last_engagement_date=Subquery(
187+
engagements.order_by("-target_start").values("target_start")[:1], output_field=DateField(),
188+
),
189+
)
201190

191+
base_findings = Finding.objects.filter(test__engagement__product_id=OuterRef("pk"))
192+
count_subquery = partial(build_count_subquery, group_field="test__engagement__product_id")
193+
prefetched_prods = prefetched_prods.annotate(
194+
active_finding_count=Coalesce(count_subquery(base_findings.filter(active=True)), Value(0)),
195+
active_verified_finding_count=Coalesce(
196+
count_subquery(base_findings.filter(active=True, verified=True)),
197+
Value(0),
198+
),
199+
)
200+
prefetched_prods = prefetched_prods.annotate(
201+
total_reimport_count=Coalesce(
202+
count_subquery(
203+
Test_Import.objects.filter(test__engagement__product_id=OuterRef("pk"), type=Test_Import.REIMPORT_TYPE),
204+
),
205+
Value(0),
206+
),
207+
)
208+
209+
active_endpoint_qs = Endpoint.objects.filter(
210+
status_endpoint__mitigated=False,
211+
status_endpoint__false_positive=False,
212+
status_endpoint__out_of_scope=False,
213+
status_endpoint__risk_accepted=False,
214+
).distinct()
215+
216+
prefetched_prods = prefetched_prods.prefetch_related(
217+
Prefetch("endpoint_set", queryset=active_endpoint_qs, to_attr="active_endpoints"),
218+
)
219+
220+
if get_system_setting("enable_github"):
221+
prefetched_prods = prefetched_prods.prefetch_related(
222+
Prefetch(
223+
"github_pkey_set", queryset=GITHUB_PKey.objects.all().select_related("git_conf"), to_attr="github_confs",
224+
),
225+
)
202226
return prefetched_prods
203227

204228

0 commit comments

Comments
 (0)