Backend db performance
🦖Curated Cursor AI agent skills, slash commands, MCP configs, subagents & rules for full-stack dev — React 19, Next.js 15, Supabase, Tailwind v4, TypeScript
npx -y skills add kensaurus/cursor-kenji --skill backend-db-performanceAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 6 stars6 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Optimize database queries, schemas, and performance. Use when fixing slow queries, adding indexes, N+1 problems, schema design, RLS policies, or when user mentions "slow query", "database performance", "timeout", "index", "query optimization", "Prisma", "Supabase", or "PostgreSQL".
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
8.9 KB, as published. Nobody here has run it
Database Optimization Skill
Systematic approach to identifying and fixing database performance issues.
When to Use
- Slow page loads (database bottleneck)
- Query timeout errors
- N+1 query problems
- Schema design review
- Index optimization
- Migration planning
CRITICAL: Check Existing First
Before ANY optimization, verify current state:
- Check existing indexes:
SELECT indexname, indexdef FROM pg_indexes
WHERE schemaname = 'public' AND tablename = 'your_table';
- Check existing migrations:
ls -la supabase/migrations/ | grep -i "index\|optim\|perf"
- Check if index already exists:
SELECT 1 FROM pg_indexes WHERE indexname = 'your_proposed_index';
- Check Supabase advisors for current issues:
- Use
get_advisorsMCP 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
1. Identify Slow Queries
Prisma - Enable query logging:
// lib/db.ts
import { PrismaClient } from '@prisma/client'
export const db = new PrismaClient({
log: [
{ emit: 'event', level: 'query' },
],
})
db.$on('query', (e) => {
if (e.duration > 100) { // Log queries > 100ms
console.log(`Slow query (${e.duration}ms):`, e.query)
}
})
Supabase - Query analysis:
-- Enable query stats
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Find slow queries
SELECT
query,
calls,
total_time / calls as avg_time_ms,
rows / calls as avg_rows
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 20;
2. Common Performance Issues
| 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 |
N+1 Query Fix
Problem: Fetching related data in loop
// Bad - N+1 queries
const posts = await db.post.findMany()
for (const post of posts) {
const author = await db.user.findUnique({ where: { id: post.authorId } })
// 1 query for posts + N queries for authors
}
Solution: Eager loading
// Good - 2 queries total
const posts = await db.post.findMany({
include: {
author: true,
},
})
// Or with select for specific fields
const posts = await db.post.findMany({
include: {
author: {
select: { id: true, name: true, avatar: true }
},
},
})
Supabase equivalent:
// Single query with join
const { data: posts } = await supabase
.from('posts')
.select(`
*,
author:users(id, name, avatar)
`)
Index Optimization
When to Add Indexes
Add index when column is used in:
WHEREclauses (filtering)JOINconditionsORDER BYclauses- 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 index
CREATE INDEX idx_posts_user_id ON posts(user_id);
-- Composite index (order matters!)
CREATE INDEX idx_posts_user_created ON posts(user_id, created_at DESC);
-- Unique index
CREATE UNIQUE INDEX idx_users_email ON users(email);
-- Partial index (index subset of rows)
CREATE INDEX idx_posts_published ON posts(created_at)
WHERE published = true;
-- GIN index for JSONB/array
CREATE INDEX idx_posts_tags ON posts USING GIN(tags);
-- Full-text search
CREATE INDEX idx_posts_search ON posts
USING GIN(to_tsvector('english', title || ' ' || content));
Prisma Index Syntax
model Post {
id String @id @default(cuid())
userId String
title String
status Status
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
// Single column index
@@index([userId])
// Composite index
@@index([userId, createdAt(sort: Desc)])
// Unique constraint (creates unique index)
@@unique([userId, title])
}
Query Optimization Patterns
Select Only Needed Fields
// Bad - fetches all columns
const users = await db.user.findMany()
// Good - fetches only needed
const users = await db.user.findMany({
select: {
id: true,
name: true,
email: true,
},
})
Pagination
Offset pagination (simple, but slow at high offsets):
const posts = await db.post.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
})
Cursor pagination (better for large datasets):
const posts = await db.post.findMany({
take: limit,
skip: cursor ? 1 : 0, // Skip cursor itself
cursor: cursor ? { id: cursor } : undefined,
orderBy: { createdAt: 'desc' },
})
// Return next cursor
const nextCursor = posts.length === limit ? posts[posts.length - 1].id : null
Batch Operations
// Bad - individual inserts
for (const item of items) {
await db.item.create({ data: item })
}
// Good - batch insert
await db.item.createMany({
data: items,
skipDuplicates: true,
})
// Good - transaction for related data
await db.$transaction([
db.order.create({ data: order }),
db.orderItem.createMany({ data: orderItems }),
db.inventory.updateMany({ where: {...}, data: {...} }),
])
Count Optimization
// Get count without fetching data
const count = await db.post.count({
where: { published: true },
})
// Combined with pagination
const [posts, count] = await db.$transaction([
db.post.findMany({ where, take: limit, skip: offset }),
db.post.count({ where }),
])
Schema Design Best Practices
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 (separate table)
CREATE TABLE post_stats (
post_id UUID PRIMARY KEY REFERENCES posts(id),
view_count INT DEFAULT 0,
like_count INT DEFAULT 0
);
-- Denormalized (same table)
ALTER TABLE posts
ADD COLUMN view_count INT DEFAULT 0,
ADD COLUMN like_count INT DEFAULT 0;
Efficient Data Types
-- Use appropriate types
id UUID DEFAULT gen_random_uuid() -- vs TEXT for IDs
status VARCHAR(20) -- vs unlimited TEXT
price DECIMAL(10,2) -- vs FLOAT for money
created_at TIMESTAMPTZ -- vs TIMESTAMP (include timezone)
-- Use enums for fixed values
CREATE TYPE status AS ENUM ('draft', 'published', 'archived');
Soft Deletes
model Post {
id String @id
deletedAt DateTime?
@@index([deletedAt]) // Index for filtering
}
// Query pattern
const posts = await db.post.findMany({
where: { deletedAt: null },
})
Supabase-Specific Optimizations
RLS Performance
-- Bad: Function call in RLS (slow)
CREATE POLICY "slow_policy" ON posts
FOR SELECT USING (
user_id IN (SELECT user_id FROM team_members WHERE team_id = get_user_team())
);
-- Good: Direct comparison (fast)
CREATE POLICY "fast_policy" ON posts
FOR SELECT USING (user_id = auth.uid());
-- Good: Join-based (when needed)
CREATE POLICY "team_policy" ON posts
FOR SELECT USING (
EXISTS (
SELECT 1 FROM team_members
WHERE team_members.team_id = posts.team_id
AND team_members.user_id = auth.uid()
)
);
Edge Functions for Complex Logic
// Move complex aggregations to Edge Functions
// instead of multiple round trips
// supabase/functions/dashboard-stats/index.ts
Deno.serve(async (req) => {
const stats = await supabase.rpc('get_dashboard_stats', {
user_id: userId
})
return new Response(JSON.stringify(stats))
})
Query Analysis
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 if N+1)
-- - High actual time
Key Metrics
| 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 |
Optimization Checklist
- 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