L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Données et recherche · SKILL.md

backend-db-performance

backend-db-performance

Optimize slow queries, indexes, and N+1s. Use when "slow query", "database performance", "add an index", or "N+1". Schema consistency → audit-db-schema. RLS access control → plan-rls-audit.

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: backend-db-performance description: > Optimize slow queries, indexes, and N+1s. Use when "slow query", "database performance", "add an index", or "N+1". Schema consistency → audit-db-schema. RLS access control → plan-rls-audit. license: MIT

Database Optimization Skill

Degree of freedom: MIXED. Which query/index/N+1 to fix [HIGH freedom]; existing-index probes and EXPLAIN ANALYZE [LOW freedom — run exactly].

How to reason

  1. Observe — EXPLAIN ANALYZE / pg_stat_statements / existing pg_indexes
  2. Interpret — seq scan vs N+1 vs over-fetch vs missing pagination
  3. Classify — add-index / eager-load / narrow-select / paginate / leave-alone
  4. Severity — write-path timeout outranks a 200ms list page

Worked example

Observe: /feed p95 2.4s; Prisma logs 81 queries; pg_indexes has no idx_posts_user_created. Interpret: findMany posts then per-row user.findUnique — N+1; ORDER BY created_at is a seq scan. Classify: eager-load include: { author } + composite index (user_id, created_at DESC). Verify: EXPLAIN ANALYZE → Index Scan; query count 2; p95 < 200ms. Did not add a duplicate index.

Self-critique before reporting

  • Existing first — listed pg_indexes / migrations before CREATE INDEX
  • EXPLAIN — the claimed winner has ANALYZE output, not intuition
  • No duplicate index — the proposed name was queried and absent
  • Right owner — schema consistency → audit-db-schema; RLS access → plan-rls-audit

Systematic approach to identifying and fixing database performance issues.

When to Use

  • Slow page loads (database bottleneck)
  • Query timeout errors
  • N+1 queries
  • Schema design review
  • Index optimization
  • Migration planning

Check existing first [LOW freedom — run exactly]

Before ANY optimization, verify current state:

  1. Check existing indexes:
SELECT indexname, indexdef FROM pg_indexes
WHERE schemaname = 'public' AND tablename = 'your_table';
  1. Check existing migrations:
ls -la supabase/migrations/ | grep -i "index\|optim\|perf"
  1. Check if index already exists:
SELECT 1 FROM pg_indexes WHERE indexname = 'your_proposed_index';
  1. Check Supabase advisors for current issues:
  • Use get_advisors MCP tool for performance/security
  • Don't re-fix already addressed issues

Why: Duplicate indexes waste storage and slow writes. Always verify before adding.

Performance Investigation [HIGH freedom]

1. Identify Slow Queries

  • Prisma: new PrismaClient({ log: [{ emit: 'event', level: 'query' }] }) and db.$on('query', ...); log anything over 100 ms with its duration.
  • Postgres/Supabase: CREATE EXTENSION IF NOT EXISTS pg_stat_statements, then order by total_time DESC, reading total_time / calls and rows / calls.

Logging snippet and the pg_stat_statements query: references/queries.md §Identify slow queries.

2. Common Performance Issues

IssueSymptomSolution
N+1 QueriesMany small queriesUse include / eager load
Missing IndexSlow WHERE/JOINAdd index on filtered columns
Full Table ScanSlow on large tablesAdd index, limit results
Over-fetchingSlow responseSelect only needed fields
No PaginationMemory issuesAdd cursor/offset pagination

N+1 Query Fix [HIGH freedom]

  • Problem: findMany then a per-row findUnique in a loop — 1 + N queries.
  • Fix: eager-load the relation; narrow it with a nested select so you do not over-fetch the join.
  • Supabase: one .select('*, author:users(id, name, avatar)') call does the join.
// Good - 2 queries total
const posts = await db.post.findMany({
  include: { author: { select: { id: true, name: true, avatar: true } } },
})

Bad/good Prisma and the Supabase equivalent: references/queries.md §N+1 query fix.

Index Optimization [HIGH freedom]

When to Add Indexes

Add index when column is used in:

  • WHERE clauses (filtering)
  • JOIN conditions
  • ORDER BY clauses
  • Unique constraints

