An embedded SQL database in Rust that keeps its tables in memory. Here it is beside SQLite, on the same disk, in the same mode.
Operations a second. A table of 100,000 rows, read by key, updated by key, and loaded in one batch. Median of five runs, ext4 on NVMe, SQLite 3.46 with WAL and prepared statements. Green is the faster one, and the grey figure is how much more it does than SQLite.
| workload | SQLite FULL | ferrite FULL | SQLite NORMAL | ferrite NORMAL |
|---|---|---|---|---|
| bulk insert | 1 174 336 | 1 755 785 +50% | 1 258 940 | 1 906 607 +51% |
| all reads | 422 125 | 1 568 619 +272% | 418 930 | 1 867 973 +346% |
| 95 reads, 5 updates | 46 832 | 81 031 +73% | 284 534 | 1 357 665 +377% |
| 50 reads, 50 updates | 10 001 | 10 823 +8% | 112 041 | 709 148 +533% |
FULL forces every commit to the disk before it returns, and both engines then wait about 400 µs for one fsync. That is why the two FULL columns sit close together on writes: the disk sets the pace, not the engine.
Microseconds for one lookup on a column that is not the key, with about a thousand rows matching. Lower is better, and the grey figure is how much less time ferrite takes.
| asking for | SQLite | ferrite |
|---|---|---|
| the key only | 123 | 53 57% less |
| the indexed column | 121 | 54 55% less |
| a text column, so the row is fetched | 582 | 277 52% less |
| walking the whole table, no index | 3 661 | 2 784 24% less |
| SQLite | ferrite | |
|---|---|---|
| Where the rows live | On disk in pages, with a cache in memory | In memory. A log and a snapshot on disk |
| How much it holds | Up to terabytes | What fits in RAM |
| SQL | The whole language. Views, triggers, subqueries, window functions. | The core. Tables with defaults, unique and foreign keys, indexes over one column or several, one inner join, WHERE with AND, OR, NOT, IS NULL, BETWEEN and arithmetic, ORDER BY, LIMIT, the four aggregates, INSERT OR IGNORE and OR REPLACE, transactions. |
| Types | Five, bent to fit the column. A string goes into an INTEGER column and stays a string. | Five, kept as declared. A string in an INTEGER column is refused. |
| Durability | FULL or NORMAL; FULL by default | FULL or NORMAL; NORMAL by default, and the README says so first |
| Crash safety | Decades of it | Killed mid-write 10,000 times in each mode. No committed row lost, no file that would not open |
| Who can use it at once | Many readers and one writer, across processes | One thread in one process |
| Answers checked against | Its own test suite | SQLite: tens of thousands of generated statements, in memory and on disk, every answer compared |
| Dependencies | None | None |
| Size | Around a quarter of a million lines of C | About 7,900 lines of Rust, bench and tests included |
| Age | Twenty-five years | Weeks |
| License | Public domain | Public domain |
A read never touches a disk page. The row is in memory, filed under its key in a tree. A prepared lookup costs the same as calling the engine directly.
A commit is one record appended to a log, with its length and a checksum. A crash leaves a torn record at the end, and that is cut off rather than believed.
The database is written out fresh only when the log holds twice as many records as there are rows. A hundred thousand rows loaded once are not rewritten for nothing.
No background thread, no timer. An open database that nobody is using makes no syscalls at all. Every speed change came with a number, and nothing merged slower than what it replaced.
A server, a database shared between processes, or a home for more data than the machine has memory. If a table would not fit in RAM, use SQLite. If two programs need the same file, use SQLite.