What is WitDatabase?
WitDatabase is a high-performance embedded SQL database engine built entirely in C# for the .NET ecosystem. It provides full relational database functionality — SQL queries, ACID transactions, ADO.NET and Entity Framework Core support — without any native dependencies.
Not a Port — Built from Scratch
WitDatabase is not a port of SQLite or any other database. The entire codebase was written from scratch in modern C#, leveraging .NET-specific capabilities:
Span<T>andMemory<T>— zero-copy data handling, reduced allocationsArrayPool<T>andMemoryPool<T>— memory reuse, less GC pressure- Async/await throughout — non-blocking I/O operations
- Modern C# features — pattern matching, records, nullable reference types
While WitDatabase uses a SQL dialect inspired by SQLite (due to its popularity and familiarity), the implementation is completely original:
| Component | Implementation |
|---|---|
| SQL Parser | Custom ANTLR4 grammar, not SQLite's Lemon parser |
| Storage Engines | Original B+Tree and LSM-Tree implementations |
| Query Optimizer | Cost-based optimizer written in C# |
| Transaction Manager | Custom MVCC implementation |
| Page Cache | .NET-optimized with Clock and LRU algorithms |
The Problem
.NET developers face a difficult choice for embedded databases:
SQLite is excellent but:
- Requires native binaries for each platform (Windows x64, ARM, Linux, macOS...)
- P/Invoke overhead for every database call
- Encryption requires paid extensions or complex setup
LiteDB is pure .NET but:
- NoSQL only — no SQL queries
- No ADO.NET provider
- No Entity Framework Core support
- No JOINs, window functions, or CTEs
- Can't share code with server databases
The Solution
WitDatabase provides SQLite-level functionality in a pure .NET package:
✅ Full SQL support (DDL, DML, JOINs, CTEs, window functions)
✅ Complete ADO.NET provider (DbConnection, DbCommand, DbDataReader)
✅ Full Entity Framework Core provider (DbContext, migrations, LINQ)
✅ Zero native dependencies
✅ Cross-platform (Windows, Linux, macOS, Blazor WebAssembly)
✅ Built-in encryption (AES-256-GCM, ChaCha20-Poly1305)
✅ Multiple storage engines (B+Tree, LSM-Tree)
✅ Modular architecture — replace any component
Core Concepts
Modularity and Extensibility
WitDatabase is built on a provider-based architecture. Every major component is defined by an interface and can be replaced with a custom implementation:
[[Svg Src="./WitDatabaseBuilder.svg" Alt="Fluent API for configuration"]]
Pluggable Interfaces
| Interface | Purpose | Built-in Implementations |
|---|---|---|
IKeyValueStore |
Storage engine | StoreBTree, StoreLsm, StoreInMemory |
IStorage |
Page-level storage | StorageFile, StorageMemory, StorageEncrypted, StorageIndexedDb |
ICryptoProvider |
Encryption algorithm | AesGcmProvider, ChaCha20Provider (BouncyCastle) |
IPageCache |
Caching strategy | PageCacheLru, PageCacheShardedClock |
ITransactionJournal |
Durability | TransactionJournalFile, WalTransactionJournal |
ISecondaryIndexFactory |
Index creation | SecondaryIndexFactoryKeyValueStore |
Custom Implementation Example
// Register a custom encryption provider
ProviderRegistry.Instance.Register<ICryptoProvider>("my-hsm-crypto",
parameters => new HsmCryptoProvider(parameters.GetRequired<string>("keyId")));
// Use it in your database
var db = new WitDatabaseBuilder()
.WithFilePath("secure.witdb")
.WithBTree()
.WithEncryption("my-hsm-crypto", new ProviderParameters()
.Set("keyId", "my-hardware-key"))
.Build();
This allows you to:
- Integrate with existing infrastructure — use your company's HSM, key vault, or custom storage
- Optimize for specific workloads — write a specialized cache or index
- Extend functionality — add features without modifying core code
Striving for Full Feature Parity
WitDatabase aims to support all features of major server databases (SQL Server, PostgreSQL, MySQL) to enable seamless provider switching. This is an ongoing effort:
v1 (Current) — Production Ready
| Feature | Status |
|---|---|
| Full DDL (CREATE, ALTER, DROP) | ✅ Complete |
| Full DML (SELECT, INSERT, UPDATE, DELETE, MERGE) | ✅ Complete |
| JOINs (INNER, LEFT, RIGHT, FULL, CROSS) | ✅ Complete |
| Subqueries and CTEs | ✅ Complete |
| Window functions | ✅ Complete |
| Transactions with savepoints | ✅ Complete |
| MVCC with 5 isolation levels | ✅ Complete |
| 60+ built-in functions | ✅ Complete |
| INFORMATION_SCHEMA views | ✅ Complete |
| EF Core migrations | ✅ Complete |
v2 (Planned) — Enhanced Compatibility
| Feature | Status | Description |
|---|---|---|
| User-defined functions | 🔮 Planned | CREATE FUNCTION ... RETURNS ... AS BEGIN END |
| Stored procedures | 🔮 Planned | CREATE PROCEDURE, CALL, EXECUTE |
EXPLAIN ANALYZE |
🔮 Planned | Runtime execution statistics |
ATTACH DATABASE |
🔮 Planned | Multi-database queries |
VACUUM |
🔮 Planned | Reclaim unused space |
ANALYZE |
🔮 Planned | Update statistics for optimizer |
| Leveled compaction (LSM) | 🔮 Planned | Alternative compaction strategy |
| Cursor support | 🔮 Planned | Forward-only and scrollable cursors |
The goal is to make WitDatabase a complete drop-in replacement for development, testing, and portable scenarios.
Primary Use Cases
Use Case 1: Embedded Database for .NET Applications
The most straightforward use case — WitDatabase as a fast, reliable file-based database for applications where a single client connects to the data.
Perfect Fit For
- Desktop applications — WPF, WinForms, Avalonia, MAUI
- Console tools and utilities — data processing, ETL scripts
- Single-user web applications — personal dashboards, local servers
- IoT and edge devices — data collection and local storage
- Games — save files, leaderboards, inventory systems
- CLI applications — configuration, caching, state persistence
Simple and Familiar
// Just point to a file — that's it
using var connection = new WitDbConnection("Data Source=myapp.witdb");
connection.Open();
// Create tables with full SQL
connection.Execute("""
CREATE TABLE IF NOT EXISTS Products (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Name VARCHAR(100) NOT NULL,
Price DECIMAL(10,2) NOT NULL,
Stock INTEGER DEFAULT 0,
CreatedAt DATETIME DEFAULT NOW()
)
""");
// Standard ADO.NET patterns
connection.Execute(
"INSERT INTO Products (Name, Price, Stock) VALUES (@name, @price, @stock)",
new { name = "Widget", price = 29.99m, stock = 100 });
// Query with familiar SQL
var products = connection.Query<Product>(
"SELECT * FROM Products WHERE Price < @maxPrice ORDER BY Name",
new { maxPrice = 50.00m });
Or Use Entity Framework Core
public class AppDbContext : DbContext
{
public DbSet<Product> Products => Set<Product>();
public DbSet<Order> Orders => Set<Order>();
public DbSet<Customer> Customers => Set<Customer>();
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseWitDb("Data Source=shop.witdb");
}
// Use like any other EF Core database
using var context = new AppDbContext();
context.Database.EnsureCreated();
var topProducts = await context.Products
.Where(p => p.Stock > 0)
.OrderByDescending(p => p.Price)
.Take(10)
.ToListAsync();
Why WitDatabase for Desktop/Single-User Apps?
| Advantage | Description |
|---|---|
| Zero setup | Just add NuGet package, no installation required |
| Single file | All data in one .witdb file, easy to backup/move |
| No server | No process to manage, no ports to configure |
| Full SQL | Not a key-value store — real relational queries |
| ACID transactions | Data integrity even on crashes |
| Built-in encryption | Protect user data without extra libraries |
| Fast for managed code | Optimized with Span |
Performance Expectations
For typical desktop/single-user workloads, WitDatabase provides excellent performance:
- Point queries: Sub-millisecond (0.2ms for PK lookup)
- Small transactions: 5-10ms for typical CRUD operations
- Bulk inserts: ~7ms per 1000 rows in transaction
- Table scans: ~0.5ms per 1000 rows
This is more than sufficient for applications with datasets up to millions of rows and typical interactive response time requirements.
Use Case 2: Testing and Development
WitDatabase excels as a drop-in replacement for server databases during testing and development.
The Challenge
Your production application uses SQL Server, PostgreSQL, or MySQL. For testing, you need:
- Fast test execution (no network latency)
- Isolated test databases (no shared state)
- CI/CD without database infrastructure
- Developer machines without installing heavy databases
The Solution: Swappable Database Provider
With Entity Framework Core, you can use the same DbContext with different providers:
// Your DbContext — works with ANY provider
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }
public DbSet<User> Users => Set<User>();
public DbSet<Order> Orders => Set<Order>();
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>(entity =>
{
entity.HasIndex(e => e.Email).IsUnique();
entity.Property(e => e.CreatedAt).HasDefaultValueSql("NOW()");
});
modelBuilder.Entity<Order>(entity =>
{
entity.HasOne(o => o.User)
.WithMany(u => u.Orders)
.HasForeignKey(o => o.UserId)
.OnDelete(DeleteBehavior.Cascade);
});
}
}
// Program.cs — Production (SQL Server)
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("Production")));
// Program.cs — Development (WitDatabase file)
services.AddDbContext<AppDbContext>(options =>
options.UseWitDb("Data Source=dev.witdb"));
// Test setup — WitDatabase in-memory
services.AddDbContext<AppDbContext>(options =>
options.UseWitDbInMemory());
Complete Testing Example
public class OrderServiceTests : IDisposable
{
private readonly AppDbContext _context;
private readonly OrderService _service;
public OrderServiceTests()
{
// Each test gets a fresh in-memory database
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseWitDbInMemory()
.Options;
_context = new AppDbContext(options);
_context.Database.EnsureCreated();
_service = new OrderService(_context);
}
[Fact]
public async Task CreateOrder_WithValidUser_ReturnsOrder()
{
// Arrange
var user = new User { Name = "John", Email = "john@example.com" };
_context.Users.Add(user);
await _context.SaveChangesAsync();
// Act
var order = await _service.CreateOrderAsync(user.Id, 99.99m);
// Assert
Assert.NotNull(order);
Assert.Equal(user.Id, order.UserId);
Assert.Equal(99.99m, order.TotalAmount);
}
[Fact]
public async Task GetUserOrders_WithMultipleOrders_ReturnsAll()
{
// Arrange
var user = new User { Name = "Jane", Email = "jane@example.com" };
_context.Users.Add(user);
_context.Orders.AddRange(
new Order { User = user, TotalAmount = 10m },
new Order { User = user, TotalAmount = 20m },
new Order { User = user, TotalAmount = 30m }
);
await _context.SaveChangesAsync();
// Act
var orders = await _service.GetUserOrdersAsync(user.Id);
// Assert
Assert.Equal(3, orders.Count);
Assert.Equal(60m, orders.Sum(o => o.TotalAmount));
}
public void Dispose() => _context.Dispose();
}
Benefits Over EF Core InMemory Provider
EF Core has a built-in UseInMemoryDatabase(), but it has significant limitations:
| Feature | WitDatabase | EF Core InMemory |
|---|---|---|
| SQL execution | ✅ Real SQL | ❌ No SQL at all |
| Constraints | ✅ Enforced | ❌ Not enforced |
| Foreign keys | ✅ Checked | ❌ Not checked |
| Transactions | ✅ Real ACID | ❌ No transactions |
| Raw SQL queries | ✅ Works | ❌ Throws exception |
| Indexes | ✅ Used | ❌ Ignored |
| LIKE / GLOB | ✅ Works | ⚠️ Different behavior |
WitDatabase tests behave like production — constraints are enforced, transactions work correctly, and SQL executes as expected.
Use Case 3: Portable and Demo Deployment
WitDatabase is perfect for scenarios where you can't or don't want to install a server database.
Same Product, Different Deployments
A powerful pattern: one codebase, multiple deployment options. Your application can support both enterprise server installation and single-user portable mode:
// Startup configuration based on deployment mode
public static void ConfigureDatabase(IServiceCollection services, IConfiguration config)
{
var deploymentMode = config["DeploymentMode"]; // "Server" or "Portable"
if (deploymentMode == "Server")
{
// Enterprise installation — SQL Server with full infrastructure
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(config.GetConnectionString("SqlServer")));
}
else
{
// Portable/single-user — WitDatabase, no installation required
var dbPath = Path.Combine(AppContext.BaseDirectory, "data", "app.witdb");
services.AddDbContext<AppDbContext>(options =>
options.UseWitDb(
Loading...
quot;Data Source={dbPath};Encryption=aes-gcm;Password={userKey}"));
}
}
Benefits of this approach:
- Single codebase — no separate "lite" version to maintain
- Same EF Core DbContext — all business logic works identically
- Customer choice — enterprise customers get SQL Server, individual users get portable version
- Easy trials — prospects can evaluate without IT involvement
- Gradual upgrade path — start portable, migrate to server when needed
Portable Applications
// Application stores data alongside the executable
var dbPath = Path.Combine(AppContext.BaseDirectory, "appdata.witdb");
services.AddDbContext<AppDbContext>(options =>
options.UseWitDb(
Loading...
quot;Data Source={dbPath}"));
- USB-portable applications — run from any drive
- Kiosk applications — self-contained installation
- Field applications — work offline, sync later
Demo and Trial Versions
// Demo version with embedded sample data
services.AddDbContext<AppDbContext>(options =>
options.UseWitDb("Data Source=demo.witdb;Mode=ReadOnly"));
- Trade show demos — no setup required
- Customer trials — instant evaluation
- Training environments — consistent starting state
Desktop Applications
// User data in application data folder
var userDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"MyApp",
"userdata.witdb"
);
services.AddDbContext<AppDbContext>(options =>
options.UseWitDb(
Loading...
quot;Data Source={userDataPath};Encryption=aes-gcm;Password={userKey}"));
- Document-based apps — each document is a .witdb file
- Personal databases — contacts, notes, collections
- Local caching — offline-first with eventual sync
Use Case 4: Cross-Platform and Blazor WebAssembly
WitDatabase runs anywhere .NET runs — including in the browser:
// Blazor WebAssembly — data persisted to IndexedDB
var db = new WitDatabaseBuilder()
.WithIndexedDbStorage("MyAppDatabase", JSRuntime)
.WithBTree()
.WithTransactions()
.Build();
This enables:
- Offline-first web applications — work without internet
- Client-side data processing — reduce server load
- Privacy-focused apps — data never leaves the browser
Note: There are other projects working on WebAssembly database support (including SQLite's own WASM build). WitDatabase's advantage is unified API across all platforms — the same code works on server, desktop, and browser.
Comparison with Alternatives
vs SQLite
Aspect
WitDatabase
SQLite
Implementation
Pure C#
Native C
Dependencies
None
Platform-specific binaries
Performance (large queries)
Slower
Faster
Performance (small transactions)
Competitive
Competitive
Encryption
Built-in
Paid extension
EF Core support
✅ Full
✅ Full
Extensibility
Full (interfaces)
Limited
Choose WitDatabase over SQLite when:
- You want zero native dependencies
- You need built-in encryption
- Deployment simplicity matters more than raw performance
- You want to extend/customize components
Choose SQLite over WitDatabase when:
- Maximum performance on large datasets is critical
- You have complex analytical queries with many JOINs
- Native dependencies are acceptable
vs LiteDB
Aspect
WitDatabase
LiteDB
Query language
Full SQL
LINQ only
Data model
Relational
Document (NoSQL)
ADO.NET
✅ Yes
❌ No
EF Core
✅ Yes
❌ No
JOINs
✅ Full support
❌ Limited
Window functions
✅ Yes
❌ No
CTEs
✅ Yes
❌ No
Performance
Generally faster
Generally slower
Memory usage
Lower
Higher
Choose WitDatabase over LiteDB when:
- You need SQL queries
- You want ADO.NET or EF Core
- You need relational features (JOINs, constraints)
- You're sharing code with a server database
Choose LiteDB over WitDatabase when:
- Document model fits your data better
- You prefer LINQ-only API
- You don't need relational features
vs EF Core InMemory
Aspect
WitDatabase
EF Core InMemory
Real SQL execution
✅ Yes
❌ No
Constraint enforcement
✅ Yes
❌ No
Transaction support
✅ Full ACID
❌ None
Raw SQL queries
✅ Works
❌ Throws
Production parity
✅ High
❌ Low
Test reliability
✅ High
⚠️ May miss bugs
Use WitDatabase for tests when:
- You want tests to behave like production
- You use raw SQL queries
- Constraint validation matters
- Transaction behavior matters
Use EF Core InMemory when:
- Speed is the only concern
- You only test simple CRUD
- You don't use any SQL features
Technical Specifications
Supported Platforms
Platform
Support
Windows (x64, x86, ARM64)
✅ Full
Linux (x64, ARM64)
✅ Full
macOS (x64, Apple Silicon)
✅ Full
Blazor WebAssembly
✅ Full
iOS / Android (via MAUI)
✅ Full
.NET Versions
Version
Support
.NET 9.0
✅ Full
.NET 10.0
✅ Full
.NET 8.0
⚠️ Not tested
.NET Framework
❌ Not supported
SQL Compatibility
WitDatabase implements a SQL dialect compatible with SQLite, with enhancements:
- DDL: CREATE/ALTER/DROP TABLE, INDEX, VIEW, TRIGGER, SEQUENCE
- DML: SELECT, INSERT, UPDATE, DELETE, MERGE
- Queries: JOINs, subqueries, CTEs (WITH), UNION/INTERSECT/EXCEPT
- Functions: 60+ built-in functions (string, math, date/time, JSON)
- Advanced: Window functions, RETURNING clause, UPSERT (ON CONFLICT)
- Transactions: BEGIN, COMMIT, ROLLBACK, SAVEPOINT
- v2: User-defined functions, stored procedures (planned)
Storage Options
Engine
Best For
Characteristics
B+Tree
General purpose, reads
Balanced read/write, good for OLTP
LSM-Tree
Write-heavy
Optimized for sequential writes
In-Memory
Testing, caching
Fastest, no persistence
IndexedDB
Browser apps
Persistent browser storage
Custom
Your needs
Implement IStorage interface
Encryption Options
Algorithm
Best For
Notes
AES-256-GCM
Desktop/Server
Hardware accelerated
ChaCha20-Poly1305
Blazor WASM, ARM
Software implementation
Custom
Enterprise
Implement ICryptoProvider
Summary: Positioning in the .NET Ecosystem
WitDatabase fills a unique position in the .NET ecosystem:
[[Svg Src="./dotnet-embedded-databases-comparison.svg" Alt=".NET Embedded Databases"]]
WitDatabase is the right choice when you need:
- Full SQL database in pure .NET
- Zero native dependencies
- Drop-in testing replacement for server databases
- Portable deployment without database servers
- Extensible architecture for custom integrations
- Consistent API across desktop, server, and browser
Key Features
WitDatabase is packed with features that make it a complete embedded database solution for .NET applications. This section provides a detailed overview of the main capabilities.
Full SQL Support
WitDatabase implements a comprehensive SQL dialect based on SQLite syntax, with enhancements for .NET compatibility.
Data Definition Language (DDL)
-- Tables with all constraint types
CREATE TABLE Orders (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
CustomerId INTEGER NOT NULL,
OrderDate DATETIME DEFAULT NOW(),
TotalAmount DECIMAL(18,2) NOT NULL,
Status VARCHAR(20) DEFAULT 'pending',
Version ROWVERSION,
CONSTRAINT FK_Orders_Customers
FOREIGN KEY (CustomerId) REFERENCES Customers(Id)
ON DELETE CASCADE,
CONSTRAINT CHK_Orders_Amount
CHECK (TotalAmount >= 0)
);
-- Indexes (unique, composite, partial, covering)
CREATE INDEX IX_Orders_Date ON Orders(OrderDate);
CREATE UNIQUE INDEX IX_Orders_Code ON Orders(OrderCode);
CREATE INDEX IX_Orders_Active ON Orders(Status) WHERE Status != 'cancelled';
CREATE INDEX IX_Orders_Cover ON Orders(CustomerId) INCLUDE (TotalAmount, Status);
-- Views
CREATE VIEW ActiveOrders AS
SELECT o.*, c.Name AS CustomerName
FROM Orders o
JOIN Customers c ON o.CustomerId = c.Id
WHERE o.Status != 'cancelled';
-- Triggers
CREATE TRIGGER TR_Orders_Audit
AFTER UPDATE ON Orders
BEGIN
INSERT INTO OrderAudit (OrderId, ChangedAt) VALUES (NEW.Id, NOW());
END;
-- Sequences
CREATE SEQUENCE OrderNumberSeq START WITH 1000 INCREMENT BY 1;
Data Manipulation Language (DML)
-- Standard CRUD
INSERT INTO Customers (Name, Email) VALUES ('John', 'john@example.com');
UPDATE Customers SET Email = 'new@example.com' WHERE Id = 1;
DELETE FROM Customers WHERE Id = 1;
-- RETURNING clause (get affected rows)
INSERT INTO Customers (Name, Email)
VALUES ('Jane', 'jane@example.com')
RETURNING Id, CreatedAt;
UPDATE Products SET Price = Price * 1.1
WHERE Category = 'Electronics'
RETURNING Id, Name, Price;
-- UPSERT (INSERT ... ON CONFLICT)
INSERT INTO Products (SKU, Name, Price)
VALUES ('ABC-123', 'Widget', 29.99)
ON CONFLICT (SKU) DO UPDATE SET Price = EXCLUDED.Price;
-- MERGE statement
MERGE INTO TargetTable t
USING SourceTable s ON t.Id = s.Id
WHEN MATCHED THEN UPDATE SET t.Value = s.Value
WHEN NOT MATCHED THEN INSERT (Id, Value) VALUES (s.Id, s.Value);
-- Bulk operations
TRUNCATE TABLE Logs;
Query Features
-- JOINs (all types)
SELECT c.Name, o.TotalAmount
FROM Customers c
INNER JOIN Orders o ON c.Id = o.CustomerId
LEFT JOIN OrderItems oi ON o.Id = oi.OrderId;
-- Subqueries
SELECT * FROM Products
WHERE Price > (SELECT AVG(Price) FROM Products);
-- Common Table Expressions (CTEs)
WITH TopCustomers AS (
SELECT CustomerId, SUM(TotalAmount) AS Total
FROM Orders
GROUP BY CustomerId
HAVING SUM(TotalAmount) > 10000
)
SELECT c.Name, tc.Total
FROM Customers c
JOIN TopCustomers tc ON c.Id = tc.CustomerId;
-- Recursive CTEs
WITH RECURSIVE CategoryTree AS (
SELECT Id, Name, ParentId, 0 AS Level
FROM Categories WHERE ParentId IS NULL
UNION ALL
SELECT c.Id, c.Name, c.ParentId, ct.Level + 1
FROM Categories c
JOIN CategoryTree ct ON c.ParentId = ct.Id
)
SELECT * FROM CategoryTree;
-- Window Functions
SELECT
Name,
Department,
Salary,
ROW_NUMBER() OVER (PARTITION BY Department ORDER BY Salary DESC) AS Rank,
AVG(Salary) OVER (PARTITION BY Department) AS DeptAvg,
LAG(Salary) OVER (ORDER BY Salary) AS PrevSalary
FROM Employees;
-- Set operations
SELECT Name FROM Customers
UNION
SELECT Name FROM Suppliers;
Built-in Functions
WitDatabase includes 60+ built-in functions across multiple categories:
Category
Functions
String
LENGTH, UPPER, LOWER, TRIM, LTRIM, RTRIM, SUBSTR, REPLACE, CONCAT, INSTR, LEFT, RIGHT, REVERSE, REPEAT, SPACE, CHAR, ASCII, LPAD, RPAD, FORMAT
Numeric
ABS, ROUND, FLOOR, CEIL, CEILING, POWER, SQRT, MOD, SIGN, RANDOM, SIN, COS, TAN, ASIN, ACOS, ATAN, LOG, LOG10, EXP, PI
Date/Time
NOW, DATE, TIME, YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, DATEADD, DATEDIFF, DAYOFWEEK, DAYOFYEAR, WEEKOFYEAR
Aggregate
COUNT, SUM, AVG, MIN, MAX, GROUP_CONCAT, TOTAL
Window
ROW_NUMBER, RANK, DENSE_RANK, NTILE, PERCENT_RANK, CUME_DIST, LAG, LEAD, FIRST_VALUE, LAST_VALUE, NTH_VALUE
JSON
JSON_VALUE, JSON_QUERY, JSON_EXTRACT, JSON_SET, JSON_INSERT, JSON_REMOVE, JSON_TYPE, JSON_VALID, JSON_LENGTH, JSON_KEYS, JSON_ARRAY, JSON_OBJECT
Conversion
CAST, CONVERT, COALESCE, NULLIF, IFNULL, IIF, TYPEOF
System
DATABASE, VERSION, CHANGES, LAST_INSERT_ROWID, NEWGUID
Dual Storage Engines
Choose the right storage engine for your workload:
B+Tree Engine (Default)
Optimized for read-heavy workloads and general-purpose use.
var db = new WitDatabaseBuilder()
.WithFilePath("data.witdb")
.WithBTree() // B+Tree engine
.WithCacheSize(2000) // 2000 pages in cache
.WithPageSize(4096) // 4KB pages
.Build();
Characteristics:
- Balanced read/write performance
- Excellent for random access patterns
- Efficient range queries
- Good for OLTP workloads
- Lower memory usage
Best for: General applications, desktop apps, random reads/writes
LSM-Tree Engine
Optimized for write-heavy workloads and sequential writes.
var db = new WitDatabaseBuilder()
.WithLsmTree("./lsm-data", opts =>
{
opts.EnableWal = true; // Write-ahead log
opts.EnableBlockCache = true; // Block caching
opts.BlockCacheSizeBytes = 64 * 1024 * 1024; // 64MB cache
opts.MemTableSizeLimit = 4 * 1024 * 1024; // 4MB memtable
opts.BackgroundCompaction = true; // Background compaction
})
.Build();
Characteristics:
- Extremely fast sequential writes
- Write amplification reduced via compaction
- Bloom filters for fast negative lookups
- Higher throughput for bulk inserts
- Higher memory usage
Best for: Logging, time-series data, event sourcing, IoT data collection
Comparison
Aspect
B+Tree
LSM-Tree
Random writes
Good
Excellent
Sequential writes
Good
Excellent
Point reads
Excellent
Good
Range scans
Excellent
Good
Space efficiency
Better
Requires compaction
Memory usage
Lower
Higher
Write amplification
Higher
Lower
ACID Transactions
Full transaction support with multiple isolation levels.
Basic Transactions
using var connection = new WitDbConnection("Data Source=app.witdb");
connection.Open();
using var transaction = connection.BeginTransaction();
try
{
using var cmd = connection.CreateCommand();
cmd.Transaction = transaction;
// Transfer money between accounts
cmd.CommandText = "UPDATE Accounts SET Balance = Balance - 100 WHERE Id = 1";
cmd.ExecuteNonQuery();
cmd.CommandText = "UPDATE Accounts SET Balance = Balance + 100 WHERE Id = 2";
cmd.ExecuteNonQuery();
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
Savepoints
Partial rollback within a transaction:
using var transaction = connection.BeginTransaction();
// First operation
cmd.CommandText = "INSERT INTO Orders ...";
cmd.ExecuteNonQuery();
// Create savepoint
transaction.Save("before_items");
try
{
// Second operation (might fail)
cmd.CommandText = "INSERT INTO OrderItems ...";
cmd.ExecuteNonQuery();
}
catch
{
// Rollback only to savepoint, keep the order
transaction.Rollback("before_items");
}
transaction.Commit(); // Order is saved, items are rolled back
Isolation Levels
WitDatabase supports 5 isolation levels:
// Set isolation level
using var transaction = connection.BeginTransaction(IsolationLevel.Serializable);
// Or via connection string
var conn = new WitDbConnection("Data Source=app.witdb;IsolationLevel=Snapshot");
Level
Dirty Read
Non-Repeatable Read
Phantom
Description
ReadUncommitted
✅ Yes
✅ Yes
✅ Yes
No isolation, maximum performance
ReadCommitted
❌ No
✅ Yes
✅ Yes
See only committed data
RepeatableRead
❌ No
❌ No
✅ Yes
Reads are consistent within transaction
Serializable
❌ No
❌ No
❌ No
Full isolation, range locks
Snapshot
❌ No
❌ No
❌ No
Point-in-time view, MVCC-based
Multi-Version Concurrency Control (MVCC)
MVCC allows readers to never block writers and vice versa.
var db = new WitDatabaseBuilder()
.WithFilePath("data.witdb")
.WithMvcc() // Enable MVCC
.WithDefaultIsolationLevel(IsolationLevel.Snapshot) // Snapshot isolation
.Build();
How it works:
- Each write creates a new version of the data
- Readers see a consistent snapshot from transaction start
- No read locks needed — excellent for read-heavy workloads
- Background garbage collection removes old versions
Row-Level Locking for fine-grained control:
-- Exclusive lock (FOR UPDATE)
SELECT * FROM Orders WHERE Id = 1 FOR UPDATE;
-- Shared lock (FOR SHARE)
SELECT * FROM Orders WHERE Status = 'pending' FOR SHARE;
-- Non-blocking variants
SELECT * FROM Orders WHERE Id = 1 FOR UPDATE NOWAIT;
SELECT * FROM Orders WHERE Status = 'pending' FOR UPDATE SKIP LOCKED;
Built-in Encryption
Protect your data at rest with industry-standard encryption algorithms.
AES-256-GCM (Default)
Hardware-accelerated on modern CPUs:
// Via builder
var db = new WitDatabaseBuilder()
.WithFilePath("secure.witdb")
.WithEncryption("MySecretPassword123!")
.Build();
// Via connection string
var conn = new WitDbConnection(
"Data Source=secure.witdb;Encryption=aes-gcm;Password=MySecretPassword123!");
ChaCha20-Poly1305
Ideal for Blazor WebAssembly and ARM devices without AES-NI:
// Requires OutWit.Database.Core.BouncyCastle package
var db = new WitDatabaseBuilder()
.WithFilePath("secure.witdb")
.WithBouncyCastleEncryption("MySecretPassword123!")
.Build();
// Via connection string
var conn = new WitDbConnection(
"Data Source=secure.witdb;Encryption=chacha20-poly1305;Password=MySecretPassword123!");
Encryption Features
Feature
Description
Page-level encryption
Each page encrypted independently
Authenticated encryption
Tamper detection via AEAD
PBKDF2 key derivation
Secure password-to-key conversion
Per-database salt
Prevents rainbow table attacks
Transparent operation
Encryption/decryption automatic
ADO.NET Provider
Full ADO.NET 2.0 compatible implementation:
// Standard ADO.NET patterns
using var connection = new WitDbConnection("Data Source=app.witdb");
connection.Open();
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT * FROM Users WHERE Email = @email";
cmd.Parameters.AddWithValue("@email", "john@example.com");
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
Console.WriteLine(
Loading...
quot;{reader["Id"]}: {reader["Name"]}");
}
Features:
WitDbConnection, WitDbCommand, WitDbDataReader
WitDbParameter with named parameters (@param)
WitDbTransaction with all isolation levels
WitDbConnectionStringBuilder
WitDbProviderFactory for DI/ADO.NET factory pattern
- Full async/await support (
OpenAsync, ExecuteReaderAsync, etc.)
- Connection pooling
- Schema information (
GetSchema)
Entity Framework Core Provider
Complete EF Core integration with migrations support:
public class AppDbContext : DbContext
{
public DbSet<User> Users => Set<User>();
public DbSet<Order> Orders => Set<Order>();
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseWitDb("Data Source=app.witdb");
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>(entity =>
{
entity.HasIndex(e => e.Email).IsUnique();
entity.Property(e => e.Version).IsRowVersion();
});
modelBuilder.Entity<Order>(entity =>
{
entity.HasOne(o => o.User)
.WithMany(u => u.Orders)
.HasForeignKey(o => o.UserId);
});
}
}
// Use with DI
services.AddDbContext<AppDbContext>(options =>
options.UseWitDb(Configuration.GetConnectionString("Default")));
Supported Features:
Category
Features
Basic
DbContext, DbSet, SaveChanges, LINQ queries
Relationships
One-to-many, many-to-many, owned entities
Indexes
Unique, composite, filtered
Constraints
Primary key, foreign key, check, default values
Concurrency
Row versioning, optimistic concurrency
Migrations
Full migration support, EnsureCreated
Advanced
Computed columns, JSON columns, raw SQL
Bulk Extensions
High-performance bulk operations, similar to EFCore.BulkExtensions:
using OutWit.Database.EntityFramework.Extensions;
// Bulk Insert — 3x faster than AddRange + SaveChanges
var users = Enumerable.Range(1, 10000)
.Select(i => new User { Name =
Loading...
quot;User{i}", Email =
Loading...
quot;user{i}@test.com" });
int inserted = await context.BulkInsertAsync(users);
// Bulk Insert with progress reporting
var options = new BulkOptions
{
BatchSize = 1000,
BatchProgress = count => Console.WriteLine(
Loading...
quot;Inserted {count} rows")
};
await context.BulkInsertAsync(users, options);
// Bulk Update — update entities by primary key
var usersToUpdate = await context.Users.Take(1000).ToListAsync();
foreach (var user in usersToUpdate)
user.Status = "Active";
await context.BulkUpdateAsync(usersToUpdate);
// Bulk Delete — delete entities by primary key
var usersToDelete = await context.Users
.Where(u => u.Status == "Inactive")
.ToListAsync();
await context.BulkDeleteAsync(usersToDelete);
// Bulk Delete with predicate
await context.BulkDeleteAsync<User>(u => u.Name.StartsWith("Test"));
// Bulk InsertOrUpdate (Upsert) — insert new or update existing
var mixedUsers = new[] {
new User { Id = 1, Name = "Updated1" }, // update if exists
new User { Id = 999, Name = "New999" } // insert if not exists
};
await context.BulkInsertOrUpdateAsync(mixedUsers);
Available Bulk Methods:
Method
Description
Performance
BulkInsert / BulkInsertAsync
Insert multiple entities
3x faster than SaveChanges
BulkUpdate / BulkUpdateAsync
Update by primary key
Prepared statement reuse
BulkDelete / BulkDeleteAsync
Delete by primary key
Prepared statement reuse
BulkInsertOrUpdate / BulkInsertOrUpdateAsync
Upsert (ON CONFLICT)
Single statement per row
LINQ Method Translations:
// All these LINQ methods translate to SQL
context.Users
.Where(u => u.Email.Contains("@gmail.com")) // LIKE '%@gmail.com%'
.Where(u => u.CreatedAt.Year == 2024) // YEAR(CreatedAt) = 2024
.Where(u => EF.Functions.Like(u.Name, "J%")) // LIKE 'J%'
.OrderBy(u => u.Name.Length) // LENGTH(Name)
.Select(u => new {
u.Name,
UpperEmail = u.Email.ToUpper(), // UPPER(Email)
Age = DateTime.Now.Year - u.BirthDate.Year // YEAR(NOW()) - YEAR(BirthDate)
})
.ToList();
Parallel and Concurrent Access
WitDatabase supports thread-safe concurrent access for multi-threaded applications.
Parallel Modes
// Via builder
var db = new WitDatabaseBuilder()
.WithFilePath("data.witdb")
.WithBTree()
.WithParallelMode(ParallelMode.Auto) // Thread-safe mode
.Build();
// Via connection string
var conn = new WitDbConnection(
"Data Source=data.witdb;ParallelMode=Auto");
Available Modes:
Mode
Description
Use Case
None
Single-threaded, best performance
Desktop apps, single-user
Auto
Detects multi-threaded access, activates safety automatically
General purpose
Latched
Reader-writer locks, multiple readers, single writer
Read-heavy workloads
Buffered
Thread-local write buffers with background merge
Write-heavy workloads
Concurrent Access Example
// Multiple threads can safely access the database
var db = new WitDatabaseBuilder()
.WithFilePath("concurrent.witdb")
.WithBTree()
.WithParallelMode(ParallelMode.Auto)
.WithTransactions()
.Build();
// Parallel reads are lock-free with MVCC
var tasks = Enumerable.Range(0, 10).Select(i => Task.Run(() =>
{
using var tx = db.BeginTransaction(IsolationLevel.Snapshot);
var value = db.Get(key, tx);
// Process value...
tx.Commit();
}));
await Task.WhenAll(tasks);
Connection Pooling with EF Core
// EF Core manages connection pooling automatically
services.AddDbContext<AppDbContext>(options =>
options.UseWitDb("Data Source=app.witdb;ParallelMode=Auto"));
// Multiple requests can access the database concurrently
// Each request gets its own connection from the pool
Note: Parallel mode provides thread safety, not parallelism speedup. Embedded databases are I/O bound — parallel writes to the same file don't improve throughput. Use parallel mode when you need concurrent access from multiple threads (e.g., web applications, background services).
Cross-Platform Support
WitDatabase runs everywhere .NET runs:
Platform
Support
Notes
Windows (x64, x86, ARM64)
✅ Full
All features
Linux (x64, ARM64)
✅ Full
All features
macOS (x64, Apple Silicon)
✅ Full
All features
Blazor WebAssembly
✅ Full
IndexedDB storage
iOS / Android (MAUI)
✅ Full
File storage
Blazor WebAssembly
Run WitDatabase entirely in the browser:
// In Blazor component
@inject IJSRuntime JSRuntime
var db = new WitDatabaseBuilder()
.WithIndexedDbStorage("MyAppDatabase", JSRuntime)
.WithBTree()
.WithEncryption("user-password") // Data encrypted in browser
.WithTransactions()
.Build();
await db.InitializeAsync();
// Now use like any other database
await db.PutAsync(key, value);
var result = await db.GetAsync(key);
Browser Features:
- Data persisted to IndexedDB
- Survives page refresh and browser restart
- Encryption works in browser (AES-GCM or ChaCha20)
- Full MVCC and transaction support
Modular Architecture
Every component can be replaced with a custom implementation:
// Register custom providers
ProviderRegistry.Instance.Register<ICryptoProvider>("my-hsm",
p => new HsmCryptoProvider(p.GetRequired<string>("keyId")));
ProviderRegistry.Instance.Register<IStorage>("cloud-storage",
p => new S3Storage(p.GetRequired<string>("bucket")));
// Use custom providers
var db = new WitDatabaseBuilder()
.WithStorage("cloud-storage", new ProviderParameters()
.Set("bucket", "my-database-bucket"))
.WithEncryption("my-hsm", new ProviderParameters()
.Set("keyId", "production-key-001"))
.Build();
Pluggable Interfaces:
Interface
Purpose
Built-in Options
IKeyValueStore
Storage engine
B+Tree, LSM-Tree, In-Memory
IStorage
Storage backend
File, Memory, Encrypted, IndexedDB
ICryptoProvider
Encryption
AES-GCM, ChaCha20-Poly1305
IPageCache
Caching
LRU, Sharded Clock
ITransactionJournal
Durability
Rollback Journal, WAL
ISecondaryIndexFactory
Indexes
B+Tree indexes
Performance
This section provides an honest assessment of WitDatabase performance, including benchmarks, comparisons with alternatives, and guidance on what to expect.
Performance Philosophy
Before diving into numbers, it's important to understand the context:
SQLite is native C code — comparing managed .NET code to native C isn't apples-to-apples. SQLite will generally be faster for large data operations.
The fair comparison is with LiteDB — both are pure managed .NET embedded databases. This is where WitDatabase truly competes.
WitDatabase optimizes for .NET workloads — using Span<T>, ArrayPool<T>, and modern .NET features to minimize allocations and maximize throughput for typical application patterns.
Benchmark Environment
All benchmarks were run with:
- OS: Windows 11
- CPU: AMD Ryzen 9 5950X
- Runtime: .NET 10.0
- Tool: BenchmarkDotNet v0.15.8
Executive Summary
Category
vs SQLite
vs LiteDB
Notes
INSERT (small batches)
Faster
5-7× faster
No P/Invoke overhead wins
INSERT (large batches)
1.5-2× slower
Comparable
Native code advantage
UPDATE
Comparable to faster
2-4× faster
Depends on batch size
Transactions (small)
10-20× faster
1.3-3× faster
P/Invoke overhead dominates
Point queries (PK)
Faster
10× faster
Small operation, no P/Invoke
Full table scans
3-5× slower
2-3× faster
Native code advantage
GROUP BY
3-7× slower
1.2-1.5× faster
Native code advantage
Complex JOINs
10-50× slower
5-10× slower
Needs optimization
Memory usage
Higher (expected)
Lower or equal
Managed vs native
Understanding the Pattern
WitDatabase is faster than SQLite when:
- Operations are small (few rows) — P/Invoke overhead dominates SQLite's cost
- Many small transactions — SQLite's overhead per transaction is significant
- Point queries by PK — single B+Tree lookup, no P/Invoke
SQLite is faster than WitDatabase when:
- Operations involve many rows — native C code processes data faster
- Complex queries (JOINs, aggregations) — SQLite's mature optimizer shines
- Large table scans — native iteration is faster than managed code
The fair comparison is with LiteDB — both are pure managed .NET. WitDatabase consistently outperforms LiteDB while providing full SQL support that LiteDB lacks.
Detailed Benchmarks
INSERT Performance
WitDatabase performs well on write operations, particularly for small batches where P/Invoke overhead would dominate SQLite's performance.
INSERT in Transaction
Operation (1000 rows)
WitDatabase
SQLite
LiteDB
INSERT in transaction
5.6ms
8.6ms
5.5ms
INSERT without transaction
0.93ms
808ms
5.35ms
INSERT with RETURNING
2.8ms
8.8ms
N/A
Key findings:
- WitDatabase is comparable to SQLite for transactional inserts (slightly faster due to no P/Invoke)
- WitDatabase is much faster than SQLite for non-transactional inserts (SQLite does fsync per row — this is a SQLite design choice, not a fair comparison)
- WitDatabase is 5.8× faster than LiteDB without transaction
- WitDatabase supports
RETURNING clause (LiteDB doesn't)
Bulk INSERT Performance
Batch Size
WitDatabase
SQLite
LiteDB
100 rows
2.7ms
1.1ms
1.2ms
1,000 rows
7.9ms
6.0ms
6.0ms
5,000 rows
34.9ms
21.2ms
21.2ms
Key findings:
- For small batches (100 rows), WitDatabase is competitive
- For larger batches, SQLite is faster — native C code advantage becomes significant
- WitDatabase remains comparable to LiteDB across all batch sizes
UPDATE Performance
WitDatabase's streaming UPDATE optimization delivers excellent results.
Operation (1000 rows)
WitDatabase
SQLite
LiteDB
UPDATE by PK
6.5ms
8.2ms
13.6ms
UPDATE by indexed column
7.4ms
7.9ms
6.9ms
Bulk UPDATE (all rows)
11.4ms
7.3ms
46ms
UPDATE with RETURNING
0.77ms
7.5ms
N/A
Key findings:
- WitDatabase is 2× faster than LiteDB for PK updates
- WitDatabase is 4× faster than LiteDB for bulk updates
UPDATE ... RETURNING is 10× faster than SQLite
DELETE Performance
Optimized DELETE with fast-path constraint validation.
Operation
WitDatabase
SQLite
LiteDB
DELETE batch (100 rows)
0.26ms
6.45ms
0.43ms
DELETE batch (1000 rows)
0.32ms
6.50ms
0.58ms
DELETE by PK (100 rows)
0.33ms
6.43ms
0.46ms
Key findings:
- WitDatabase DELETE is faster than both SQLite and LiteDB
- ~20× faster than SQLite for batch deletes
Transaction Performance
WitDatabase's in-memory transaction staging combined with no P/Invoke overhead makes small transactions very fast.
Operation
WitDatabase
SQLite
LiteDB
N operations in single TX
0.67ms
6.9ms
0.89ms
Mixed TX (INSERT/UPDATE/SELECT)
0.67ms
7.1ms
1.5ms
TX with savepoint
0.33ms
6.8ms
N/A
Rollback
0.34ms
1.2ms
1.0ms
Key findings:
- WitDatabase is 10× faster than SQLite for small transaction operations — P/Invoke overhead dominates SQLite's cost
- WitDatabase is 2× faster than LiteDB for mixed transactions
- Savepoints are very fast (LiteDB doesn't support savepoints)
Note: The large difference vs SQLite is primarily due to P/Invoke overhead per operation. For transactions with thousands of operations, the gap narrows significantly as actual data processing time dominates.
SELECT Performance
Point Queries (Primary Key Lookup)
Operation (100 lookups)
WitDatabase
SQLite
LiteDB
Point query by PK
0.22ms
5.33ms
2.39ms
Key findings:
- WitDatabase shows excellent results here — but context matters
- The large difference vs SQLite is due to P/Invoke overhead per query — each SQLite call crosses the managed/native boundary
- For small operations like single-row lookups, this overhead dominates
- WitDatabase is 10× faster than LiteDB (fair managed-to-managed comparison)
- B+Tree index implementation is efficient
Full Table Scans
Operation (1000 rows)
WitDatabase
SQLite
LiteDB
SELECT *
0.56ms
0.18ms
1.5ms
SELECT with WHERE
1.30ms
0.24ms
1.41ms
SELECT with ORDER BY
1.40ms
0.22ms
2.39ms
SELECT with LIMIT
0.15ms
0.07ms
0.16ms
Key findings:
- SQLite is faster for full scans (native code advantage)
- WitDatabase is 2.7× faster than LiteDB for full scans
- WitDatabase is 1.7× faster than LiteDB for ORDER BY
Large Table Performance (10,000 rows)
Operation
WitDatabase
SQLite
LiteDB
SELECT *
9.44ms
1.8ms
14.91ms
SELECT with ORDER BY
43.2ms
2.2ms
22.96ms
Key findings:
- For large tables, SQLite's native code advantage is more pronounced
- WitDatabase still beats LiteDB by 1.6× on full scans
- ORDER BY on large tables favors LiteDB (1.9× faster)
Aggregation Performance
Operation (10,000 rows)
WitDatabase
SQLite
LiteDB
COUNT(*)
10.0ms
0.06ms
3.1ms
SUM/AVG
10.5ms
0.45ms
16.0ms
GROUP BY (single column)
12.4ms
1.7ms
17.1ms
GROUP BY (multiple columns)
14.1ms
4.7ms
16.9ms
GROUP BY with HAVING
18.8ms
1.6ms
16.8ms
Key findings:
- SQLite dominates aggregations (native code + optimized execution)
- WitDatabase is 1.4× faster than LiteDB for GROUP BY
- WitDatabase beats LiteDB in all aggregation scenarios
JOIN Performance
JOINs are currently WitDatabase's weakest area.
Operation (100 rows per table)
WitDatabase
SQLite
LiteDB
INNER JOIN (2 tables)
0.77ms
0.07ms
0.15ms
LEFT JOIN
0.72ms
0.07ms
0.15ms
INNER JOIN (3 tables)
2.9ms
0.08ms
0.22ms
INNER JOIN (4 tables)
3.6ms
0.09ms
0.26ms
JOIN with GROUP BY
0.75ms
0.09ms
0.16ms
Key findings:
- JOINs are 10-50× slower than SQLite
- JOINs are 5-14× slower than LiteDB
- Current nested loop implementation doesn't use hash joins
- This is a known area for future optimization
Memory Usage
Operation
WitDatabase
SQLite
LiteDB
INSERT 1000 rows
7.3MB
0.6MB
9.3MB
INSERT 5000 rows
7.5MB
N/A
54.4MB
UPDATE 1000 rows
9.8MB
0.4MB
34MB
SELECT 1000 rows
2.0MB
0.7KB
2.3MB
GROUP BY 10000 rows
21.9MB
1KB
23.2MB
Key findings:
- SQLite uses minimal managed memory (native code)
- WitDatabase uses 7× less memory than LiteDB for bulk inserts
- WitDatabase uses 3.5× less memory than LiteDB for updates
- Memory usage is comparable to or better than LiteDB in all scenarios
Performance by Use Case
When WitDatabase Excels
Use Case
Performance
Why
CRUD operations
✅ Excellent
Optimized for typical app patterns
Transactions
✅ Excellent
Lightweight in-memory staging
Point queries (by PK)
✅ Excellent
Efficient B+Tree implementation
Mixed workloads (OLTP)
✅ Excellent
Balanced read/write performance
Small-medium datasets
✅ Excellent
Sweet spot for embedded use
When WitDatabase is Competitive
Use Case
Performance
Notes
Full table scans
⚠️ Good
Faster than LiteDB, slower than SQLite
Aggregations
⚠️ Good
Beats LiteDB, slower than SQLite
Large datasets (>100K rows)
⚠️ Acceptable
Consider SQLite if this dominates
When to Consider Alternatives
Use Case
Recommendation
Complex multi-table JOINs
Consider SQLite if JOINs dominate workload
Heavy analytical queries
Consider SQLite for OLAP patterns
Minimal memory footprint
SQLite uses less managed memory
Storage Engine Performance
B+Tree vs LSM-Tree
Operation
B+Tree
LSM-Tree
Recommendation
Random reads
✅ Faster
Slower
B+Tree for read-heavy
Sequential writes
Good
✅ Faster
LSM for write-heavy
Random writes
Good
✅ Faster
LSM for write-heavy
Range scans
✅ Faster
Good
B+Tree for queries
Space efficiency
✅ Better
Requires compaction
B+Tree for space
Memory usage
✅ Lower
Higher
B+Tree for memory
General guidance:
- B+Tree (default): Best for most applications
- LSM-Tree: Best for logging, time-series, event sourcing
Optimization Tips
1. Use Transactions for Batch Operations
// ❌ Slow — each insert is a separate transaction
foreach (var item in items)
connection.Execute("INSERT INTO T VALUES (@v)", new { v = item });
// ✅ Fast — single transaction
using var tx = connection.BeginTransaction();
foreach (var item in items)
connection.Execute("INSERT INTO T VALUES (@v)", new { v = item }, tx);
tx.Commit();
2. Use Bulk Operations in EF Core
// ❌ Slow — N database round trips
context.AddRange(entities);
await context.SaveChangesAsync();
// ✅ Fast — single optimized operation
await context.BulkInsertAsync(entities);
3. Create Indexes for Frequent Queries
-- Speeds up WHERE and JOIN on these columns
CREATE INDEX IX_Orders_CustomerId ON Orders(CustomerId);
CREATE INDEX IX_Orders_Date ON Orders(OrderDate);
4. Use LIMIT for Large Results
// ❌ May load millions of rows
var all = context.Orders.ToList();
// ✅ Paginated — predictable performance
var page = context.Orders
.OrderBy(o => o.Id)
.Skip(offset)
.Take(100)
.ToList();
5. Choose the Right Storage Engine
// For read-heavy workloads (default)
.WithBTree()
// For write-heavy workloads (logging, events)
.WithLsmTree()
6. Tune Cache Size
// More cache = fewer disk reads (memory tradeoff)
.WithCacheSize(5000) // 5000 pages (~20MB with 4KB pages)
Benchmark Methodology
All benchmarks use BenchmarkDotNet with:
- Multiple warmup iterations
- Statistical analysis (mean, median, standard deviation)
- Memory allocation tracking
- GC collection counting
To run benchmarks yourself:
cd Benchmarks/OutWit.Database.Benchmarks
dotnet run -c Release
# Specific benchmark
dotnet run -c Release -- --filter "*InsertBenchmarks*"
# Export results
dotnet run -c Release -- --exporters json csv markdown
Future Optimizations
The following optimizations are planned for future releases:
Area
Optimization
Expected Impact
JOINs
Hash join implementation
5-10× faster for equality JOINs
JOINs
Merge join for sorted inputs
2-3× faster for sorted JOINs
Aggregations
Parallel aggregation
2-4× faster on multi-core
Index seeks
Cursor caching
3-5× faster for repeated lookups
Scans
SIMD acceleration
2× faster for numeric aggregates
Summary
Performance Reality
WitDatabase vs SQLite:
- For small operations (few rows, point queries, small transactions): WitDatabase can be faster due to no P/Invoke overhead
- For large operations (bulk processing, complex queries, table scans): SQLite is faster due to native C code
- This is the fundamental managed vs native tradeoff
WitDatabase vs LiteDB (fair comparison — both managed .NET):
- WitDatabase is consistently faster across most operations
- WitDatabase uses less memory for bulk operations
- WitDatabase provides full SQL while LiteDB is NoSQL only
When to Choose WitDatabase
✅ Good fit:
- Pure .NET requirement (no native dependencies)
- Typical OLTP patterns (CRUD, small transactions)
- Testing/development database replacement
- Desktop/mobile applications with moderate data sizes
- When you need SQL but can't use SQLite
⚠️ Consider carefully:
- Large datasets (>100K rows) with complex queries
- JOIN-heavy analytical workloads
- Maximum raw performance is critical
❌ Consider SQLite instead:
- Complex multi-table JOINs dominate workload
- Heavy analytical/OLAP queries
- Native dependencies are acceptable
- Absolute maximum performance required
Bottom line: WitDatabase offers the best SQL database option for pure .NET scenarios, outperforming LiteDB while providing SQL features it lacks. For scenarios where native dependencies are acceptable and raw performance on large datasets is critical, SQLite remains the faster choice.