Benchmarks¶
hare-orm against SQLAlchemy, tortoise-orm, yara-orm and Django on PostgreSQL, SQLite and ClickHouse: the same scenarios in every ORM, written the way its documentation writes them, plus a concurrent load test. Each database has a page of its own with every scenario.
Summary¶
How many times slower than hare-orm each ORM is on each database, on average: the ratio of geometric means of the times over every scenario both run, except the cold start. hare-orm stands for its fastest driver of the database; a dash marks a database the ORM doesn't run on. The last row is hare-orm's operations per second in the load test, measured warm: every pool connection has run every operation of the load before the timing starts.
By database¶
- PostgreSQL — 50 scenarios on a table of 1,000 widgets, each with a gadget, a tag and a JSON document.
- SQLite — 49 scenarios on a table of 1,000 widgets, each with a gadget, a tag and a JSON document.
- ClickHouse — 12 scenarios on a table of 1,000,000 events, sorted by their key.
How it was measured¶
| Date | 2026-10-30 |
| Machine | Windows 11, 13th Gen Intel(R) Core(TM) i5-13400, 16 CPU |
| Python | 3.14.3 |
| hare-orm release | 0.12.0 |
| Runs | 5, median |
- On PostgreSQL every ORM gets a pool of 50 connections, all opened before anything is timed.
- On PostgreSQL every table gets a
VACUUM (FULL, ANALYZE), untimed, before each group of scenarios and each load test, and autovacuum is off for them: every group starts from compact tables. Otherwise autovacuum clears the dead rows the writes leave at a random moment of the run, and whether it has come by decides the query plan — with a parallel scan or without. - Before a load test, every connection of the pool runs every operation of the load, untimed: the test measures an application that has been running, not one just started — each connection is open and has prepared its statements.
- Within a run, a scenario is repeated 3–5 times and its fastest repetition counts; the rows a write
scenario puts back are cleared before each repetition, untimed. The deletes and the many-to-many
add()change the data for good and run once. - Each ORM runs in a process of its own. The runs go round after round, the ORMs in a shuffled order each round; a scenario's result is the median over the runs.
- On Windows a run's process turns off its power throttling (EcoQoS): the scheduler otherwise may move a process without a window onto a hybrid processor's efficiency cores, where it runs at half the speed.
- SQLAlchemy runs twice on PostgreSQL and SQLite: with a transaction around each session (its default) and with the engine in autocommit mode, where every statement commits on its own, as in hare and Django.
- A scenario an ORM has no way to write is marked “not in this ORM” on its chart and left out of its average.
- The start is the library's init and its first query, timed with the library already imported; it is left out of the average.
- On SQLite every ORM runs on one file the way it does by default; SQLAlchemy's connections wait up
to 30 s for the file's write lock instead of the default 5 s — its pool of five otherwise stops
the write load at “database is locked”.
select_for_update()isn't measured on SQLite, which has no row locks. - The server fills the ClickHouse table before the scenarios; a mutation (
update(),delete()) finishes before its call returns in every library. SQLAlchemy runs through clickhouse-sqlalchemy 0.3.2 with the asynch 0.2.5 driver on SQLAlchemy 2.0.30 and Python 3.12 — its asynchronous driver doesn't run on newer versions. - Every scenario is written the way that ORM's documentation writes it.
The benchmark is
benchmarks/bench.py in the
repository; its README lists
every scenario and how to run it on your own machine.