perf: optimize getAllLinks query from ~2.8s to <200ms#39
Conversation
- 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
There was a problem hiding this comment.
💡 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".
| UPDATE public.links | ||
| SET usage_count = usage_count + 1, | ||
| last_accessed_at = GREATEST(last_accessed_at, NEW.accessed_at) | ||
| WHERE id = NEW.link_id; |
There was a problem hiding this comment.
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 👍 / 👎.
Problem
The
getAllLinksGraphQL 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:linkUsageMetrics { totalCount }—COUNT(*)with RLS per linkrecentUsageMetrics— full scan per link (no composite index)linkAllowedEmails— indexed lookup per link (cheap, kept as-is)This resulted in ~150 SQL queries per page load.
Changes
Database
links:usage_count BIGINTandlast_accessed_at TIMESTAMP— denormalized fromlink_usage_metricslink_usage_metrics (link_id, accessed_at DESC)— speeds uprecentUsageMetricsrange scantrg_update_link_usage_statsonlink_usage_metrics— keepsusage_countandlast_accessed_atin sync on INSERT/DELETElink_usage_metricsGraphQL
linkUsageMetrics { totalCount }(N+1 COUNT query) withusageCount(scalar column read, zero additional SQL)getAllLinksandsearchLinksqueriesFrontend
LinkTablenow readslink.usageCountdirectly instead oflink.linkUsageMetrics.totalCountExpected Impact
linkUsageMetricsrecentUsageMetricsDeployment Notes
linksrowslink_usage_metricsINSERT/DELETE operations-- migrate:downsection)