Try Opteryx

Why Opteryx Is Fast

Opteryx doesn't get its speed from one big trick. It comes from a single habit applied at every layer of the engine: don't do work the query doesn't need. Don't read columns nobody asked for. Don't decode row groups that can't match. Don't compare a string 100 million times when it has 200 distinct values. Don't make the same decision once per row when it can be made once per query.

This page walks through where that habit shows up, then gives the published numbers, with the machine, version and date for each. It ends with where Opteryx isn't the fastest option, because that's worth knowing too.

Read Less

The cheapest byte is the one you never read. Before anything is decoded, Opteryx narrows a scan down in stages. Each stage is described in Rugo, the engine's native file reader:

How a scan narrows before decoding Five stages, each narrower than the last: the dataset on storage; only the columns the query names; only row groups whose statistics could match; only pages whose dictionary holds a match; and finally only the surviving rows, decoded into typed values. Dataset on storage every column, every row group Projection only the columns the query names Row-group pruning min/max, bloom filters Dictionary skipping Decode survivors typed values built last, for what's left
Each stage only reads what the previous one couldn't rule out. The widths are illustrative; how much each stage removes depends on the query and the data.
  • Projection. Only the columns a query references are decoded. The others are never read from disk.
  • Row-group pruning. Parquet keeps min/max statistics for every row group. If a row group's range proves it holds no matching row, it's skipped without decoding a single value. For = and IN, which min/max handles poorly, Rugo also checks bloom filters where the file has them. Pruning fails open: if a statistic is missing, the row group is read rather than wrongly skipped.
  • Dictionary skipping. A dictionary-encoded column carries a small table of its distinct values. Rugo tests the predicate against that table first. If no entry matches, the column's data pages are never decoded.
  • Materialise last. Projection and filtering happen before bytes are turned into typed values. The expensive step runs on the smallest amount of data the query allows.

The optimizer feeds all of this. Predicate and projection pushdown move filters and column lists as close to the scan as they'll go, so the reader knows exactly what it may skip. See How Opteryx Plans and Runs a Query.

Keep Data Compact

Once data is in memory, Opteryx keeps the structure the file already had instead of flattening it. Columns live in Draken vectors, which come in three shapes: dense, dictionary and constant.

A dictionary column stores each distinct value once, plus a small integer code per row. That saves memory, but it saves even more work:

Evaluating country = 'AU' on a dictionary column The predicate is tested once against each of four dictionary entries, numbered 0 to 3: AU, GB, NZ and US, producing a four-entry match table: yes, no, no, no. Each row stores only a code, and that code is looked up in the table, and rows with code 0 match. The per-row work is a table lookup, not a string comparison. 1. Test each distinct value once 0 AU 1 GB 2 NZ 3 US ✓ ✗ ✗ ✗ 2. Each row is a lookup into that table, not a string compare code match 2 ✗ 0 ✓ 3 ✗ 0 ✓ 1 ✗ 0 ✓ 0 ✓ 2 ✗ 3 ✗ 1 ✗ … Work scales with distinct values, not rows. 100 million rows with 200 countries: 200 string comparisons, then 100 million table lookups. Parquet's dictionary pages map straight onto this shape, so nothing is expanded on read.
A predicate on a dictionary column, after How Draken Stores Column Data.

A constant shape goes further: a literal in WHERE region = 'APAC', or a page where every value is the same, is stored once with a row count. Reading it costs the same for any number of rows.

Strings get their own layout. Every value takes a fixed 16-byte slot. Strings of 12 bytes or fewer, which covers most identifiers, codes and categories, sit entirely inside the slot. Longer strings keep their length and first four bytes there. Equality is usually settled by a single 64-bit compare, and ordering by a 4-byte prefix compare, without touching the string bytes at all. See How Draken Stores Strings.

Decide Once, Then Run

Expressions such as price * quantity, a > 10 AND b LIKE 'x%' or a CASE are compiled by the bytecode engine once, while the query is planned, not per row and not per batch:

  • Each literal becomes a constant vector once, rather than a value created per row.
  • Each operation is bound to a native function pointer up front, so running it is a direct call rather than a name lookup.
  • Temporary results come from a per-batch memory arena that's freed in one go, so there are no per-row objects to clean up.

By the time data flows, every decision has already been made. The data path just runs.

Stay Native While It Runs

