Skip to content

Add composite index main_detection(occurrence_id, timestamp DESC)#1251

Open
mihow wants to merge 2 commits intomainfrom
perf/index-detection-occurrence-timestamp
Open

Add composite index main_detection(occurrence_id, timestamp DESC)#1251
mihow wants to merge 2 commits intomainfrom
perf/index-detection-occurrence-timestamp

Conversation

@mihow
Copy link
Copy Markdown
Collaborator

@mihow mihow commented Apr 17, 2026

Summary

Add a composite index on main_detection (occurrence_id, timestamp DESC) to speed up the /captures/ list endpoint, which joins main_detection and main_occurrence, filters by project_id, and sorts by timestamp DESC.

Without this index PostgreSQL falls back to a nested-loop join that scans ~517k detection rows before filtering by project, then sorts the full set.

Measured impact (external perf report, warm DB, caching disabled)

Stage Exec time vs. baseline
Before — nested loop, ~517k rows scanned ~1,470 ms baseline
After — parallel hash join + index-ordered scan ~251 ms −83%

This is a straight-up plan change: the query planner picks a parallel hash join because it can read detections ordered by timestamp for each occurrence directly from the index, instead of collecting all qualifying detections and sorting.

Implementation

  • ami/main/models.py — add Meta.indexes = [Index(fields=[\"occurrence\", \"-timestamp\"], name=\"detection_occurrence_ts_desc\")] on Detection.
  • ami/main/migrations/0084_index_detection_occurrence_timestamp.py — uses django.contrib.postgres.operations.AddIndexConcurrently with atomic = False.

Verified locally that the migration emits the correct SQL:

CREATE INDEX CONCURRENTLY \"detection_occurrence_ts_desc\"
ON \"main_detection\" (\"occurrence_id\", \"timestamp\" DESC);

…and the resulting index is visible on the table:

\"detection_occurrence_ts_desc\" btree (occurrence_id, \"timestamp\" DESC)

Why AddIndexConcurrently

main_detection is one of the largest tables in the system. A plain CREATE INDEX takes an ACCESS EXCLUSIVE-level table lock while it builds, blocking writes for the duration. CREATE INDEX CONCURRENTLY trades a longer build for an online build that only needs a SHARE UPDATE EXCLUSIVE lock — safe during production traffic.

Gotcha: AddIndexConcurrently can't run inside a transaction, so the migration sets atomic = False. If the build fails partway, the index is left in an invalid state and must be dropped manually (DROP INDEX CONCURRENTLY) before re-running — standard for concurrent index builds.

Test plan

  • python manage.py migrate main applies cleanly in CI stack
  • sqlmigrate emits CREATE INDEX CONCURRENTLY
  • Post-apply \\d main_detection shows the composite index
  • Confirm on staging before prod rollout: EXPLAIN (ANALYZE, BUFFERS) on /captures/ query with a large project, compare plan before/after

Related

Pairs with #1250 (require project_id on hot list endpoints) as the second half of the "fix the major culprits now" pass. Neither depends on the other — can merge independently.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores
    • Implemented database query optimization through indexing to improve performance for detection lookups.

Adds a composite index to speed up the /captures/ list endpoint, which
joins main_detection and main_occurrence, filters by project_id, and
sorts by timestamp DESC. Without this index PostgreSQL falls back to a
nested-loop join scanning ~517k detection rows before filtering.

Measured impact (per external perf report):
  Before: ~1,470 ms  (nested loop, full timestamp sort)
  After:  ~251 ms    (parallel hash join on the filtered result)
  ~83% reduction on cold cache.

Uses django.contrib.postgres.operations.AddIndexConcurrently with
atomic=False so the index builds without holding a table-level write
lock — safe for online prod deployment on a table this size.

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 17, 2026 20:50
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 17, 2026

Deploy Preview for antenna-ssec canceled.

Name Link
🔨 Latest commit a185a4a
🔍 Latest deploy log https://app.netlify.com/projects/antenna-ssec/deploys/69e986ae8933380008541cef

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 17, 2026

Deploy Preview for antenna-preview canceled.

Name Link
🔨 Latest commit a185a4a
🔍 Latest deploy log https://app.netlify.com/projects/antenna-preview/deploys/69e986ae849b120008732897

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a composite database index on the Detection model to optimize queries filtering by occurrence and ordering by timestamp. The changes include both the model definition update and the corresponding Django schema migration using AddIndexConcurrently.

Changes

Cohort / File(s) Summary
Database Index Definition
ami/main/models.py
Added composite index to Detection.Meta covering occurrence (ascending) and timestamp (descending) with explicit name detection_occurrence_ts_desc.
Schema Migration
ami/main/migrations/0084_index_detection_occurrence_timestamp.py
Created new migration that applies the composite index concurrently to the main.detection table, with atomic = False to support concurrent index creation.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 With twitching whiskers and indexing delight,
Occurrence and timestamp aligned just right,
Queries now swift through the detection array,
Database searches faster each day! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and clearly summarizes the main change: adding a composite index on the detection table with specific field ordering.
Description check ✅ Passed The description is comprehensive and covers all key template sections: summary, implementation details, rationale, test plan, and related issues.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/index-detection-occurrence-timestamp

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a PostgreSQL composite index on main_detection (occurrence_id, timestamp DESC) to improve performance of the /captures/ hot-path query patterns that join detections to occurrences and sort by newest detection timestamp.

Changes:

  • Add a composite ordered index definition to the Detection model (occurrence, -timestamp).
  • Add a non-atomic migration that creates the index using CREATE INDEX CONCURRENTLY via AddIndexConcurrently.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
ami/main/models.py Declares the new composite index on Detection to support join + timestamp DESC ordering patterns.
ami/main/migrations/0084_index_detection_occurrence_timestamp.py Applies the index online with AddIndexConcurrently and atomic = False for production safety on a large table.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@mihow
Copy link
Copy Markdown
Collaborator Author

mihow commented Apr 17, 2026

@coderabbitai review this please if you haven't already

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 17, 2026

@mihow Sure! I'll kick off a review of the PR right away.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mihow
Copy link
Copy Markdown
Collaborator Author

mihow commented Apr 17, 2026

I have tested locally on a large dataset, will test in staging & demo env next

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
ami/main/migrations/0084_index_detection_occurrence_timestamp.py (1)

1-21: Concurrent index migration looks correct; consider documenting recovery for invalid-index failures.

AddIndexConcurrently + atomic = False is the right approach for a large main_detection table to avoid an ACCESS EXCLUSIVE lock. Index definition matches the model Meta.indexes entry (same name and field order), so makemigrations --check should remain clean.

Operational note (per the PR description): if CREATE INDEX CONCURRENTLY fails mid-build, Postgres leaves an INVALID index that must be dropped manually (DROP INDEX CONCURRENTLY detection_occurrence_ts_desc;) before the migration can be retried — otherwise the retry will fail on the duplicate relation. Worth calling out in the deploy runbook, and verifying on staging before prod.

Also, Ruff RUF012 warnings on dependencies/operations are false positives — these are standard Django migration class attributes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ami/main/migrations/0084_index_detection_occurrence_timestamp.py` around
lines 1 - 21, The migration uses AddIndexConcurrently with atomic = False to
avoid locks, but you must document operational recovery for a failed
CONCURRENTLY build: add a brief comment in the migration (near
AddIndexConcurrently / atomic = False) and/or update the deploy runbook
explaining that a mid-build failure leaves an INVALID index named
detection_occurrence_ts_desc and operators must run DROP INDEX CONCURRENTLY
detection_occurrence_ts_desc before retrying the migration; you can also note
that Ruff RUF012 warnings on dependencies/operations are false positives so no
code change is needed for those.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@ami/main/migrations/0084_index_detection_occurrence_timestamp.py`:
- Around line 1-21: The migration uses AddIndexConcurrently with atomic = False
to avoid locks, but you must document operational recovery for a failed
CONCURRENTLY build: add a brief comment in the migration (near
AddIndexConcurrently / atomic = False) and/or update the deploy runbook
explaining that a mid-build failure leaves an INVALID index named
detection_occurrence_ts_desc and operators must run DROP INDEX CONCURRENTLY
detection_occurrence_ts_desc before retrying the migration; you can also note
that Ruff RUF012 warnings on dependencies/operations are false positives so no
code change is needed for those.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8493ad21-4fe2-429b-aac0-38fddbd13b9e

📥 Commits

Reviewing files that changed from the base of the PR and between ae14c7b and a185a4a.

📒 Files selected for processing (2)
  • ami/main/migrations/0084_index_detection_occurrence_timestamp.py
  • ami/main/models.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants