Performance Benchmark
Fullfinity’s ORM was benchmarked against Odoo 18 and five popular Python ORMs across 11 standard operations covering inserts, reads, updates, and deletes.
Results
Section titled “Results”
| Operation | Fullfinity | Odoo 18 | Django | peewee | SA async | Tortoise | Piccolo |
|---|---|---|---|---|---|---|---|
| Insert: Single | 2,164 | 1,922 | 776 | 651 | 584 | 3,455 | 3,240 |
| Insert: Batch | 12,133 | 9,595 | 2,069 | 2,418 | 1,154 | 14,033 | 12,138 |
| Insert: Bulk | 10,693 | 10,225 | 5,224 | 5,009 | 1,264 | 18,807 | 18,060 |
| Filter: Large | 188,601 | 133,461 | 57,451 | 62,009 | 32,318 | 143,810 | 147,031 |
| Filter: Small | 17,991 | 16,763 | 19,491 | 18,066 | 7,532 | 60,699 | 45,805 |
| Get | 256,649 | 17,164 | 2,183 | 2,590 | 1,894 | 6,420 | 6,854 |
| Filter: dict | 275,327 | 134,453 | 64,394 | 81,652 | 33,798 | 209,380 | 328,124 |
| Filter: tuple | 257,209 | 159,148 | 55,080 | 66,827 | 37,933 | 197,175 | 332,310 |
| Update: Whole | 18,983 | 7,401 | 2,896 | 3,555 | 1,743 | 17,407 | 13,491 |
| Update: Partial | 28,552 | 11,272 | 3,281 | 4,812 | 1,526 | 18,255 | 16,180 |
| Delete | 4,478 | 594 | 3,386 | 5,701 | 1,869 | 21,010 | 18,968 |
| Geometric Mean | 32,450 | 14,838 | 7,108 | 8,258 | 3,817 | 29,381 | 29,514 |
All values are ops/sec or rows/sec — higher is better.
Key Takeaways
Section titled “Key Takeaways”2.19x faster than Odoo 18 overall (geometric mean). Competitive with purpose-built async ORMs like Tortoise and Piccolo — while carrying the full weight of an ERP framework: system fields, access control, computed field cascades, identity map, and audit trail.
Writes
Section titled “Writes”Fullfinity beats Odoo on every write operation:
- Insert: Single — 2,164 vs 1,922 (1.13x)
- Insert: Batch — 12,133 vs 9,595 (1.26x)
- Insert: Bulk — 10,693 vs 10,225 (1.05x)
- Update: Whole — 18,983 vs 7,401 (2.56x)
- Update: Partial — 28,552 vs 11,272 (2.53x)
- Delete — 4,478 vs 594 (7.54x)
Updates use deferred writes with batch flush — field changes are buffered in-memory and written to the database in a single batched SQL operation at flush time. Both Fullfinity and Odoo use this approach; the timing includes the flush.
- Get (PK lookup) — 256,649 vs 17,164 (14.9x). The identity map returns cached instances for previously-fetched records without a database round-trip. In ERP workloads, the same records are accessed repeatedly during compute cascades, validation, and relational hydration — the identity map turns all of these into near-zero-cost lookups.
- Filter: Large — 188,601 vs 133,461 (1.41x). Querying the same dataset repeatedly benefits from the identity map fast-path — cached instances are returned directly without per-field processing overhead.
- Filter: dict — 275,327 vs 134,453 (2.05x). Raw dictionary results via
.values(), no instance creation or caching — pure query-to-dict throughput. - Filter: tuple — 257,209 vs 159,148 (1.62x). Raw tuple results via
.values_list(), same story.
How It Works
Section titled “How It Works”The performance comes from architectural decisions, not tricks:
Identity Map
Section titled “Identity Map”Every request maintains a single instance per record in the identity map. When the same record is accessed multiple times (common in ERP — compute cascades, validation, relational hydration), subsequent lookups are a Python dict access with no database round-trip.
Deferred Writes
Section titled “Deferred Writes”Scalar field updates are buffered in-memory. Multiple updates to the same record merge into a single UPDATE statement. Multiple records with the same changed fields are batched via executemany. The flush happens automatically before reads on dirty models and at transaction commit.
asyncpg
Section titled “asyncpg”Fullfinity uses asyncpg with the PostgreSQL binary protocol, providing 3-5x lower overhead per query compared to psycopg2 (used by Odoo and Django).
Minimal Instance Overhead
Section titled “Minimal Instance Overhead”The _from_db_row() fast-path constructor bypasses __init__ validation, UUID generation, and datetime defaults for database rows. Combined with the identity map fast-path that skips all per-field processing for already-cached instances, the per-row overhead for bulk reads is minimal.
Methodology
Section titled “Methodology”- Model:
BenchmarkJournal— 3 user fields (timestamp, level, text) + 4 system fields (id, identifier, created_date, updated_date, active). Comparable to the 4-field model used in the tortoise-orm-benchmark suite. - Database: PostgreSQL 17, local connection
- Driver: asyncpg (Fullfinity, Tortoise, Piccolo, SA async), psycopg2 (Odoo, Django, peewee)
- Seed data: 1,000 rows pre-populated for read benchmarks
- Odoo 18: Tested locally on the same machine with the same methodology (deferred writes +
cr.flush()for updates) - Reference numbers: Other ORM numbers from the tortoise-orm-benchmark suite (PostgreSQL 17, Docker, matching model shape)
Running the Benchmark
Section titled “Running the Benchmark”./fullfinity-server -c config.yaml -db <dbname> test --module benchmark --verbose