NestJS Profiler
Tutorials

MikroORM query profiling

Capture every MikroORM SQL query and inspect duration and slow queries in the profiler's Database panel.

This tutorial shows how to add the MikroORM collector to profile SQL queries in a NestJS application that uses PostgreSQL via @mikro-orm/nestjs.

Prerequisites

  • @eleven-labs/nest-profiler installed and configured
  • @mikro-orm/core, @mikro-orm/nestjs and a driver (e.g. @mikro-orm/postgresql) installed with a working MikroOrmModule.forRoot()

Step 1 - Install the package

pnpm add @eleven-labs/nest-profiler-mikro-orm@alpha

Step 2 - Register the collector

Add MikroOrmCollectorModule after MikroOrmModule in your root module:

app.module.ts
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { ConditionalModule } from '@nestjs/config';
import { PostgreSqlDriver } from '@mikro-orm/postgresql';
import { ProfilerModule } from '@eleven-labs/nest-profiler';
import { MikroOrmCollectorModule } from '@eleven-labs/nest-profiler-mikro-orm';

const isProfilerEnabled = (env: NodeJS.ProcessEnv) => env['PROFILER_ENABLED'] === 'true';

@Module({
  imports: [
    MikroOrmModule.forRoot({ driver: PostgreSqlDriver /* ... */ }),
    ConditionalModule.registerWhen(ProfilerModule.forRoot({ isGlobal: true }), isProfilerEnabled),
    ConditionalModule.registerWhen(
      MikroOrmCollectorModule.forRoot({ slowThreshold: 100 }), // queries > 100ms highlighted
      isProfilerEnabled,
    ),
  ],
})
export class AppModule {}

No other configuration is needed - the collector wraps the MikroORM logger automatically and requires no debug flag.

Step 3 - Instrument your services with spans

Use startSpan() to add meaningful labels to the Timeline panel alongside your MikroORM calls:

import { ProfilerService } from '@eleven-labs/nest-profiler';
import { EntityManager } from '@mikro-orm/core';

@Injectable()
export class ProductsService {
  constructor(
    private readonly em: EntityManager,
    private readonly profiler: ProfilerService,
  ) {}

  async findAll(): Promise<Product[]> {
    const stop = this.profiler.startSpan('db.products.findAll');
    const result = await this.em.fork().find(Product, {}, { orderBy: { createdAt: 'DESC' } });
    stop();
    return result;
  }
}

Step 4 - Test it

Start your application and make a request that triggers a database query:

curl -i http://localhost:3000/api/v1/products

Copy the X-Debug-Token from the response headers, open /_profiler/{token}, and click the Database tab.

You will see:

  • Each SQL query with its type badge (SELECT, INSERT, …)
  • Duration per query (MikroORM's measured took) with a bar chart indicator
  • Slow queries and N+1 patterns flagged as performance tags
  • Bound parameters

The Timeline panel shows the db.products.findAll span alongside other phases.

Driving options from ConfigService

Use forRootAsync() to resolve the options from ConfigService (or any provider), gated per environment with ConditionalModule — the recommended way (for the enabled flag, see Enabling and disabling the profiler):

import { ConditionalModule, ConfigService } from '@nestjs/config';

ConditionalModule.registerWhen(
  MikroOrmCollectorModule.forRootAsync({
    inject: [ConfigService],
    useFactory: (config: ConfigService) => ({
      slowThreshold: config.get<number>('PROFILER_SLOW_QUERY_MS') ?? 50,
    }),
  }),
  isProfilerEnabled,
);

How it works

At module initialization the collector wraps MikroORM's Logger.logQuery to record every executed query into the active request profile. Streaming reads (QueryBuilder.stream()) are captured too and flagged as streaming, but with duration: 0 — a documented limitation. See How it works on the package page for the full mechanism.

Sharing the SQL panel

The TypeORM and MikroORM collectors share the same Database panel rendering and QueryEntry shape (provided by the core AbstractSqlQueryCollector), so the UI is identical whichever SQL ORM you use.

Explain slow queries

Each query in the Database panel has an Explain button. Click it and the profiler runs EXPLAIN for that one query over the MikroORM connection and renders the execution plan inline — the top plan node, a ⚠ warning on a full-table (sequential) scan, the scanned relations, estimated rows/cost, and the raw plan. Supported dialects: PostgreSQL, MySQL/MariaDB and SQLite.

It runs on demand only — nothing happens until you click, so the profiled request is never slowed. EXPLAIN alone does not execute the statement; the opt-in analyze variant (EXPLAIN ANALYZE) does run the query and is therefore restricted to SELECT.

app.module.ts
MikroOrmCollectorModule.forRoot({
  explain: { enabled: true }, // default; set `enabled: false` to hide the button
  // explain: { analyze: true }, // dev only — runs the query to measure real timings (SELECT only)
});

Runs against your database

The captured SQL and its parameters are replayed on the live connection when you click, so the action sits behind the profiler's security. Values that looked like credentials were redacted at capture, so a plan for such a query may differ slightly from production.

Inspect the schema

Alongside the per-request Database panel, the package ships MikroOrmSchemaCollectorModule — a global collector that adds a Schema · MikroORM panel to the profiler home page. It introspects the ORM metadata once at startup and lists every registered entity with its columns, relations and indexes, so you can recall an entity's shape without leaving the profiler.

Register it next to the query collector, gated the same way:

app.module.ts
import { MikroOrmSchemaCollectorModule } from '@eleven-labs/nest-profiler-mikro-orm';

ConditionalModule.registerWhen(MikroOrmSchemaCollectorModule.forRoot(), isProfilerEnabled),

Open /_profiler and expand the Schema · MikroORM panel:

Schema panel — MikroORM entities with their columns, types, primary keys and defaults

The panel is introspection-only (it never touches data), honours a connectionName option for named contexts, redacts secrets embedded in column defaults, and simply does not appear when no MikroORM context is wired.

Powered & maintained by

On this page