ORM Benchmarks

February 9, 2026 · View on GitHub

Comprehensive performance benchmarks comparing popular Python ORMs across PostgreSQL, MySQL, and SQLite.

Tested ORMs:

ORMTypePostgreSQLMySQLSQLite
Tortoise ORMasyncasyncpgasyncmyaiosqlite
Djangosyncpsycopg2mysqlclientsqlite3
peeweesyncpsycopg2pymysqlsqlite3
SQLAlchemy ORM (async)asyncasyncpgaiomysqlaiosqlite
SQLObjectsyncpsycopg2mysqlclientsqlite3
Piccoloasyncasyncpgaiosqlite
ormarasyncasyncpgaiomysqlaiosqlite
SQLModelasyncasyncpgaiomysqlaiosqlite

Environment: Python 3.14, macOS (Apple Silicon), 100 iterations per operation. Piccolo does not support MySQL.

Operations

CodeOperationDescription
AInsert: SingleInsert one row at a time
BInsert: BatchInsert many rows in a single transaction
CInsert: BulkUse bulk insert operations
DFilter: LargeFetch a large result set
EFilter: SmallFetch limit 20 with random offset
FGetFetch a single row by primary key
GFilter: dictFetch large result set as dicts
HFilter: tupleFetch large result set as tuples
IUpdate: WholeUpdate all fields of a row
JUpdate: PartialUpdate a single field
KDeleteDelete a single row

Test Models

Test 1 — Simple model (4 fields): id, timestamp, level (indexed), text (indexed)

Test 2 — FK relations: Same as Test 1, plus self-referential foreign key, reverse FK, and M2M relation

Test 3 — Wide model (32+ fields): Same as Test 1, plus 4 sets of 8 typed columns (float, smallint, int, bigint, char, text, decimal, json) — 2 with defaults, 2 nullable


Results: PostgreSQL 17

PostgreSQL 17 in Docker. Driver: asyncpg for async ORMs, psycopg2 for sync ORMs.

Overview

PostgreSQL Summary

Test 1: Simple Model (4 fields)

PostgreSQL Test 1

OperationDjangopeeweeSA ORM asyncSQLObjectTortoise ORMPiccoloormarSQLModelBest
Insert: Single7766515846993,4553,240283540Tortoise ORM
Insert: Batch2,0692,4181,1541,57514,03312,1381,0481,081Tortoise ORM
Insert: Bulk5,2245,0091,26418,80718,0601,1781,079Tortoise ORM
Filter: Large57,45162,00932,31850,694143,810147,03119,73532,389Piccolo
Filter: Small19,49118,0667,53224,84460,69945,8055,5016,444Tortoise ORM
Get2,1832,5901,8943,3416,4206,8548461,792Piccolo
Filter: dict64,39481,65233,798209,380328,12426,77731,117Piccolo
Filter: tuple55,08066,82737,933197,175332,31023,28634,411Piccolo
Update: Whole2,8963,5551,7431,99517,40713,4912,5381,781Tortoise ORM
Update: Partial3,2814,8121,5263,80218,25516,1802,7401,856Tortoise ORM
Delete3,3865,7011,86984321,01018,9682,8901,895Tortoise ORM
Geometric Mean7,1088,2573,8163,62229,38129,5143,2333,660Piccolo

Test 2: FK Relations

PostgreSQL Test 2

