Skip to content

perf(mcp-registry): index the columns the linking lookups filter on - #95865

Draft
posthog[bot] wants to merge 2 commits into
masterfrom
posthog-self-driving/perfmcp-registry-index-the-columns-the-54c62f
Draft

perf(mcp-registry): index the columns the linking lookups filter on#95865
posthog[bot] wants to merge 2 commits into
masterfrom
posthog-self-driving/perfmcp-registry-index-the-columns-the-54c62f

Conversation

@posthog

@posthog posthog Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Problem

  • The MCP registry aggregation loop pays a full table scan twice per measured server, and the cost grows with every registry crawl and every project aggregated.
  • resolve_measured_server runs two Django .first() lookups per measured (team, server_name) pair. Neither could reach an index.
  • pganalyze flagged public.mcp_registry_server in EU prod on the ORDER BY id ASC LIMIT $2 shape that .first() produces.
Lookup Filter columns Index before Plan before
Known-link override registry_name partial unique only parallel seq scan
Standalone reuse display_name, listed_in_registry, is_measured none on display_name bitmap scan on is_measured

The registry_name case is the subtle one. The column already carries the partial unique index unique_registry_name_when_set, whose predicate excludes the empty string. Postgres uses it for a literal value, but a parameterized equality cannot be proved to satisfy that predicate, so a generic plan drops the index and scans.

Changes

  • Both lookups now resolve through a single index scan instead of a table scan, so aggregation cost stops tracking table size.
  • Adds a plain, non-partial index on registry_name, which a generic plan can use where the partial unique index is unavailable.
  • Adds a composite index over display_name, listed_in_registry, is_measured, matching the standalone-row lookup exactly.
  • Both are built with SafeAddIndexConcurrently, so the migration takes no ACCESS EXCLUSIVE lock on the table.
  • The existing single-column boolean indexes stay. Removing them is a separate call.

No behavior change, no schema change beyond the two indexes.

How did you test this code?

Automated tests run locally: products/mcp_registry/backend/tests/test_linking.py and test_aggregation.py — 6 passed. makemigrations --check reports no state drift. analyze_migration_risk --fail-on-blocked rates both operations Safe.

The plans were measured directly, because Postgres planner statistics are not visible from PostHog analytics and pganalyze is not reachable from the agent surface.

Plan comparison, PostgreSQL scratch database, 200k synthetic rows

Queries taken from sql_with_params() on the two real querysets, then forced to a generic plan (plan_cache_mode = force_generic_plan) to reproduce the parameterized form the ORM sends.

-- registry_name, before: partial unique index unusable
Parallel Seq Scan on mcp_registry_server  (cost=0.00..4677.67)
      Filter: ((registry_name)::text = $1)

-- registry_name, after
Index Scan using mcp_registry_name_idx  (cost=0.42..8.44)
      Index Cond: ((registry_name)::text = $1)

-- standalone lookup, before
Bitmap Heap Scan on mcp_registry_server  (cost=419.30..4540.30)
      Filter: (is_measured AND (NOT listed_in_registry) AND ((display_name)::text = $1))
      ->  Bitmap Index Scan on mcp_registr_is_meas_b12741_idx  (rows=38800)

-- standalone lookup, after
Index Scan using mcp_registry_srv_name_idx  (cost=0.42..8.44)
      Index Cond: (((display_name)::text = $1) AND (listed_in_registry = false) AND (is_measured = true))

No new tests. A test asserting an index exists would restate migration state that makemigrations --check already verifies, and would not catch the planner behavior this PR fixes.

Automatic notifications

  • Publish to changelog?

Docs update

None. No user-facing behavior, API, or documented workflow changes.

🤖 Agent context

Autonomy: Fully autonomous

  • Written by Claude Opus 5 in PostHog Desktop. Skills invoked: /django-migrations, /writing-pr-descriptions, /writing-code-comments, /writing-simplified-technical-english.
  • No duplicate: gh pr list --state open --search on mcp_registry and on index keywords found nothing touching these lookups. PR feat(mcp-registry): make discovery reachable by agents and people #95309 is open on the same product but on the discovery surface, not on linking.
  • Public artifact: nothing here comes from a non-public source. The pganalyze finding named a table and a normalized query shape; the fix was derived from this repository. The 200k rows used for the plan comparison are generated by generate_series, not copied from anywhere.
  • Two decisions worth naming. The plain registry_name index looked redundant at first, since a literal-value EXPLAIN does use the partial unique index; only forcing a generic plan showed the seq scan, which is what the ORM actually gets. And display_name leads the composite index rather than the two booleans, so the index also serves a display_name-only equality later.

