meta:
updated: "2026-07-09"
hardware:
machine: "Apple M1 Pro"
cpu: "10-core (8 performance + 2 efficiency)"
ram_gb: 16
os: "macOS 26.2 (arm64)"
databases:
- { engine: postgres, version: "16", scheme: "postgresql://" }
- { engine: mysql, version: "8.0", scheme: "mysql://" }
- { engine: sqlserver, version: "2022", scheme: "sqlserver://" }
- { engine: mongodb, version: "8.0", scheme: "mongodb://", note: "JSON-blob model; CI matrix 4.4→8.0. Read by rivet / sling / dlt / ingestr only (duckdb/clickhouse/odbc2parquet are SQL-only)." }
seed:
tool: "cargo run --release --bin seed --features dev-seed --"
profile: fast target: both sizes: users: 100000 orders_per_user: 10 events_per_user: 50 page_views: 2000000 content_items: 2000000 coalesce_rows: 2000 coalesce_null_ratio: 0.35
mongo:
collections:
- { name: content_items, docs: 200000, shape: "id + heavy-text document (~1 KB)" }
- { name: events, docs: 5000000, shape: "small event document" }
- { name: users, docs: 100000, shape: "profile document" }
tools:
- name: rivet
role: subject
lang: Rust
version: "0.18.0"
version_date: "2026-07-09"
latest: "rivet --version"
steelman:
applied: "mode: full, tuning.profile: fast, compression: zstd — one clean file, cursor-streamed"
knobs: "mode (full/chunked/date-chunked), tuning.profile (safe/balanced/fast — default balanced throttles 50ms/batch), chunk_size (default 100k), chunk_size_memory_mb, chunk_count, batch_size, compression, parallel_exports, meta_columns"
note: "profile:fast drops the Balanced 50ms/batch throttle — MEASURED +24% rows/s, -13% source CPU, -12s open-txn, same 56MB peak / 0 type-loss; only cost oltp_p99 4.0→5.1x. zstd is free here (same speed as snappy/none, 4-18x smaller). chunked mode = source-safety for huge tables (separate scenario)"
- name: sling
role: competitor
lang: Go
version: "1.5.20"
version_date: "2026-07-09"
latest: "sling --version # brew install slingdata-io/sling/sling"
steelman:
applied: "tgt format parquet, file_max_rows=250000 (rotate parts — required to complete at scale)"
knobs: "chunk_size (Pro/paywalled), target file_max_rows / file_max_bytes, SLING_THREADS, source range/limit, sslmode=disable"
note: "no native parquet writer — streams source→CSV→local HTTP→duckdb read_csv→parquet, which TIMES OUT on a single 2M-row stream; file_max_rows rotation is what makes it complete"
- name: ingestr
role: competitor
lang: Go
version: "1.0.73"
version_date: "2026-07-09"
latest: "curl -LsSf https://getbruin.com/install/ingestr | sh # or download the Darwin_arm64 release tarball"
steelman:
applied: "default streaming; parquet:// dest, sslmode=disable"
knobs: "--loader-file-size, --interval-start/--interval-end (incremental), --sql-limit, --page-size (source cursor)"
note: "single-binary Go CLI; streams by construction; parquet:// local dest since 1.0.73"
- name: dlt
role: competitor
lang: Python
version: "1.28.1"
version_date: "2026-07-09"
latest: "pip install -U dlt"
steelman:
applied: "DATA_WRITER__BUFFER_MAX_ITEMS=5000; loader_file_format=parquet"
knobs: "buffer_max_items, file_max_items, file_max_bytes, DATA_WRITER__*, LOAD__WORKERS, extract/normalize parallelism"
note: "~1.27 GB tuned; Python + sqlalchemy overhead dominates CPU. Needs a per-engine SQLAlchemy driver: postgres = psycopg2, mysql = pymysql, mssql = pyodbc — pip install alongside dlt[sql_database]"
- name: duckdb
role: competitor
lang: C++
version: "1.5.4"
version_date: "2026-07-09"
latest: "pip install -U duckdb"
steelman:
applied: "SET memory_limit='4GB'; postgres_scanner ATTACH READ_ONLY; COPY … TO parquet (zstd)"
knobs: "memory_limit, threads, preserve_insertion_order, temp_directory, max_temp_directory_size, parquet row_group_size"
note: "lowest COMPLETING limit; OOMs at 512MB/2GB (its floor, not a choice)"
- name: clickhouse-local
role: competitor
lang: C++
version: "26.6.1"
version_date: "2026-07-09"
latest: "brew install clickhouse # unsigned cask: xattr -dr com.apple.quarantine <bin>; codesign -s - <bin>"
steelman:
applied: "--max_memory_usage=4000000000 (lowest completing floor); threads at default (all cores); postgresql() → INTO OUTFILE FORMAT Parquet"
knobs: "max_memory_usage, max_threads, max_block_size, max_bytes_before_external_group_by, output_format_parquet_row_group_size"
note: "OOMs (code 241) below its floor; the memory cap lowers its RSS (flatters it). No thread cap — that would only slow it without serving the min-memory goal"
- name: odbc2parquet
role: competitor
lang: Rust
version: "11.0.1"
version_date: "2026-07-09"
latest: "cargo install odbc2parquet"
steelman:
applied: "--batch-size-memory 256Mb; one SELECT * per table"
knobs: "--batch-size-memory, --batch-size-row, --row-groups-per-file, --file-size-threshold"
note: "needs a VENDOR ODBC DRIVER per engine (portability cost): postgres = PostgreSQL Unicode (psqlodbc); mysql = MariaDB Unicode (brew install mariadb-connector-odbc, register the .dylib via odbcinst -i -d), Server=127.0.0.1 to force TCP; mssql = ODBC Driver 18 for SQL Server. No mongo (not SQL)."
benchmark_metrics:
- { key: status, label: status, desc: "ok | mismatch | error" }
- { key: wall_s, label: wall_s, unit: s, desc: "total wall time" }
- { key: rows, label: rows, unit: count, desc: "rows exported (correctness gate — must equal source)" }
- { key: rows_s, label: rows_s, unit: count, desc: "extraction throughput (rows/wall)" }
- { key: peak_rss_mb, label: peak_mb, unit: MB, desc: "peak resident memory of the tool process" }
- { key: out_mb, label: out_mb, unit: MB, desc: "parquet output size (*.parquet only)" }
- { key: out_files, label: out_files, unit: count, desc: "parquet files written (1 = single file; many = row-group/partition split — affects downstream small-file overhead)" }
- { key: type_deg, label: type_deg, unit: count, desc: "columns whose parquet type family drifts from the source — the matrix-3 headline" }
harm_metrics:
universal: - { key: oltp_x, label: oltp_p99_x, group: "ground truth", desc: "co-running OLTP point-read probe: p99 latency during the extract ÷ idle baseline (the number a DBA signs off)" }
- { key: cache_mb, label: cache_mb, group: "cache", desc: "buffer-pool pages holding the table after the run — the hot working set the extract displaced" }
- { key: locks, label: locks_peak, group: "contention", desc: "peak locks held on the source" }
- { key: blocked, label: blocked_peak, group: "contention", desc: "peak OTHER sessions blocked by the tool (locks only harm when they block)" }
- { key: longq, label: longq_s, group: "query hold", desc: "longest single active query held (source-safety)" }
- { key: q_p50, label: q_p50_s, group: "query hold", desc: "median active query duration" }
- { key: longtxn, label: longtxn_s, group: "query hold", desc: "longest open transaction" }
- { key: conns, label: conns_peak, group: "query hold", desc: "peak concurrent connections held" }
- { key: cpu_ms, label: cpu_ms, group: "resource", desc: "source CPU consumed (cgroup v2 cpu.stat)" }
postgres: - { key: xmin_age, label: xmin_held_xid, group: "mvcc/vacuum", desc: "xid age the tool pins backend_xmin to — how far back it starves VACUUM (DB-wide bloat)" }
- { key: dead, label: dead_tup, group: "mvcc/vacuum", desc: "dead tuples created (≈0 for a pure read)" }
- { key: tup_returned, label: tup_read, group: "i/o", desc: "tuples read from the source (read amplification, incl. catalog)" }
- { key: tup_fetched, label: tup_fetch, group: "i/o", desc: "tuples fetched" }
- { key: temp_mb, label: temp_mb, group: "i/o", desc: "temp-file spill (a naive SELECT * spills; chunked does not)" }
- { key: blks_read, label: blks_read, group: "i/o", desc: "disk blocks read" }
- { key: blk_read_time, label: blkread_ms, group: "i/o", desc: "time spent reading blocks" }
- { key: wal_kb, label: wal_kb, group: "resource", desc: "WAL generated (≈0 for reads; matters for CDC)" }
mysql: - { key: Innodb_rows_read, desc: "rows read from InnoDB" }
- { key: Innodb_buffer_pool_reads, desc: "physical page reads (cache miss)" }
- { key: Innodb_buffer_pool_read_requests, desc: "logical page reads" }
- { key: Innodb_log_waits, desc: "log-buffer waits (write pressure)" }
- { key: tmp_disk_tables, desc: "temp tables spilled to disk" }
- { key: Handler_read_rnd_next, desc: "full-scan row reads" }
sqlserver: - { key: logical_reads, desc: "logical page reads" }
- { key: physical_reads, desc: "physical page reads" }
- { key: worker_time_ms, desc: "CPU worker time on the source" }
mongodb: - { key: mongo_docs_scanned, desc: "documents examined (scannedObjects) — read amplification" }
- { key: mongo_keys_scanned, desc: "index keys examined" }
- { key: mongo_docs_returned, desc: "documents returned to the client" }
- { key: mongo_op_query, desc: "query opcounter" }
- { key: mongo_op_getmore, desc: "getMore opcounter (cursor batches — streaming footprint)" }
- { key: mongo_wt_cache_bytes_read, desc: "WiredTiger bytes read into cache (I/O vs cache split)" }
smoke:
seed_sizes: { users: 2000, page_views: 20000, content_items: 5000 }
mongo_seed: { content_items: 5000 } scenarios:
- database: postgres
objects: [content_items, page_views, orders] tools: [rivet, sling, ingestr, dlt, duckdb, clickhouse-local, odbc2parquet]
- database: mongodb
objects: [content_items] tools: [rivet, sling, ingestr, dlt]