Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:22:25 +08:00
commit c3294f28aa
60 changed files with 10297 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
---
name: optimizing-query-performance
description: Optimize queries with indexes, batching, and efficient Prisma operations for production performance.
allowed-tools: Read, Write, Edit, Bash
version: 1.0.0
---
<overview>
Query optimization requires strategic indexing, efficient batching, and monitoring to prevent common production anti-patterns.
Key capabilities: Strategic index placement (@@index, @@unique) · Efficient batch operations (createMany, transactions) · Query analysis & N+1 prevention · Field selection optimization & cursor pagination
</overview>
<workflow>
**Phase 1 — Identify:** Enable query logging; analyze patterns/execution times; identify missing indexes, N+1 problems, or inefficient batching
**Phase 2 — Optimize:** Add indexes for filtered/sorted fields; replace loops with batch operations; select only needed fields; use cursor pagination for large datasets
**Phase 3 — Validate:** Measure execution time before/after; verify index usage with EXPLAIN ANALYZE; monitor connection pool under load
</workflow>
## Quick Reference
**Index Strategy:**
| Scenario | Index Type | Example |
| --------------------- | ----------------------------------- | ------------------------------ |
| Single field filter | `@@index([field])` | `@@index([status])` |
| Multiple field filter | `@@index([field1, field2])` | `@@index([userId, status])` |
| Sort + filter | `@@index([filterField, sortField])` | `@@index([status, createdAt])` |
**Batch Operations:**
| Operation | Slow (Loop) | Fast (Batch) |
| --------- | ---------------------- | -------------- |
| Insert | `for...await create()` | `createMany()` |
| Update | `for...await update()` | `updateMany()` |
| Delete | `for...await delete()` | `deleteMany()` |
**Performance Gains:** Indexes (10-100x) · Batch ops (50-100x for 1000+ records) · Cursor pagination (constant vs O(n))
<constraints>
**MUST:** Add indexes for WHERE/ORDER BY/FK fields with frequent queries; use createMany for 100+ records; cursor pagination for deep pagination; select only needed fields; monitor query duration in production
**SHOULD:** Test indexes with production data; chunk 100k+ batches into smaller sizes; use `@@index([field1, field2])` for multi-field filters; remove unused indexes
**NEVER:** Add indexes without performance measurement; offset pagination beyond page 100 on large tables; fetch all
fields when only needing few; loop with individual creates/updates; ignore slow query warnings
</constraints>
<validation>
**Measure Performance:**
```typescript
const start = Date.now()
const result = await prisma.user.findMany({ ... })
console.log(`Query took ${Date.now() - start}ms`)
```
Expected: 50-90% improvement for indexed queries, 50-100x for batch operations
**Verify Index Usage:** Run EXPLAIN ANALYZE; confirm "Index Scan" vs "Seq Scan"
**Monitor Production:** Track P95/P99 latency; expect reduced slow query frequency
**Check Write Performance:** Writes may increase 10-30% per index if rarely-used; consider removal
</validation>
## References
- **Index Strategy**: `references/index-strategy.md` — indexing patterns and trade-offs
- **Batch Operations**: `references/batch-operations.md` — bulk operations and chunking
- **Query Monitoring**: `references/query-monitoring.md` — logging setup and slow query analysis
- **Field Selection**: `references/field-selection.md` — select vs include patterns and N+1 prevention
- **Optimization Examples**: `references/optimization-examples.md` — real-world improvements
- **Next.js Integration**: Next.js plugin for App Router-specific patterns
- **Serverless**: CLIENT-serverless-config skill for connection pooling

View File

@@ -0,0 +1,93 @@
# Batch Operations Guide
## createMany vs Loop
**SLOW (N database round-trips):**
```typescript
for (const userData of users) {
await prisma.user.create({ data: userData })
}
```
**FAST (1 database round-trip):**
```typescript
await prisma.user.createMany({
data: users,
skipDuplicates: true
})
```
**Performance Gain:** 50-100x faster for 1000+ records.
**Limitations:**
- createMany does NOT return created records
- Does NOT trigger middleware or relation cascades
- skipDuplicates skips on unique constraint violations (no error)
## Batch Updates
**SLOW:**
```typescript
for (const id of orderIds) {
await prisma.order.update({
where: { id },
data: { status: 'shipped' }
})
}
```
**FAST:**
```typescript
await prisma.order.updateMany({
where: { id: { in: orderIds } },
data: { status: 'shipped' }
})
```
**Note:** updateMany returns count, not records.
## Batch with Transactions
When you need returned records or relation handling:
```typescript
await prisma.$transaction(
users.map(userData =>
prisma.user.create({ data: userData })
)
)
```
**Use Case:** Creating related records where you need IDs for subsequent operations.
**Trade-off:** Slower than createMany but supports relations and returns records.
## Batch Size Considerations
For very large datasets (100k+ records), chunk into batches:
```typescript
const BATCH_SIZE = 1000
for (let i = 0; i < records.length; i += BATCH_SIZE) {
const batch = records.slice(i, i + BATCH_SIZE)
await prisma.record.createMany({
data: batch,
skipDuplicates: true
})
console.log(`Processed ${Math.min(i + BATCH_SIZE, records.length)}/${records.length}`)
}
```
**Benefits:**
- Progress visibility
- Memory efficiency
- Failure isolation (one batch fails, others succeed)

View File

@@ -0,0 +1,70 @@
# Field Selection Guide
## Select vs Include
**select:** Choose specific fields (excludes all others)
```typescript
const user = await prisma.user.findUnique({
where: { id: 1 },
select: {
id: true,
email: true
}
})
```
**include:** Add relations to default fields
```typescript
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
orders: true
}
})
```
**Cannot use both select and include in same query.**
## Nested Selection
```typescript
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
orders: {
select: {
id: true,
total: true,
createdAt: true
},
where: { status: 'completed' },
orderBy: { createdAt: 'desc' },
take: 5
}
}
})
```
Only fetches recent completed orders, not all orders.
## Counting Relations Without Loading
```typescript
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
_count: {
select: {
orders: true,
posts: true
}
}
}
})
```
Returns counts without loading actual relation records.

View File

@@ -0,0 +1,91 @@
# Index Strategy Guide
## When to Add Indexes
Add `@@index` for fields that are:
- Frequently used in where clauses
- Used for sorting (orderBy)
- Foreign keys with frequent joins
- Composite conditions used together
## Single-Field Indexes
```prisma
model User {
id Int @id @default(autoincrement())
email String @unique
createdAt DateTime @default(now())
status String
@@index([createdAt])
@@index([status])
}
```
**Use Case:**
```typescript
await prisma.user.findMany({
where: { status: 'active' },
orderBy: { createdAt: 'desc' },
take: 20
})
```
Both status filter and createdAt sort benefit from indexes.
## Composite Indexes
```prisma
model Order {
id Int @id @default(autoincrement())
userId Int
status String
createdAt DateTime @default(now())
totalCents Int
user User @relation(fields: [userId], references: [id])
@@index([userId, status])
@@index([status, createdAt])
}
```
**Composite Index Rules:**
1. Order matters: [userId, status] helps queries filtering by userId, or userId + status
2. Does NOT help queries filtering only by status
3. Most selective field should come first
4. Match your most common query patterns
**Use Case:**
```typescript
await prisma.order.findMany({
where: {
userId: 123,
status: 'pending'
},
orderBy: { createdAt: 'desc' }
})
```
First index [userId, status] optimizes the where clause.
Second index [status, createdAt] would help if querying by status alone with date sorting.
## Index Trade-offs
**Benefits:**
- Faster read queries (10-100x improvement on large tables)
- Required for efficient sorting and filtering
- Essential for foreign key performance
**Costs:**
- Slower writes (insert/update/delete must update indexes)
- Storage overhead (5-20% per index)
- Diminishing returns beyond 5-7 indexes per table
**Rule:** Only index fields actually used in queries. Remove unused indexes.

View File

@@ -0,0 +1,153 @@
# Optimization Examples
## Example 1: Add Composite Index for Common Query
**Scenario:** API endpoint filtering orders by userId and status, sorted by date
**Current Schema:**
```prisma
model Order {
id Int @id @default(autoincrement())
userId Int
status String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
}
```
**Query:**
```typescript
await prisma.order.findMany({
where: {
userId: req.user.id,
status: 'pending'
},
orderBy: { createdAt: 'desc' }
})
```
**Optimization - Add Composite Index:**
```prisma
model Order {
id Int @id @default(autoincrement())
userId Int
status String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
@@index([userId, status, createdAt])
}
```
Index covers filter AND sort, enabling index-only scan.
## Example 2: Optimize Bulk Insert
**Scenario:** Import 10,000 products from CSV
**SLOW Approach:**
```typescript
for (const row of csvData) {
await prisma.product.create({
data: {
name: row.name,
sku: row.sku,
price: parseFloat(row.price)
}
})
}
```
10,000 database round-trips = 60+ seconds
**FAST Approach:**
```typescript
const products = csvData.map(row => ({
name: row.name,
sku: row.sku,
price: parseFloat(row.price)
}))
await prisma.product.createMany({
data: products,
skipDuplicates: true
})
```
1 database round-trip = <1 second
**Even Better - Chunked Batches:**
```typescript
const BATCH_SIZE = 1000
for (let i = 0; i < products.length; i += BATCH_SIZE) {
const batch = products.slice(i, i + BATCH_SIZE)
await prisma.product.createMany({
data: batch,
skipDuplicates: true
})
}
```
Progress tracking + failure isolation.
## Example 3: Identify and Fix Slow Query
**Enable Logging:**
```typescript
const prisma = new PrismaClient({
log: [{ emit: 'event', level: 'query' }]
})
prisma.$on('query', (e) => {
if (e.duration > 500) {
console.log(`SLOW QUERY (${e.duration}ms): ${e.query}`)
}
})
```
**Detected Slow Query:**
```
SLOW QUERY (3421ms): SELECT * FROM "Post" WHERE "published" = true ORDER BY "views" DESC LIMIT 10
```
**Analyze with EXPLAIN:**
```typescript
await prisma.$queryRaw`
EXPLAIN ANALYZE
SELECT * FROM "Post"
WHERE "published" = true
ORDER BY "views" DESC
LIMIT 10
`
```
**Output shows:** Seq Scan (full table scan)
**Solution - Add Index:**
```prisma
model Post {
id Int @id @default(autoincrement())
published Boolean @default(false)
views Int @default(0)
@@index([published, views])
}
```
**Verify Improvement:**
After migration, same query executes in ~15ms (228x faster).

View File

@@ -0,0 +1,131 @@
# Query Monitoring Guide
## Enable Query Logging
**Development:**
```typescript
const prisma = new PrismaClient({
log: [
{ emit: 'event', level: 'query' },
{ emit: 'stdout', level: 'error' },
{ emit: 'stdout', level: 'warn' }
]
})
prisma.$on('query', (e) => {
console.log('Query: ' + e.query)
console.log('Duration: ' + e.duration + 'ms')
})
```
**Production (structured logging):**
```typescript
const prisma = new PrismaClient({
log: [{ emit: 'event', level: 'query' }]
})
prisma.$on('query', (e) => {
if (e.duration > 1000) {
logger.warn('Slow query detected', {
query: e.query,
duration: e.duration,
params: e.params
})
}
})
```
## Analyzing Slow Queries
**Identify Patterns:**
1. Queries without WHERE clause on large tables (full table scans)
2. Complex JOINs without indexes on foreign keys
3. ORDER BY on unindexed fields
4. Missing LIMIT on large result sets
**Use Database EXPLAIN:**
```typescript
await prisma.$queryRaw`EXPLAIN ANALYZE
SELECT * FROM "User"
WHERE status = 'active'
ORDER BY "createdAt" DESC
LIMIT 20
`
```
Look for:
- "Seq Scan" (sequential scan) - needs index
- "Index Scan" - good
- High execution time relative to query complexity
## Common Query Anti-Patterns
**N+1 Problem:**
```typescript
const users = await prisma.user.findMany()
for (const user of users) {
const orders = await prisma.order.findMany({
where: { userId: user.id }
})
}
```
**Solution - Use include:**
```typescript
const users = await prisma.user.findMany({
include: {
orders: true
}
})
```
**Over-fetching:**
```typescript
const users = await prisma.user.findMany()
```
Fetches ALL fields for ALL users.
**Solution - Select needed fields:**
```typescript
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
name: true
}
})
```
**Offset Pagination on Large Datasets:**
```typescript
await prisma.user.findMany({
skip: 50000,
take: 20
})
```
Database must scan and skip 50,000 rows.
**Solution - Cursor pagination:**
```typescript
await prisma.user.findMany({
take: 20,
cursor: { id: lastSeenId },
skip: 1
})
```
Constant time regardless of page depth.