better-drizzle
Performance

Benchmarks

Wrapper overhead vs raw Drizzle across latency and memory, measured with fair API-parity comparisons.

better-drizzle is not trying to beat raw Drizzle at being raw Drizzle. It is trying to stay close while giving you a higher-level repository API. The benchmark suite quantifies exactly that overhead - including where the overhead is real.

How these numbers are produced

Generated by bun run bench:report on SQLite in-memory, to isolate wrapper overhead from I/O. Both sides of every pair are sampled in the same window, alternating which one leads, and the median across samples is reported. Measurement uses mitata's engine, the same one behind bun run bench.

Read the percentages, not the microseconds

Absolute timings drift with machine load - the same lookup reads 53 µs on an idle machine and 93 µs under load. The ratio between the two sides is stable across runs; the absolute values are specific to the machine that produced them. Reproduce locally rather than treating the µs column as a spec.

Latency

Per-operation latency at the API-parity level - raw Drizzle does the same work and returns the same shape.

Reads

OperationDrizzlebetter-drizzleOverhead
Point lookup74.94 µs74.46 µs−0.6%
Filtered list256.28 µs268.41 µs+4.7%
Relation graph10.24 ms1.12 ms9.1× faster
Relation counts33.62 ms34.17 ms+1.6%
Active count68.52 µs74.23 µs+8.3%
Exists62.90 µs65.39 µs+4.0%
Offset pagination121.23 µs131.15 µs+8.2%
Cursor pagination119.01 µs128.91 µs+8.3%

Writes

OperationDrizzlebetter-drizzleOverhead
Create + delete roundtrip119.92 µs125.25 µs+4.4%
Update + reload57.92 µs59.94 µs+3.5%

Transactions

OperationDrizzlebetter-drizzleOverhead
Simple transaction138.22 µs147.90 µs+7.0%
Multi-op transaction383.83 µs359.67 µs−6.3%
Read-only transaction117.68 µs130.25 µs+10.7%

Memory

Heap delta across batches of operations, from bun run bench:memory.

Heap deltas are noisier than latency ratios, so these are the ranges observed across runs rather than a single snapshot.

BatchHeap overhead
Single read−50%
Write−18% to −25%
Mixed read+165% to +210%
Transaction+91% to +106%

How to read this

  • Reads land within ~9% of raw Drizzle, and several are within noise. Point lookups, filtered lists, and existence checks are effectively free.
  • Relation loading is the standout win. The batched loader issues one query per relation node instead of the per-row work the equivalent raw code ends up doing, which is why the relation graph is ~9× faster rather than marginally slower. This gap grows with the number of parent rows.
  • Cursor pagination is at parity. A populated page ordered by one primary key computes exact navigation metadata with one indexed query; empty pages and more complex shapes keep a probe when they need one. Note that both sides resolve hasPrevious - the raw comparison does it with a correlated exists subquery rather than assuming it. An earlier revision of this suite let the raw side hardcode that value, which quietly compared one query against two. The manual data-only reference is still faster because it skips the navigation check entirely.
  • Writes are within ~5% at parity.
  • Transactions are within ~11%. Nested savepoints are a better-drizzle-only capability with no raw Drizzle equivalent, so they are not part of the parity table.
  • Memory is mixed. Single reads use materially less heap; mixed-read batches and transactions use more, because relation state, hooks, and savepoint tracking have to be held somewhere.

Comparison groups

The suite reports three views on purpose:

  1. API parity - the fair comparison. Same work, same shape (relations loaded, pagination metadata computed).
  2. Transaction parity - the same operations wrapped in a transaction.
  3. Manual Drizzle reference - intentionally lower-level raw queries (flat joins, no relation resolution, no pagination metadata). These are faster but do less.

Don't compare across groups

The manual reference does less work, so it is not a fair overhead claim. Use the API-parity group as the headline number - see parity.

PostgreSQL JSONB

Typed JSONB path filters have their own suite (bun run bench:jsonb) because they need a real PostgreSQL instance. It seeds 100,000 rows and asserts two things before timing:

  • Row parity - both sides return the same rows.
  • Plan parity - both sides reach them the same way, verified with EXPLAIN on each side.

The typed form compiles to the same index scan as the hand-written SQL; PostgreSQL folds the parameterised path into the indexed expression. better-drizzle adds one jsonb_typeof filter that the hand-written version omits, which is what keeps a row with a string where its neighbours have a number from failing the cast.

That suite is I/O bound, so the two sides land within run-to-run noise of each other. It exists to prove the query is the same, not to produce an overhead figure.

On this page