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,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.