OperationDjangopeeweeSA ORM asyncSQLObjectTortoise ORMPiccoloormarSQLModelBest
Insert: Single7658225645173,3503,075264521Tortoise ORM
Insert: Batch2,6542,4771,1841,14012,3558,7851,0671,061Tortoise ORM
Insert: Bulk4,7335,8631,14621,96912,4611,1571,120Tortoise ORM
Filter: Large59,18158,02632,64147,745137,275115,75414,40032,614Tortoise ORM
Filter: Small16,46019,5146,79229,20047,95150,4084,8998,051Piccolo
Get2,3942,6051,9051,4405,8705,6657871,922Tortoise ORM
Filter: dict73,17478,31333,632207,239303,73725,84831,831Piccolo
Filter: tuple65,45970,25634,130192,246284,73329,59736,273Piccolo
Update: Whole3,2163,7911,5981,90716,64712,2172,4001,665Tortoise ORM
Update: Partial3,3924,5861,7294,02218,79615,0652,4421,781Tortoise ORM
Delete8665,88089662917,92112,9842,9401,711Tortoise ORM
Geometric Mean6,5878,6313,4842,94927,86024,9183,0823,710Tortoise ORM

Test 3: Wide Model (32+ fields)

PostgreSQL Test 3

OperationDjangopeeweeSA ORM asyncSQLObjectTortoise ORMPiccoloormarSQLModelBest
Insert: Single6607125556831,5602,761177507Piccolo
Insert: Batch1,5801,3381,1371,2808,0774,627542953Tortoise ORM
Insert: Bulk3,3743,0741,1879,5715,596814971Tortoise ORM
Filter: Large32,18825,68626,03126,71050,99529,4312,89323,445Tortoise ORM
Filter: Small8,9469,5467,16720,49731,24718,6522,1055,997Tortoise ORM
Get1,5411,3111,7732,2885,0563,5636261,708Tortoise ORM
Filter: dict28,90032,29922,35171,887121,8136,88322,634Piccolo
Filter: tuple34,61930,98029,79466,990121,7986,35329,111Piccolo
Update: Whole2,0671,3861,6202,15012,5905,1811,3751,559Tortoise ORM
Update: Partial3,1133,9601,6893,87319,9865,512591,680Tortoise ORM
Delete3,4804,9151,72876922,21414,3052,5951,574Tortoise ORM
Geometric Mean4,8514,7123,4323,02516,58712,1451,0973,152Tortoise ORM

Results: MySQL 8

MySQL 8 in Docker. Driver: asyncmy for Tortoise, aiomysql for SA async / ormar, mysqlclient for Django, pymysql for peewee / SQLObject. Piccolo does not support MySQL.

Overview

MySQL Summary

Test 1: Simple Model (4 fields)

MySQL Test 1

OperationDjangopeeweeSA ORM asyncSQLObjectTortoise ORMormarSQLModelBest
Insert: Single4604751,2603862,2261,2181,198Tortoise ORM
Insert: Batch1,6853,4034,4251,74310,3523,9033,639Tortoise ORM
Insert: Bulk2,8888,2444,70413,5455,6693,614Tortoise ORM
Filter: Large53,09357,70670,55249,27498,36129,05962,415Tortoise ORM
Filter: Small13,21619,76831,25123,63752,59616,70233,064Tortoise ORM
Get2,0582,0654,4863,3406,8911,7534,132Tortoise ORM
Filter: dict65,18564,73577,599126,41167,87865,702Tortoise ORM
Filter: tuple62,06857,817101,337124,06969,06499,444Tortoise ORM
Update: Whole2,4223,2032,3762,21112,4855,0862,152Tortoise ORM
Update: Partial3,5684,6242,5106,08814,0406,0452,439Tortoise ORM
Delete3,4214,9372,57440613,7408,2432,515Tortoise ORM
Geometric Mean6,0358,0008,9983,30721,3518,8858,221Tortoise ORM

Test 2: FK Relations

MySQL Test 2