Don't add index when:

  • Table is small (< 1000 rows)
  • Column has low cardinality (few unique values)
  • Column is rarely queried
  • Table has heavy writes

Index Types

  • Single-column for one filter; composite when WHERE a = ? ORDER BY b (column order matters: equality first, then sort).
  • Unique index for constraints; partial index (WHERE published = true) for a hot subset; GIN for JSONB, arrays and to_tsvector search.
  • Prisma: @@index([userId, createdAt(sort: Desc)]), @@unique([...]) mirror the SQL.
CREATE INDEX idx_posts_user_created ON posts(user_id, created_at DESC);  -- composite, order matters
CREATE INDEX idx_posts_published ON posts(created_at) WHERE published = true;  -- partial

All six index forms and the Prisma model: references/schema-and-indexes.md §Index types, §Prisma index syntax.

Query Optimization Patterns [HIGH freedom]

  • Select only needed fields — select: { id, name, email } instead of the whole row.
  • Pagination — offset (skip/take) is fine for small pages; cursor (cursor: { id }, skip: 1) for large or infinite lists; return nextCursor from the last row.
  • Batch — createMany({ skipDuplicates }) instead of a create loop; $transaction([...]) for related writes.
  • Count — db.post.count({ where }); pair it with the page query inside one $transaction.
const posts = await db.post.findMany({
  take: limit,
  skip: cursor ? 1 : 0,                        // skip the cursor row itself
  cursor: cursor ? { id: cursor } : undefined,
  orderBy: { createdAt: 'desc' },
})
const nextCursor = posts.length === limit ? posts[posts.length - 1].id : null

Select, both pagination styles, batching and count: references/queries.md §Select only needed fields through §Count optimization.

Schema Design Best Practices [HIGH freedom]

Normalization vs Denormalization

Normalize when:

  • Data changes frequently
  • Data integrity is critical
  • Storage is a concern

Denormalize when:

  • Read performance is critical

  • Data rarely changes

  • Complex joins are slow

  • Normalized: a post_stats(post_id PK → posts, view_count, like_count) table; denormalized: counters as columns on posts.

  • Data types — UUID ids, VARCHAR(n) for bounded strings, DECIMAL(10,2) for money, TIMESTAMPTZ always, ENUM for fixed sets.

  • Soft deletes — nullable deletedAt with @@index([deletedAt]); every read filters deletedAt: null.

SQL for both shapes, the data-type list and the soft-delete model: references/schema-and-indexes.md §Normalization, §Efficient data types, §Soft deletes.

Supabase-Specific Optimizations [HIGH freedom]

  • RLS — a function call or sub-select per row (get_user_team()) is slow; prefer user_id = auth.uid(), or an EXISTS join against the membership table when the rule needs it.
  • Edge Functions — move multi-query aggregations into one supabase.rpc('get_dashboard_stats') call behind Deno.serve instead of several client round trips.
CREATE POLICY "fast_policy" ON posts FOR SELECT USING (user_id = auth.uid());

Slow/fast/join policies and the Edge Function: references/schema-and-indexes.md §RLS performance, §Edge Functions.

Query Analysis [LOW freedom — run exactly]

EXPLAIN ANALYZE

EXPLAIN ANALYZE SELECT * FROM posts WHERE user_id = 'abc123' ORDER BY created_at DESC LIMIT 20;

Look for: Seq Scan (bad on large tables), Index Scan (good), Nested Loop (check for N+1), high actual time. Annotated version: references/queries.md §EXPLAIN ANALYZE.

Key Metrics

MetricTargetAction if Exceeded
Query time< 100msAdd index, optimize
Rows scanned< 10x returnedAdd index
Memory usage< 256MBAdd LIMIT, pagination
Connection count< pool sizeUse connection pooling

Optimization Checklist [LOW freedom — do not skip]

  • Queries logged and monitored
  • Indexes on filtered/joined columns
  • No N+1 queries (eager loading)
  • Pagination on all list endpoints
  • Select only needed fields
  • Batch operations where possible
  • Connection pooling configured
  • RLS policies optimized
  • EXPLAIN ANALYZE on slow queries
  • Appropriate data types used
Skills enregistrées