analysis

Performance

Nine performance anti-patterns detected across seven ORMs with volume-aware severity scaling.

Performance Patterns

The performance detector identifies 9 data-access anti-patterns that cause slow queries, excessive memory usage, or database overload. Detection works across seven ORM/query libraries, and severity scales automatically based on declared data volumes.

Supported ORMs

ORMEntity DetectionQuery Pattern Matching
Prismaprisma.user.findMany() chainfindMany, findFirst, count, $queryRaw, $transaction
TypeORMthis.userRepo.find(), getRepository(User)find, findOne, createQueryBuilder, query
SequelizeUser.findAll(), this.userModel.findAll()findAll, findOne, count, findAndCountAll
MongooseUserModel.find(), this.userModel.find()find, findOne, countDocuments, aggregate
Drizzledb.select().from(users)select, insert, update, delete
Knexknex('users').select()Table-name based detection
MikroORMem.find(User), this.userRepo.find()find, findOne, count, nativeDelete

Volume-Aware Severity

Severity is not fixed -- it scales based on the data volume you declare for each entity in radar.yml:

data_volumes:
  users: M          #   10K - 100K rows
  orders: L         #  100K - 1M rows
  audit_events: XL  #    1M - 50M rows
  logs: XXL         #   50M+ rows
  settings: S       #    0  - 10K rows

The same pattern gets different severities based on volume:

VolumeMax RowsSeverityGate Action
S10,000info (suppressed)none
M100,000warningwarn
L1,000,000criticalwarn
XL50,000,000criticalblock
XXLInfinitycriticalblock

Important: Entities without a declared volume default to S (suppressed). You must declare volumes for tables you care about -- this prevents noisy false positives on small lookup tables.

Entity Name Resolution

The detector uses fuzzy matching to resolve entity names to volume declarations. It handles:

  • Case normalization: User matches users
  • Pluralization: Order matches orders, Country matches countries
  • Snake case: AuditEvent matches audit_events
  • ORM suffixes: OrderEntity and OrderModel are stripped to Order before matching

Pattern Reference

1. unbounded-find-many

A findMany/find/findAll call without a take/limit option. On large tables, this loads the entire dataset into memory.

Prisma -- Violation:

async getAllOrders(): Promise<Order[]> {
  return this.prisma.order.findMany({
    where: { status: 'active' },
    // Missing: take/limit
  });
}

Prisma -- Fix:

async getAllOrders(page = 1, pageSize = 50): Promise<Order[]> {
  return this.prisma.order.findMany({
    where: { status: 'active' },
    take: pageSize,
    skip: (page - 1) * pageSize,
    orderBy: { createdAt: 'desc' },
  });
}

TypeORM -- Violation:

async getAllOrders(): Promise<Order[]> {
  return this.orderRepo.find({
    where: { status: 'active' },
    // Missing: take
  });
}

TypeORM -- Fix:

async getAllOrders(page = 1, pageSize = 50): Promise<Order[]> {
  return this.orderRepo.find({
    where: { status: 'active' },
    take: pageSize,
    skip: (page - 1) * pageSize,
    order: { createdAt: 'DESC' },
  });
}

Sequelize -- Violation:

const orders = await Order.findAll({
  where: { status: 'active' },
});

Sequelize -- Fix:

const orders = await Order.findAll({
  where: { status: 'active' },
  limit: 50,
  offset: (page - 1) * 50,
});

Mongoose -- Violation:

const orders = await this.orderModel.find({ status: 'active' });

Mongoose -- Fix:

const orders = await this.orderModel
  .find({ status: 'active' })
  .limit(50)
  .skip((page - 1) * 50)
  .sort({ createdAt: -1 });

2. find-many-no-where

A findMany/find/findAll with no where clause at all -- returning the entire table.

Violation:

// Prisma: loads every row in the table
const users = await this.prisma.user.findMany();

// TypeORM: loads every row
const users = await this.userRepo.find();

// Mongoose: loads every document
const users = await this.userModel.find();

Fix:

// Always filter or paginate
const users = await this.prisma.user.findMany({
  where: { isActive: true },
  take: 100,
});

3. nested-include-large-relation

A nested include/relations/populate on a relation that references a large table. This generates joins or subqueries that multiply result set size.

Prisma -- Violation:

const user = await this.prisma.user.findUnique({
  where: { id },
  include: {
    orders: {                    // orders table is XL
      include: {
        items: true,             // Deep nesting multiplies rows
        payments: true,
      },
    },
  },
});

Prisma -- Fix:

// Separate queries with explicit limits
const user = await this.prisma.user.findUnique({ where: { id } });
const recentOrders = await this.prisma.order.findMany({
  where: { userId: id },
  take: 20,
  orderBy: { createdAt: 'desc' },
  include: { items: true },
});

Mongoose -- Violation:

const user = await this.userModel.findById(id).populate({
  path: 'orders',
  populate: { path: 'items' },
});

Mongoose -- Fix:

const user = await this.userModel.findById(id);
const orders = await this.orderModel
  .find({ userId: id })
  .limit(20)
  .sort({ createdAt: -1 })
  .populate('items');

4. n-plus-one-query

A database query executed inside a loop. For N parent records, this generates N+1 queries (1 parent query + N child queries).

Violation:

async getOrdersWithCustomers() {
  const orders = await this.prisma.order.findMany({ take: 100 });

  for (const order of orders) {
    // N+1: one query per order inside the loop
    order.customer = await this.prisma.user.findUnique({
      where: { id: order.customerId },
    });
  }

  return orders;
}

