Rendimiento y optimización en TypeORM
Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
1. Introducción a la optimización de rendimiento
-
En aplicaciones que usan ORM como TypeORM, la optimización de rendimiento es crucial para garantizar que el sistema funcione sin problemas, minimice la latencia y utilice los recursos de manera eficiente.
-
Los desafíos comunes al usar ORM incluyen la recuperación innecesaria de datos, problemas de consultas N+1 y no aprovechar herramientas de optimización como indexación o caché.
-
Los principales objetivos de la optimización incluyen:
- Reducir el número de consultas SQL enviadas a la base de datos.
- Optimizar consultas complejas para que se ejecuten más rápido.
- Usar caché e indexación para acelerar la recuperación de datos.
- Garantizar una recuperación eficiente de datos usando métodos de carga apropiados (Carga diferida vs. Carga ansiosa).
2. Uso eficiente del Query Builder
2.1. Evitar el problema de consultas N+1
-
El Problema de Consultas N+1 ocurre cuando el sistema ejecuta demasiadas subconsultas por cada fila de datos recuperada.
-
Para evitarlo, puedes usar
leftJoinAndSelectoinnerJoinAndSelectpara combinar tablas en una sola consulta en lugar de ejecutar múltiples consultas.
const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.leftJoinAndSelect("user.posts", "post")
.getMany()
- Aquí,
leftJoinAndSelectayuda a recuperar todas las publicaciones del usuario en una sola consulta en lugar de muchas consultas pequeñas.
2.2. Usar getRawMany() cuando solo se necesitan datos sin procesar
- En casos donde no se requieren objetos completos, puedes usar
getRawMany()para obtener datos sin procesar y evitar que TypeORM procese demasiada información.
const rawPosts = await dataSource
.getRepository(Post)
.createQueryBuilder("post")
.select("post.title, post.createdAt")
.getRawMany()
2.3. Limitar campos usando select
- Para optimizar el uso de memoria y reducir datos innecesarios, selecciona solo los campos requeridos usando
select.
const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.select(["user.name", "user.email"])
.getMany()
3. Uso de índices
- Los índices aceleran el rendimiento de las consultas en la base de datos al reducir la cantidad de datos escaneados. TypeORM admite crear índices en columnas de tablas usando el decorador
@Index.
3.1. Creación de un índice
- Los índices pueden crearse directamente en las entidades usando el decorador
@Index.
import { Entity, Column, Index } from "typeorm"
@Entity()
@Index(["firstName", "lastName"]) // Composite index
export class User {
@Column()
firstName: string
@Column()
lastName: string
}
3.2. Índice único
- Puedes crear índices únicos para garantizar que no haya valores duplicados en una columna.
@Index(["email"], { unique: true })
4. Carga diferida y carga ansiosa
TypeORM proporciona dos métodos principales para cargar relaciones de datos: Carga diferida (Lazy Loading) y Carga ansiosa (Eager Loading). Cada uno tiene un impacto diferente en el rendimiento de tu aplicación.
4.1. Carga diferida
- La carga diferida (Lazy loading) carga los datos de relación solo cuando se necesitan, reduciendo la carga en la base de datos cuando no siempre se requieren todos los datos relacionados.
@Entity()
export class User {
@OneToMany(() => Post, (post) => post.user, { lazy: true })
posts: Promise<Post[]>
}
- Cuando necesites recuperar los datos, simplemente llama a:
const user = await userRepository.findOne(userId)
const posts = await user.posts
-
Ventajas:
- Eficiencia de recursos: Solo carga los datos necesarios cuando realmente se requieren, reduciendo costos de consulta y uso de memoria.
- Ideal para uso selectivo de datos: Adecuado para escenarios donde no se necesitan todos los datos relacionados.
-
Desventajas:
- Mayor complejidad de consultas: Cada acceso a datos relacionados desencadena una consulta adicional a la base de datos, lo que puede aumentar la latencia si no se gestiona adecuadamente.
- Difícil de rastrear: Puede llevar al problema de consultas n+1 si se usa sin cuidado.
4.2. Carga ansiosa
- La carga ansiosa (Eager loading) recupera automáticamente todos los datos relacionados cuando se ejecuta la consulta principal. Esto puede ser conveniente pero puede causar problemas de rendimiento si hay demasiadas relaciones complejas.
@Entity()
export class User {
@OneToMany(() => Post, (post) => post.user, { eager: true })
posts: Post[]
}
-
En este caso, las publicaciones se cargarán tan pronto como se recuperen los datos del usuario.
-
Ventajas:
- Carga automáticamente los datos relacionados, facilitando el acceso a las relaciones sin consultas adicionales.
- Evita el problema de consultas n+1: Como todos los datos se obtienen en una sola consulta, no hay riesgo de generar múltiples consultas innecesarias.
-
Desventajas:
- Recuperar todos los datos relacionados a la vez puede resultar en consultas grandes, incluso si no se necesitan todos los datos.
- No es adecuado para escenarios donde solo se requiere un subconjunto de datos relacionados, ya que puede llevar a un uso ineficiente de los datos.
-
Para explorar más detalles y ejemplos sobre cómo configurar y usar relaciones diferidas y ansiosas, visita la documentación oficial de TypeORM: Relaciones ansiosas y diferidas
5. Optimización avanzada
5.1. Uso de sugerencias de consulta (Query Hints)
-
Las Sugerencias de Consulta (Query Hints) son instrucciones enviadas junto con las consultas SQL, que ayudan a la base de datos a decidir estrategias de ejecución más eficientes.
-
Diferentes sistemas RDBMS admiten distintos tipos de sugerencias, como recomendar el uso de índices o elegir el tipo de JOIN apropiado.
await dataSource.query(`
SELECT /*+ MAX_EXECUTION_TIME(1000) */ *
FROM user
WHERE email = 'example@example.com'
`)
- En el ejemplo anterior,
MAX_EXECUTION_TIME(1000)indica a MySQL que detenga la consulta si tarda más de 1 segundo.
5.2. Paginación
-
La paginación es una técnica crucial para mejorar el rendimiento al recuperar grandes cantidades de datos. En lugar de obtener todos los datos a la vez, la paginación divide los datos en páginas más pequeñas, reduciendo la carga en la base de datos y optimizando el uso de memoria.
-
En TypeORM, puedes usar
limityoffsetpara paginación.
const users = await userRepository
.createQueryBuilder("user")
.limit(10) // Number of records to fetch per page
.offset(20) // Skip the first 20 records
.getMany()
- La paginación evita recuperar grandes volúmenes de datos simultáneamente, minimizando la latencia y optimizando el uso de memoria. Al implementarla, considera usar cursores de paginación para manejar datos dinámicos con mayor eficiencia.
5.3. Almacenamiento en caché
-
El almacenamiento en caché es la técnica de guardar temporalmente resultados de consultas o datos para reutilizarlos en solicitudes futuras sin acceder a la base de datos cada vez.
-
TypeORM incluye soporte integrado para caché, permitiendo personalizar su implementación.
const users = await userRepository
.createQueryBuilder("user")
.cache(true) // Enable caching
.getMany()
- Adicionalmente, puedes configurar la duración del caché o utilizar herramientas externas como Redis para mejorar la eficiencia.
const dataSource = new DataSource({
type: "mysql",
host: "localhost",
port: 3306,
username: "test",
password: "test",
database: "test",
cache: {
type: "redis",
options: {
host: "localhost",
port: 6379
}
}
});