Planning is Python; execution is native. The handover happens once, when the physical plan is built, and the executor never calls back into Python to decide anything. Data moves through the operators in morsels, batches of columnar vectors, so each operator handles a batch in one pass instead of once per row.

The native scheduler runs operators across worker threads with Python's interpreter lock released, so execution isn't held up by Python. Reading is part of the same pipeline: Rugo fetches byte ranges on worker threads while other threads decode, so waiting for one row group overlaps with work on another. See Execution Model.

Start Instantly

Speed also means not having to wait before the first query.

  • No load step. Opteryx queries Parquet, JSONL and CSV where they already are. Its ClickBench entry records a load time of zero, and its JSONBench entry reads the published files directly with no schema and no index.
  • No cluster. It's one process. Embedded, that process is yours. Hosted at opteryx.app, there's nothing to provision or size.
  • A small footprint. Rugo, the file reader underneath, installs at 11.6 MB with no dependencies and imports in 30 ms. PyArrow is 153.7 MB and 88 ms, Polars 213.8 MB and 186 ms (rugo.dev, Debian 12 x86_64, CPython 3.12, measured 14 August 2026).

Reading Less Costs Less

On the hosted service, the data queried part of the bill is the data a query actually reads (Cost Model). Each technique under Read Less that skips a column, row group or page therefore lowers what that query costs as well as how long it takes. Studio shows the bytes scanned under each result, so you can see the effect query by query.

The Numbers

Benchmarks deserve scepticism, including ours. Benchmarking explains what we run, how it's measured, and what the numbers don't tell you. Every figure below links to its source, so you can check it and rerun it.

ClickBench

ClickBench runs 43 analytical queries over a 100-million-row web-analytics table. The Opteryx entry is in the ClickBench repository and reads the partitioned Parquet files directly. On ClickBench's reference machine:

Measure Opteryx 0.9.155, c6a.4xlarge, 4 October 2026
Queries completed 43 of 43
Load time 0 s (queries the Parquet files where they are)
Total, first run of each query (cold) 144.1 s
Total, best warm run of each query 41.3 s

"Cold" here is strict. Before the first run of every query, the harness restarts the process and drops the operating system's page cache.

Opteryx isn't the fastest entry. On the same machine, ClickHouse, DuckDB and DataFusion reading the same Parquet files are faster on warm runs, and DuckDB is faster cold. Use the live comparison to see where Opteryx lands for the machine and engines you care about.

Release Over Release

Wrenchy Bench reruns the suites release over release, on AWS Graviton4 with 16 CPUs, to catch regressions and track progress. As of engine 0.9.155 on 4 October 2026, across 48 recorded runs:

Suite Slowest recorded run Latest run
ClickBench (Parquet) 29.44 s 15.41 s
JOB, 113 join-heavy queries 23.02 s 16.30 s
TPC-H SF10 9.12 s 4.69 s
TPC-H SF100 67.30 s 48.09 s
Whole suite 183.68 s 121.69 s

The same harness also runs ClickBench against Skene, the engine's own file format, in 10.89 s. Skene is still a draft format, so treat that figure as a preview.

JSONBench

JSONBench queries Bluesky event data stored as JSON. Opteryx reads the NDJSON files directly, with no load step, schema or index. The entry is awaiting review; these figures are from that submission (opteryx-core 0.9.153, m6i.8xlarge):

Documents Warm, sum of 5 queries Cold, sum of 5 queries
1 million 1.38 s 15.8 s
10 million 2.50 s 183.0 s
100 million 13.76 s 1,822.7 s

The cold runs are limited by the benchmark's disk (a default gp3 volume, 125 MB/s), not by the engine. Nothing is loaded or cached ahead of time, so a cold query reads every byte from storage.

Where It Isn't Fast

Knowing the limits matters as much as the numbers:

  • One machine. Opteryx scales up, not out. There's no distributed execution, so a working set bigger than one machine can handle needs a different engine. See Known Limits.
  • Cold reads are bound by storage. Because there's no load step, the first query over files on slow storage runs at the speed of that storage. Pruning helps exactly as much as the files' statistics allow.
  • Files without statistics or dictionaries prune less. Row-group and dictionary skipping depend on what the writer put in the file. A Parquet file with no min/max statistics, or a CSV, has to be read in full.

When to Use Opteryx covers where it fits, and where something else will serve you better.