OperationDjangopeeweeSA ORM asyncSQLObjectTortoise ORMormarSQLModelBest
Insert: Single4775251,1733991,7779521,063Tortoise ORM
Insert: Batch2,4583,3744,8731,8258,2993,2783,853Tortoise ORM
Insert: Bulk4,02912,4013,8077,8525,2613,452peewee
Filter: Large60,20150,58271,47646,07695,29821,05769,532Tortoise ORM
Filter: Small13,40414,10926,65221,83858,0978,35431,293Tortoise ORM
Get2,1462,1713,9653,2277,1881,7194,333Tortoise ORM
Filter: dict70,52563,72570,878122,09148,11554,117Tortoise ORM
Filter: tuple59,95542,729102,362120,41941,905103,458Tortoise ORM
Update: Whole2,4433,2292,0262,1407,5884,0452,187Tortoise ORM
Update: Partial3,7743,9532,1834,64211,8065,5552,565Tortoise ORM
Delete9175,19188332812,9825,4912,461Tortoise ORM
Geometric Mean5,8837,7597,5673,06118,3376,7078,134Tortoise ORM

Test 3: Wide Model (32+ fields)

MySQL Test 3

OperationDjangopeeweeSA ORM asyncSQLObjectTortoise ORMormarSQLModelBest
Insert: Single3683661,3183421,3317031,071Tortoise ORM
Insert: Batch1,5832,0764,2251,3724,3848602,405Tortoise ORM
Insert: Bulk2,6233,5063,4036,1672,2902,400Tortoise ORM
Filter: Large30,55716,56427,00030,33935,6723,39322,934Tortoise ORM
Filter: Small8,8126,83014,42116,18023,7232,76514,136Tortoise ORM
Get1,6161,0172,2702,6564,0148392,187Tortoise ORM
Filter: dict30,03218,52522,79348,7237,50422,733Tortoise ORM
Filter: tuple35,39221,98331,74041,7797,65929,487Tortoise ORM
Update: Whole1,8521,5901,4562,2078,6841,7151,410Tortoise ORM
Update: Partial3,4074,1481,5125,09113,9815,0521,517Tortoise ORM
Delete3,1664,3521,55940116,3207,6461,450Tortoise ORM
Geometric Mean4,4663,9434,9282,69211,5602,6294,292Tortoise ORM

Results: SQLite

SQLite on local filesystem (/tmp/db.sqlite3). This benchmark is inherently single-threaded due to SQLite's write lock — async ORMs pay overhead without concurrency benefit.

Overview

SQLite Summary

Test 1: Simple Model (4 fields)

SQLite Test 1

OperationDjangopeeweeSA ORM asyncSQLObjectTortoise ORMPiccoloormarSQLModelBest
Insert: Single2,1776,8871,2472,3685,693922982755peewee
Insert: Batch13,05015,82378812,29412,693473787742peewee
Insert: Bulk19,04337,10878333,0607721,957727peewee
Filter: Large181,04187,91239,86384,364167,77739,24826,14040,523Django
Filter: Small55,65347,69926,65279,46078,91521,40913,59924,095SQLObject
Get8,4758,5873,56719,6919,1682,6972,3083,506SQLObject
Filter: dict223,613118,76450,330272,53451,68356,67350,218Tortoise ORM
Filter: tuple231,802121,04159,008253,99551,49150,31463,741Tortoise ORM
Update: Whole11,26015,28778028,61220,5411,4091,096781SQLObject
Update: Partial13,21421,10079251,52125,9211,1321,117797SQLObject
Delete14,54228,9857802,59527,8071,4321,117788peewee
Geometric Mean26,82830,2733,99818,66138,7664,0374,0523,770Tortoise ORM

Test 2: FK Relations

SQLite Test 2

OperationDjangopeeweeSA ORM asyncSQLObjectTortoise ORMPiccoloormarSQLModelBest
Insert: Single2,3186,0549632,3787,196811959743Tortoise ORM
Insert: Batch11,46015,17364011,59414,6744791,025446peewee
Insert: Bulk22,41637,59378434,2489991,993595peewee
Filter: Large165,46290,44147,52282,871158,48940,06317,43538,255Django
Filter: Small62,59647,16317,56176,09879,92521,6548,50622,694Tortoise ORM
Get7,7218,1142,86018,6818,9932,3232,1303,650SQLObject
Filter: dict207,814115,18229,339247,15551,36948,80745,120Tortoise ORM
Filter: tuple218,169115,58046,677237,58450,87651,57564,276Tortoise ORM
Update: Whole10,88614,30960231,42320,1181,1381,118779SQLObject
Update: Partial12,61920,45677752,46525,6821,4421,135796SQLObject
Delete2,68028,1033622,47220,6701,1141,113788peewee
Geometric Mean22,62429,1863,11418,41838,2763,9583,7723,473Tortoise ORM

