<title>rivet benchmark — cross-engine extraction to Parquet</title>
<style>
:root {
--bg: #f5f7fa; --surface: #ffffff; --surface-2: #eef1f6;
--text: #171c26; --muted: #5b6472; --faint: #98a2b3;
--border: #e1e6ee; --border-strong: #ccd3de;
--accent: #0c8f8a; --accent-soft: #d6efee;
--good: #15803d; --good-bg: #dcfce7;
--warn: #b45309; --warn-bg: #fef1dc;
--crit: #c02626; --crit-bg: #fbe3e3;
--shadow: 0 1px 2px rgba(20,28,40,.04), 0 4px 16px rgba(20,28,40,.05);
--mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, sans-serif;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0d1014; --surface: #141922; --surface-2: #1b212c;
--text: #e7ebf2; --muted: #97a1b2; --faint: #6a7488;
--border: #232b38; --border-strong: #2f3a49;
--accent: #2fd4c8; --accent-soft: #113230;
--good: #4ade80; --good-bg: #123020;
--warn: #fbbf24; --warn-bg: #35270d;
--crit: #f87171; --crit-bg: #351717;
--shadow: 0 1px 2px rgba(0,0,0,.3), 0 6px 20px rgba(0,0,0,.35);
}
}
:root[data-theme="light"] {
--bg: #f5f7fa; --surface: #ffffff; --surface-2: #eef1f6;
--text: #171c26; --muted: #5b6472; --faint: #98a2b3;
--border: #e1e6ee; --border-strong: #ccd3de;
--accent: #0c8f8a; --accent-soft: #d6efee;
--good: #15803d; --good-bg: #dcfce7; --warn: #b45309; --warn-bg: #fef1dc;
--crit: #c02626; --crit-bg: #fbe3e3;
--shadow: 0 1px 2px rgba(20,28,40,.04), 0 4px 16px rgba(20,28,40,.05);
}
:root[data-theme="dark"] {
--bg: #0d1014; --surface: #141922; --surface-2: #1b212c;
--text: #e7ebf2; --muted: #97a1b2; --faint: #6a7488;
--border: #232b38; --border-strong: #2f3a49;
--accent: #2fd4c8; --accent-soft: #113230;
--good: #4ade80; --good-bg: #123020; --warn: #fbbf24; --warn-bg: #35270d;
--crit: #f87171; --crit-bg: #351717;
--shadow: 0 1px 2px rgba(0,0,0,.3), 0 6px 20px rgba(0,0,0,.35);
}
* { box-sizing: border-box; }
body {
margin: 0; background: var(--bg); color: var(--text);
font-family: var(--sans); line-height: 1.55;
font-variant-numeric: tabular-nums;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 1080px; margin: 0 auto; padding: 0 24px 96px; }
a { color: var(--accent); }
header.mast { padding: 64px 0 36px; border-bottom: 1px solid var(--border); }
.eyebrow {
font-family: var(--mono); font-size: 12px; letter-spacing: .14em;
text-transform: uppercase; color: var(--accent); margin: 0 0 14px;
}
h1 {
font-size: clamp(30px, 5vw, 46px); line-height: 1.05; margin: 0 0 18px;
letter-spacing: -.02em; text-wrap: balance; font-weight: 680;
}
h1 .sub { color: var(--muted); font-weight: 500; }
.lede { font-size: 18px; color: var(--muted); max-width: 62ch; margin: 0; }
.stamp {
display: flex; flex-wrap: wrap; gap: 8px 20px; margin-top: 26px;
font-family: var(--mono); font-size: 12.5px; color: var(--faint);
}
.stamp b { color: var(--muted); font-weight: 500; }
section { padding-top: 52px; }
h2 {
font-size: 13px; font-family: var(--mono); text-transform: uppercase;
letter-spacing: .12em; color: var(--muted); margin: 0 0 6px;
display: flex; align-items: baseline; gap: 10px;
}
h2 .n { color: var(--accent); }
.h2sub { font-family: var(--sans); font-size: 22px; color: var(--text);
text-transform: none; letter-spacing: -.01em; margin: 0 0 22px;
font-weight: 620; text-wrap: balance; }
.verdict { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-top: 30px; }
.vcard {
background: var(--surface); border: 1px solid var(--border);
border-radius: 12px; padding: 20px; box-shadow: var(--shadow);
}
.vcard .k { font-family: var(--mono); font-size: 11.5px; letter-spacing: .08em;
text-transform: uppercase; color: var(--muted); margin: 0 0 8px; }
.vcard .v { font-size: 30px; font-weight: 700; letter-spacing: -.02em; line-height: 1; }
.vcard .v small { font-size: 15px; color: var(--muted); font-weight: 500; }
.vcard .note { font-size: 13px; color: var(--muted); margin: 8px 0 0; }
.vcard.hero { border-color: var(--accent); background:
linear-gradient(180deg, var(--accent-soft), var(--surface) 60%); }
.vcard.hero .v { color: var(--accent); }
.bars { display: flex; flex-direction: column; gap: 9px; margin-top: 8px; }
.bar-row { display: grid; grid-template-columns: 132px 1fr 96px; align-items: center; gap: 12px; }
.bar-row .tool { font-family: var(--mono); font-size: 13px; color: var(--text); text-align: right; }
.bar-track { height: 22px; background: var(--surface-2); border-radius: 5px; overflow: hidden; position: relative; }
.bar-fill { height: 100%; border-radius: 5px; background: var(--border-strong); }
.bar-fill.win { background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 70%, #fff)); }
.bar-val { font-family: var(--mono); font-size: 12.5px; color: var(--muted); }
.bar-val.win { color: var(--accent); font-weight: 600; }
.caption { font-size: 13px; color: var(--faint); margin: 14px 0 0; }
.tbl-wrap { overflow-x: auto; border: 1px solid var(--border);
border-radius: 12px; box-shadow: var(--shadow); background: var(--surface); }
table { border-collapse: collapse; width: 100%; font-family: var(--mono); font-size: 13px; }
thead th {
text-align: right; padding: 12px 14px; font-weight: 600; color: var(--muted);
border-bottom: 1px solid var(--border-strong); white-space: nowrap;
font-size: 11.5px; letter-spacing: .04em; text-transform: uppercase;
position: sticky; top: 0; background: var(--surface);
}
thead th:first-child { text-align: left; }
tbody td { text-align: right; padding: 10px 14px; border-bottom: 1px solid var(--border);
white-space: nowrap; }
tbody td:first-child { text-align: left; color: var(--text); }
tbody tr:last-child td { border-bottom: none; }
tr.rivet td { background: var(--accent-soft); }
tr.rivet td:first-child { font-weight: 600; }
tr.rivet td:first-child::before { content: "▹ "; color: var(--accent); }
.win { color: var(--accent); font-weight: 600; }
.cell-good { background: var(--good-bg); color: var(--good); }
.cell-warn { background: var(--warn-bg); color: var(--warn); }
.cell-crit { background: var(--crit-bg); color: var(--crit); font-weight: 600; }
.na { color: var(--faint); }
.drift { color: var(--crit); }
.keep { color: var(--good); }
.legend { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 12px 2px 0;
font-family: var(--mono); font-size: 11.5px; color: var(--muted); }
.chip { display: inline-flex; align-items: center; gap: 6px; }
.sw { width: 11px; height: 11px; border-radius: 3px; display: inline-block; }
.prose p { max-width: 68ch; color: var(--muted); font-size: 15px; }
.prose strong { color: var(--text); font-weight: 600; }
.callout {
border-left: 3px solid var(--crit); background: var(--crit-bg);
border-radius: 0 10px 10px 0; padding: 16px 20px; margin-top: 18px;
}
.callout .h { font-weight: 700; color: var(--text); margin: 0 0 4px; font-size: 15px; }
.callout p { margin: 0; color: var(--text); font-size: 14px; opacity: .9; max-width: 70ch; }
.callout.teal { border-color: var(--accent); background: var(--accent-soft); }
.foot { margin-top: 60px; padding-top: 24px; border-top: 1px solid var(--border);
font-size: 13px; color: var(--faint); }
.foot code { font-family: var(--mono); background: var(--surface-2);
padding: 1px 6px; border-radius: 4px; font-size: 12px; }
@media (max-width: 720px) {
.verdict { grid-template-columns: 1fr; }
.bar-row { grid-template-columns: 92px 1fr 74px; }
}
@media (prefers-reduced-motion: no-preference) {
.bar-fill { transition: none; }
}
</style>
<div class="wrap">
<header class="mast">
<p class="eyebrow">Benchmark · extraction to Parquet</p>
<h1>rivet vs the field <span class="sub">— across Postgres, MySQL, SQL Server & Mongo</span></h1>
<p class="lede">Eight tools, four engines, every axis in one pass: throughput,
peak memory, source-harm, and type fidelity. Same fixture, each competitor on
its own best-effort (steelman) config. No axis hidden — rivet is not the
fastest, and this says so.</p>
<div class="stamp">
<span><b>rivet</b> 0.18.0</span>
<span><b>hardware</b> Apple M1 Pro · 10-core · 16 GB</span>
<span><b>os</b> macOS 26.2 (arm64)</span>
<span><b>date</b> 2026-07-09</span>
<span><b>source</b> dev/bench/smoke.py + matrix.yaml</span>
</div>
</header>
<section>
<h2><span class="n">01</span> The one-line read</h2>
<div class="verdict">
<div class="vcard hero">
<p class="k">Peak memory</p>
<div class="v">15–60×<small> lower</small></div>
<p class="note">57 MB where competitors take 0.8–3.6 GB. Streaming vs
buffering — architectural, not a config trick.</p>
</div>
<div class="vcard">
<p class="k">Type fidelity</p>
<div class="v">0<small> drift, everywhere</small></div>
<p class="note">The only tool that never loses a type across all engines
and tables — while also checksumming every value.</p>
</div>
<div class="vcard">
<p class="k">Throughput</p>
<div class="v">mid-pack</div>
<p class="note">Competitive, not the leader — ingestr & clickhouse
beat its rows/s. Named, not hidden.</p>
</div>
</div>
</section>
<section>
<h2><span class="n">02</span> The picture · peak RSS</h2>
<p class="h2sub">Postgres · content_items (2 M heavy-text rows) → Parquet</p>
<div class="bars">
<div class="bar-row"><span class="tool">rivet</span><div class="bar-track"><div class="bar-fill win" style="width:1.6%"></div></div><span class="bar-val win">57 MB</span></div>
<div class="bar-row"><span class="tool">sling</span><div class="bar-track"><div class="bar-fill" style="width:3.6%"></div></div><span class="bar-val">129 MB</span></div>
<div class="bar-row"><span class="tool">clickhouse</span><div class="bar-track"><div class="bar-fill" style="width:22.9%"></div></div><span class="bar-val">820 MB</span></div>
<div class="bar-row"><span class="tool">ingestr</span><div class="bar-track"><div class="bar-fill" style="width:36%"></div></div><span class="bar-val">1 288 MB</span></div>
<div class="bar-row"><span class="tool">dlt</span><div class="bar-track"><div class="bar-fill" style="width:48.5%"></div></div><span class="bar-val">1 735 MB</span></div>
<div class="bar-row"><span class="tool">duckdb</span><div class="bar-track"><div class="bar-fill" style="width:57.8%"></div></div><span class="bar-val">2 067 MB</span></div>
<div class="bar-row"><span class="tool">odbc2parquet</span><div class="bar-track"><div class="bar-fill" style="width:100%"></div></div><span class="bar-val">3 579 MB</span></div>
</div>
<p class="caption">Bars are linear on the real numbers. rivet is the sliver by
design — it streams the source through a server-side cursor and never holds
the result set in memory.</p>
</section>
<section>
<h2><span class="n">03</span> Full matrix · Postgres</h2>
<p class="h2sub">content_items, 2 M rows — all eight tools, steelman configs</p>
<div class="tbl-wrap">
<table>
<thead><tr>
<th>tool</th><th>rows/s</th><th>peak MB</th><th>out MB</th><th>files</th>
<th>oltp p99×</th><th>longq s</th><th>locks</th><th>type drift</th>
</tr></thead>
<tbody>
<tr class="rivet"><td>rivet</td><td class="win">38 204</td><td class="win">57</td><td>11.3</td><td>1</td><td>5.1</td><td class="cell-good">0.00</td><td>3</td><td class="cell-good">0</td></tr>
<tr><td>rivet-chunked</td><td>29 307</td><td>57</td><td>12.7</td><td>4</td><td class="cell-good">3.3</td><td class="cell-good">0.00</td><td>26</td><td class="cell-good">0</td></tr>
<tr><td>duckdb</td><td>31 635</td><td class="cell-crit">2 067</td><td class="win">9.3</td><td>1</td><td>7.1</td><td class="cell-warn">7.7</td><td class="cell-crit">63</td><td class="cell-warn">2</td></tr>
<tr><td>clickhouse</td><td class="win">39 164</td><td class="cell-warn">820</td><td>38.7</td><td>1</td><td>2.7</td><td class="cell-crit">50.3</td><td>3</td><td class="cell-crit">5</td></tr>
<tr><td>odbc2parquet</td><td>27 447</td><td class="cell-crit">3 579</td><td>31.1</td><td>1</td><td class="cell-good">2.7</td><td class="cell-warn">40.0</td><td>3</td><td class="cell-warn">2</td></tr>
<tr><td>sling</td><td>19 853</td><td>129</td><td>46.2</td><td>9</td><td>4.3</td><td class="cell-crit">94.6</td><td>3</td><td class="cell-good">0</td></tr>
<tr><td>ingestr</td><td class="win">51 677</td><td class="cell-warn">1 288</td><td>41.3</td><td>1</td><td class="cell-crit">6.1</td><td class="cell-warn">36.9</td><td>3</td><td class="cell-warn">2</td></tr>
<tr><td>dlt</td><td class="cell-crit">7 267</td><td class="cell-crit">1 735</td><td class="cell-crit">114.4</td><td>1</td><td class="cell-good">1.8</td><td>1.5</td><td class="cell-warn">34</td><td class="cell-warn">2</td></tr>
</tbody>
</table>
</div>
<div class="legend">
<span class="chip"><span class="sw" style="background:var(--good-bg);border:1px solid var(--good)"></span> best / harmless</span>
<span class="chip"><span class="sw" style="background:var(--warn-bg);border:1px solid var(--warn)"></span> notable</span>
<span class="chip"><span class="sw" style="background:var(--crit-bg);border:1px solid var(--crit)"></span> costly</span>
<span class="chip"><span class="sw" style="background:var(--accent-soft);border:1px solid var(--accent)"></span> rivet</span>
</div>
<div class="prose"><p style="margin-top:22px"><strong>longq</strong> is the
source-safety headline: rivet holds no long-running query (a server-side
cursor, not a single 40–95 s scan), while clickhouse/odbc/sling pin one
query for the whole read. <strong>duckdb</strong> is fast but buys it with
2 GB and 63 locks; <strong>dlt</strong> spills 751 MB of temp on the source
(not shown) and runs 7× slower.</p></div>
</section>
<section>
<h2><span class="n">04</span> Cross-engine · rivet holds; the field wobbles</h2>
<p class="h2sub">Same tool, three engines — where the others break</p>
<div class="tbl-wrap">
<table>
<thead><tr>
<th>engine · table</th><th>rivet rows/s</th><th>rivet MB</th>
<th>rivet drift</th><th>notable competitor result</th>
</tr></thead>
<tbody>
<tr class="rivet"><td>postgres · content_items 2M</td><td>38 204</td><td>57</td><td class="cell-good">0</td><td style="text-align:left" class="na">duckdb fast but 2 GB / 63 locks</td></tr>
<tr class="rivet"><td>mysql · content_items 2M</td><td>31 855</td><td>143</td><td class="cell-good">0</td><td style="text-align:left" class="drift">duckdb 3 180 rows/s — one query held 8.6 min</td></tr>
<tr class="rivet"><td>mssql · orders 1M</td><td>387 710</td><td>81</td><td class="cell-good">0</td><td style="text-align:left" class="na">duckdb & clickhouse: no native reader</td></tr>
<tr class="rivet"><td>mongo · content_items 200k</td><td>29 795</td><td>369</td><td class="na">n/a</td><td style="text-align:left" class="na">rivet 369 MB vs ingestr 875 · sling 510</td></tr>
</tbody>
</table>
</div>
<div class="callout">
<p class="h">duckdb's mysql_scanner collapses</p>
<p>King of throughput on Postgres (627 k rows/s on page_views), duckdb falls
to <strong>3 180 rows/s</strong> on MySQL content_items — a single query
held open for <strong>8.6 minutes</strong> at 1.7 GB. Same tool, same
data, one engine away. rivet's per-engine reader keeps it at 32 k / 143 MB.</p>
</div>
</section>
<section>
<h2><span class="n">05</span> Type fidelity · what the others drop</h2>
<p class="h2sub">Source column → each tool's Parquet type (Postgres content_items + page_views)</p>
<div class="tbl-wrap">
<table>
<thead><tr>
<th>source column</th><th>type</th><th>rivet</th><th>duckdb</th>
<th>clickhouse</th><th>odbc</th><th>sling</th><th>ingestr</th><th>dlt</th>
</tr></thead>
<tbody>
<tr><td>metadata</td><td class="na">jsonb</td><td class="keep">json</td><td class="drift">text</td><td class="drift">text</td><td class="drift">text</td><td class="keep">json</td><td class="drift">text</td><td class="drift">text</td></tr>
<tr><td>created_at</td><td class="na">timestamp</td><td class="keep">ts</td><td class="keep">ts</td><td class="drift">tz-shift</td><td class="keep">ts</td><td class="keep">ts</td><td class="keep">ts</td><td class="keep">ts</td></tr>
<tr><td>is_bounce</td><td class="na">boolean</td><td class="keep">bool</td><td class="keep">bool</td><td class="drift">int</td><td class="drift">text</td><td class="keep">bool</td><td class="keep">bool</td><td class="keep">bool</td></tr>
</tbody>
</table>
</div>
<div class="prose"><p style="margin-top:20px">Every competitor flattens
<strong>jsonb → plain text</strong> (loses the JSON logical type a reader
needs). <strong>clickhouse</strong> also promotes naive timestamps to
timestamptz — a silent wall-clock shift — and renders booleans as ints;
<strong>odbc</strong> renders booleans as text. rivet and sling keep JSON;
only rivet keeps <em>everything</em>, across every engine.</p></div>
</section>
<section>
<h2><span class="n">06</span> How this stays honest</h2>
<div class="prose">
<p><strong>Steelman, applied to everyone.</strong> Each tool runs its
lowest-memory config that still completes — the memory caps <em>flatter</em>
competitors (a capped duckdb reports less RSS, not more). No arbitrary
throttles; a self-audit removed a stray clickhouse thread cap.</p>
<p><strong>Every axis reported side by side.</strong> rivet is beaten on
rows/s by ingestr and clickhouse, and that is on the table — the
transparency is the fairness. rivet's numbers already include its
always-on per-value checksum (~7 %) that no competitor performs.</p>
<p><strong>Measured, not theorised.</strong> rivet's steelman
(<code>profile: fast</code>) came from a measured +24 % rows/s — dropping
a 50 ms/batch throttle — not a guess; zstd was measured free vs snappy.</p>
</div>
<div class="callout teal">
<p class="h">The honest position</p>
<p>rivet is the best where the thesis lives — <strong>memory footprint and
data integrity</strong> — and the only tool that verifies every value while
winning them. It is not the throughput leader, and this report does not
pretend otherwise.</p>
</div>
</section>
<div class="foot">
Single source of truth: <code>docs/bench/matrix.yaml</code> (metric catalog +
steelman + seed) driving <code>dev/bench/smoke.py</code>, which guards against
metric drift. Fixtures seeded into a dedicated <code>rivet_bench</code> per
engine. MongoDB (JSON-blob, non-SQL) is covered — rivet / sling / ingestr, no
type dimension. All figures from live
runs, 2026-07-09.
</div>
</div>