Benchmarks

View Source

Performance benchmarks for Barrel DocDB operations using the built-in benchmark suite.

Test Environment

  • Hardware: Apple M1, 16GB RAM, SSD
  • Erlang/OTP: 27
  • Dataset: 5,000 documents (~500 bytes each)
  • Database: Single node, default configuration

CRUD Operations

Basic document operations show strong single-document performance:

OperationThroughputp50 Latencyp99 Latency
Insert5,785 ops/s147 us252 us
Read111,111 ops/s8 us22 us
Update118,343 ops/s8 us21 us
Delete5,660 ops/s8 us20 us

Key observations:

  • Single-document reads are very fast (8 us median)
  • Updates are read-modify-write cycles with excellent latency
  • Writes include RocksDB sync and indexing overhead
  • Bulk inserts can achieve higher throughput with batching

Query Performance

Query performance varies based on query pattern and result set size.

Index-Only Queries

These queries use include_docs => false and benefit from pure index scans:

Query TypeThroughputp50 LatencyNotes
Prefix with LIMIT 1039,354 ops/s23 usAutocomplete use case
Prefix (all matches)44,033 ops/s18 us~1,111 docs returned
Simple equality + LIMIT 1027,800 ops/s27 usEarly termination
Pure compare + LIMIT 106,222 ops/s157 usRange scan with limit
Pure compare (age>50)2,101 ops/s459 us~2,380 docs returned
Pure Top-K (ORDER BY + LIMIT)3,889 ops/s233 usNo filter, just sort
Selective equality488 ops/s2.0 ms~1,666 docs (1/3)
Nested path55 ops/s18 msNested object access
Top-K with filter82 ops/s12 msORDER BY + LIMIT + filter

Paginated Queries

Paginated queries with continuation tokens deliver excellent performance:

Page SizeThroughputp50 Latency
100 docs/page4,906 pages/s187 us
500 docs/page3,163 pages/s347 us

Pagination is Essential

Always paginate large result sets. Fetching thousands of documents in a single query is slow and memory-intensive. Use limit and continuation tokens to stream results efficiently:

