From d2fa958ab33dc579cadcc8f734e1cde2ce29b934 Mon Sep 17 00:00:00 2001 From: Justin Joyce Date: Sun, 16 Aug 2026 10:32:37 +0100 Subject: [PATCH 1/2] Opteryx: rebuild the entry after the engine refactor, and add the native-format variant The previous entry targeted Python 3.11 and no longer ran. Opteryx has changed substantially since: the planning engine is largely as it was, still in Python, while the execution engine has been significantly refactored. It now ships as `opteryx-core` (previously `opteryx`), with a different execution API. install: stock CPython 3.14 and opteryx-core. Not the free-threaded 3.14t build -- the free-threaded experiment was abandoned in June 2026 (the parallelism target is native C++ threads under a released GIL) and opteryx-core stopped publishing cp314t wheels after 0.9.16, so a 3.14t interpreter falls through to the sdist and tries to build Rust/C++ on the box. The wheel has no runtime dependencies, so this is a single binary download. Ends with a version banner so a fallback to a source build fails loudly there rather than 43 queries later. check: `version()` is no longer a function -- the version is the system variable `@@version` -- and the old `python -m opteryx` path caught the resulting error and still exited 0, a false green that let a broken install through. Probe `@@version` through the Python API and assert the shape of the result. This matters because bench_load() calls ./check unconditionally, after the ~14 GB download. query: rewritten for the current surface (session.execute_to_morsels); opteryx.query() / Cursor.arrow() are gone. Times the morsel drain, which is the execution, and emits the full result as TSV afterwards. queries.sql: adapt 8 queries to the dialect -- explicit casts on the EventDate/EventTime comparisons, TRUNC for DATE_TRUNC, and b''/r'' literals in the REGEXP_REPLACE. Syntactic only; the unadapted forms produce null rows for exactly those 8 rather than wrong numbers. template.json: rename the system to "Opteryx (Parquet, partitioned)", following duckdb-parquet-partitioned and chdb-parquet-partitioned, which frees the unqualified name for the native-format entry. Historical result files keep "Opteryx": they were accurate when recorded and are not rewritten. Add the "C++" tag, matching the implementation-language tag carried by comparable embedded engines. opteryx-skene/ is that native-format entry. skene is Opteryx's own columnar format, so it takes the unqualified name, as `duckdb` does against `duckdb-parquet-partitioned`. install, check, query, queries.sql, start and stop are shared with the Parquet entry verbatim -- query needs no change at all, since a skene dataset is addressed by the same directory resolution and `FROM hits` finds ./hits/*.skene. What differs is the load. ClickBench distributes Parquet, so the dataset must be converted before it can be queried, and that conversion is the load step -- Load time is its wall-clock, as for any entry ingesting source data into a native store. convert.py is self-contained rather than reusing the engine repo's dev tooling, which a ClickBench run does not have; everything it needs (skene, draken, rugo) is in the one wheel. Conversion runs across processes, which costs layout: row groups do not span chunk boundaries, so a 12-worker build holds 28 files against 24 for a single worker and measures ~7% slower to query. Row count is verified per chunk and in total, and a mismatch fails the run. Written lz4 with read acceleration, the engine's read-first posture. --- opteryx-skene/README.md | 126 +++++++++++++++++ opteryx-skene/benchmark.sh | 9 ++ opteryx-skene/check | 34 +++++ opteryx-skene/convert.py | 265 ++++++++++++++++++++++++++++++++++++ opteryx-skene/data-size | 4 + opteryx-skene/install | 44 ++++++ opteryx-skene/load | 19 +++ opteryx-skene/queries.sql | 43 ++++++ opteryx-skene/query | 42 ++++++ opteryx-skene/start | 2 + opteryx-skene/stop | 2 + opteryx-skene/template.json | 12 ++ opteryx/README.md | 62 +++++++-- opteryx/check | 32 ++++- opteryx/install | 40 ++++-- opteryx/queries.sql | 18 +-- opteryx/query | 53 +++----- opteryx/template.json | 3 +- 18 files changed, 747 insertions(+), 63 deletions(-) create mode 100644 opteryx-skene/README.md create mode 100755 opteryx-skene/benchmark.sh create mode 100755 opteryx-skene/check create mode 100644 opteryx-skene/convert.py create mode 100755 opteryx-skene/data-size create mode 100755 opteryx-skene/install create mode 100755 opteryx-skene/load create mode 100644 opteryx-skene/queries.sql create mode 100755 opteryx-skene/query create mode 100755 opteryx-skene/start create mode 100755 opteryx-skene/stop create mode 100644 opteryx-skene/template.json diff --git a/opteryx-skene/README.md b/opteryx-skene/README.md new file mode 100644 index 0000000000..0bbda538e9 --- /dev/null +++ b/opteryx-skene/README.md @@ -0,0 +1,126 @@ +# Opteryx + +Opteryx is an in-process SQL query engine. Query **planning** (parse, bind, +optimize) runs in Python; query **execution** is native (Cython/C++). It +queries Parquet directly from storage with no preloading or preprocessing, +which makes it well suited to ad hoc analytics. + +For more information, visit: + +- [Opteryx Documentation](https://docs.opteryx.app/) +- [Opteryx GitHub Repository](https://github.com/mabel-dev/opteryx-core) + +This entry benchmarks Opteryx on **skene**, its native columnar storage +format. It is the native-format counterpart to `Opteryx (Parquet, partitioned)`. + +--- + +## Generating Benchmark Results + +### High-level Steps +1. Set up the environment. +2. Install Python and the required dependencies. +3. Download the benchmark dataset. +4. Convert it to skene (this is the load step). +5. Run the benchmark script. + +### Detailed Instructions + +1. **Start an AWS EC2 instance** + - OS: Ubuntu 24 + - Architecture: 64-bit (x86_64 or AArch64) + - Instance Type: `c6a.4xlarge` + - Root Storage: 500 GB gp2 SSD + - Advanced Details: ensure 'EBS-optimized instance' is **disabled**. + +2. **SSH into the instance** (after status checks complete): + ~~~bash + ssh ubuntu@{ip} + ~~~ + +3. **Update the package list and install Git** + ~~~bash + sudo apt-get update -y + sudo apt-get install git -y + ~~~ + +4. **Clone the ClickBench repository** + ~~~bash + git clone https://github.com/ClickHouse/ClickBench + cd ClickBench/opteryx-skene + ~~~ + +5. **Run the benchmark script** + ~~~bash + sudo ./benchmark.sh + ~~~ + +### Loading + +ClickBench distributes the dataset as Parquet, so this entry converts it to +skene before querying. That conversion is the load step, and `Load time` is its +wall-clock — comparable to any entry that ingests the source data into a native +store, and unlike the Parquet entry, whose load performs no conversion at all. + +`convert.py` performs the conversion using the writer that ships inside the +`opteryx-core` wheel. skene, draken and rugo are all packaged in that single +wheel, so the conversion needs no additional dependency and no source checkout. +Row groups are packed 16 per file at 262144 rows each, matching the engine's own +mirrors: packing is per directory rather than per source file. + +The conversion runs across processes. A worker owns a contiguous range of input +files end to end and writes its own output files, because morsels hold raw +pointers and cannot cross a process boundary. Workers default to three quarters +of the cores; `-j` overrides it. + +That parallelism has a cost in layout: row groups do not +span chunk boundaries, so each worker's last row group — and last file — is +short. The published mirror was built with 12 workers and holds **28 files**, +against 24 for a single-worker build of the same data, and it measures about 7% +slower across the 43 queries for that reason. `-j 1` reproduces the +single-worker layout exactly. Row count is invariant either way: it is verified +per chunk against the source footers and again on the total, and a mismatch +fails the run rather than warning. + +The source Parquet is deleted once the conversion completes, so `data-size` +measures the skene dataset alone. + +### Compression posture + +The mirror is written with the engine's read-first ("performance") posture: +skene decodes substantially faster uncompressed or lz4-compressed than with +per-section zstd, at the cost of more bytes on disk. It is a deliberate choice +for locally attached storage, where the disk is not the bottleneck and decompression +is pure cost. Remotely read data, where bytes dominate, is written differently. + +The ClickBench Parquet corpus is published pre-compressed, so +`Opteryx (Parquet, partitioned)` reads whatever that corpus contains. In +practice the two are close in size — the skene mirror is 15.39 GB against +14.74 GB of Parquet, about 4% larger. + +### Python version + +`opteryx-core` publishes cp314 x86_64 and AArch64 manylinux wheels and declares +no runtime dependencies, so `install` is a single binary-wheel download with no +on-box compilation and no toolchain. + +### Query dialect + +`queries.sql` adapts queries to Opteryx's dialect. The adaptations are syntactic +— they do not change what is computed, the rows returned, or the work the engine has to do: + +- **Q19, Q43** — `EventTime` is stored as an integer epoch, so it is cast + explicitly (`EventTime::TIMESTAMP[s]`) before `extract(minute FROM ...)` and + before truncation. +- **Q43** — `TRUNC(, 'minute')` rather than `DATE_TRUNC('minute', )`. +- **Q29** — the `REGEXP_REPLACE` pattern and replacement use `b''` and `r''` + literals so the backslash reference survives to the regex engine. +- **Q37-Q42** — `EventDate` comparisons cast both sides to `DATE` + (`EventDate::DATE >= '2013-07-01'::DATE`). + +### Hardware coverage + +Results are published for instance types with **32 or fewer vCPUs**. The account +used for these runs is limited to 32 concurrent on-demand vCPUs, so the 192-vCPU +machines in the ClickBench fleet (`c6a.metal`, `c7a.metal-48xl`, +`c8g.metal-48xl`) could not be launched. diff --git a/opteryx-skene/benchmark.sh b/opteryx-skene/benchmark.sh new file mode 100755 index 0000000000..b7ae16ae72 --- /dev/null +++ b/opteryx-skene/benchmark.sh @@ -0,0 +1,9 @@ +#!/bin/bash +export BENCH_DOWNLOAD_SCRIPT="download-hits-parquet-partitioned" +export BENCH_RESTARTABLE=no +# Single-process engine: each query forks a fresh full-machine process with no +# shared scheduler across connections, so the concurrent-QPS test only +# oversubscribes RAM rather than measuring throughput. Skip it by default; +# override BENCH_CONCURRENT_DURATION to re-enable. See issue #946. +export BENCH_CONCURRENT_DURATION="${BENCH_CONCURRENT_DURATION:-0}" +exec ../lib/benchmark-common.sh diff --git a/opteryx-skene/check b/opteryx-skene/check new file mode 100755 index 0000000000..ed305e8015 --- /dev/null +++ b/opteryx-skene/check @@ -0,0 +1,34 @@ +#!/bin/bash +set -e + +# Smoke test that actually executes through the engine and propagates a +# non-zero exit on failure. The old `python -m opteryx "SELECT version()"` +# CLI path is unusable here for two reasons: +# +# 1. opteryx-core's single-run CLI calls the removed opteryx.query(), +# catches the AttributeError, and STILL exits 0 -- a false green that +# would let a broken install pass this check. +# 2. `version()` is not a function in the current engine; the version is +# exposed as the system variable `@@version`. +# +# This matters more than it looks: bench_load() in lib/benchmark-common.sh +# calls ./check unconditionally, AFTER the ~14 GB download and load. A check +# that cannot pass burns the whole setup cost before failing. +# +# Assert the shape of the value rather than a literal, so the check does not +# need editing on every release. +"$HOME/opteryx_venv/bin/python" - <<'PY' +import re + +import opteryx + +session = opteryx.session() +morsels = list(session.execute_to_morsels("SELECT @@version")) +session.close() + +assert morsels, "SELECT @@version returned no morsels" +value = list(morsels[0][0])[0] +if isinstance(value, (bytes, bytearray)): + value = value.decode() +assert re.fullmatch(r"\d+\.\d+\.\d+.*", str(value)), f"unexpected version {value!r}" +PY diff --git a/opteryx-skene/convert.py b/opteryx-skene/convert.py new file mode 100644 index 0000000000..ba6c13aea5 --- /dev/null +++ b/opteryx-skene/convert.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +""" +Convert ClickBench's partitioned parquet `hits` into its skene mirror. + +Self-contained on purpose: skene, draken and rugo all ship inside the +`opteryx-core` wheel, so this needs nothing but the benchmark's own install. +(The engine repo's dev/parquet_to_skene.py is dev-only tooling and is not +available to a ClickBench run. This file is a port of it; keep them in step.) + +Packing: 16 row groups per file at 262144 rows per row group, matching the +engine's own mirrors. Packing is per DIRECTORY, not per file — a row group can +span two source files, so output files do not correspond to input files. + +PARALLELISM: conversion runs across PROCESSES, not threads. Both hot phases +release the GIL (rugo's decode, skene's add_row_group/write_to), but the morsel +construction between them does not, which caps a thread pool at ~2.5x against +~5x for processes. Morsels hold raw pointers and cannot cross a process +boundary, so a worker owns a CONTIGUOUS RANGE OF INPUT FILES end to end and +writes its own output files; only counts come back. + +CONSEQUENCE — the output is NOT byte-identical to a serial build. Row groups do +not span chunk boundaries, so each worker's last row group is short, where +serially only the very last one was. On this dataset that is ~16 short row +groups of ~397 rather than 1. The layout is a function of the worker count; +`-j 1` reproduces the serial layout exactly. ROW COUNT is invariant, is checked +per chunk against the source footers, and a mismatch is a hard failure. + +Usage: convert.py [codec] [-j N] codec: none|lz4|zstd +""" + +import os +import sys +from concurrent.futures import ProcessPoolExecutor + +import skene +from draken.morsels.morsel import Morsel +from rugo.parquet import read_metadata +from rugo.parquet import read_parquet + +ROWS_PER_ROW_GROUP = 262144 +ROW_GROUPS_PER_FILE = 16 + + +class Packer: + def __init__(self, out_dir, stem, codec, zstd_level, first_index=0): + self.out_dir = out_dir + self.stem = stem + self.codec = codec + self.zstd_level = zstd_level + self._writer = None + self._pending = [] + self._pending_rows = 0 + self._row_groups_in_file = 0 + self.files = 0 + self.rows = 0 + self.nbytes = 0 + # Each worker reserves a disjoint output index range, so two workers + # can never write the same filename. + self._first_index = first_index + os.makedirs(out_dir, exist_ok=True) + + def add(self, morsel): + if morsel.num_rows == 0: + return + self._pending.append(morsel) + self._pending_rows += morsel.num_rows + while self._pending_rows >= ROWS_PER_ROW_GROUP: + merged = self._merge() + self._emit(merged.slice(0, ROWS_PER_ROW_GROUP)) + remainder = merged.num_rows - ROWS_PER_ROW_GROUP + if remainder > 0: + self._pending = [merged.slice(ROWS_PER_ROW_GROUP, remainder)] + else: + self._pending = [] + self._pending_rows = remainder + + def _merge(self): + # Morsel has no `concat`; `combine` is the n-way merge. Getting this + # wrong loses rows silently, which a benchmark cannot survive. + return self._pending[0] if len(self._pending) == 1 else Morsel.combine(self._pending) + + def close(self): + # The final row group is SHORT, and so is the final file - a dataset + # does not divide evenly and padding or dropping the tail are both wrong. + if self._pending_rows > 0: + self._emit(self._merge()) + self._pending = [] + self._pending_rows = 0 + self._close_file() + + def _emit(self, row_group): + if self._writer is None: + self._writer = skene.SkeneWriter( + read_acceleration=True, codec=self.codec, zstd_level=self.zstd_level + ) + self._row_groups_in_file = 0 + self._writer.add_row_group(row_group) + self._row_groups_in_file += 1 + self.rows += row_group.num_rows + if self._row_groups_in_file >= ROW_GROUPS_PER_FILE: + self._close_file() + + def _close_file(self): + if self._writer is None: + return + index = self._first_index + self.files + path = os.path.join(self.out_dir, f"{self.stem}-{index:04d}.skene") + # write_to() completes the file in place; finish() would double peak RSS + # on a wide schema for bytes nobody keeps. + self.nbytes += self._writer.write_to(path) + self.files += 1 + self._writer = None + self._row_groups_in_file = 0 + + +def convert_dir(paths, out_dir, stem, codec, zstd_level, first_index=0): + packer = Packer(out_dir, stem, codec, zstd_level, first_index) + for p in paths: + with read_parquet(p) as reader: + for morsel in reader: + packer.add(morsel) + packer.close() + return packer.files, packer.rows, packer.nbytes + + +def _default_workers(): + """Three quarters of the cores, never all of them. + + Saturating the cores measured ~50% SLOWER than the plateau: a worker holds + a whole decoded file plus up to ROW_GROUPS_PER_FILE row groups buffered in + its writer, so the run goes memory- and scheduler-bound. + """ + return max(1, (os.cpu_count() or 1) * 3 // 4) + + +def _output_files_for(rows): + """Exactly how many .skene files a chunk of `rows` rows produces — this is + what reserves each worker's output index range, so it must be exact.""" + if rows == 0: + return 0 + row_groups = -(-rows // ROWS_PER_ROW_GROUP) + return -(-row_groups // ROW_GROUPS_PER_FILE) + + +def _plan_chunks(paths, row_counts, workers): + """Split into <= `workers` CONTIGUOUS chunks of whole files, balanced by ROW + COUNT rather than file count. Returns [(paths, rows)] in input order so + output names stay sequential.""" + total = sum(row_counts) + # Never split into more chunks than there are full output files of rows: + # each chunk starts a new output file, so over-splitting a small table + # shatters it into undersized files with undersized row groups — precisely + # what packing 16 row groups per file exists to avoid. + rows_per_file = ROWS_PER_ROW_GROUP * ROW_GROUPS_PER_FILE + n = min(workers, len(paths), max(1, total // rows_per_file)) + if n <= 1 or total == 0: + return [(list(paths), total)] + + chunks = [] + start = 0 + assigned = 0 + for _ in range(n - 1): + remaining_chunks = n - len(chunks) - 1 + target = (total - assigned) / (remaining_chunks + 1) + rows = 0 + end = start + while end < len(paths): + if len(paths) - (end + 1) < remaining_chunks: + break + rows += row_counts[end] + end += 1 + if rows >= target: + break + if end == start: + break + chunks.append((paths[start:end], rows)) + assigned += rows + start = end + if start < len(paths): + chunks.append((paths[start:], sum(row_counts[start:]))) + return chunks + + +def _convert_chunk(task): + """Process-pool entry point. Top-level and picklable-only arguments because + spawn-start platforms re-import this module in the child.""" + paths, out_dir, stem, codec, zstd_level, first_index, expected_rows = task + files, rows, nbytes = convert_dir(paths, out_dir, stem, codec, zstd_level, first_index) + if rows != expected_rows: + # Catches the failure that destroyed an earlier converter quietly: a bad + # morsel merge dropped 76% of the rows and still produced a plausible, + # fast, completely wrong dataset. + raise RuntimeError( + f"{out_dir}: chunk at index {first_index} wrote {rows:,} rows " + f"but its sources hold {expected_rows:,}" + ) + return files, rows, nbytes + + +def main(): + argv = [a for a in sys.argv[1:]] + workers = _default_workers() + out = [] + i = 0 + while i < len(argv): + if argv[i] in ("-j", "--workers"): + workers = int(argv[i + 1]); i += 2; continue + if argv[i].startswith("--workers="): + workers = int(argv[i].split("=", 1)[1]); i += 1; continue + out.append(argv[i]); i += 1 + if len(out) not in (2, 3): + print(__doc__) + return 1 + src, dst = out[0], out[1] + codec = out[2] if len(out) == 3 else "lz4" + if codec not in ("none", "lz4", "zstd"): + print(f"ERROR: unknown codec {codec!r}") + return 1 + zstd_level = 9 if codec == "zstd" else 0 + + if not os.path.isdir(src): + print(f"ERROR: source not found: {src}") + return 1 + stale = [f for f in os.listdir(dst) if f.endswith(".skene")] if os.path.isdir(dst) else [] + if stale: + print(f"ERROR: {dst} already holds {len(stale)} .skene file(s); rm -rf it first") + return 1 + + paths = sorted(os.path.join(src, f) for f in os.listdir(src) if f.endswith(".parquet")) + if not paths: + print(f"ERROR: no parquet files in {src}") + return 1 + + # Row counts come from the source footers up front, so output index ranges + # can be reserved before any worker starts and no worker has to ask another + # where its files begin. + row_counts = [read_metadata(p).num_rows for p in paths] + expected_total = sum(row_counts) + + tasks = [] + next_index = 0 + for chunk_paths, chunk_rows in _plan_chunks(paths, row_counts, workers): + tasks.append((chunk_paths, dst, "hits", codec, zstd_level, next_index, chunk_rows)) + next_index += _output_files_for(chunk_rows) + + os.makedirs(dst, exist_ok=True) + if len(tasks) == 1: + results = [_convert_chunk(tasks[0])] + else: + with ProcessPoolExecutor(max_workers=len(tasks)) as pool: + results = list(pool.map(_convert_chunk, tasks)) + + files = sum(r[0] for r in results) + rows = sum(r[1] for r in results) + nbytes = sum(r[2] for r in results) + if rows != expected_total: + raise RuntimeError(f"wrote {rows:,} rows, sources hold {expected_total:,}") + if files == 0: + raise RuntimeError("no row groups read - refusing to write an empty table") + print(f"codec={codec} workers={len(tasks)} files={files} rows={rows} bytes={nbytes}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/opteryx-skene/data-size b/opteryx-skene/data-size new file mode 100755 index 0000000000..8e65ea4b35 --- /dev/null +++ b/opteryx-skene/data-size @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +du -bcs hits | awk '/total$/ { print $1 }' diff --git a/opteryx-skene/install b/opteryx-skene/install new file mode 100755 index 0000000000..21ba004257 --- /dev/null +++ b/opteryx-skene/install @@ -0,0 +1,44 @@ +#!/bin/bash +set -e + +# Opteryx (PyPI package: opteryx-core) on stock CPython 3.14. +# +# NOT the free-threaded build. Opteryx abandoned the free-threaded-Python +# experiment in June 2026; the engine's parallelism target is native (C++) +# threads under a released GIL, so the GIL build is the supported and the +# representative configuration. opteryx-core stopped publishing cp314t wheels +# after 0.9.16 accordingly -- installing onto a 3.14t interpreter would fall +# through to the sdist and try to build Rust/C++ on the box. +# +# opteryx-core publishes cp314 x86_64 manylinux wheels and declares no runtime +# dependencies, so this is a single binary-wheel download -- no on-box +# compilation and no toolchain. x86_64 only, which is fine: the canonical +# c6a.4xlarge is x86_64. + +sudo apt-get update -y +sudo apt-get install -y software-properties-common +sudo add-apt-repository -y ppa:deadsnakes/ppa +sudo apt-get update -y + +# Ubuntu 24.04 (noble) ships 3.12; 3.14 comes from deadsnakes, which carries +# python3.14 for noble. python3.14-venv provides the venv module. +sudo apt-get install -y python3.14 python3.14-venv git wget + +if [ ! -d "$HOME/opteryx_venv" ]; then + python3.14 -m venv "$HOME/opteryx_venv" +fi + +"$HOME/opteryx_venv/bin/python" -m pip install --upgrade pip +# Pulls the cp314 wheel for the latest release. +"$HOME/opteryx_venv/bin/python" -m pip install --upgrade opteryx-core + +# Fail loudly here rather than 43 queries later if pip silently fell back to a +# source build or resolved an interpreter we did not expect. +"$HOME/opteryx_venv/bin/python" - <<'PY' +import sys + +import opteryx + +print(f"python {sys.version.split()[0]} (GIL enabled: {sys._is_gil_enabled()})") +print(f"opteryx-core {opteryx.__version__}") +PY diff --git a/opteryx-skene/load b/opteryx-skene/load new file mode 100755 index 0000000000..f714884db7 --- /dev/null +++ b/opteryx-skene/load @@ -0,0 +1,19 @@ +#!/bin/bash +# ClickBench ships parquet; skene is Opteryx's native format, so the dataset has +# to be converted before it can be queried. That conversion IS the load step, +# and its wall-clock is what `Load time` reports -- the same shape as any entry +# that ingests the source data into a native store. +set -e + +mkdir -p parquet_src +mv hits_*.parquet parquet_src/ 2>/dev/null || true + +# lz4 is the engine's read-first ("performance") posture. See README.md: it is +# deliberately NOT the codec the published parquet corpus uses, so this entry +# and `Opteryx (Parquet, partitioned)` differ in codec as well as in format. +"$HOME/opteryx_venv/bin/python" convert.py parquet_src hits lz4 + +# Drop the source once converted: `data-size` must measure the skene dataset, +# not skene plus a parquet copy, and 500 GB does not need to hold both. +rm -rf parquet_src +sync diff --git a/opteryx-skene/queries.sql b/opteryx-skene/queries.sql new file mode 100644 index 0000000000..906e5cd6ec --- /dev/null +++ b/opteryx-skene/queries.sql @@ -0,0 +1,43 @@ +SELECT COUNT(*) FROM hits; +SELECT COUNT(*) FROM hits WHERE AdvEngineID <> 0; +SELECT SUM(AdvEngineID), COUNT(*), AVG(ResolutionWidth) FROM hits; +SELECT AVG(UserID) FROM hits; +SELECT COUNT(DISTINCT UserID) FROM hits; +SELECT COUNT(DISTINCT SearchPhrase) FROM hits; +SELECT MIN(EventDate), MAX(EventDate) FROM hits; +SELECT AdvEngineID, COUNT(*) FROM hits WHERE AdvEngineID <> 0 GROUP BY AdvEngineID ORDER BY COUNT(*) DESC; +SELECT RegionID, COUNT(DISTINCT UserID) AS u FROM hits GROUP BY RegionID ORDER BY u DESC LIMIT 10; +SELECT RegionID, SUM(AdvEngineID), COUNT(*) AS c, AVG(ResolutionWidth), COUNT(DISTINCT UserID) FROM hits GROUP BY RegionID ORDER BY c DESC LIMIT 10; +SELECT MobilePhoneModel, COUNT(DISTINCT UserID) AS u FROM hits WHERE MobilePhoneModel <> '' GROUP BY MobilePhoneModel ORDER BY u DESC LIMIT 10; +SELECT MobilePhone, MobilePhoneModel, COUNT(DISTINCT UserID) AS u FROM hits WHERE MobilePhoneModel <> '' GROUP BY MobilePhone, MobilePhoneModel ORDER BY u DESC LIMIT 10; +SELECT SearchPhrase, COUNT(*) AS c FROM hits WHERE SearchPhrase <> '' GROUP BY SearchPhrase ORDER BY c DESC LIMIT 10; +SELECT SearchPhrase, COUNT(DISTINCT UserID) AS u FROM hits WHERE SearchPhrase <> '' GROUP BY SearchPhrase ORDER BY u DESC LIMIT 10; +SELECT SearchEngineID, SearchPhrase, COUNT(*) AS c FROM hits WHERE SearchPhrase <> '' GROUP BY SearchEngineID, SearchPhrase ORDER BY c DESC LIMIT 10; +SELECT UserID, COUNT(*) FROM hits GROUP BY UserID ORDER BY COUNT(*) DESC LIMIT 10; +SELECT UserID, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, SearchPhrase ORDER BY COUNT(*) DESC LIMIT 10; +SELECT UserID, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, SearchPhrase LIMIT 10; +SELECT UserID, extract(minute FROM EventTime::TIMESTAMP[s]) AS m, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, extract(minute FROM EventTime::TIMESTAMP[s]), SearchPhrase ORDER BY COUNT(*) DESC LIMIT 10; +SELECT UserID FROM hits WHERE UserID = 435090932899640449; +SELECT COUNT(*) FROM hits WHERE URL LIKE '%google%'; +SELECT SearchPhrase, MIN(URL), COUNT(*) AS c FROM hits WHERE URL LIKE '%google%' AND SearchPhrase <> '' GROUP BY SearchPhrase ORDER BY c DESC LIMIT 10; +SELECT SearchPhrase, MIN(URL), MIN(Title), COUNT(*) AS c, COUNT(DISTINCT UserID) FROM hits WHERE Title LIKE '%Google%' AND URL NOT LIKE '%.google.%' AND SearchPhrase <> '' GROUP BY SearchPhrase ORDER BY c DESC LIMIT 10; +SELECT * FROM hits WHERE URL LIKE '%google%' ORDER BY EventTime LIMIT 10; +SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY EventTime LIMIT 10; +SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY SearchPhrase LIMIT 10; +SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY EventTime, SearchPhrase LIMIT 10; +SELECT CounterID, AVG(length(URL)) AS l, COUNT(*) AS c FROM hits WHERE URL <> '' GROUP BY CounterID HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25; +SELECT REGEXP_REPLACE(Referer, b'^https?://(?:www\.)?([^/]+)/.*$', r'\1') AS k, AVG(length(Referer)) AS l, COUNT(*) AS c, MIN(Referer) FROM hits WHERE Referer <> '' GROUP BY REGEXP_REPLACE(Referer, b'^https?://(?:www\.)?([^/]+)/.*$', r'\1') HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25; +SELECT SUM(ResolutionWidth), SUM(ResolutionWidth + 1), SUM(ResolutionWidth + 2), SUM(ResolutionWidth + 3), SUM(ResolutionWidth + 4), SUM(ResolutionWidth + 5), SUM(ResolutionWidth + 6), SUM(ResolutionWidth + 7), SUM(ResolutionWidth + 8), SUM(ResolutionWidth + 9), SUM(ResolutionWidth + 10), SUM(ResolutionWidth + 11), SUM(ResolutionWidth + 12), SUM(ResolutionWidth + 13), SUM(ResolutionWidth + 14), SUM(ResolutionWidth + 15), SUM(ResolutionWidth + 16), SUM(ResolutionWidth + 17), SUM(ResolutionWidth + 18), SUM(ResolutionWidth + 19), SUM(ResolutionWidth + 20), SUM(ResolutionWidth + 21), SUM(ResolutionWidth + 22), SUM(ResolutionWidth + 23), SUM(ResolutionWidth + 24), SUM(ResolutionWidth + 25), SUM(ResolutionWidth + 26), SUM(ResolutionWidth + 27), SUM(ResolutionWidth + 28), SUM(ResolutionWidth + 29), SUM(ResolutionWidth + 30), SUM(ResolutionWidth + 31), SUM(ResolutionWidth + 32), SUM(ResolutionWidth + 33), SUM(ResolutionWidth + 34), SUM(ResolutionWidth + 35), SUM(ResolutionWidth + 36), SUM(ResolutionWidth + 37), SUM(ResolutionWidth + 38), SUM(ResolutionWidth + 39), SUM(ResolutionWidth + 40), SUM(ResolutionWidth + 41), SUM(ResolutionWidth + 42), SUM(ResolutionWidth + 43), SUM(ResolutionWidth + 44), SUM(ResolutionWidth + 45), SUM(ResolutionWidth + 46), SUM(ResolutionWidth + 47), SUM(ResolutionWidth + 48), SUM(ResolutionWidth + 49), SUM(ResolutionWidth + 50), SUM(ResolutionWidth + 51), SUM(ResolutionWidth + 52), SUM(ResolutionWidth + 53), SUM(ResolutionWidth + 54), SUM(ResolutionWidth + 55), SUM(ResolutionWidth + 56), SUM(ResolutionWidth + 57), SUM(ResolutionWidth + 58), SUM(ResolutionWidth + 59), SUM(ResolutionWidth + 60), SUM(ResolutionWidth + 61), SUM(ResolutionWidth + 62), SUM(ResolutionWidth + 63), SUM(ResolutionWidth + 64), SUM(ResolutionWidth + 65), SUM(ResolutionWidth + 66), SUM(ResolutionWidth + 67), SUM(ResolutionWidth + 68), SUM(ResolutionWidth + 69), SUM(ResolutionWidth + 70), SUM(ResolutionWidth + 71), SUM(ResolutionWidth + 72), SUM(ResolutionWidth + 73), SUM(ResolutionWidth + 74), SUM(ResolutionWidth + 75), SUM(ResolutionWidth + 76), SUM(ResolutionWidth + 77), SUM(ResolutionWidth + 78), SUM(ResolutionWidth + 79), SUM(ResolutionWidth + 80), SUM(ResolutionWidth + 81), SUM(ResolutionWidth + 82), SUM(ResolutionWidth + 83), SUM(ResolutionWidth + 84), SUM(ResolutionWidth + 85), SUM(ResolutionWidth + 86), SUM(ResolutionWidth + 87), SUM(ResolutionWidth + 88), SUM(ResolutionWidth + 89) FROM hits; +SELECT SearchEngineID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) FROM hits WHERE SearchPhrase <> '' GROUP BY SearchEngineID, ClientIP ORDER BY c DESC LIMIT 10; +SELECT WatchID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) FROM hits WHERE SearchPhrase <> '' GROUP BY WatchID, ClientIP ORDER BY c DESC LIMIT 10; +SELECT WatchID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) FROM hits GROUP BY WatchID, ClientIP ORDER BY c DESC LIMIT 10; +SELECT URL, COUNT(*) AS c FROM hits GROUP BY URL ORDER BY c DESC LIMIT 10; +SELECT 1, URL, COUNT(*) AS c FROM hits GROUP BY 1, URL ORDER BY c DESC LIMIT 10; +SELECT ClientIP, ClientIP - 1, ClientIP - 2, ClientIP - 3, COUNT(*) AS c FROM hits GROUP BY ClientIP, ClientIP - 1, ClientIP - 2, ClientIP - 3 ORDER BY c DESC LIMIT 10; +SELECT URL, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate::DATE >= '2013-07-01'::DATE AND EventDate::DATE <= '2013-07-31'::DATE AND DontCountHits = 0 AND IsRefresh = 0 AND URL <> '' GROUP BY URL ORDER BY PageViews DESC LIMIT 10; +SELECT Title, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate::DATE >= '2013-07-01'::DATE AND EventDate::DATE <= '2013-07-31'::DATE AND DontCountHits = 0 AND IsRefresh = 0 AND Title <> '' GROUP BY Title ORDER BY PageViews DESC LIMIT 10; +SELECT URL, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate::DATE >= '2013-07-01'::DATE AND EventDate::DATE <= '2013-07-31'::DATE AND IsRefresh = 0 AND IsLink <> 0 AND IsDownload = 0 GROUP BY URL ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; +SELECT TraficSourceID, SearchEngineID, AdvEngineID, CASE WHEN (SearchEngineID = 0 AND AdvEngineID = 0) THEN Referer ELSE '' END AS Src, URL AS Dst, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate::DATE >= '2013-07-01'::DATE AND EventDate::DATE <= '2013-07-31'::DATE AND IsRefresh = 0 GROUP BY TraficSourceID, SearchEngineID, AdvEngineID, CASE WHEN (SearchEngineID = 0 AND AdvEngineID = 0) THEN Referer ELSE '' END, URL ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; +SELECT URLHash, EventDate, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate::DATE >= '2013-07-01'::DATE AND EventDate::DATE <= '2013-07-31'::DATE AND IsRefresh = 0 AND TraficSourceID IN (-1, 6) AND RefererHash = 3594120000172545465 GROUP BY URLHash, EventDate ORDER BY PageViews DESC LIMIT 10 OFFSET 100; +SELECT WindowClientWidth, WindowClientHeight, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate::DATE >= '2013-07-01'::DATE AND EventDate::DATE <= '2013-07-31'::DATE AND IsRefresh = 0 AND DontCountHits = 0 AND URLHash = 2868770270353813622 GROUP BY WindowClientWidth, WindowClientHeight ORDER BY PageViews DESC LIMIT 10 OFFSET 10000; +SELECT TRUNC(EventTime::TIMESTAMP[s], 'minute') AS M, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate::DATE >= '2013-07-14'::DATE AND EventDate::DATE <= '2013-07-15'::DATE AND IsRefresh = 0 AND DontCountHits = 0 GROUP BY TRUNC(EventTime::TIMESTAMP[s], 'minute') ORDER BY M LIMIT 10 OFFSET 1000; diff --git a/opteryx-skene/query b/opteryx-skene/query new file mode 100755 index 0000000000..f2ca00e871 --- /dev/null +++ b/opteryx-skene/query @@ -0,0 +1,42 @@ +#!/bin/bash +# Reads a SQL query from stdin, runs it via opteryx-core (Python in-process) +# against the partitioned parquet under ./hits/. +# Stdout: query result as TSV (header + rows). +# Stderr: query runtime in fractional seconds on the last line. +set -e + +query=$(cat) + +"$HOME/opteryx_venv/bin/python" - "$query" <<'PY' +import sys +import timeit + +import opteryx + +query = sys.argv[1] + +# opteryx-core execution surface: a Session that yields native +# morsels. `execute_to_morsels` runs entirely in the native engine; draining +# the generator is the execution. We time the drain (matching the engine's own +# ClickBench runner), holding the morsels so results can be emitted afterwards. +# NB: opteryx.query() / Cursor.arrow() from the 0.x line no longer exist here. +session = opteryx.session() +start = timeit.default_timer() +morsels = list(session.execute_to_morsels(query)) +end = timeit.default_timer() + +cols = None +for morsel in morsels: + if cols is None: + cols = morsel.column_names + print("\t".join( + c.decode() if isinstance(c, (bytes, bytearray)) else str(c) + for c in cols + )) + for i in range(morsel.num_rows): + print("\t".join("" if v is None else str(v) for v in morsel[i])) + +session.close() + +print(f"{end - start:.3f}", file=sys.stderr) +PY diff --git a/opteryx-skene/start b/opteryx-skene/start new file mode 100755 index 0000000000..06bd986563 --- /dev/null +++ b/opteryx-skene/start @@ -0,0 +1,2 @@ +#!/bin/bash +exit 0 diff --git a/opteryx-skene/stop b/opteryx-skene/stop new file mode 100755 index 0000000000..06bd986563 --- /dev/null +++ b/opteryx-skene/stop @@ -0,0 +1,2 @@ +#!/bin/bash +exit 0 diff --git a/opteryx-skene/template.json b/opteryx-skene/template.json new file mode 100644 index 0000000000..36032e7542 --- /dev/null +++ b/opteryx-skene/template.json @@ -0,0 +1,12 @@ +{ + "system": "Opteryx", + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": [ + "C++", + "stateless", + "column-oriented", + "embedded" + ] +} diff --git a/opteryx/README.md b/opteryx/README.md index 877bee0ed1..2338ac4c10 100644 --- a/opteryx/README.md +++ b/opteryx/README.md @@ -1,38 +1,40 @@ # Opteryx -Opteryx is an in-process SQL query engine written in Python/Cython that leverages Apache Arrow as its in-memory format. Designed for ad hoc queries, Opteryx directly queries data from storage without requiring any preloading or preprocessing. +Opteryx is an in-process SQL query engine. Query **planning** (parse, bind, +optimize) runs in Python; query **execution** is native (Cython/C++). It +queries Parquet directly from storage with no preloading or preprocessing, +which makes it well suited to ad hoc analytics. For more information, visit: -- [Opteryx Documentation](https://opteryx.dev/) -- [Opteryx GitHub Repository](https://github.com/mabel-dev/opteryx) +- [Opteryx Documentation](https://docs.opteryx.app/) +- [Opteryx GitHub Repository](https://github.com/mabel-dev/opteryx-core) -This page provides instructions for benchmarking Opteryx using the split Parquet files provided by ClickBench. +This page benchmarks Opteryx (PyPI package `opteryx-core`) using the split +Parquet files provided by ClickBench. --- ## Generating Benchmark Results -To generate benchmark results, follow these steps: - -### **High-level Steps** +### High-level Steps 1. Set up the environment. 2. Install Python and the required dependencies. 3. Download the benchmark dataset. 4. Run the benchmark script. -### **Detailed Instructions** +### Detailed Instructions 1. **Start an AWS EC2 instance** - OS: Ubuntu 24 - - Architecture: 64-bit + - Architecture: 64-bit (x86_64 or AArch64) - Instance Type: `c6a.4xlarge` - Root Storage: 500 GB gp2 SSD - - Advanced Details: Ensure 'EBS-optimized instance' is **disabled**. + - Advanced Details: ensure 'EBS-optimized instance' is **disabled**. -2. **SSH into the instance** (after the status checks are complete): +2. **SSH into the instance** (after status checks complete): ~~~bash - ssh ec2-user@{ip} + ssh ubuntu@{ip} ~~~ 3. **Update the package list and install Git** @@ -52,6 +54,40 @@ To generate benchmark results, follow these steps: sudo ./benchmark.sh ~~~ +### Python version + +`opteryx-core` publishes cp314 x86_64 and AArch64 manylinux wheels and declares +no runtime dependencies, so `install` is a single binary-wheel download with no +on-box compilation and no toolchain. + +### Query dialect + +`queries.sql` adapts queries to Opteryx's dialect. The adaptations are syntactic +— they do not change what is computed, the rows returned, or the work the engine has to do: + +- **Q19, Q43** — `EventTime` is stored as an integer epoch, so it is cast + explicitly (`EventTime::TIMESTAMP[s]`) before `extract(minute FROM ...)` and + before truncation. +- **Q43** — `TRUNC(, 'minute')` rather than `DATE_TRUNC('minute', )`. +- **Q29** — the `REGEXP_REPLACE` pattern and replacement use `b''` and `r''` + literals so the backslash reference survives to the regex engine. +- **Q37-Q42** — `EventDate` comparisons cast both sides to `DATE` + (`EventDate::DATE >= '2013-07-01'::DATE`). + +### Hardware coverage + +Results are published for instance types with **32 or fewer vCPUs**. The account +used for these runs is limited to 32 concurrent on-demand vCPUs, so the 192-vCPU +machines in the ClickBench fleet (`c6a.metal`, `c7a.metal-48xl`, +`c8g.metal-48xl`) could not be launched. The published set spans 2 to 16 vCPUs +on both x86_64 (`c6a.*`, `t3a.small`) and AArch64 (`c8g.*`), which covers the +small/medium range of the standard fleet on both architectures. + ### Known Issues -- Queries 33 and 34 fail due to Out of Memory (OOM) errors. +- On the memory-constrained instances the heaviest `GROUP BY` queries do not fit + in RAM and spill to swap rather than failing. They complete, but two orders of + magnitude slower — on `c6a.xlarge` (8 GB) three queries account for more than + half the total runtime. The benchmark environment provides the 16 GB swapfile + that ClickBench's `cloud-init` configures for every system; without it these + queries would be `null` instead of slow. diff --git a/opteryx/check b/opteryx/check index 4d4c12fd75..ed305e8015 100755 --- a/opteryx/check +++ b/opteryx/check @@ -1,4 +1,34 @@ #!/bin/bash set -e -"$HOME/opteryx_venv/bin/python" -m opteryx "SELECT version()" >/dev/null +# Smoke test that actually executes through the engine and propagates a +# non-zero exit on failure. The old `python -m opteryx "SELECT version()"` +# CLI path is unusable here for two reasons: +# +# 1. opteryx-core's single-run CLI calls the removed opteryx.query(), +# catches the AttributeError, and STILL exits 0 -- a false green that +# would let a broken install pass this check. +# 2. `version()` is not a function in the current engine; the version is +# exposed as the system variable `@@version`. +# +# This matters more than it looks: bench_load() in lib/benchmark-common.sh +# calls ./check unconditionally, AFTER the ~14 GB download and load. A check +# that cannot pass burns the whole setup cost before failing. +# +# Assert the shape of the value rather than a literal, so the check does not +# need editing on every release. +"$HOME/opteryx_venv/bin/python" - <<'PY' +import re + +import opteryx + +session = opteryx.session() +morsels = list(session.execute_to_morsels("SELECT @@version")) +session.close() + +assert morsels, "SELECT @@version returned no morsels" +value = list(morsels[0][0])[0] +if isinstance(value, (bytes, bytearray)): + value = value.decode() +assert re.fullmatch(r"\d+\.\d+\.\d+.*", str(value)), f"unexpected version {value!r}" +PY diff --git a/opteryx/install b/opteryx/install index f2c4ed9349..21ba004257 100755 --- a/opteryx/install +++ b/opteryx/install @@ -1,20 +1,44 @@ #!/bin/bash set -e +# Opteryx (PyPI package: opteryx-core) on stock CPython 3.14. +# +# NOT the free-threaded build. Opteryx abandoned the free-threaded-Python +# experiment in June 2026; the engine's parallelism target is native (C++) +# threads under a released GIL, so the GIL build is the supported and the +# representative configuration. opteryx-core stopped publishing cp314t wheels +# after 0.9.16 accordingly -- installing onto a 3.14t interpreter would fall +# through to the sdist and try to build Rust/C++ on the box. +# +# opteryx-core publishes cp314 x86_64 manylinux wheels and declares no runtime +# dependencies, so this is a single binary-wheel download -- no on-box +# compilation and no toolchain. x86_64 only, which is fine: the canonical +# c6a.4xlarge is x86_64. + sudo apt-get update -y sudo apt-get install -y software-properties-common sudo add-apt-repository -y ppa:deadsnakes/ppa sudo apt-get update -y -sudo apt-get install -y python3.11 python3.11-venv git wget build-essential python3.11-dev + +# Ubuntu 24.04 (noble) ships 3.12; 3.14 comes from deadsnakes, which carries +# python3.14 for noble. python3.14-venv provides the venv module. +sudo apt-get install -y python3.14 python3.14-venv git wget if [ ! -d "$HOME/opteryx_venv" ]; then - python3.11 -m venv "$HOME/opteryx_venv" + python3.14 -m venv "$HOME/opteryx_venv" fi "$HOME/opteryx_venv/bin/python" -m pip install --upgrade pip -# 0.26.1 only ships x86_64 wheels, so arm64 hosts (c8g.*) fell through -# to sdist where the build failed at "opteryx/third_party/abseil/ -# containers.pyx doesn't match any files". 0.26.8 publishes -# manylinux2014_aarch64 wheels for cp310-cp313, which fixes arm64 -# without changing anything for x86_64. -"$HOME/opteryx_venv/bin/python" -m pip install --upgrade opteryx==0.26.8 +# Pulls the cp314 wheel for the latest release. +"$HOME/opteryx_venv/bin/python" -m pip install --upgrade opteryx-core + +# Fail loudly here rather than 43 queries later if pip silently fell back to a +# source build or resolved an interpreter we did not expect. +"$HOME/opteryx_venv/bin/python" - <<'PY' +import sys + +import opteryx + +print(f"python {sys.version.split()[0]} (GIL enabled: {sys._is_gil_enabled()})") +print(f"opteryx-core {opteryx.__version__}") +PY diff --git a/opteryx/queries.sql b/opteryx/queries.sql index fa5568c632..906e5cd6ec 100644 --- a/opteryx/queries.sql +++ b/opteryx/queries.sql @@ -16,7 +16,7 @@ SELECT SearchEngineID, SearchPhrase, COUNT(*) AS c FROM hits WHERE SearchPhrase SELECT UserID, COUNT(*) FROM hits GROUP BY UserID ORDER BY COUNT(*) DESC LIMIT 10; SELECT UserID, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, SearchPhrase ORDER BY COUNT(*) DESC LIMIT 10; SELECT UserID, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, SearchPhrase LIMIT 10; -SELECT UserID, extract(minute FROM EventTime) AS m, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, extract(minute FROM EventTime), SearchPhrase ORDER BY COUNT(*) DESC LIMIT 10; +SELECT UserID, extract(minute FROM EventTime::TIMESTAMP[s]) AS m, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, extract(minute FROM EventTime::TIMESTAMP[s]), SearchPhrase ORDER BY COUNT(*) DESC LIMIT 10; SELECT UserID FROM hits WHERE UserID = 435090932899640449; SELECT COUNT(*) FROM hits WHERE URL LIKE '%google%'; SELECT SearchPhrase, MIN(URL), COUNT(*) AS c FROM hits WHERE URL LIKE '%google%' AND SearchPhrase <> '' GROUP BY SearchPhrase ORDER BY c DESC LIMIT 10; @@ -26,7 +26,7 @@ SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY EventTime LIMIT SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY SearchPhrase LIMIT 10; SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY EventTime, SearchPhrase LIMIT 10; SELECT CounterID, AVG(length(URL)) AS l, COUNT(*) AS c FROM hits WHERE URL <> '' GROUP BY CounterID HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25; -SELECT REGEXP_REPLACE(Referer, b'^https?://(?:www\.)?([^/]+)/.*$', r'\\1') AS k, AVG(length(Referer)) AS l, COUNT(*) AS c, MIN(Referer) FROM hits WHERE Referer <> '' GROUP BY REGEXP_REPLACE(Referer, b'^https?://(?:www\.)?([^/]+)/.*$', r'\\1') HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25; +SELECT REGEXP_REPLACE(Referer, b'^https?://(?:www\.)?([^/]+)/.*$', r'\1') AS k, AVG(length(Referer)) AS l, COUNT(*) AS c, MIN(Referer) FROM hits WHERE Referer <> '' GROUP BY REGEXP_REPLACE(Referer, b'^https?://(?:www\.)?([^/]+)/.*$', r'\1') HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25; SELECT SUM(ResolutionWidth), SUM(ResolutionWidth + 1), SUM(ResolutionWidth + 2), SUM(ResolutionWidth + 3), SUM(ResolutionWidth + 4), SUM(ResolutionWidth + 5), SUM(ResolutionWidth + 6), SUM(ResolutionWidth + 7), SUM(ResolutionWidth + 8), SUM(ResolutionWidth + 9), SUM(ResolutionWidth + 10), SUM(ResolutionWidth + 11), SUM(ResolutionWidth + 12), SUM(ResolutionWidth + 13), SUM(ResolutionWidth + 14), SUM(ResolutionWidth + 15), SUM(ResolutionWidth + 16), SUM(ResolutionWidth + 17), SUM(ResolutionWidth + 18), SUM(ResolutionWidth + 19), SUM(ResolutionWidth + 20), SUM(ResolutionWidth + 21), SUM(ResolutionWidth + 22), SUM(ResolutionWidth + 23), SUM(ResolutionWidth + 24), SUM(ResolutionWidth + 25), SUM(ResolutionWidth + 26), SUM(ResolutionWidth + 27), SUM(ResolutionWidth + 28), SUM(ResolutionWidth + 29), SUM(ResolutionWidth + 30), SUM(ResolutionWidth + 31), SUM(ResolutionWidth + 32), SUM(ResolutionWidth + 33), SUM(ResolutionWidth + 34), SUM(ResolutionWidth + 35), SUM(ResolutionWidth + 36), SUM(ResolutionWidth + 37), SUM(ResolutionWidth + 38), SUM(ResolutionWidth + 39), SUM(ResolutionWidth + 40), SUM(ResolutionWidth + 41), SUM(ResolutionWidth + 42), SUM(ResolutionWidth + 43), SUM(ResolutionWidth + 44), SUM(ResolutionWidth + 45), SUM(ResolutionWidth + 46), SUM(ResolutionWidth + 47), SUM(ResolutionWidth + 48), SUM(ResolutionWidth + 49), SUM(ResolutionWidth + 50), SUM(ResolutionWidth + 51), SUM(ResolutionWidth + 52), SUM(ResolutionWidth + 53), SUM(ResolutionWidth + 54), SUM(ResolutionWidth + 55), SUM(ResolutionWidth + 56), SUM(ResolutionWidth + 57), SUM(ResolutionWidth + 58), SUM(ResolutionWidth + 59), SUM(ResolutionWidth + 60), SUM(ResolutionWidth + 61), SUM(ResolutionWidth + 62), SUM(ResolutionWidth + 63), SUM(ResolutionWidth + 64), SUM(ResolutionWidth + 65), SUM(ResolutionWidth + 66), SUM(ResolutionWidth + 67), SUM(ResolutionWidth + 68), SUM(ResolutionWidth + 69), SUM(ResolutionWidth + 70), SUM(ResolutionWidth + 71), SUM(ResolutionWidth + 72), SUM(ResolutionWidth + 73), SUM(ResolutionWidth + 74), SUM(ResolutionWidth + 75), SUM(ResolutionWidth + 76), SUM(ResolutionWidth + 77), SUM(ResolutionWidth + 78), SUM(ResolutionWidth + 79), SUM(ResolutionWidth + 80), SUM(ResolutionWidth + 81), SUM(ResolutionWidth + 82), SUM(ResolutionWidth + 83), SUM(ResolutionWidth + 84), SUM(ResolutionWidth + 85), SUM(ResolutionWidth + 86), SUM(ResolutionWidth + 87), SUM(ResolutionWidth + 88), SUM(ResolutionWidth + 89) FROM hits; SELECT SearchEngineID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) FROM hits WHERE SearchPhrase <> '' GROUP BY SearchEngineID, ClientIP ORDER BY c DESC LIMIT 10; SELECT WatchID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) FROM hits WHERE SearchPhrase <> '' GROUP BY WatchID, ClientIP ORDER BY c DESC LIMIT 10; @@ -34,10 +34,10 @@ SELECT WatchID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) FR SELECT URL, COUNT(*) AS c FROM hits GROUP BY URL ORDER BY c DESC LIMIT 10; SELECT 1, URL, COUNT(*) AS c FROM hits GROUP BY 1, URL ORDER BY c DESC LIMIT 10; SELECT ClientIP, ClientIP - 1, ClientIP - 2, ClientIP - 3, COUNT(*) AS c FROM hits GROUP BY ClientIP, ClientIP - 1, ClientIP - 2, ClientIP - 3 ORDER BY c DESC LIMIT 10; -SELECT URL, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND DontCountHits = 0 AND IsRefresh = 0 AND URL <> '' GROUP BY URL ORDER BY PageViews DESC LIMIT 10; -SELECT Title, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND DontCountHits = 0 AND IsRefresh = 0 AND Title <> '' GROUP BY Title ORDER BY PageViews DESC LIMIT 10; -SELECT URL, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND IsRefresh = 0 AND IsLink <> 0 AND IsDownload = 0 GROUP BY URL ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; -SELECT TraficSourceID, SearchEngineID, AdvEngineID, CASE WHEN (SearchEngineID = 0 AND AdvEngineID = 0) THEN Referer ELSE '' END AS Src, URL AS Dst, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND IsRefresh = 0 GROUP BY TraficSourceID, SearchEngineID, AdvEngineID, CASE WHEN (SearchEngineID = 0 AND AdvEngineID = 0) THEN Referer ELSE '' END, URL ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; -SELECT URLHash, EventDate, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND IsRefresh = 0 AND TraficSourceID IN (-1, 6) AND RefererHash = 3594120000172545465 GROUP BY URLHash, EventDate ORDER BY PageViews DESC LIMIT 10 OFFSET 100; -SELECT WindowClientWidth, WindowClientHeight, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND IsRefresh = 0 AND DontCountHits = 0 AND URLHash = 2868770270353813622 GROUP BY WindowClientWidth, WindowClientHeight ORDER BY PageViews DESC LIMIT 10 OFFSET 10000; -SELECT DATE_TRUNC('minute', EventTime) AS M, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-14' AND EventDate <= '2013-07-15' AND IsRefresh = 0 AND DontCountHits = 0 GROUP BY DATE_TRUNC('minute', EventTime) ORDER BY M LIMIT 10 OFFSET 1000; +SELECT URL, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate::DATE >= '2013-07-01'::DATE AND EventDate::DATE <= '2013-07-31'::DATE AND DontCountHits = 0 AND IsRefresh = 0 AND URL <> '' GROUP BY URL ORDER BY PageViews DESC LIMIT 10; +SELECT Title, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate::DATE >= '2013-07-01'::DATE AND EventDate::DATE <= '2013-07-31'::DATE AND DontCountHits = 0 AND IsRefresh = 0 AND Title <> '' GROUP BY Title ORDER BY PageViews DESC LIMIT 10; +SELECT URL, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate::DATE >= '2013-07-01'::DATE AND EventDate::DATE <= '2013-07-31'::DATE AND IsRefresh = 0 AND IsLink <> 0 AND IsDownload = 0 GROUP BY URL ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; +SELECT TraficSourceID, SearchEngineID, AdvEngineID, CASE WHEN (SearchEngineID = 0 AND AdvEngineID = 0) THEN Referer ELSE '' END AS Src, URL AS Dst, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate::DATE >= '2013-07-01'::DATE AND EventDate::DATE <= '2013-07-31'::DATE AND IsRefresh = 0 GROUP BY TraficSourceID, SearchEngineID, AdvEngineID, CASE WHEN (SearchEngineID = 0 AND AdvEngineID = 0) THEN Referer ELSE '' END, URL ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; +SELECT URLHash, EventDate, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate::DATE >= '2013-07-01'::DATE AND EventDate::DATE <= '2013-07-31'::DATE AND IsRefresh = 0 AND TraficSourceID IN (-1, 6) AND RefererHash = 3594120000172545465 GROUP BY URLHash, EventDate ORDER BY PageViews DESC LIMIT 10 OFFSET 100; +SELECT WindowClientWidth, WindowClientHeight, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate::DATE >= '2013-07-01'::DATE AND EventDate::DATE <= '2013-07-31'::DATE AND IsRefresh = 0 AND DontCountHits = 0 AND URLHash = 2868770270353813622 GROUP BY WindowClientWidth, WindowClientHeight ORDER BY PageViews DESC LIMIT 10 OFFSET 10000; +SELECT TRUNC(EventTime::TIMESTAMP[s], 'minute') AS M, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate::DATE >= '2013-07-14'::DATE AND EventDate::DATE <= '2013-07-15'::DATE AND IsRefresh = 0 AND DontCountHits = 0 GROUP BY TRUNC(EventTime::TIMESTAMP[s], 'minute') ORDER BY M LIMIT 10 OFFSET 1000; diff --git a/opteryx/query b/opteryx/query index 6d37357757..f2ca00e871 100755 --- a/opteryx/query +++ b/opteryx/query @@ -1,7 +1,7 @@ #!/bin/bash -# Reads a SQL query from stdin, runs it via opteryx (Python in-process) +# Reads a SQL query from stdin, runs it via opteryx-core (Python in-process) # against the partitioned parquet under ./hits/. -# Stdout: query result. +# Stdout: query result as TSV (header + rows). # Stderr: query runtime in fractional seconds on the last line. set -e @@ -10,42 +10,33 @@ query=$(cat) "$HOME/opteryx_venv/bin/python" - "$query" <<'PY' import sys import timeit + import opteryx query = sys.argv[1] +# opteryx-core execution surface: a Session that yields native +# morsels. `execute_to_morsels` runs entirely in the native engine; draining +# the generator is the execution. We time the drain (matching the engine's own +# ClickBench runner), holding the morsels so results can be emitted afterwards. +# NB: opteryx.query() / Cursor.arrow() from the 0.x line no longer exist here. +session = opteryx.session() start = timeit.default_timer() -res = opteryx.query(query) -# In opteryx 0.26 `opteryx.query()` returns a Cursor / Relation. Probe -# the common materialisation methods in priority order — different -# versions expose different combinations. -out = None -for method in ("arrow", "to_arrow_table", "fetchall"): - fn = getattr(res, method, None) - if callable(fn): - try: - out = fn() - break - except Exception: - continue +morsels = list(session.execute_to_morsels(query)) end = timeit.default_timer() -if out is None: - # Final fallback: iterate the cursor directly (older API). - for row in res: - print(row) -elif hasattr(out, "to_pylist"): - # pyarrow.Table — print as TSV header + rows. - cols = out.column_names - print("\t".join(cols)) - for row in out.to_pylist(): - print("\t".join("" if row.get(c) is None else str(row.get(c)) - for c in cols)) -elif isinstance(out, (list, tuple)): - for row in out: - print(row) -else: - print(out) +cols = None +for morsel in morsels: + if cols is None: + cols = morsel.column_names + print("\t".join( + c.decode() if isinstance(c, (bytes, bytearray)) else str(c) + for c in cols + )) + for i in range(morsel.num_rows): + print("\t".join("" if v is None else str(v) for v in morsel[i])) + +session.close() print(f"{end - start:.3f}", file=sys.stderr) PY diff --git a/opteryx/template.json b/opteryx/template.json index 548d083dd8..159898b59b 100644 --- a/opteryx/template.json +++ b/opteryx/template.json @@ -1,9 +1,10 @@ { - "system": "Opteryx", + "system": "Opteryx (Parquet, partitioned)", "proprietary": "no", "hardware": "cpu", "tuned": "no", "tags": [ + "C++", "stateless", "column-oriented", "embedded" From 670bc906532cd1fb324c0c47e86b80a5e2bac8bc Mon Sep 17 00:00:00 2001 From: Justin Joyce Date: Sun, 16 Aug 2026 10:32:37 +0100 Subject: [PATCH 2/2] Opteryx: results for nine machines, 43/43 with no nulls opteryx-core 0.9.69 on CPython 3.14, three tries per query with the page cache dropped before each first try, run through the shared driver. Parquet, eight machines spanning 2 to 16 vCPUs on both architectures: c6a.large, c6a.xlarge, c6a.2xlarge, c6a.4xlarge, c8g.large, c8g.xlarge, c8g.2xlarge, t3a.small. skene on c8g.4xlarge. Every query returns on every machine -- 387 results, no nulls. The published 0.26.x runs nulled 17 of 43 on t3a.small, 15 on c6a.large and 12 on c6a.xlarge; those all now produce numbers. The smaller instances are slow rather than failing: the heavy GROUP BYs run out of swap (10 GB against 2 GB of RAM on t3a.small) using the 16 GB swapfile ClickBench's cloud-init configures for every system. The c8g.* machines are the first AArch64 results for this entry, which 0.9.69 made possible by publishing manylinux aarch64 wheels. Measured at matched sizes, Graviton4 leads the equivalent c6a by 50% at 16 vCPU, 30% at 8, 25% at 4 and 18% at 2 -- the advantage narrowing as the workload moves from CPU-bound to memory-bound. Machines above 32 vCPUs are absent: the account running these is capped at 32 concurrent on-demand vCPUs, so the 192-vCPU metal instances could not be launched. Both READMEs say so. --- .../results/20260815/c8g.4xlarge.json | 59 +++++++++++++++++++ opteryx/results/20260815/c6a.2xlarge.json | 59 +++++++++++++++++++ opteryx/results/20260815/c6a.4xlarge.json | 59 +++++++++++++++++++ opteryx/results/20260815/c6a.large.json | 59 +++++++++++++++++++ opteryx/results/20260815/c6a.xlarge.json | 59 +++++++++++++++++++ opteryx/results/20260815/c8g.2xlarge.json | 59 +++++++++++++++++++ opteryx/results/20260815/c8g.4xlarge.json | 59 +++++++++++++++++++ opteryx/results/20260815/c8g.large.json | 59 +++++++++++++++++++ opteryx/results/20260815/c8g.xlarge.json | 59 +++++++++++++++++++ opteryx/results/20260815/t3a.small.json | 59 +++++++++++++++++++ 10 files changed, 590 insertions(+) create mode 100644 opteryx-skene/results/20260815/c8g.4xlarge.json create mode 100644 opteryx/results/20260815/c6a.2xlarge.json create mode 100644 opteryx/results/20260815/c6a.4xlarge.json create mode 100644 opteryx/results/20260815/c6a.large.json create mode 100644 opteryx/results/20260815/c6a.xlarge.json create mode 100644 opteryx/results/20260815/c8g.2xlarge.json create mode 100644 opteryx/results/20260815/c8g.4xlarge.json create mode 100644 opteryx/results/20260815/c8g.large.json create mode 100644 opteryx/results/20260815/c8g.xlarge.json create mode 100644 opteryx/results/20260815/t3a.small.json diff --git a/opteryx-skene/results/20260815/c8g.4xlarge.json b/opteryx-skene/results/20260815/c8g.4xlarge.json new file mode 100644 index 0000000000..0ae38b9d7c --- /dev/null +++ b/opteryx-skene/results/20260815/c8g.4xlarge.json @@ -0,0 +1,59 @@ +{ + "system": "Opteryx", + "date": "2026-08-15", + "machine": "c8g.4xlarge", + "cluster_size": 1, + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": ["C++", "stateless", "column-oriented", "embedded"], + "load_time": 306.218, + "data_size": 15397554043, + "concurrent_qps": null, + "concurrent_error_ratio": null, + "result": [ + [0.598, 0.084, 0.084], + [0.74, 0.137, 0.138], + [0.884, 0.154, 0.152], + [0.827, 0.138, 0.137], + [1.07, 0.47, 0.472], + [1.789, 0.281, 0.29], + [0.533, 0.086, 0.085], + [0.648, 0.138, 0.139], + [1.594, 0.566, 0.579], + [2.735, 0.662, 0.651], + [1.472, 0.243, 0.241], + [1.707, 0.252, 0.252], + [1.853, 0.456, 0.472], + [2.978, 0.562, 0.556], + [2.153, 0.527, 0.529], + [1.249, 0.541, 0.537], + [3.205, 1.057, 1.052], + [3.223, 1.047, 1.033], + [5.806, 2.317, 2.3], + [0.833, 0.137, 0.136], + [7.582, 0.603, 0.601], + [9.524, 0.661, 0.65], + [15.593, 0.897, 0.908], + [10.127, 0.701, 0.696], + [3.501, 0.247, 0.248], + [1.74, 0.219, 0.217], + [3.533, 0.259, 0.258], + [7.957, 0.752, 0.744], + [7.292, 1.595, 1.573], + [0.817, 0.187, 0.189], + [4.029, 0.439, 0.444], + [6.983, 0.585, 0.577], + [7.014, 3.376, 3.333], + [8.373, 1.947, 1.953], + [8.382, 2.011, 2.023], + [1.046, 0.469, 0.473], + [0.76, 0.199, 0.2], + [0.677, 0.144, 0.146], + [0.7, 0.163, 0.165], + [0.966, 0.369, 0.365], + [0.63, 0.137, 0.136], + [0.631, 0.132, 0.133], + [0.665, 0.129, 0.129] + ] +} diff --git a/opteryx/results/20260815/c6a.2xlarge.json b/opteryx/results/20260815/c6a.2xlarge.json new file mode 100644 index 0000000000..e9f043403c --- /dev/null +++ b/opteryx/results/20260815/c6a.2xlarge.json @@ -0,0 +1,59 @@ +{ + "system": "Opteryx (Parquet, partitioned)", + "date": "2026-08-15", + "machine": "c6a.2xlarge", + "cluster_size": 1, + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": ["C++", "stateless", "column-oriented", "embedded"], + "load_time": 10.227, + "data_size": 14737666736, + "concurrent_qps": null, + "concurrent_error_ratio": null, + "result": [ + [0.709, 0.109, 0.108], + [1.041, 0.389, 0.385], + [1.258, 0.586, 0.593], + [1.15, 0.42, 0.412], + [2.011, 1.355, 1.349], + [1.776, 1.092, 1.079], + [0.745, 0.111, 0.11], + [1.058, 0.388, 0.388], + [2.09, 1.407, 1.419], + [2.524, 1.807, 1.816], + [1.404, 0.653, 0.652], + [1.516, 0.758, 0.768], + [2.196, 1.531, 1.526], + [3.131, 1.934, 1.949], + [2.361, 1.764, 1.768], + [2.131, 1.646, 1.626], + [4.078, 3.441, 3.471], + [4.114, 3.414, 3.366], + [7.986, 7.319, 7.489], + [1.071, 0.232, 0.233], + [10.228, 3.937, 3.92], + [11.871, 4.473, 4.469], + [22.065, 6.993, 6.974], + [12.522, 5.345, 5.432], + [3.148, 1.21, 1.202], + [1.451, 0.871, 0.908], + [3.157, 1.23, 1.245], + [10.184, 3.62, 3.597], + [9.382, 5.992, 6.069], + [1.04, 0.502, 0.499], + [2.709, 1.898, 1.927], + [6.599, 2.387, 2.403], + [27.392, 33.487, 28.97], + [12.506, 7.733, 7.718], + [12.562, 7.849, 7.823], + [1.976, 1.377, 1.382], + [1.078, 0.46, 0.434], + [0.84, 0.269, 0.266], + [1.009, 0.415, 0.407], + [1.439, 0.843, 0.808], + [0.904, 0.214, 0.216], + [0.879, 0.213, 0.211], + [0.934, 0.219, 0.228] + ] +} diff --git a/opteryx/results/20260815/c6a.4xlarge.json b/opteryx/results/20260815/c6a.4xlarge.json new file mode 100644 index 0000000000..fe89cdad0c --- /dev/null +++ b/opteryx/results/20260815/c6a.4xlarge.json @@ -0,0 +1,59 @@ +{ + "system": "Opteryx (Parquet, partitioned)", + "date": "2026-08-15", + "machine": "c6a.4xlarge", + "cluster_size": 1, + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": ["C++", "stateless", "column-oriented", "embedded"], + "load_time": 19.961, + "data_size": 14737666736, + "concurrent_qps": null, + "concurrent_error_ratio": null, + "result": [ + [0.787, 0.109, 0.11], + [0.992, 0.3, 0.304], + [1.093, 0.42, 0.421], + [1.09, 0.373, 0.379], + [1.424, 0.859, 0.868], + [1.532, 0.836, 0.815], + [0.609, 0.112, 0.111], + [0.834, 0.294, 0.296], + [1.945, 1.299, 1.231], + [2.306, 1.608, 1.599], + [1.337, 0.625, 0.634], + [1.415, 0.738, 0.764], + [1.838, 1.228, 1.213], + [3.111, 1.533, 1.563], + [1.943, 1.411, 1.39], + [1.872, 1.177, 1.173], + [3.796, 2.603, 2.527], + [3.626, 2.516, 2.652], + [7.096, 5.322, 5.086], + [0.836, 0.221, 0.227], + [10.708, 3.128, 3.236], + [12.482, 3.721, 3.687], + [23.204, 5.796, 5.685], + [14.072, 4.6, 4.41], + [3.311, 1.014, 1.006], + [1.353, 0.744, 0.76], + [3.58, 1.023, 0.998], + [10.675, 2.966, 2.897], + [9.804, 4.936, 4.921], + [0.929, 0.399, 0.392], + [2.87, 1.482, 1.473], + [6.832, 1.993, 2.1], + [8.367, 7.093, 7.313], + [11.756, 5.995, 6.109], + [11.837, 6.367, 6.227], + [1.485, 0.976, 0.976], + [1.195, 0.435, 0.455], + [0.984, 0.259, 0.259], + [1.135, 0.407, 0.407], + [1.595, 0.82, 0.821], + [0.953, 0.227, 0.218], + [0.936, 0.213, 0.216], + [0.941, 0.225, 0.225] + ] +} diff --git a/opteryx/results/20260815/c6a.large.json b/opteryx/results/20260815/c6a.large.json new file mode 100644 index 0000000000..250923b754 --- /dev/null +++ b/opteryx/results/20260815/c6a.large.json @@ -0,0 +1,59 @@ +{ + "system": "Opteryx (Parquet, partitioned)", + "date": "2026-08-15", + "machine": "c6a.large", + "cluster_size": 1, + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": ["C++", "stateless", "column-oriented", "embedded"], + "load_time": 1.791, + "data_size": 14737666736, + "concurrent_qps": null, + "concurrent_error_ratio": null, + "result": [ + [0.729, 0.11, 0.11], + [1.794, 1.09, 1.11], + [2.7, 2.067, 2.05], + [2.036, 1.261, 1.238], + [5.593, 4.88, 4.877], + [4.45, 3.816, 3.799], + [0.768, 0.111, 0.111], + [1.769, 1.074, 1.105], + [5.56, 4.826, 4.801], + [7.272, 6.506, 6.528], + [2.615, 1.782, 1.792], + [2.985, 2.196, 2.266], + [5.988, 5.255, 5.309], + [7.815, 7.184, 7.151], + [6.956, 6.209, 6.333], + [6.552, 5.919, 5.885], + [63.53, 72.904, 83.217], + [55.844, 81.193, 54.057], + [288.341, 291.671, 297.275], + [1.247, 0.402, 0.42], + [15.208, 16.513, 16.588], + [17.746, 17.507, 19.836], + [33.155, 32.506, 31.197], + [17.244, 22.482, 17.788], + [4.949, 4.128, 4.171], + [3.612, 2.887, 2.913], + [5.084, 4.199, 4.27], + [14.34, 13.57, 13.318], + [31.249, 75.172, 74.917], + [2.065, 1.345, 1.333], + [7.533, 7.005, 6.742], + [9.911, 9.338, 9.305], + [464.745, 462.227, 463.451], + [231.567, 232.135, 236.148], + [230.621, 234.118, 231.38], + [5.68, 4.982, 4.911], + [1.497, 0.755, 0.769], + [1.077, 0.357, 0.359], + [1.321, 0.604, 0.608], + [2.412, 1.401, 1.397], + [1.005, 0.282, 0.278], + [0.979, 0.275, 0.274], + [1.0, 0.282, 0.28] + ] +} diff --git a/opteryx/results/20260815/c6a.xlarge.json b/opteryx/results/20260815/c6a.xlarge.json new file mode 100644 index 0000000000..436010e18f --- /dev/null +++ b/opteryx/results/20260815/c6a.xlarge.json @@ -0,0 +1,59 @@ +{ + "system": "Opteryx (Parquet, partitioned)", + "date": "2026-08-15", + "machine": "c6a.xlarge", + "cluster_size": 1, + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": ["C++", "stateless", "column-oriented", "embedded"], + "load_time": 4.866, + "data_size": 14737666736, + "concurrent_qps": null, + "concurrent_error_ratio": null, + "result": [ + [0.751, 0.109, 0.11], + [1.34, 0.635, 0.639], + [1.762, 1.094, 1.065], + [1.524, 0.686, 0.678], + [3.385, 2.697, 2.663], + [2.725, 1.959, 2.026], + [0.782, 0.11, 0.112], + [1.376, 0.626, 0.632], + [3.258, 2.509, 2.503], + [4.069, 3.399, 3.341], + [1.839, 1.027, 1.023], + [2.009, 1.218, 1.192], + [3.402, 2.748, 2.74], + [4.345, 3.617, 3.659], + [3.874, 3.256, 3.212], + [3.82, 3.118, 3.094], + [7.776, 6.721, 6.799], + [7.414, 6.649, 6.722], + [99.047, 107.276, 76.531], + [1.136, 0.302, 0.294], + [10.34, 6.987, 6.971], + [11.894, 7.975, 7.948], + [22.036, 13.002, 12.957], + [12.669, 8.486, 8.119], + [3.255, 2.158, 2.122], + [2.201, 1.518, 1.508], + [3.305, 2.171, 2.204], + [10.231, 6.462, 6.516], + [12.327, 11.218, 11.502], + [1.48, 0.788, 0.796], + [4.233, 3.49, 3.512], + [7.285, 4.408, 4.349], + [263.252, 266.336, 264.852], + [123.347, 176.668, 129.127], + [124.625, 169.0, 127.945], + [3.322, 2.602, 2.655], + [1.247, 0.51, 0.519], + [1.031, 0.291, 0.285], + [1.186, 0.438, 0.453], + [1.663, 1.03, 1.003], + [0.94, 0.233, 0.237], + [0.953, 0.231, 0.225], + [0.925, 0.243, 0.234] + ] +} diff --git a/opteryx/results/20260815/c8g.2xlarge.json b/opteryx/results/20260815/c8g.2xlarge.json new file mode 100644 index 0000000000..ec48001dcd --- /dev/null +++ b/opteryx/results/20260815/c8g.2xlarge.json @@ -0,0 +1,59 @@ +{ + "system": "Opteryx (Parquet, partitioned)", + "date": "2026-08-15", + "machine": "c8g.2xlarge", + "cluster_size": 1, + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": ["C++", "stateless", "column-oriented", "embedded"], + "load_time": 9.874, + "data_size": 14737666736, + "concurrent_qps": null, + "concurrent_error_ratio": null, + "result": [ + [0.599, 0.095, 0.094], + [0.781, 0.27, 0.264], + [0.871, 0.344, 0.338], + [1.003, 0.259, 0.256], + [1.498, 0.962, 0.978], + [1.55, 0.765, 0.765], + [0.607, 0.095, 0.096], + [0.79, 0.275, 0.266], + [1.562, 0.923, 0.932], + [1.857, 1.145, 1.134], + [1.143, 0.431, 0.43], + [1.207, 0.509, 0.506], + [1.711, 0.939, 0.936], + [3.104, 1.128, 1.148], + [1.666, 1.059, 1.064], + [1.513, 1.031, 1.025], + [3.59, 1.972, 1.991], + [3.473, 1.921, 1.942], + [6.9, 4.204, 4.2], + [0.535, 0.182, 0.184], + [10.87, 2.435, 2.439], + [12.359, 2.779, 2.723], + [23.336, 4.461, 4.413], + [14.181, 3.55, 3.392], + [3.237, 0.773, 0.764], + [1.311, 0.609, 0.62], + [3.504, 0.79, 0.787], + [9.875, 2.294, 2.301], + [9.045, 3.676, 3.652], + [0.677, 0.357, 0.362], + [2.484, 1.184, 1.185], + [6.309, 1.409, 1.414], + [16.04, 30.989, 27.716], + [11.143, 4.279, 4.298], + [11.106, 4.293, 4.286], + [1.199, 0.89, 0.879], + [0.912, 0.347, 0.356], + [0.768, 0.228, 0.23], + [0.886, 0.313, 0.31], + [1.214, 0.603, 0.608], + [0.729, 0.187, 0.187], + [0.717, 0.184, 0.183], + [0.722, 0.19, 0.191] + ] +} diff --git a/opteryx/results/20260815/c8g.4xlarge.json b/opteryx/results/20260815/c8g.4xlarge.json new file mode 100644 index 0000000000..d2cb97b1fa --- /dev/null +++ b/opteryx/results/20260815/c8g.4xlarge.json @@ -0,0 +1,59 @@ +{ + "system": "Opteryx (Parquet, partitioned)", + "date": "2026-08-15", + "machine": "c8g.4xlarge", + "cluster_size": 1, + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": ["C++", "stateless", "column-oriented", "embedded"], + "load_time": 21.539, + "data_size": 14737666736, + "concurrent_qps": null, + "concurrent_error_ratio": null, + "result": [ + [0.526, 0.097, 0.093], + [0.64, 0.209, 0.222], + [0.666, 0.243, 0.244], + [0.949, 0.201, 0.199], + [1.328, 0.519, 0.492], + [1.436, 0.476, 0.46], + [0.52, 0.095, 0.096], + [0.632, 0.21, 0.207], + [1.407, 0.622, 0.654], + [1.765, 0.777, 0.798], + [1.096, 0.341, 0.336], + [1.169, 0.365, 0.377], + [1.559, 0.641, 0.646], + [2.928, 0.769, 0.754], + [1.631, 0.714, 0.699], + [1.15, 0.62, 0.603], + [3.271, 1.221, 1.187], + [3.244, 1.156, 1.177], + [5.994, 2.434, 2.41], + [0.738, 0.168, 0.167], + [10.824, 1.6, 1.649], + [12.488, 1.771, 1.786], + [22.028, 2.579, 2.551], + [12.408, 2.536, 2.578], + [3.088, 0.506, 0.51], + [1.242, 0.434, 0.416], + [3.088, 0.503, 0.509], + [10.057, 1.432, 1.459], + [8.87, 2.08, 2.074], + [0.709, 0.297, 0.293], + [2.482, 0.741, 0.732], + [6.229, 0.89, 0.917], + [6.489, 3.453, 3.493], + [10.91, 2.862, 2.807], + [10.946, 2.832, 2.877], + [0.96, 0.532, 0.553], + [0.818, 0.361, 0.341], + [0.677, 0.233, 0.232], + [0.756, 0.315, 0.326], + [1.209, 0.6, 0.595], + [0.725, 0.186, 0.188], + [0.717, 0.182, 0.188], + [0.723, 0.189, 0.187] + ] +} diff --git a/opteryx/results/20260815/c8g.large.json b/opteryx/results/20260815/c8g.large.json new file mode 100644 index 0000000000..3905c0478a --- /dev/null +++ b/opteryx/results/20260815/c8g.large.json @@ -0,0 +1,59 @@ +{ + "system": "Opteryx (Parquet, partitioned)", + "date": "2026-08-15", + "machine": "c8g.large", + "cluster_size": 1, + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": ["C++", "stateless", "column-oriented", "embedded"], + "load_time": 2.23, + "data_size": 14737666736, + "concurrent_qps": null, + "concurrent_error_ratio": null, + "result": [ + [0.547, 0.092, 0.091], + [1.265, 0.681, 0.67], + [1.588, 1.014, 1.03], + [1.229, 0.655, 0.657], + [4.371, 3.844, 4.042], + [3.274, 2.718, 2.702], + [0.619, 0.093, 0.092], + [1.224, 0.685, 0.681], + [3.728, 3.198, 3.153], + [4.766, 4.249, 4.144], + [1.675, 1.065, 1.132], + [1.965, 1.385, 1.388], + [3.828, 3.267, 3.327], + [4.879, 4.374, 4.374], + [4.334, 3.804, 3.748], + [4.682, 4.109, 4.1], + [41.001, 36.233, 41.636], + [38.374, 39.271, 38.346], + [255.487, 256.046, 261.783], + [0.983, 0.286, 0.289], + [10.444, 8.854, 8.698], + [21.627, 11.015, 10.888], + [22.265, 22.125, 22.081], + [12.631, 13.604, 12.237], + [3.167, 2.487, 2.495], + [2.314, 1.777, 1.77], + [3.219, 2.465, 2.514], + [10.233, 8.354, 8.547], + [15.11, 14.888, 85.803], + [1.451, 0.884, 0.86], + [5.035, 4.366, 4.389], + [7.698, 5.708, 5.383], + [428.846, 420.054, 422.962], + [219.228, 213.903, 217.292], + [212.937, 214.483, 212.148], + [4.441, 3.874, 3.869], + [1.086, 0.527, 0.47], + [0.849, 0.248, 0.259], + [1.015, 0.409, 0.405], + [1.558, 0.922, 0.879], + [0.811, 0.2, 0.209], + [0.8, 0.207, 0.201], + [0.813, 0.209, 0.214] + ] +} diff --git a/opteryx/results/20260815/c8g.xlarge.json b/opteryx/results/20260815/c8g.xlarge.json new file mode 100644 index 0000000000..7e228f607f --- /dev/null +++ b/opteryx/results/20260815/c8g.xlarge.json @@ -0,0 +1,59 @@ +{ + "system": "Opteryx (Parquet, partitioned)", + "date": "2026-08-15", + "machine": "c8g.xlarge", + "cluster_size": 1, + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": ["C++", "stateless", "column-oriented", "embedded"], + "load_time": 4.92, + "data_size": 14737666736, + "concurrent_qps": null, + "concurrent_error_ratio": null, + "result": [ + [0.62, 0.091, 0.091], + [0.96, 0.411, 0.408], + [1.137, 0.561, 0.567], + [1.449, 0.378, 0.38], + [2.704, 2.175, 2.215], + [1.927, 1.41, 1.405], + [0.608, 0.092, 0.091], + [0.915, 0.407, 0.407], + [2.2, 1.676, 1.709], + [2.735, 2.129, 2.162], + [1.218, 0.632, 0.629], + [1.369, 0.78, 0.771], + [2.255, 1.691, 1.686], + [3.7, 2.172, 2.163], + [2.482, 1.944, 1.907], + [2.811, 2.306, 2.297], + [4.78, 4.331, 4.295], + [4.782, 4.16, 4.187], + [51.637, 99.697, 50.288], + [0.78, 0.21, 0.207], + [10.167, 4.254, 4.231], + [11.843, 4.887, 4.899], + [22.153, 8.196, 8.307], + [12.586, 5.313, 5.19], + [3.161, 1.301, 1.298], + [1.559, 1.015, 0.979], + [3.163, 1.316, 1.314], + [10.044, 4.092, 4.12], + [10.256, 7.668, 7.579], + [1.026, 0.521, 0.525], + [2.848, 2.21, 2.198], + [6.915, 2.69, 2.717], + [248.89, 262.448, 251.582], + [95.442, 91.001, 76.6], + [78.277, 79.312, 87.043], + [2.531, 2.032, 2.043], + [0.87, 0.345, 0.361], + [0.713, 0.219, 0.221], + [0.916, 0.303, 0.303], + [1.267, 0.611, 0.612], + [0.779, 0.187, 0.18], + [0.769, 0.178, 0.182], + [0.773, 0.186, 0.188] + ] +} diff --git a/opteryx/results/20260815/t3a.small.json b/opteryx/results/20260815/t3a.small.json new file mode 100644 index 0000000000..9d87527e15 --- /dev/null +++ b/opteryx/results/20260815/t3a.small.json @@ -0,0 +1,59 @@ +{ + "system": "Opteryx (Parquet, partitioned)", + "date": "2026-08-15", + "machine": "t3a.small", + "cluster_size": 1, + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": ["C++", "stateless", "column-oriented", "embedded"], + "load_time": 0.831, + "data_size": 14737666736, + "concurrent_qps": null, + "concurrent_error_ratio": null, + "result": [ + [0.963, 0.225, 0.238], + [2.788, 1.987, 2.003], + [3.996, 3.269, 3.257], + [2.868, 2.044, 1.989], + [8.964, 8.43, 8.517], + [7.968, 6.923, 7.04], + [0.967, 0.23, 0.225], + [2.744, 1.97, 1.956], + [9.832, 9.19, 9.301], + [13.482, 12.197, 12.603], + [3.965, 3.105, 3.133], + [4.591, 3.805, 3.8], + [24.073, 23.396, 35.311], + [61.334, 66.601, 65.993], + [25.446, 26.664, 27.621], + [26.403, 36.877, 19.085], + [186.012, 188.44, 184.945], + [176.291, 178.607, 181.335], + [482.633, 480.791, 489.376], + [1.667, 0.751, 0.79], + [30.875, 30.703, 29.52], + [31.816, 35.812, 33.771], + [55.578, 53.536, 55.04], + [34.533, 35.541, 34.627], + [8.326, 7.2, 7.607], + [6.453, 5.267, 5.042], + [8.366, 7.12, 7.323], + [28.308, 28.719, 28.845], + [159.206, 160.902, 157.964], + [3.145, 2.324, 2.332], + [17.203, 37.142, 13.147], + [67.94, 73.254, 73.663], + [657.015, 663.441, 669.696], + [328.858, 331.288, 331.363], + [328.836, 328.604, 329.162], + [13.175, 10.538, 10.607], + [2.19, 1.487, 1.336], + [1.443, 0.659, 0.657], + [1.87, 1.047, 1.059], + [3.276, 2.434, 2.515], + [1.305, 0.527, 0.528], + [1.285, 0.536, 0.547], + [1.335, 0.542, 0.566] + ] +}