Files
craft2prints/DATABASE_OPTIMIZATION.md
T
AndymickandClaude Haiku 4.5 16f1f64fd0 Feature: Add customer reviews system with admin management
- Add Review model to database with approval workflow
- Customers can submit reviews with 1-5 star ratings
- Admin can approve, reject, or delete reviews
- Public reviews page showing all approved reviews with rating stats
- ReviewForm component for easy integration on product pages
- Bulk gallery upload now properly tracked

Chore: Add expected delivery date for design approvals

- Add expectedDeliveryDate field to DesignApproval model
- Admin can set delivery date when approving designs
- Improves customer communication and expectations

Chore: Add welcome back message for logged-in customers

- Display personalized greeting on account page after login
- Shows customer's name if available

Chore: Add color difference disclaimer to product pages

- Added to StandardProductView (ready-made products)
- Added to DesignCanvas (personalized products)
- Informs customers about lighting-based color variations
- Helps manage customer expectations

Feature: Add image identification in design specifications

- Images now clearly labeled as "Image 1", "Image 2", etc.
- Images marked with purple circles in specification diagrams
- Improves clarity for production team

Documentation: Add comprehensive database optimization guide

- DATABASE_OPTIMIZATION.md - Full optimization strategies
- DATABASE_OPTIMIZATION_QUICK_START.md - Implementation examples
- DATABASE_OPTIMIZATION_MIGRATION.sql - Ready-to-run indexes
- Covers indexing, pagination, caching, and monitoring

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-19 18:24:58 +01:00

442 lines
9.6 KiB
Markdown

