Skip to content

perf: optimize getAllLinks query from ~2.8s to <200ms#39

Open
armand1m wants to merge 1 commit into
masterfrom
perf/optimize-getAllLinks-query
Open

perf: optimize getAllLinks query from ~2.8s to <200ms#39
armand1m wants to merge 1 commit into
masterfrom
perf/optimize-getAllLinks-query

Conversation

@armand1m

@armand1m armand1m commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Problem

The getAllLinks GraphQL query was taking ~2.77s in production due to an N+1 query pattern. For each of the 50 links returned, PostGraphile issued 3 separate SQL queries:

  1. linkUsageMetrics { totalCount }COUNT(*) with RLS per link
  2. recentUsageMetrics — full scan per link (no composite index)
  3. linkAllowedEmails — indexed lookup per link (cheap, kept as-is)

This resulted in ~150 SQL queries per page load.

Changes

Database

  • New columns on links: usage_count BIGINT and last_accessed_at TIMESTAMP — denormalized from link_usage_metrics
  • Composite index on link_usage_metrics (link_id, accessed_at DESC) — speeds up recentUsageMetrics range scan
  • Trigger trg_update_link_usage_stats on link_usage_metrics — keeps usage_count and last_accessed_at in sync on INSERT/DELETE
  • Backfill — populates existing data from link_usage_metrics

GraphQL

  • Replaced linkUsageMetrics { totalCount } (N+1 COUNT query) with usageCount (scalar column read, zero additional SQL)
  • Applied to both getAllLinks and searchLinks queries

Frontend

  • LinkTable now reads link.usageCount directly instead of link.linkUsageMetrics.totalCount

Expected Impact

Metric Before After
Total SQL queries ~150 ~51
linkUsageMetrics COUNT(*) with RLS per link Scalar column read (0 queries)
recentUsageMetrics Full scan per link Composite index range scan

Deployment Notes

  • The migration includes a backfill step that updates all existing links rows
  • The trigger adds minimal overhead to link_usage_metrics INSERT/DELETE operations
  • The migration is reversible (see -- migrate:down section)

- Add denormalized usage_count and last_accessed_at columns to links table
- Add composite index on link_usage_metrics (link_id, accessed_at DESC)
- Add trigger to keep usage_count and last_accessed_at in sync
- Backfill existing data from link_usage_metrics
- Replace linkUsageMetrics { totalCount } with usageCount scalar in GraphQL queries
- Update LinkTable to read usageCount directly
- Regenerate GraphQL schema and TypeScript types

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b757d77a03

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +31 to +34
UPDATE public.links
SET usage_count = usage_count + 1,
last_accessed_at = GREATEST(last_accessed_at, NEW.accessed_at)
WHERE id = NEW.link_id;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Make metric trigger bypass link RLS

When Auth0 is enabled, anonymous users only get read:golinks, and create_link_usage_metric allows inserts with that permission, but this trigger's UPDATE public.links still runs as the invoking postgraphile role and is subject to the update_links RLS policy requiring update:golinks. In the redirect flow (pages/[...alias].tsx records this mutation after a link is opened), read-only viewers will have their metric insert fail as soon as this trigger tries to update the denormalized columns; define the function with appropriate definer privileges/RLS handling so read-only metric inserts can still succeed.

Useful? React with 👍 / 👎.

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.

1 participant