Created with PostHog Desktop from this inbox report.

Both `.first()` lookups in `resolve_measured_server` run once per measured
(team, server_name) pair, and neither could reach an index.

`registry_name` was only covered by the partial unique index
`unique_registry_name_when_set`. A parameterized equality cannot be proved to
exclude the empty string, so a generic plan discards the partial index and
scans the table. `display_name` had no index at all, so the standalone lookup
fell back to a bitmap scan over the near-useless `is_measured` index.

Adds a plain index on `registry_name` and a composite index over
(display_name, listed_in_registry, is_measured), both built CONCURRENTLY.

Generated-By: PostHog Desktop
Task-Id: 61d1912d-652b-4077-b0a3-c52e9745d90c
@trunk-io

trunk-io Bot commented Sep 7, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@posthog

posthog Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

🦔 PostHog Review reviewed this pull request

Found 0 must fix, 1 should fix, 0 consider.

Published 1 finding (view the review).

Resolved comments: 1 fixed

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Trunk lane — backend Python lane

This PR is assigned to the backend Python lane. It runs backend Python tests and may merge in parallel with PRs in other lanes.

⚠️ Django migration SQL — 1 new migration to review

We've detected new migrations on this PR. Review the SQL output for each migration:

products/mcp_registry/backend/migrations/0003_mcp_registry_server_lookup_indexes.py

--
-- Concurrently create index mcp_registry_srv_name_idx on field(s) display_name, listed_in_registry, is_measured of model mcpregistryserver
--
SET lock_timeout = 0;
SET statement_timeout = 0;
CREATE INDEX CONCURRENTLY "mcp_registry_srv_name_idx" ON "mcp_registry_server" ("display_name", "listed_in_registry", "is_measured");

Last updated: 2026-09-07 06:14 UTC (3b19027)

Django migration risk — migration analysis complete

We've analyzed your migrations for potential risks.

Summary: 1 Safe | 0 Needs Review | 0 Blocked

✅ Safe

Brief or no lock, backwards compatible

mcp_registry.0003_mcp_registry_server_lookup_indexes
  └─ #1 ✅ SafeAddIndexConcurrently
     PostHog concurrent-index helper: idempotent (timeout disabling + invalid-leftover recovery)
     model: mcpregistryserver, index: mcp_registry_srv_name_idx

Last updated: 2026-09-07 06:14 UTC (3b19027)

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not approved yet — waiting on the conditions below.

The Migration risk check has not finished for this commit, so stamphog cannot tell a safe migration from a risky one yet. The review runs again on the next push, or you can re-request it once the check reports.

Gate mechanics and policy version
Gate Result
prerequisites all clear
deny-list matches: migrations
size 37L, 2F substantive, 39L/3F incl. docs/generated/snapshots — within ceiling
tier classified as T2-never: T2-never (39L, 3F, single-area, perf)
stamphog 2.0.0b4 .stamphog/policy.yml @ 38550c6 · reviewed head 38550c6

@posthog

posthog Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

PostHog Review alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

PostHog Review

Found 1 should fix.

Comment thread products/mcp_registry/backend/models.py Outdated
The plain index was justified by a generic plan that this codebase never
produces. Django's PostgreSQL backend binds parameters client-side and leaves
prepared statements disabled, so an ORM equality lookup reaches the server with
the value inlined as a literal. PostgreSQL then plans it as a custom plan, which
can prove that `registry_name = '<literal>'` satisfies the partial predicate on
`unique_registry_name_when_set` and uses that index.

Keeping the index would add a concurrent build, storage, and a write on every
registry crawl upsert without removing a scan.

The composite (display_name, listed_in_registry, is_measured) index stays. It
covers the standalone-row lookup the aggregation loop runs per measured server,
where no index existed at all.

Generated-By: PostHog Desktop
Task-Id: 67edfeb8-2632-44d7-aec7-d85a63dddb2d

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not approved yet — waiting on the conditions below.

The Migration risk check has not finished for this commit, so stamphog cannot tell a safe migration from a risky one yet. The review runs again on the next push, or you can re-request it once the check reports.

Gate mechanics and policy version
Gate Result
prerequisites all clear
deny-list matches: migrations
size 29L, 2F substantive, 31L/3F incl. docs/generated/snapshots — within ceiling
tier classified as T2-never: T2-never (31L, 3F, single-area, perf)
stamphog 2.0.0b4 .stamphog/policy.yml @ 3b19027 · reviewed head 3b19027

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.

0 participants