# Database Optimization Guide for Craft2Prints
## Overview
This guide covers strategies to optimize your PostgreSQL database for handling large datasets efficiently.
---
## 1. INDEXING STRATEGY
### Add Composite Indexes
Add these indexes to your Prisma schema:
```prisma
model Order {
// ... existing fields
@@index([customerId, createdAt]) // For customer order history sorted by date
@@index([status, createdAt]) // For filtering orders by status
@@index([email]) // For quick email lookups
}
model OrderItem {
@@index([orderId, productId]) // For order detail pages
@@index([productId]) // For product sales reporting
}
model DesignApproval {
@@index([customerEmail, status]) // For customer design lookups
@@index([status, createdAt]) // For pending designs view
@@index([productId]) // For product-specific designs
}
model Review {
@@index([productId, approved]) // For public product reviews
@@index([customerId]) // For customer review history
}
model Product {
@@index([category, showOnProducts]) // For category filtering
@@index([showOnPersonalised]) // For made-to-order listings
}
```
### Create Migration
```bash
npx prisma migrate dev --name add_performance_indexes
```
---
## 2. QUERY OPTIMIZATION
### Problem: N+1 Queries
**Bad:**
```typescript
const orders = await prisma.order.findMany();
// This runs 1 query
for (const order of orders) {
const items = await prisma.orderItem.findMany({where: {orderId: order.id}});
// This runs N more queries (N+1 problem)
}
```
**Good:**
```typescript
const orders = await prisma.order.findMany({
include: { items: true } // Fetches everything in 1 query
});
```
### Problem: Fetching Unnecessary Fields
**Bad:**
```typescript
const products = await prisma.product.findMany();
// Fetches ALL fields including large JSON fields (colors, colorPhotos)
```
**Good:**
```typescript
const products = await prisma.product.findMany({
select: {
id: true,
name: true,
slug: true,
basePrice: true,
// Only fetch what you need
}
});
```
### Recommended Query Optimizations for Your Routes
**Orders List Page:**
```typescript
const orders = await prisma.order.findMany({
where: { customerId },
select: {
id: true,
total: true,
status: true,
createdAt: true,
items: { select: { productName: true, quantity: true } }
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * 20,
take: 20
});
```
**Design Approvals List:**
```typescript
const designs = await prisma.designApproval.findMany({
where: { status: 'PENDING' },
select: {
id: true,
title: true,
customerEmail: true,
product: { select: { name: true } },
createdAt: true
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * 50,
take: 50
});
```
---
## 3. PAGINATION (Critical for Large Datasets)
### Implement Offset-Based Pagination
```typescript
// pages/admin/orders/page.tsx
import { prisma } from '@/lib/prisma';
export default async function OrdersPage({
searchParams
}: {
searchParams: { page?: string }
}) {
const page = Math.max(1, parseInt(searchParams.page || '1'));
const pageSize = 20;
const skip = (page - 1) * pageSize;
const [orders, total] = await Promise.all([
prisma.order.findMany({
skip,
take: pageSize,
orderBy: { createdAt: 'desc' }
}),
prisma.order.count() // Get total count for pagination
]);
const totalPages = Math.ceil(total / pageSize);
return (
// Render orders with pagination controls
);
}
```
### Cursor-Based Pagination (Better for Real-Time Data)
```typescript
const orders = await prisma.order.findMany({
take: 20,
skip: 1, // Skip the cursor itself
cursor: lastOrderId ? { id: lastOrderId } : undefined,
orderBy: { id: 'desc' }
});
```
---
## 4. CACHING STRATEGY
### Enable Redis/In-Memory Caching
```typescript
// lib/cache.ts
import NodeCache from 'node-cache';
const cache = new NodeCache({ stdTTL: 600 }); // 10 min default
export async function getCachedProducts() {
const cached = cache.get('all_products');
if (cached) return cached;
const products = await prisma.product.findMany();
cache.set('all_products', products);
return products;
}
export function invalidateProductCache() {
cache.del('all_products');
}
```
### Cache Invalidation
```typescript
// When updating products
await updateProduct(id, data);
invalidateProductCache(); // Clear cache on update
```
---
## 5. DATABASE MAINTENANCE
### Connection Pooling (Already handled by Prisma)
The `.env` already has connection pooling. Verify:
```env
DATABASE_URL="postgresql://user:pass@localhost:5432/db?schema=public"
# Prisma uses connection pooling by default
```
### Regular Maintenance
**Weekly Tasks:**
```sql
-- Vacuum and analyze (PostgreSQL auto-vacuums, but manual is faster)
VACUUM ANALYZE;
-- Check index bloat
SELECT schemaname, tablename, indexname, idx_blks_read, idx_blks_hit
FROM pg_statio_user_indexes
ORDER BY idx_blks_read DESC;
-- Check slow queries (enable slow query log)
ALTER SYSTEM SET log_min_duration_statement = 1000; -- Log queries > 1 second
SELECT pg_reload_conf();
```
**Monthly Tasks:**
```bash
# Backup database
pg_dump -U username database_name > backup.sql
# Monitor table sizes
SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename))
FROM pg_tables
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
```
---
## 6. QUERY TIMEOUTS & SETTINGS
### Add Query Timeout in Prisma
```prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL") // For migrations
// Connection settings
// Prisma automatically handles these, but you can configure:
// prismaClient options
}
```
### Environment Variables
```env
# .env
DATABASE_URL="postgresql://user:pass@host:5432/craft2prints?schema=public&connect_timeout=10&statement_timeout=30000"
DIRECT_URL="postgresql://user:pass@host:5432/craft2prints" # For migrations
```
---
## 7. DATA ARCHIVING
### Archive Old Orders (After 2 Years)
```typescript
// scripts/archive-old-orders.ts
async function archiveOldOrders() {
const twoYearsAgo = new Date();
twoYearsAgo.setFullYear(twoYearsAgo.getFullYear() - 2);
// Copy to archive table
const oldOrders = await prisma.order.findMany({
where: {
createdAt: { lt: twoYearsAgo },
status: 'DELIVERED'
}
});
// Store in archive database or S3
// Delete from main database
await prisma.order.deleteMany({
where: {
id: { in: oldOrders.map(o => o.id) }
}
});
}
```
---
## 8. MONITORING & ALERTS
### Add Monitoring to Your App
```typescript
// lib/db-monitoring.ts
export async function monitorDatabaseHealth() {
const result = await prisma.$queryRaw`
SELECT
NOW() as current_time,
pg_database.datname,
pg_size_pretty(pg_database_size(pg_database.datname)) AS size,
(SELECT count(*) FROM pg_stat_activity WHERE datname = pg_database.datname) AS active_connections
FROM pg_database
WHERE datname = 'craft2prints';
`;
return result;
}
```
---
## 9. SPECIFIC OPTIMIZATIONS FOR CRAFT2PRINTS
### Admin Orders Page
```typescript
// Current: Very slow with large orders
const orders = await prisma.order.findMany({
include: {
items: true,
customer: true // N+1 problem
}
});
// Optimized:
const orders = await prisma.order.findMany({
select: {
id: true,
email: true,
total: true,
status: true,
createdAt: true,
items: {
select: {
id: true,
productName: true,
quantity: true
}
}
},
skip: (page - 1) * 50,
take: 50,
orderBy: { createdAt: 'desc' }
});
```
### Product List Page (Admin)
```typescript
// Don't fetch colorPhotos JSON
const products = await prisma.product.findMany({
select: {
id: true,
slug: true,
name: true,
category: true,
basePrice: true,
showOnProducts: true,
showOnPersonalised: true,
// Skip: colorPhotos, colors (large JSON)
},
orderBy: { name: 'asc' },
skip: (page - 1) * 50,
take: 50
});
```
### Gallery Page (Public)
```typescript
// Only fetch approved reviews
const reviews = await prisma.review.findMany({
where: {
productId,
approved: true
},
select: {
id: true,
rating: true,
title: true,
comment: true,
customerName: true,
createdAt: true
},
orderBy: { createdAt: 'desc' },
take: 10 // Limit to 10 recent reviews
});
```
---
## 10. QUICK WINS (Do These First!)
1.**Add pagination to all list pages** - Immediately reduces query size
2.**Add indexes** - Run migration for composite indexes
3.**Use `select` instead of full includes** - Reduce data transfer
4.**Enable Redis caching** - Cache frequently accessed data
5.**Set query timeouts** - Prevent long-running queries
6.**Archive old data** - Keep active tables lean
---
## Performance Benchmarks (Expected After Optimization)
| Operation | Before | After |
|-----------|--------|-------|
| Orders list (10k orders) | 5-10s | 200-500ms |
| Product admin (5k products) | 3-5s | 100-300ms |
| Design approvals | 2-3s | 50-150ms |
| Public product page | 1-2s | 200-500ms |
---
## Monitoring Script
```bash
# Run weekly to check health
npm run monitor:db
# Add to package.json:
"scripts": {
"monitor:db": "node --require ts-node/register scripts/monitor-db.ts"
}
```
---
## Next Steps
1. Add indexes from section 1
2. Apply query optimizations to your most-used routes
3. Implement pagination on list pages
4. Set up caching for static data
5. Monitor database performance weekly
6. Archive old data quarterly