Skip to content

Use the bugscache trigram index in search and cut N+1 queries in the job details panel#9689

Open
camd wants to merge 2 commits into
masterfrom
camd/query-optimization-failure-lines
Open

Use the bugscache trigram index in search and cut N+1 queries in the job details panel#9689
camd wants to merge 2 commits into
masterfrom
camd/query-optimization-failure-lines

Conversation

@camd

@camd camd commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Optimizes the database queries behind the job details / failure summary panel, where the bug_suggestions endpoint has been reported as slow. The main fix makes an already-shipped index actually get used; two smaller commits remove N+1 queries on the same path.

1. Bugscache.search() never used the trigram index (main fix)

search() filtered with summary__icontains, which Django compiles to UPPER(summary) LIKE UPPER(%s) on PostgreSQL. Because the predicate is on UPPER(summary) rather than the bare column, the planner cannot use the gin_trgm_ops index on bugscache.summary (added in #9555) and runs a full sequential scan on every call — and search() runs once per distinct error term in the bug suggestions panel.

A small TrigramILike lookup applies ILIKE directly to the column (same wildcard-escaping as __icontains), so the existing trigram GIN index serves the query. No migration required.

Measured on the prod replica (bugscache ≈ 52,800 rows / 106 MB)

EXPLAIN (ANALYZE, BUFFERS) for three representative terms:

Term Before After
test-verify (generic) Seq Scan, 52,806 rows removed, 128 ms Bitmap Index Scan, 5.8 ms
shutdownleaks (leak) Seq Scan, 52,793 rows removed, 131 ms Bitmap Index Scan, 5.3 ms
long test-path string Seq Scan, whole table, 123 ms Bitmap Index Scan, 64 ms

Shared buffers per call drop from ~5,724 to 76–702. The cost goes from O(table size) — which grows unbounded as bugs accumulate — to O(matches).

2. Batch the per-row occurrences count

Bugscache.serialize() ran jobmap.count() for every internal-issue row, i.e. one query per internal issue in a search result. Added internal_occurrences() to compute all counts in a single grouped query and pass them into serialize().

3. Remove N+1 in job note serialization

  • JobNoteSerializer.failure_classification_id used SlugRelatedField(slug_field="id"), loading the whole related FailureClassification per note just to read its id (already present as the FK column). Switched to PrimaryKeyRelatedField (pk-only, no query).
  • push_notes reads failure_classification.name, so its queryset now select_relateds the classification instead of fetching it per note.

Testing

  • All existing Bugscache.search() parametrized tests pass, confirming behaviour parity (literal _/% handling and case-insensitivity are unchanged).
  • Added a test asserting the compiled search SQL uses ILIKE on the bare column (no UPPER(...) wrapping) and escapes wildcards.
  • Added a test for internal_occurrences verifying per-bug counts, window exclusion, and that it runs in a single query.
  • error_summary, note, and job-note suites pass. ruff clean.

@codecov-commenter

codecov-commenter commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.68%. Comparing base (6a820e6) to head (85c36e4).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #9689      +/-   ##
==========================================
+ Coverage   82.66%   82.68%   +0.02%     
==========================================
  Files         626      626              
  Lines       36753    36793      +40     
  Branches     3348     3333      -15     
==========================================
+ Hits        30383    30424      +41     
+ Misses       5988     5987       -1     
  Partials      382      382              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

camd added 2 commits July 18, 2026 10:20
Bugscache.search() filtered with __icontains, which Django compiles to
UPPER(summary) LIKE UPPER(%s) on PostgreSQL. Because the predicate is on
UPPER(summary) rather than the bare column, the planner could not use the
gin_trgm_ops index on bugscache.summary and ran a full sequential scan on
every call (once per distinct error term in the bug suggestions panel).

Add a TrigramILike lookup that applies ILIKE directly to the column, with
the same wildcard escaping as __icontains, so the existing trigram GIN
index serves the search. On the prod replica (52k rows) this turns a
~125ms sequential scan into a ~5ms bitmap index scan for typical terms.

Behaviour is unchanged: the existing search() parametrized tests (literal
_/%% handling, case-insensitivity) still pass.
Two follow-on query fixes in the job details panel path:

- Bugscache.serialize() ran a jobmap.count() per internal-issue row, so
  serializing a search result fired one query per internal issue. Add
  Bugscache.internal_occurrences() to compute all counts in a single
  grouped query and pass them into serialize().

- JobNoteSerializer.failure_classification_id used a SlugRelatedField on
  the related FailureClassification, loading the whole related row per note
  just to read its id (which is already the FK column). Switch to
  PrimaryKeyRelatedField (pk-only, no query). For the push_notes detail
  view, which reads failure_classification.name, add the classification to
  select_related so the name is joined instead of fetched per note.
@camd
camd force-pushed the camd/query-optimization-failure-lines branch from 36ba1f3 to 85c36e4 Compare July 18, 2026 17:20
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.

2 participants