Dominar las técnicas avanzadas de optimización para mejorar el rendimiento de aplicaciones TypeORM en producción, desde la identificación de problemas comunes hasta la implementación de soluciones empresariales.
- Problema N+1 y sus soluciones
- Consultas ineficientes y optimización
- Monitoring de performance en tiempo real
- Detección de queries lentas
- Índices estratégicos simples y compuestos
- SELECT específicos para reducir transferencia
- JOINs optimizados con campos selectivos
- Agregaciones eficientes
- Inserción masiva de datos
- Actualizaciones en lote
- Eliminación masiva
- Operaciones Upsert
- Transacciones atómicas para operaciones complejas
- Manejo de errores y rollback
- Optimización de bloqueos
- Campos calculados para evitar agregaciones
- Información duplicada estratégica
- Mantenimiento de estadísticas
- Paginación básica con LIMIT/OFFSET
- Paginación con cursor para datasets grandes
- Paginación con agregación
👤 User (usuarios)
├── 📦 Order (pedidos) [1:N]
│ └── 📋 OrderItem (items) [1:N]
│ └── 🛍️ Product (productos) [N:1]
│ └── 📂 Category (categorías) [N:1]
├── ⭐ Review (reseñas) [1:N]
│ └── 🛍️ Product (productos) [N:1]
idx_user_email- Búsquedas por emailidx_user_status_created- Filtrado por estado y fechaidx_user_last_login- Ordenamiento por último loginidx_user_role_status- Consultas por rol y estadoidx_user_total_spent- Ordenamiento por gasto total
idx_product_name- Búsqueda por nombreidx_product_category_price- Consultas por categoría y precioidx_product_category_rating- Consultas por categoría y ratingidx_product_active_stock- Productos disponiblesidx_product_sales_popularity- Productos populares
idx_order_user_status- Pedidos por usuario y estadoidx_order_status_date- Consultas por estado y fechaidx_order_tracking- Búsqueda por tracking
@Column({ default: 0 })
totalSpent!: number; // Evita SUM() en orders
@Column({ default: 0 })
orderCount!: number; // Evita COUNT() en orders@Column({ default: 0 })
totalSales!: number; // Evita SUM() en order_items
@Column({ default: 0 })
reviewCount!: number; // Evita COUNT() en reviews@Column({ length: 300 })
productName!: string; // Evita JOIN con products
@Column({ length: 50 })
productSku!: string; // Histórico al momento de compranpm installImplementá en exercise.ts y corré los tests:
npm run test:ej09// Esto ejecuta 1 + N queries (muy ineficiente)
const users = await userRepo.find({ take: 10 });
for (const user of users) {
const orders = await user.orders; // Query adicional por usuario
}
// Total: 1 query inicial + 10 queries de pedidos = 11 queries// Una sola query optimizada
const usersWithOrders = await userRepo
.createQueryBuilder("user")
.leftJoinAndSelect("user.orders", "order")
.take(10)
.getMany();
// Total: 1 query con JOIN// Cuando solo necesitas estadísticas
const userStats = await userRepo
.createQueryBuilder("user")
.leftJoin("user.orders", "order")
.select("user.id", "userId")
.addSelect("COUNT(order.id)", "orderCount")
.addSelect("SUM(order.total)", "totalSpent")
.groupBy("user.id")
.getRawMany();
// Total: 1 query con agregación@Index("idx_product_name", ["name"]) // Búsquedas por nombre
@Index("idx_product_price", ["price"]) // Ordenamiento por precio
@Index("idx_product_created", ["createdAt"]) // Ordenamiento por fecha@Index("idx_product_category_price", ["categoryId", "price"])
// Optimiza: WHERE categoryId = ? AND price > ?
// Optimiza: WHERE categoryId = ? ORDER BY price// Usa índice compuesto eficientemente
const products = await productRepo
.createQueryBuilder("product")
.where("product.categoryId = :categoryId", { categoryId: 1 })
.andWhere("product.price > :minPrice", { minPrice: 100 })
.orderBy("product.price", "DESC")
.getMany();// ❌ Ineficiente - N queries
for (const productData of products) {
await productRepo.save(productData);
}
// ✅ Eficiente - 1 query
await productRepo.save(products);// Actualizar múltiples registros
const result = await productRepo
.createQueryBuilder()
.update(Product)
.set({ price: () => "price * 1.1" })
.where("categoryId = :categoryId", { categoryId: 1 })
.execute();await userRepo
.createQueryBuilder()
.insert()
.into(User)
.values({
email: "usuario@ejemplo.com",
firstName: "Juan",
lastName: "Pérez",
})
.orUpdate(["firstName", "lastName"], ["email"])
.execute();await dataSource.transaction(async (manager) => {
// 1. Crear pedido
const order = await manager.save(Order, orderData);
// 2. Crear items y actualizar stock
for (const itemData of items) {
const product = await manager.findOne(Product, {
where: { id: itemData.productId },
});
if (product.stock < itemData.quantity) {
throw new Error("Stock insuficiente");
}
// Actualizar stock
await manager.update(Product, itemData.productId, {
stock: product.stock - itemData.quantity,
});
// Crear item
await manager.save(OrderItem, orderItem);
}
// 3. Actualizar estadísticas del usuario
await manager.increment(User, { id: order.userId }, "orderCount", 1);
await manager.increment(User, { id: order.userId }, "totalSpent", total);
});const dataSource = new DataSource({
type: "postgres",
logging: ["query", "error"],
maxQueryExecutionTime: 1000, // Log queries > 1s
// Pool de conexiones
extra: {
max: 20, // Conexiones máximas
min: 5, // Conexiones mínimas
acquire: 30000, // Timeout para obtener conexión
idle: 10000, // Timeout de inactividad
},
});class PerformanceMonitor {
static async measure<T>(operation: string, fn: () => Promise<T>): Promise<T> {
const start = Date.now();
const result = await fn();
const duration = Date.now() - start;
if (duration > 100) {
console.warn(`⚠️ Operación lenta: ${operation} tomó ${duration}ms`);
}
return result;
}
}const [items, total] = await repository.findAndCount({
skip: (page - 1) * pageSize,
take: pageSize,
order: { createdAt: "DESC" },
});const items = await repository
.createQueryBuilder("item")
.where("item.id > :cursor", { cursor: lastId })
.orderBy("item.id", "ASC")
.take(pageSize)
.getMany();@Entity()
class User {
@Column({ default: 0 })
totalSpent!: number; // Evita SUM() queries
@Column({ default: 0 })
orderCount!: number; // Evita COUNT() queries
}@Entity()
class OrderItem {
@Column()
productName!: string; // Duplicado para evitar JOINs
@Column()
productSku!: string; // Histórico al momento de compra
}// Actualizar estadísticas cuando cambian los datos
async function updateUserStats(userId: number) {
const stats = await orderRepo
.createQueryBuilder("order")
.select("COUNT(order.id)", "orderCount")
.addSelect("SUM(order.total)", "totalSpent")
.where("order.userId = :userId", { userId })
.getRawOne();
await userRepo.update(userId, {
orderCount: stats.orderCount,
totalSpent: stats.totalSpent,
});
}{
type: "postgres",
host: "localhost",
port: 5432,
// Pool configuration
poolSize: 20, // Conexiones concurrentes
acquireTimeout: 60000, // Timeout para obtener conexión
timeout: 60000, // Query timeout
// Connection limits
extra: {
max: 20,
min: 5,
acquire: 30000,
idle: 10000
}
}// Cache a nivel de aplicación
const cachedResult = await repository.find({
cache: {
id: "products_active",
milliseconds: 300000, // 5 minutos
},
});// Obtener consultas más lentas
const slowQueries = await dataSource.query(`
SELECT query, calls, total_time, mean_time
FROM pg_stat_statements
WHERE mean_time > 1000
ORDER BY mean_time DESC
LIMIT 10;
`);// Verificar uso de índices
const indexUsage = await dataSource.query(`
SELECT schemaname, tablename, indexname, idx_scan, idx_tup_read, idx_tup_fetch
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY schemaname, tablename;
`);- Identificar 3 consultas lentas en tu aplicación
- Crear índices apropiados
- Medir la mejora de performance
- Implementar cache Redis para consultas frecuentes
- Configurar invalidación automática
- Medir hit ratio del cache
- Encontrar problemas N+1 en relaciones complejas
- Implementar soluciones con DataLoader
- Comparar performance antes y después
- Implementar importación masiva de datos
- Optimizar con transacciones
- Agregar progress tracking
// Habilitar logging detallado
{
logging: ["query", "error", "warn"],
logger: "advanced-console",
maxQueryExecutionTime: 1000
}const result = await dataSource.query(
`
EXPLAIN ANALYZE
SELECT * FROM products
WHERE category_id = $1 AND price > $2
ORDER BY price DESC
LIMIT 10;
`,
[1, 100]
);- TypeORM Performance Tips
- Database Indexing Best Practices
- PostgreSQL Query Performance
- Node.js Performance Monitoring
- Crear índices basados en consultas reales
- Usar índices compuestos para consultas multi-columna
- Monitorear uso de índices regularmente
- Usar SELECT específicos en lugar de SELECT *
- Evitar N+1 con eager loading apropiado
- Implementar paginación para datasets grandes
- Mantener transacciones cortas
- Usar isolation levels apropiados
- Implementar retry logic para deadlocks
- Cachear consultas frecuentes y costosas
- Implementar invalidación inteligente
- Usar diferentes niveles de cache
- Implementar logging de performance
- Monitorear métricas de base de datos
- Alertas para queries lentas
¡Optimiza tu aplicación TypeORM para producción con estas técnicas avanzadas!