Este módulo demuestra técnicas avanzadas de optimización para mejorar el rendimiento de aplicaciones TypeORM, cubriendo desde problemas comunes hasta soluciones empresariales.
Al completar este ejemplo, dominarás:
- Identificar y resolver el problema N+1
- Optimizar consultas con índices y selects específicos
- Implementar operaciones en lote para mejor performance
- Usar transacciones eficientemente
- Monitorear performance de aplicaciones
- Gestionar memoria en datasets grandes
- Aplicar mejores prácticas de TypeORM
El ejemplo usa un sistema de e-commerce optimizado:
👤 User (usuarios)
├── 📦 Order (pedidos) [1:N]
└── 📋 OrderItem (items) [1:N]
└── 🛍️ Product (productos) [N:1]
└── 📂 Category (categorías) [N:1]
idx_user_email- Búsquedas por emailidx_user_status_created- Filtrado por estado y fechaidx_user_last_login- Ordenamiento por último login
idx_product_name- Búsqueda por nombreidx_product_category_price- Consultas por categoría y precioidx_product_active_stock- Productos disponibles
idx_order_user_status- Pedidos por usuario y estadoidx_order_status_date- Consultas por estado y fecha
npm run optimization// Esto ejecuta 1 + N queries (muy ineficiente)
const users = await userRepo.find({ take: 5 });
for (const user of users) {
const orders = await user.orders; // Query adicional por usuario
}
// Total: 1 query inicial + 5 queries de pedidos = 6 queries// Una sola query optimizada
const usersWithOrders = await userRepo
.createQueryBuilder("user")
.leftJoinAndSelect("user.orders", "order")
.take(5)
.getMany();
// Total: 1 query con JOIN@Index("idx_product_name", ["name"]) // Búsquedas por nombre
@Index("idx_product_price", ["price"]) // Ordenamiento por precio@Index("idx_product_category_price", ["categoryId", "price"])
// Optimiza: WHERE categoryId = ? AND 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();// Carga todos los campos (puede ser pesado)
const products = await productRepo.find();// Solo campos necesarios
const products = await productRepo
.createQueryBuilder("product")
.select(["product.id", "product.name", "product.price"])
.getMany();// N queries separadas (lento)
for (const userData of users) {
await userRepo.save(userData);
}// Una sola query (mucho más rápido)
await userRepo.save(users);await dataSource.transaction(async (manager) => {
// Todas las operaciones en una transacción
const order = await manager.save(Order, orderData);
const items = await manager.save(OrderItem, itemsData);
await manager.update(Order, order.id, { total: calculatedTotal });
});// En DataSource config
{
logging: ["query", "error"],
maxQueryExecutionTime: 1000, // Log queries > 1s
}const start = Date.now();
const result = await repository.find();
const duration = Date.now() - start;
if (duration > 100) {
console.warn(`Slow query detected: ${duration}ms`);
}// Procesa sin cargar todo en memoria
const stream = await repository.createQueryBuilder("entity").stream();
stream.on("data", (row) => {
// Procesar fila por fila
});// Cargar datos por páginas
const [items, total] = await repository.findAndCount({
skip: page * pageSize,
take: pageSize,
});@Entity()
class Order {
@Column({ default: 0 })
itemCount!: number; // Evita COUNT() queries
@Column({ type: "decimal" })
total!: number; // Evita SUM() queries
}@Entity()
class OrderItem {
@Column()
productName!: string; // Duplicado para evitar JOINs en reportes
@Column()
productSku!: string; // Histórico al momento de la compra
}{
type: "postgres",
poolSize: 20, // Conexiones concurrentes
acquireTimeout: 60000, // Timeout para obtener conexión
timeout: 60000, // Query timeout
}// Cache a nivel de aplicación
const cachedResult = await repository.find({
cache: {
id: "products_active",
milliseconds: 300000, // 5 minutos
},
});@OneToMany(() => Order, order => order.user, {
lazy: true // Evita cargas innecesarias
})
orders!: Promise<Order[]>;@Column()
userId!: number; // FK explícita
@ManyToOne(() => User)
@JoinColumn({ name: "userId" })
user!: Promise<User>;// Cualquier columna en WHERE debe tener índice
@Index("idx_status", ["status"])
@Index("idx_created_at", ["createdAt"])// ❌ MAL
for (const order of orders) {
const items = await order.orderItems; // N queries
}
// ✅ BIEN
const ordersWithItems = await orderRepo.find({ relations: ["orderItems"] });El ejemplo demuestra mejoras significativas:
| Técnica | Antes | Después | Mejora |
|---|---|---|---|
| N+1 Problem | 1 + N queries | 1 query | ~90% |
| Batch Operations | N inserts | 1 insert | ~85% |
| Specific Selects | Full table scan | Index scan | ~60% |
| Memory Streaming | Full load | Streaming | ~95% memoria |
- Eager Loading: Datos que siempre necesitas
- Lazy Loading: Datos que raramente necesitas
- Batch Operations: Múltiples inserts/updates
- Índices: Columnas de WHERE, ORDER BY, JOIN
- Streaming: Datasets > 10,000 registros
- Caching: Datos que cambian poco
logging: ["query", "schema", "error", "warn", "info", "log"];// Medir tiempo de ejecución
console.time("query");
await repository.find();
console.timeEnd("query");EXPLAIN ANALYZE SELECT * FROM products WHERE category_id = 1;- Queries > 100ms - Revisar índices
- N+1 Patterns - Usar eager loading o joins
- Full Table Scans - Agregar índices
- High Memory Usage - Implementar streaming
- Connection Pool Exhaustion - Optimizar connection management
- TypeORM Performance Tips
- Database Indexing Best Practices
- SQL Performance Explained
- Node.js Performance Monitoring
- Los índices son cruciales para consultas rápidas
- El problema N+1 es muy común - siempre estar alerta
- Las operaciones en lote pueden mejorar performance dramáticamente
- El monitoring es esencial para detectar problemas
- La optimización es un proceso iterativo - medir, optimizar, repetir
¡La performance no es un accidente, es una decisión de diseño! 🚀