Voltar para Artigos
Banco de Dados4 min de leitura

TypeORM QueryBuilder: Performance, Aggregations e Prevenção do N+1

Como usar o QueryBuilder para consultas avançadas: leftJoinAndSelect vs innerJoin, funções de agregação (SUM, COUNT) com group by, raw results e como estruturar subqueries sem perder a tipagem do TypeScript.

12 de agosto de 2026

O método repository.find() resolve 80% dos casos de uso de um CRUD. Mas, e quando precisamos de agregações ('Qual o faturamento mensal de cada vendedor ativo?') ou filtros dinâmicos complexos? Trazer todos os dados para a memória do Node.js e usar Array.reduce() é um suicídio de performance. O processamento pesado deve ocorrer onde os dados vivem: no banco de dados. É para isso que usamos o QueryBuilder do TypeORM.

Filtros Dinâmicos e Prevenção de SQL Injection

O QueryBuilder permite montar queries condicionalmente sem concatenar strings (o que causaria vulnerabilidade a SQL Injection). Sempre use os parâmetros com a sintaxe :parametro — o TypeORM passa os valores ao banco como prepared statements, tornando SQL Injection estruturalmente impossível. Filtros condicionais com andWhere são acumulados somente quando o valor existe, evitando a necessidade de if/else por toda a query.

TypeORMOrdersRepository.ts
import { DataSource, Repository } from 'typeorm';
import { Order } from '../entities/Order';

export class TypeORMOrdersRepository {
  private repo: Repository<Order>;

  constructor(dataSource: DataSource) {
    this.repo = dataSource.getRepository(Order);
  }

  async searchOrders(status?: string, minAmount?: number) {
    // 'order' é o alias principal (a raiz da query)
    const query = this.repo.createQueryBuilder('order');

    if (status) {
      // andWhere: acumula as condições (WHERE ... AND ...)
      query.andWhere('order.status = :status', { status });
    }

    if (minAmount) {
      query.andWhere('order.totalAmount >= :minAmount', { minAmount });
    }

    // getMany() retorna instâncias da entidade Order perfeitamente tipadas
    return query.orderBy('order.createdAt', 'DESC').getMany();
  }
}

O Problema do N+1 e Eager Loading

Fazer um find() em Usuários e, num for loop, fazer outro find() para buscar os Endereços de cada um, gera o temido problema do N+1 queries. Com 100 usuários, isso resulta em 101 queries — 1 para listar + 100 para buscar endereços. O QueryBuilder resolve isso com leftJoinAndSelect, gerando uma única query com LEFT JOIN que traz tudo de uma vez. A diferença de performance é geralmente de 10x a 100x em listas grandes.

typescript
// Executa apenas UMA query (LEFT JOIN) e mapeia os objetos aninhados
const users = await this.repo.createQueryBuilder('user')
  // user.addresses aponta para a relação na Entidade @OneToMany
  .leftJoinAndSelect('user.addresses', 'address')
  .leftJoinAndSelect('user.company', 'company')
  .where('user.active = :active', { active: true })
  .getMany();

// users[0].addresses[0].street estará perfeitamente preenchido

Agregações e Raw Results (Relatórios)

Quando usamos SUM() ou COUNT(), o resultado não mapeia perfeitamente para a Entidade padrão. Nesses casos de relatórios, usamos getRawMany(), que devolve os dados brutos como vieram do banco (ideal para dashboards).

Relatório de Vendas Mensal
async getMonthlyRevenue(year: number) {
  const results = await this.repo.createQueryBuilder('order')
    .select("EXTRACT(MONTH FROM order.createdAt)", "month")
    .addSelect("SUM(order.totalAmount)", "revenue")
    .addSelect("COUNT(order.id)", "salesCount")
    .where("EXTRACT(YEAR FROM order.createdAt) = :year", { year })
    .andWhere("order.status = 'PAID'")
    .groupBy("EXTRACT(MONTH FROM order.createdAt)")
    .orderBy("month", "ASC")
    .getRawMany(); // Retorna o JSON exato do SELECT

  // results: [{ month: 1, revenue: "15000.00", salesCount: "42" }, ...]
  return results.map(r => ({
    month: Number(r.month),
    revenue: Number(r.revenue),
    salesCount: Number(r.salesCount)
  }));
}

`innerJoin` vs `leftJoin`: Use innerJoinAndSelect apenas se a relação for OBRIGATÓRIA (ex: Pedido e Cliente). Se a relação puder ser nula e você usar innerJoin, o registro principal nem será retornado. Em dúvida, o leftJoinAndSelect é o mais seguro para não perder registros primários.

Conclusão

O QueryBuilder do TypeORM é a ponte entre a tipagem rigorosa do TypeScript e o poder bruto do SQL. Ele permite que lógicas pesadas (como somas, agrupamentos e paginações com junções) ocorram no banco de dados, protegendo a memória da sua aplicação Node.js e evitando gargalos de N+1 queries nas listagens complexas.