An object store with types, trees, and real SQL right inside your PostgreSQL/MS SQL/SQLite. FK to objects, EF and Dapper alongside. redb vs Mongo and Raven.
To store objects, graphs, and trees with types, indexes, and full queries you don't need another database. You need the one you already run.
redb turns PostgreSQL, MS SQL, or SQLite into a typed object store without taking away SQL, EF Core, or Dapper. That's a fundamentally different conversation than "MongoDB vs RavenDB," where you pick a separate engine and operate it separately. Here the objects land in the database already running in your production. Below: where this beats document databases, with code, and where redb has honest limits.
To avoid strawmen: MongoDB is a mature server document database with horizontal scaling and a huge ecosystem, and it has had multi-document ACID transactions since 4.0 (2018). RavenDB is a .NET-native document database, fully ACID, with typed LINQ and automatic indexes. Both are good products. redb just plays a different game and on that field it holds strong cards.
And there's almost nothing to learn: it's the same LINQ you already write. Code against redb and against Raven is nearly identical (you'll see below), and the entry barrier is lower than EF Core no DbContext, no configuration, no migrations.
At a glance: redb vs Mongo and Raven
| redb | MongoDB | RavenDB | |
|---|---|---|---|
| Where it lives | inside your PostgreSQL / MS SQL / SQLite | its own server | its own server |
FK and JOIN to your tables |
yes, a real foreign key | no | no |
| One transaction with EF/Dapper | yes (TransactionScope) |
no | no |
| Update: what goes to the DB | changed fields only, auto-diff | you build $set yourself |
whole document, auto (fields via Patch) |
| Object cache (skip re-materialization) | yes, by _hash, transparent |
no (your own layer) | yes (aggressive, by change-vector) |
| Query language | native SQL (LINQ → SQL) | MQL + aggregation | RQL (LINQ → RQL) |
| Field types in storage | yes, RTTI | no (BSON) | typed client, document on disk |
| Trees | recursive CTEs, polymorphic |
$graphLookup, limited |
no native |
| Embedded / in-browser (WASM) | yes, SQLite Pro (pure C#) | no | embedded yes, WASM no |
| Entry barrier | LINQ, lower than EF Core | its own driver and API | LINQ → RQL |
| ACID | yes (RDBMS transactions) | yes (since 4.0) | yes |
| Indexes | covered out of the box, tuning = drop the extras | you declare them | automatic indexes |
| Horizontal sharding | in development (domain model) | mature, battle-tested | clustering and sharding |
| Record-level permissions | yes, built-in | by hand (app-level) | by hand (app-level) |
| Full-text out of the box | via the RDBMS | Atlas Search | built-in Lucene |
Your database knows your types
In the document model the schema lives in the application's head. Mongo stores BSON: the type and requiredness of a field are held by code, not the database. Raven is closer its .NET client is typed but on disk it's a document, and queries go through indexes you declared up front.
redb stores both the data and a description of its types. Inside is a chain _types → _schemes → _structures → _values: at query time the database knows that Salary is a decimal and HireDate is a DateTime, not "field #42." This is runtime type information at the storage level, and the payoff is direct: values sit in typed columns with ordinary RDBMS indexes, so LINQ turns into native SQL over an index, not a document scan.
// MongoDB filter over an untyped document
var res = await coll.Find(Builders<BsonDocument>.Filter.Gt("Salary", 100000)).ToListAsync();
// RavenDB LINQ (async session), but translated to RQL over its own engine
var res = await asyncSession.Query<Employee>().Where(e => e.Salary > 100000).ToListAsync();
// redb LINQ compiles to NATIVE SQL over an indexed column
var res = await redb.Query<Employee>().Where(e => e.Salary > 100000).ToListAsync();
Employee here is your ordinary class: the Props suffix you'll sometimes see in our examples is just a naming convention redb works with your type directly. The syntax for Raven and redb is nearly the same; the difference is where the query goes: Raven to RQL (its own SQL-like language over the document engine's Lucene indexes), redb to your RDBMS's SQL, over its native indexes. And the schema arrives already covered with indexes: in practice you don't add missing ones, you drop the extras for your workload with ordinary RDBMS tools, like any table.
Drops in with one package and breaks nothing
Here's the main trump card, and it's architecturally inevitable: redb is tables in your database, not a separate server.
Add redb and it creates _objects, _values, _schemes and service tables it doesn't touch your tables. You can move a single aggregate to the object model and keep the rest as classic relational in the same database, the same transaction. And since _objects is an ordinary table with primary key _id, your tables reference redb objects with a real foreign key:
CREATE TABLE trends (
id bigserial PRIMARY KEY,
captured_at timestamptz NOT NULL,
metric numeric NOT NULL,
redb_object_id bigint NOT NULL REFERENCES _objects(_id) ON DELETE CASCADE
);
A trend physically cannot dangle on a deleted object the database constraint enforces that, not your code. And you read it all with one JOIN in plain SQL: one query, one plan, one backup.
Your data-access stack stays yours, too redb sits next to it, not instead of it:
using var tx = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
// the object via redb
var order = new RedbObject<Order> { Props = new() { Total = 4200m } };
order.id = await redb.SaveAsync(order);
// the trend via Dapper, same connection to the same database
await conn.ExecuteAsync(
"INSERT INTO trends(captured_at, metric, redb_object_id) VALUES (@t, @m, @id)",
new { t = DateTime.UtcNow, m = 4200m, id = order.id });
tx.Complete(); // redb and Dapper commit atomically
One RDBMS, one ADO.NET connection so TransactionScope covers both the redb write and the Dapper insert: both or neither. EF Core, Dapper, raw SQL work over your tables without a single change.
Now the same thing with Mongo or Raven: you can't. A separate engine means no cross-store foreign key, no JOIN between your tables and their documents, and an ambient transaction won't span them. "A trend that references an entity" becomes ETL between two stores, eventual consistency, two query languages, two backups, two monitoring stacks. For many teams that's the real cost of a document database not performance, but a second stack to operate. redb doesn't charge it: it moves into the database you already have.
All of SQL is yours, trees included
Because the query goes to your RDBMS's SQL, you get all of its power, not the subset a document engine reimplemented.
And _values is an ordinary table: _id_object, _id_structure, and a column per type (_String, _Long, _Numeric, _DateTimeOffset…). So you can query the value space of every property of every object with native, indexable SQL directly:
-- objects where ANY string property starts with 'ACME-'
SELECT DISTINCT _id_object FROM _values WHERE _String LIKE 'ACME-%';
-- objects where ANY numeric property equals 42
SELECT DISTINCT _id_object FROM _values WHERE _Long = 42;
The query isn't tied to a specific schema field it goes by value, across all types and properties. And this isn't "just SQL, index it someday": _values._String has a trigram GIN index (gin_trgm_ops) out of the box, so LIKE, ILIKE, prefix, substring, and even regex over any string property hit an index immediately. Numeric and date values are covered by composite and covering indexes when scoped to a property; for a bare "find anywhere" over a single numeric column you enable a dedicated index its line already sits in the schema, off by default to save write cost. redb ships 40+ indexes in total, and you own them. In a document database that same "find anywhere" needs a wildcard index over the whole document; here it's one line of SQL over an ordinary, already-indexed table.
And nesting is queried all the way through, in a single query:
// filter by a deeply nested field no .Include() cascade, no N+1
var res = await redb.Query<Order>()
.Where(o => o.Customer.Address.City == "London")
.ToListAsync();
No .Include() chains like EF, no N+1: the path Customer.Address.City compiles into one SQL statement. Same for analytics aggregations, GROUP BY, and window functions go to native SQL, not a separate aggregation pipeline with its own rules. But SQL's power shows brightest on trees.
Hierarchies are first-class in redb: subtree, leaves, roots, levels all compile to recursive CTEs on live SQL, identically across all three dialects:
// leaves of THIS subtree only a recursive CTE from the root
var leaves = await redb.TreeQuery<Category>(root).WhereLeaves().ToListAsync();
And the trees are polymorphic children of different types in one hierarchy, because the database knows each node's type. Document databases don't have this: in Mongo graph traversal is $graphLookup inside the aggregation pipeline, with its own limits and within a collection; in Raven there's no native recursive traversal at all, hierarchies are chased by id references.
We learned how sharp this spot is the hard way. A user of a chat product reported a bug: a new participant's first turn arrived at the model with someone else's history. The root was in the core query provider WhereLeaves() replaced the root CTE instead of being a predicate over it, and "the leaves of this tree" actually meant "the newest leaves of the whole scheme." The defect lived in all six providers (Postgres, MSSql, SQLite × Free/Pro) and was fixed the same way. The moral, redb aside: a filter that "didn't fire" and a filter that "replaced the search space" look identical in code and differ catastrophically in the data the second throws no error, it just returns a plausible answer about the wrong thing. Things like that get caught precisely because underneath is honest SQL you can read and explain.
One codebase from server to browser
The same typed LINQ works unchanged on PostgreSQL, MS SQL, and SQLite, and stretches from a production server to Blazor WebAssembly and a mobile app. In the embedded profile (SQLite Pro) redb is pure C#, materialization in managed code, with no native dependency: the same code runs on the server and in the browser. Mongo isn't embedded at all; Raven has embedded but not "pure managed inside WASM."
"Shredding an object into a pile of _values rows" sounds costlier than it works out to be. SaveAsync takes both a single entity and a collection a whole insert batch goes through the RDBMS's native bulk load (SqlBulkCopy on SQL Server, COPY … FROM STDIN on PostgreSQL), not row by row. On reads, Pro (which is free) assembles objects from rows in parallel, across cores. And with the transparent object cache on, it validates by _hash if an object hasn't changed in the DB, redb serves it from cache without re-materializing at all (an option even skips the hash check). The cache is transparent: LoadAsync serves from it on its own, with no changes in your calling code.
Honestly about the cache: RavenDB has aggressive client-side caching by change-vector same idea (don't fetch and re-deserialize what hasn't changed), so here it's roughly parity with Raven. The MongoDB driver, though, has no built-in object cache or identity map caching there is your own layer. And this whole materialization-and-cache discussion exists because redb reconstructs an object from many rows; a document database stores it whole there's nothing to reconstruct. It's less a redb advantage than proof that its storage model doesn't make reads expensive.
But updates are the more interesting part. You load 1,000 objects and change one field on all of them, or on a hundred, good luck telling which. You hand the same collection to SaveAsync, and Pro's change tracking figures out what changed and on which objects itself: a state-tree diff (in memory vs the DB), parallel across cores, and an UPDATE only for the changed fields of the changed objects. No dirty flags, no manual "figure out which" you don't tell the engine what you changed, it computes it.
This one isn't parity. MongoDB has no automatic tracking you build updates by hand ($set on the fields you need, and first work out which those are). RavenDB's session sees the changed documents, but by default writes the whole document; a field-level delta is a manual Patch. redb writes the field delta itself, batched. (Bulk insert, to be fair, all three have redb isn't exclusive there.)
Full sharding is in development and redb's domain-and-provider model was built for it. The RTTI layer (types, schemes, structures) is small and identical: cheap to keep as a copy on every node, while objects and values are split across separate databases, each with its own domain. Keys come from a single provider over a master sequence key generation in redb is already decoupled from a specific database and batched, so the master is hit rarely. The honest cost remains: a cross-shard query is a fan-out over all databases, real engineering, not a "turn it on" toggle.
Batteries included
What you'd hand-write against a document database is already here in redb and reachable through the same typed API:
-
Record-level permissions. Access at the row level (
get_user_permissions_for_object, thev_user_permissionsview) "this is my object, only I see it" without a homegrown ACL layer. -
Instant soft delete. An object and its subtree go to the trash with a single mark they vanish from queries immediately, while the heavy physical purge of rows runs in the background. An ordinary cascading
DELETEof a large graph is a slow operation; here deletion feels instant, with a recycle bin and restore. On Mongo/Raven, trash-with-restore is hand-built (a flag plus a filter in every query). - Change history. Who changed an object and when built in.
- Bulk operations. Update hundreds of thousands of objects in batches, without timeouts.
-
Polymorphic trees. An org structure with different types per level (
division → team → person) as one tree, not "a table per type" and not JOIN cascades. - Transparent, domain-isolated caches metadata, lists, objects; enabled by a flag, with no code from you (the object cache with hash validation, see above).
-
Tooling alongside.
redb.Exportstreams schema and data as JSONL (optional compression) for backup, migration, and replication;redb.CLI(adotnet tool, commandredb) does export, import, and schema init from the terminal.
None of this needs a second stack it all lives in the same database of yours.
The limits honestly
- Ecosystem and managed hosting. Mongo and Raven have cloud services, ready-made integrations, mature drivers for every language. That market doesn't exist around redb yet: it's younger. (This doesn't affect the entry barrier you already know LINQ it's about the surrounding infrastructure.)
-
Full-text out of the box. Raven has built-in Lucene, Mongo has Atlas Search. In redb full-text is the underlying RDBMS's facility (
tsvectorin PostgreSQL, full-text in SQL Server), not a turnkey store feature. - Proven mega-scale. Full sharding in redb is in development; Mongo's horizontal scaling has years of production mileage. If you need proven petabyte scale right now that's Mongo.
What to pick
- MongoDB a server document model with proven horizontal scaling and a rich ecosystem, when connectivity to the relational world is secondary.
- RavenDB a .NET-native document database with ACID, automatic indexes, and built-in full-text, if you're ready to stand up and operate it as a separate database.
- redb when your data is typed, you care about real SQL, trees, and referential integrity with the rest of your relational data, and you don't want a second stack: the objects live in the same database as everything else, EF and Dapper stay with you, and one codebase runs from server to browser.
Document flexibility and relational rigor are usually pitched as an either/or. redb puts typed objects and trees into the database you already run and asks for no separate engine in return. If that's your situation, you probably don't need a second database.
More of my writing: redbase.app/articles, and on dev.to.
If this was useful — a ⭐ on GitHub helps others find it.

Top comments (0)