技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
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.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Degree of freedom: MIXED. Which query/index/N+1 to fix [HIGH freedom];
existing-index probes and EXPLAIN ANALYZE [LOW freedom — run exactly].
pg_stat_statements / existing pg_indexesObserve:
/feedp95 2.4s; Prisma logs 81 queries;pg_indexeshas noidx_posts_user_created. Interpret:findManyposts then per-rowuser.findUnique— N+1;ORDER BY created_atis a seq scan. Classify: eager-loadinclude: { author }+ composite index(user_id, created_at DESC). Verify: EXPLAIN ANALYZE → Index Scan; query count 2; p95 < 200ms. Did not add a duplicate index.
pg_indexes / migrations before CREATE INDEXaudit-db-schema; RLS access → plan-rls-auditSystematic approach to identifying and fixing database performance issues.
Before ANY optimization, verify current state:
SELECT indexname, indexdef FROM pg_indexes
WHERE schemaname = 'public' AND tablename = 'your_table';
ls -la supabase/migrations/ | grep -i "index\|optim\|perf"
SELECT 1 FROM pg_indexes WHERE indexname = 'your_proposed_index';
get_advisors MCP tool for performance/securityWhy: Duplicate indexes waste storage and slow writes. Always verify before adding.
new PrismaClient({ log: [{ emit: 'event', level: 'query' }] }) and db.$on('query', ...); log anything over 100 ms with its duration.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.
| Issue | Symptom | Solution |
|---|---|---|
| N+1 Queries | Many small queries | Use include / eager load |
| Missing Index | Slow WHERE/JOIN | Add index on filtered columns |
| Full Table Scan | Slow on large tables | Add index, limit results |
| Over-fetching | Slow response | Select only needed fields |
| No Pagination | Memory issues | Add cursor/offset pagination |
findMany then a per-row findUnique in a loop — 1 + N queries.select so you do not over-fetch the join..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.
Add index when column is used in:
WHERE clauses (filtering)JOIN conditionsORDER BY clausesDon't add index when:
WHERE a = ? ORDER BY b (column order matters: equality first, then sort).WHERE published = true) for a hot subset; GIN for JSONB, arrays and to_tsvector search.@@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.
select: { id, name, email } instead of the whole row.skip/take) is fine for small pages; cursor (cursor: { id }, skip: 1) for large or infinite lists; return nextCursor from the last row.createMany({ skipDuplicates }) instead of a create loop; $transaction([...]) for related writes.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.
Normalize when:
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.
get_user_team()) is slow; prefer user_id = auth.uid(), or an EXISTS join against the membership table when the rule needs it.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.
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.
| Metric | Target | Action if Exceeded |
|---|---|---|
| Query time | < 100ms | Add index, optimize |
| Rows scanned | < 10x returned | Add index |
| Memory usage | < 256MB | Add LIMIT, pagination |
| Connection count | < pool size | Use connection pooling |