PostgreSQL¶
hare-orm against SQLAlchemy, tortoise-orm, yara-orm and Django on PostgreSQL: 50 scenarios on a table of 1,000 widgets, each with a gadget, a tag and a JSON document, and a concurrent load test. A shorter bar is faster, except in the load test, which counts operations per second.
Summary¶
How many times slower than hare-orm on the Rust driver (its faster driver in this run) each library is, on average: the ratio of geometric means of the times over every scenario both run, except the cold start.
The load test: 50 workers run 1,000 operations on one connection pool: 80% get(), 15% a filtered
read, 5% get() + save(); the write load runs half get() + save(). Measured warm: every pool
connection runs every operation of the load before the timing starts, as in an application that has
been running.
Operations per second:
Operations per second, the write load:
By scenario¶
Each scenario is drawn on its own scale: compare the bars within one scenario, not across scenarios. The fastest time is in bold. A bar is the median of 5 runs, and the thin line over it spans the best run to the worst.
Reads¶
Relations¶
Aggregates¶
Writes¶
Transactions, concurrency, start¶
Where and 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 |
| PostgreSQL | 18.6 (Debian 18.6-1.pgdg13+2) |
| asyncpg | 0.31.0 |
| psycopg | 3.3.6 |
| hare-orm | 0.12.0 |
| SQLAlchemy | 2.1.1 |
| tortoise-orm | 1.1.8 |
| yara-orm | 1.17.0 |
| Django | 6.1.1 |
| Rows in the table | 1,000 |
- 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.