> Every UQL docs page, as Markdown: https://uql-orm.dev/llms.txt
> The same docs over MCP: https://uql-orm.dev/mcp
> Before writing UQL code, read the skill: https://uql-orm.dev/.well-known/agent-skills/uql-orm/SKILL.md

# ORM Benchmark

> What each TypeScript ORM costs per request against a real PostgreSQL: Drizzle vs MikroORM vs Prisma vs Sequelize vs TypeORM vs UQL.

Source: https://uql-orm.dev/benchmark

Our [open-source benchmark](https://github.com/rogerpadilla/ts-orm-benchmark) puts each ORM through a full PostgreSQL lifecycle: insert, read, update, read, nested read, delete, read. Here is the latest run.

> PostgreSQL 18.6 (Homebrew), Bun 1.4.2, Apple M4 Max, September 2026. Median µs per operation over 250 rounds, after 125 warmup rounds, interleaved and rotated. Every median is ±2.4% or tighter at 95% confidence.

*Versions: [Drizzle](https://orm.drizzle.team) 0.45.2 · [MikroORM](https://mikro-orm.io) 7.2.0 · [Prisma](https://www.prisma.io) 7.10.0 · [Sequelize](https://sequelize.org) 6.37.8 · [TypeORM](https://typeorm.io) 1.1.1 · [UQL](https://uql-orm.dev) 0.68.1.*

## Results

**Adds** is what the ORM itself costs, everything on top of what the driver would have spent anyway. The two `ref` rows are the floors: hand-written SQL with the rows mapped by hand, which is what an ORM has to earn its keep against. Bun SQL entries are measured against `bun sql` and the rest against `raw pg`, so a fast driver is never counted as the ORM’s doing.

| # | Entry | Adds µs | Total µs |
| - | - | - | - |
| ref | *bun sql* | floor | 1138 |
| ref | *raw pg* | floor | 1181 |
| 1 | **UQL (bunSql)** | +247 | 1385 |
| 1 | **UQL** | +275 | 1456 |
| 3 | Drizzle (bunSql) | +385 | 1523 |
| 3 | Drizzle | +419 | 1600 |
| 5 | TypeORM | +544 | 1725 |
| 6 | Sequelize | +875 | 2056 |
| 6 | Prisma | +929 | 2110 |
| 8 | MikroORM | +1571 | 2752 |

Entries share a place when their confidence intervals overlap: an equal number means a difference this run cannot resolve, not a tie broken in someone’s favour.

Totals only span 2.0x, because every entry pays the same database cost. What the ORM itself adds spans 6x: 247µs for UQL (bunSql), 1571µs for MikroORM.

Each entry is measured against its own driver’s floor, so a faster driver is never counted as the ORM’s win. Running the same UQL code on Bun SQL instead of `pg` saves 71µs, but only 28µs of that is UQL: the other 43µs is the gap between the two floors, free to anything on that driver.

### Per step

The three steps where how much data is bound and hydrated decides the number. The [interactive charts](https://rogerpadilla.github.io/ts-orm-benchmark/chart.html) break down every step.

| Entry (µs) | insert | read | nested | Total, 7 steps |
| - | - | - | - | - |
| *[bun sql](https://bun.sh/docs/api/sql)* | 357 | 181 | 194 | 1138 |
| *[raw pg](https://node-postgres.com)* | 346 | 206 | 219 | 1181 |
| [UQL (bunSql)](https://uql-orm.dev) | **376** | **224** | **318** | **1385** |
| [UQL](https://uql-orm.dev) | 380 | 259 | 352 | 1456 |
| [Drizzle (bunSql)](https://orm.drizzle.team) | 427 | 241 | 362 | 1523 |
| [Drizzle](https://orm.drizzle.team) | 433 | 267 | 408 | 1600 |
| [TypeORM](https://typeorm.io) | 485 | 340 | 344 | 1725 |
| [Sequelize](https://sequelize.org) | 485 | 396 | 546 | 2056 |
| [Prisma](https://www.prisma.io) | 892 | 287 | 396 | 2110 |
| [MikroORM](https://mikro-orm.io) | 463 | 744 | 882 | 2752 |

Columns: insert is INSERT 10 rows, returning ids; read is SELECT with WHERE, SORT, LIMIT 200; nested is SELECT 50 parents with their children. The biggest gap is Prisma’s insert: 892µs against 376-485µs for everyone else. The other 4 steps are asserted every round but not published: they are round trips with almost nothing in them, worth 465-663µs of each total and separating the field by at most 110µs.

### Three runtimes

The same lifecycle runs on Bun, Node and Deno, from one bundle built by Bun so the runtime is the only variable. The Bun SQL rows sit out here, since that client is a Bun API.

> Bun 1.4.2, Node 24.20.0, Deno 2.9.6, all running the same bundled JavaScript, one at a time against the same database. PostgreSQL 18.6 (Homebrew), Apple M4 Max, September 2026. µs for a whole lifecycle, nearest-rank percentiles over 2000 rounds after 250 warmup, so a p99 is drawn from the 21 slowest rounds.

| Entry (µs) | Bun p50 | Bun p99 | Node p50 | Node p99 | Deno p50 | Deno p99 |
| - | - | - | - | - | - | - |
| [raw pg](https://node-postgres.com) | **1183** | 3940 | 1250 | **3765** | 1276 | 3860 |
| [UQL](https://uql-orm.dev) | **1486** | 5178 | 1553 | **3766** | 1540 | 4588 |
| [Drizzle](https://orm.drizzle.team) | **1612** | 6217 | 1804 | 4803 | 1808 | **4771** |
| [TypeORM](https://typeorm.io) | **1661** | 6180 | 1853 | **4643** | 1750 | 4772 |
| [Sequelize](https://sequelize.org) | **1966** | 7426 | 2202 | 5763 | 2214 | **5457** |
| [Prisma](https://www.prisma.io) | **2073** | 8038 | 2325 | **6224** | 2488 | 6487 |
| [MikroORM](https://mikro-orm.io) | **2712** | 11011 | 3785 | 9103 | 3787 | **8483** |

On `raw pg`, the same code on all of them, the runtimes are 93µs apart at p50 but 175µs apart at p99: Bun leads the median, Node the tail, and each p99 is 233% on Bun, 201% on Node, 203% on Deno above its own p50. Switching runtime moves any single entry by at most 1075µs at p50 (MikroORM), where switching ORM on one runtime moves it 1191-2223µs. Both are differences of measured medians, known to ±38µs and ±39µs, so read them as ranges rather than as a ranking. The one pair that changes places between runtimes is Drizzle and TypeORM, 41µs apart.

### Memory

The same lifecycle weighed for what it allocates, on Node, one process per entry. **Adds KB** is the heap above the hand-written floor, per request.

> PostgreSQL 18.6 (Homebrew), Node 24.20.0, Apple M4 Max, September 2026. Median KB allocated per step over 60 rounds after 60 warmup of a 7-step lifecycle. Rounds a garbage collection ran in are discarded, never corrected, and no entry lost more than 1% of its own (MikroORM).

| Entry | insert | read | nested | Total KB | Adds KB |
| - | - | - | - | - | - |
| *[raw pg](https://node-postgres.com)* | 14 | 87 | 106 | 245 | floor |
| [UQL](https://uql-orm.dev) | 44 | 105 | 82 | 320 | **+75** |
| [Drizzle](https://orm.drizzle.team) | 134 | 248 | 238 | 751 | +506 |
| [Prisma](https://www.prisma.io) | 273 | 220 | 374 | 1055 | +810 |
| [TypeORM](https://typeorm.io) | 126 | 295 | 503 | 1066 | +821 |
| [Sequelize](https://sequelize.org) | 100 | 425 | 592 | 1295 | +1050 |
| [MikroORM](https://mikro-orm.io) | 77 | 1470 | 2071 | 3877 | +3632 |

Above the floor the field spans 48.4x: 75KB for UQL, 3632KB for MikroORM, and nested opens it widest: MikroORM’s 2071KB against UQL’s 82KB.

Almost none of it survives: another 60 lifecycles, collected either side, leave at most 31KB behind (Sequelize), identity maps included. What the table prices is collector pressure, not a resident set that grows.

## Type safety

The same benchmark writes ordinary mistakes in each tool’s own API (a misspelled column in a projection, a filter on a column that is not there, a text operator on a number, a sum over a text column, a read of a column the projection left out) and reports which ones the compiler refuses. Each file is compiled twice, once as written and once with every mistake corrected, so a green mark means the mistake errored *and* the correction was clean.

> Checked with TypeScript 7.0.2, 11 probes per entry.

| Mistake | [Drizzle](https://orm.drizzle.team) | [MikroORM](https://mikro-orm.io) | [Prisma](https://www.prisma.io) | [Sequelize](https://sequelize.org) | [TypeORM](https://typeorm.io) | [UQL](https://uql-orm.dev) |
| - | - | - | - | - | - | - |
| Misspelled column in the projection | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ |
| Misspelled column in the filter | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| String value against a numeric column | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
| Text operator against a numeric column | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
| Misspelled column in the sort | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
| Sum over a text column | ❌ | ❌ | ✅ | ❌ | ✅ | ✅ |
| Misspelled column inside a loaded relation | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
| Misspelled column in inserted data | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Number written into a text column | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Reading a column the projection left out | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
| Reading a misspelled column off a loaded relation | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Caught**, of 11 | 9 | 9 | 10 | 5 | 10 | **11** |

UQL catches 11 of the 11, Sequelize 5. Every mistake here is caught by at least one entry. The corrected copy of every file compiles clean, which is what makes a red mark a missing check rather than a broken query.

The columns are alphabetical, not ranked: a handful of probes cannot separate these tools the way a microsecond can, and ties are common. The queries behind each mark are [in the benchmark](https://github.com/rogerpadilla/ts-orm-benchmark/tree/main/type-safety).

## Why UQL is fast

- Schema metadata is worked out once at startup, so nothing is looked up while a query runs.
- SQL is written straight into a string buffer, with no intermediate builder objects.

## Method

- Everyone defines the same `Company` and `User`, runs the same seven steps through its own idiomatic API, and gets one connection with no pooling. Entries are interleaved and rotated, so none of them keeps a favourable position.
- Every step asserts on the rows it returns, so a step that silently does nothing fails instead of scoring well.
- Medians, never means, so one GC pause cannot dominate a number, and each carries the 95% confidence interval the caption above states. Places are assigned over those intervals, so entries the run cannot separate share one instead of being ordered by noise.
- The runtime figures come from one Bun-built bundle run on each runtime in turn, so no runtime is charged for its own TypeScript loader. The memory figures come from a process per entry, since a shared heap cannot be attributed.
- The type-safety probes are compiled with TypeScript 7.0.2 at `strict`, against the same entity definitions the timed lifecycle queries: one definition per ORM, scored and measured, so neither half is judged on a model the other never used.

```bash
git clone https://github.com/rogerpadilla/ts-orm-benchmark.git
cd ts-orm-benchmark
bun install

DATABASE_URL=postgres:///postgres bun run bench
DATABASE_URL=postgres:///postgres bun run bench.runtimes
DATABASE_URL=postgres:///postgres bun run bench.memory
bun run bench.types
```

---

Speed is only one axis. For how the APIs and features line up, see the [comparison](https://uql-orm.dev/comparison.md).

[Comparison](https://uql-orm.dev/comparison.md)Drizzle vs MikroORM vs Prisma vs TypeORM vs UQL, with actual code for every common operation.

---

## Try it

```sh
npm install uql-orm
```

Entities are plain classes and queries are plain JSON: nothing to generate, and no schema file to keep in sync.

[Quick Start](https://uql-orm.dev/getting-started.md) [GitHub](https://github.com/rogerpadilla/uql)

---
