- 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>
442 lines
9.6 KiB
Markdown
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
|
|
|