Fix:

async getOrdersWithCustomers() {
  // Single query with join
  return this.prisma.order.findMany({
    take: 100,
    include: { customer: true },
  });
}

// Or batch manually:
async getOrdersWithCustomers() {
  const orders = await this.prisma.order.findMany({ take: 100 });
  const customerIds = [...new Set(orders.map(o => o.customerId))];
  const customers = await this.prisma.user.findMany({
    where: { id: { in: customerIds } },
  });
  const customerMap = new Map(customers.map(c => [c.id, c]));

  return orders.map(o => ({
    ...o,
    customer: customerMap.get(o.customerId),
  }));
}

TypeORM -- Violation:

const orders = await this.orderRepo.find();
for (const order of orders) {
  order.customer = await this.userRepo.findOne({
    where: { id: order.customerId },
  });
}

TypeORM -- Fix:

const orders = await this.orderRepo.find({
  relations: ['customer'],
  take: 100,
});

The detector also catches Promise.all(items.map(...)) patterns that batch N queries in parallel -- while faster than sequential N+1, it still creates N database connections:

// Also detected: parallel N+1
const enriched = await Promise.all(
  orders.map(o => this.prisma.user.findUnique({ where: { id: o.userId } }))
);

5. fetch-all-filter-in-memory

Fetching all records from the database and then filtering with .filter() in JavaScript. The filtering should happen in the database query.

Violation:

async getActiveOrders() {
  const allOrders = await this.prisma.order.findMany();
  return allOrders.filter(o => o.status === 'active');  // Filter in JS, not DB
}

Fix:

async getActiveOrders() {
  return this.prisma.order.findMany({
    where: { status: 'active' },
    take: 100,
  });
}

6. missing-pagination-endpoint

A controller endpoint method that returns a findMany/find result without pagination parameters (take/skip, limit/offset, cursor).

Violation:

@Controller('users')
export class UserController {
  @Get()
  async findAll() {
    return this.userService.findAll();  // No pagination params accepted
  }
}

Fix:

@Controller('users')
export class UserController {
  @Get()
  async findAll(
    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
    @Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit: number,
  ) {
    return this.userService.findPaginated(page, Math.min(limit, 100));
  }
}

7. unfiltered-count-large-table

A count() or countDocuments() call without a where clause on a large table. Unfiltered counts on tables with millions of rows are slow because the database must scan the entire table.

Violation:

// audit_events declared as XL (1M-50M rows)
const total = await this.prisma.auditEvent.count();

Fix:

// Filter the count to a relevant subset
const total = await this.prisma.auditEvent.count({
  where: {
    createdAt: { gte: thirtyDaysAgo },
    userId: currentUserId,
  },
});

// Or use an approximate count for display purposes
const approx = await this.prisma.$queryRaw`
  SELECT reltuples AS estimate
  FROM pg_class WHERE relname = 'audit_events'
`;

8. raw-sql-no-limit

A raw SQL query (via $queryRaw, query(), raw(), or template literals) that contains a SELECT but no LIMIT clause.

Violation:

const results = await this.prisma.$queryRaw`
  SELECT * FROM orders WHERE status = 'active'
`;

Fix:

const results = await this.prisma.$queryRaw`
  SELECT * FROM orders WHERE status = 'active' LIMIT 100
`;

9. Recursive N+1

A recursive or self-referencing query pattern where a function calls itself or a sibling function in a loop to traverse a tree structure, generating one query per node.

Violation:

async getFullCategory(id: string): Promise<Category> {
  const category = await this.prisma.category.findUnique({ where: { id } });
  if (category.parentId) {
    category.parent = await this.getFullCategory(category.parentId);  // Recursive N+1
  }
  return category;
}

Fix:

async getFullCategory(id: string): Promise<Category> {
  // Use a recursive CTE for tree traversal in a single query
  const categories = await this.prisma.$queryRaw`
    WITH RECURSIVE tree AS (
      SELECT * FROM categories WHERE id = ${id}
      UNION ALL
      SELECT c.* FROM categories c
      JOIN tree t ON c.id = t.parent_id
    )
    SELECT * FROM tree
  `;
  return buildTree(categories);
}

Volume-Aware Severity in Practice

Here is how the same unbounded query gets different severities:

// settings table: S volume (< 10K rows)
await this.prisma.setting.findMany();
// Result: info severity, suppressed (no violation reported)

// users table: M volume (10K - 100K rows)
await this.prisma.user.findMany();
// Result: warning severity, shows in PR comment

// orders table: L volume (100K - 1M rows)
await this.prisma.order.findMany();
// Result: critical severity, shows in PR comment with warning badge

// audit_events table: XL volume (1M - 50M rows)
await this.prisma.auditEvent.findMany();
// Result: critical severity, BLOCKS MERGE

// logs table: XXL volume (50M+ rows)
await this.prisma.log.findMany();
// Result: critical severity, BLOCKS MERGE

Configuration

Performance rules are configured in radar.yml:

data_volumes:
  users: M
  orders: L
  order_items: L
  audit_events: XL
  logs: XXL
  settings: S
  countries: S

Performance scoring weights:

scoring:
  performance_risk_critical: 8
  performance_risk_warning: 3

Exceptions

exceptions:
  - rule: unbounded-find-many
    file: "src/admin/**"
    expires: "2026-06-01"
    reason: "Admin panel has internal-only access with small result sets"

  - rule: n-plus-one-query
    file: "src/seed/**"
    expires: "2026-12-31"
    reason: "Database seeder runs once, performance is not critical"
Technical Debt Radar Documentation