Test 3: Wide Model (32+ fields)

SQLite Test 3

OperationDjangopeeweeSA ORM asyncSQLObjectTortoise ORMPiccoloormarSQLModelBest
Insert: Single2,0213,6617781,9122,165783574736peewee
Insert: Batch6,1826,6386365,9037,752639442573Tortoise ORM
Insert: Bulk8,70514,0581,46712,2931,2331,177699peewee
Filter: Large50,49131,87411,28144,50844,7688,4482,16911,641Django
Filter: Small29,98017,50912,00641,66728,1396,5831,9089,970SQLObject
Get4,2002,7062,79810,6276,0391,6567232,574SQLObject
Filter: dict64,34942,89711,46550,00911,8795,34610,259Django
Filter: tuple70,89045,65710,09348,59612,9784,92612,255Django
Update: Whole5,8653,59661824,90412,4751,129768464SQLObject
Update: Partial11,28921,40777345,53824,9971,1241,422371SQLObject
Delete14,02729,2257782,27027,3561,1481,416774peewee
Geometric Mean13,59413,2842,34612,43716,9202,3621,3861,941Tortoise ORM

Analysis

PostgreSQL — Async ORMs dominate

Tortoise ORM and Piccolo share the top spots. Both use asyncpg, which provides binary protocol encoding and connection pooling — a massive advantage over psycopg2-based sync ORMs. Piccolo leads on simple reads (Filter: dict/tuple) thanks to its ultra-thin result mapping layer, while Tortoise excels at writes (Insert, Update, Delete) and wins overall on the more complex Test 2 and Test 3 schemas.

MySQL — Tortoise ORM leads decisively

Tortoise ORM wins every test by a wide margin (2x over the runner-up). The asyncmy driver (native async MySQL protocol) outperforms both aiomysql and sync mysqlclient/pymysql. SA ORM async is competitive on reads but falls behind on writes. Piccolo does not support MySQL.

SQLite — Sync ORMs are faster

On SQLite, async overhead hurts more than it helps — there is no concurrency to exploit due to SQLite's single-writer lock. Sync ORMs (peewee, Django, SQLObject) avoid the aiosqlite polling overhead and win handily. peewee takes the overall crown, with Django dominating large filter operations and SQLObject excelling at single-row operations (Get, Update). Tortoise ORM remains the fastest async ORM on SQLite.

Key takeaways

  • For PostgreSQL/MySQL production workloads: Tortoise ORM and Piccolo (PG only) offer the best throughput
  • For SQLite/local development: peewee and Django are faster due to lower async overhead
  • SA ORM async / SQLModel: Strong on reads but consistently slow on writes due to Session/UoW overhead. SQLModel tracks closely with SA ORM async (expected — it's a thin wrapper around SQLAlchemy)
  • ormar: Pydantic validation overhead causes significant slowdown, especially on wide models (Test 3)
  • SQLObject: Missing bulk insert and dict/tuple filter operations, but surprisingly fast on single-row SQLite operations

Running the benchmarks

# Clone and install
git clone https://github.com/tortoise/orm-benchmarks.git
cd orm-benchmarks
uv venv && source .venv/bin/activate
uv pip install -e .

# SQLite (default)
cd benchmarks && sh bench.sh

# PostgreSQL
export DBTYPE=postgres PASSWORD=yourpassword PGPORT=5432
cd benchmarks && sh bench.sh

# MySQL
export DBTYPE=mysql PASSWORD=yourpassword MYPORT=3306
cd benchmarks && sh bench.sh