%% First page
{ok, Results, #{continuation := Token}} =
    barrel_docdb:find(Db, #{where => Query, limit => 100}).

%% Next pages
{ok, More, #{continuation := NextToken}} =
    barrel_docdb:find(Db, #{where => Query, limit => 100, continuation => Token}).

Paginated queries run in microseconds per page vs hundreds of milliseconds for unbounded queries.

Changes Feed

OperationThroughputp50 LatencyNotes
Full scan (5K docs)-98 msOne-time scan
Incremental (100/batch)2,857 batches/s319 usContinuous polling
Subscription notification13,823 ops/s65 usPub/sub latency

Subscription latency of 65 us is excellent for real-time applications.

Architecture: ARS Model

Barrel DocDB is built on an ARS (Append, Reduce, Stream) storage model:

  • Append: All writes are append-only with MVCC versioning
  • Reduce: Indexes are derived by reducing over the append-only log
  • Stream: Changes feed provides a replayable event stream

This architecture enables:

  1. Flexible query engines: The storage layer is decoupled from query execution. Different query engines can be built on top of the same storage.
  2. Custom databases: The ARS model can support different data models (document, graph, time-series) with the same underlying storage.
  3. Efficient replication: Append-only logs are naturally suited for P2P sync.
  4. Time-travel queries: MVCC enables querying historical states.

The benchmark numbers reflect the current document query engine. Future query engines could optimize for different access patterns (e.g., graph traversal, analytics) while reusing the same storage layer.

When to Use Barrel DocDB

Best Use Cases

Use CaseWhy
P2P ReplicationBuilt-in one-shot and continuous sync
Real-time subscriptions65 us notification latency
Prefix/autocomplete18-23 us query latency
Edge computingEmbedded in Erlang, sync when online
MVCC conflict detectionRevision tracking built-in
Paginated APIsFast continuation-based pagination

Trade-offs

When other tools may be better:

If you need...Consider instead
Maximum single-node query speedSQLite with JSON1, DuckDB
Simple key-value accessDirect RocksDB, ETS
Complex SQL analyticsPostgreSQL, DuckDB
Full-text searchMeilisearch, Elasticsearch

Honest Comparison

For single-node performance only, other databases will often be faster:

  • SQLite: 3-10x faster for complex queries, excellent JSON support
  • RocksDB direct: 2-5x faster for raw key-value access
  • Mnesia: Native Erlang, different consistency model

Barrel's value is in the combination of:

  1. Embedded Erlang integration
  2. Document model with automatic indexing
  3. P2P replication topologies
  4. Real-time change subscriptions
  5. MVCC for conflict-free sync
  6. ARS architecture for future extensibility

If you only need single-node storage without replication, simpler tools exist. Choose Barrel when you need sync, real-time, and architectural flexibility.

Remote access

These numbers are for the embedded Erlang API, which is how barrel_docdb is used. To reach a database over HTTP, run the barrel_server app; network I/O and JSON serialization add overhead on top of the figures above.

Running Benchmarks

Run the benchmark suite on your hardware:

cd bench
./run_bench.sh                 # Default: 10,000 docs, 10,000 iterations
./run_bench.sh 5000 100        # Custom: 5,000 docs, 100 iterations
./run_bench.sh doc_types       # Document type comparison
./run_bench.sh doc_types 500 200  # Doc types with custom docs and iterations

Or from Erlang:

barrel_bench:run(#{num_docs => 5000, iterations => 100}).

%% Run specific workloads
barrel_bench:run_crud(#{num_docs => 1000}).
barrel_bench:run_query(#{num_docs => 5000, iterations => 100}).
barrel_bench:run_changes(#{num_docs => 1000}).

Results are saved to bench/results/ as JSON with timestamps.

Query Building Guidelines

Building efficient queries is crucial for performance. See the Query Guide for full syntax reference.

Rule 1: Always Paginate

Never fetch unbounded result sets. Use limit and continuation tokens:

%% BAD: Fetches all matching documents
{ok, All, _} = barrel_docdb:find(Db, #{where => Query}).

%% GOOD: Paginate with continuation
{ok, Page, #{continuation := Token}} =
    barrel_docdb:find(Db, #{where => Query, limit => 100}).

Rule 2: Use Index-Only Queries When Possible

Set include_docs => false to skip document body fetches:

%% Returns only doc IDs - 18-27 us
{ok, Ids, _} = barrel_docdb:find(Db, #{
    where => [{path, [<<"type">>], <<"user">>}],
    include_docs => false,
    limit => 100
}).

%% Then fetch specific docs you need
{ok, Doc} = barrel_docdb:get_doc(Db, hd(Ids)).

Rule 3: Put Selective Conditions First

More selective conditions reduce the search space:

%% GOOD: Most selective condition first
#{where => [
    {path, [<<"user_id">>], <<"specific_user">>},  %% Very selective
    {path, [<<"type">>], <<"event">>}               %% Less selective
]}

%% LESS OPTIMAL: Broad condition first
#{where => [
    {path, [<<"type">>], <<"event">>},              %% Matches many docs
    {path, [<<"user_id">>], <<"specific_user">>}
]}

Rule 4: Prefer Prefix Over Regex

Prefix queries use efficient index range scans:

%% FAST: 18-23 us - Uses index range scan
{prefix, [<<"name">>], <<"John">>}

%% SLOW: Full scan with regex matching
{regex, [<<"name">>], <<"^John.*">>}

Rule 5: Use LIMIT with ORDER BY

Top-K queries are fast when limited:

%% FAST: 233 us - Early termination
#{where => [],
  order_by => {[<<"created_at">>], desc},
  limit => 10}

%% SLOW: Must sort all documents first
#{where => [],
  order_by => {[<<"created_at">>], desc}}

Query Execution Strategies

Use explain/2 to see how queries execute:

{ok, Plan} = barrel_docdb:explain(Db, Query).
%% Plan.strategy tells you the execution path
StrategyPerformanceWhen Used
index_seekExcellentEquality on indexed path
index_scanGoodRange queries, prefix
multi_indexGoodMultiple conditions
full_scanAvoidNo index available

Anti-Patterns to Avoid

PatternProblemSolution
No LIMITFetches entire databaseAlways paginate
OR with many termsCreates large unionsConsider multiple queries
NOT on large setsScans exclusionsRestructure query
Regex for prefixFull scanUse {prefix, ...}
Sorting without limitSorts all resultsAdd LIMIT

Optimizing Performance

Write Optimization

  1. Batch writes for bulk inserts
  2. Disable sync for non-critical writes: #{sync => false}
  3. Use specific paths in change subscriptions vs wildcards

Configuration Tuning

See Architecture for RocksDB tuning:

  • Adjust block cache size for your memory budget
  • Configure write buffer size for write-heavy workloads
  • Enable compression for large documents