Skip to content

Commit 5fc11c8

Browse files
rossopsclaudeMaffooch
authored
perf(finding): add sla_expiration_date index for global finding list (DefectDojo#15103)
The global finding list ordered by sla_expiration_date seq-scanned and sorted the entire authorized finding set on every page load. Add a full (non-partial) btree on sla_expiration_date so the planner can walk the index and stop at the LIMIT instead. The existing partial idx_finding_sla_open_cov cannot serve this query because the query has no is_mitigated predicate. Measured on a 10M-finding clone, global finding list ORDER BY sla_expiration_date ASC LIMIT 25 (per-user authorization filter), under load test: - Before: mean 6591 ms, max 20732 ms, total 1582 s - After: mean 115 ms, max 4992 ms, total 96 s (~57x mean improvement; the query dropped from the #1 offender to negligible). Index size ~68 MB on the clone. Built CONCURRENTLY (atomic = False) so the migration takes no ACCESS EXCLUSIVE lock on the large dojo_finding table. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com>
1 parent 9a2f377 commit 5fc11c8

2 files changed

Lines changed: 36 additions & 0 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from django.contrib.postgres.operations import AddIndexConcurrently
2+
from django.db import migrations, models
3+
4+
5+
class Migration(migrations.Migration):
6+
# CREATE INDEX CONCURRENTLY cannot run inside a transaction block, and avoids
7+
# an ACCESS EXCLUSIVE lock on the (large) dojo_finding table.
8+
atomic = False
9+
10+
dependencies = [
11+
("dojo", "0273_product_upper_name_index"),
12+
]
13+
14+
operations = [
15+
# Full (non-partial) btree on sla_expiration_date. The global finding
16+
# list ordered by sla_expiration_date currently seq-scans + sorts the
17+
# entire authorized finding set; this lets the planner walk the index and
18+
# stop at the LIMIT. The existing partial idx_finding_sla_open_cov cannot
19+
# serve it because that query has no is_mitigated predicate.
20+
AddIndexConcurrently(
21+
model_name="finding",
22+
index=models.Index(
23+
fields=["sla_expiration_date"],
24+
name="idx_finding_sla_exp",
25+
),
26+
),
27+
]

dojo/finding/models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,15 @@ class Meta:
511511
include=["id"],
512512
condition=models.Q(is_mitigated=False),
513513
),
514+
# Full (non-partial) index so the global finding list ordered by
515+
# sla_expiration_date can be served by an index walk + LIMIT instead
516+
# of sorting the entire authorized finding set. The partial
517+
# idx_finding_sla_open_cov can't serve it (query has no is_mitigated
518+
# filter).
519+
models.Index(
520+
fields=["sla_expiration_date"],
521+
name="idx_finding_sla_exp",
522+
),
514523
]
515524

516525
def __init__(self, *args, **kwargs):

0 commit comments

Comments
 (0)