Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions pgrust/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# pgrust

[pgrust](https://github.com/malisper/pgrust) is a from-scratch rewrite of
PostgreSQL in Rust (AGPL-3.0), wire- and SQL-compatible with PostgreSQL 18.3
(`SELECT version()` reports `pgrust 0.2 (PostgreSQL 18.3 compatible)`).

Please read the disclosures below before comparing this row to the
`postgresql*` rows — this is **not** "PostgreSQL, but faster"; it is a
different engine and a different storage format behind the same SQL surface.

## Disclosures

- **Columnar storage, not Postgres heap.** `create.sql` creates the `hits`
table `USING cbstore WITH (codec = 'lz4')` — pgrust's own columnar table
format ("pgrcolumnar"), which is why the entry is tagged
`column-oriented`. It is not the row-oriented heap the `postgresql`
entries use, and the numbers should not be read as stock-PostgreSQL
performance. (Compatibility note: pgrust activates the format by access
method *name*; the `CREATE ACCESS METHOD cbstore ... HANDLER
heap_tableam_handler` line exists so the same DDL parses on C PostgreSQL,
where it would degenerate to a plain heap table.)
- **Maturity.** pgrust is an experimental system and is **not
production-ready**. It cannot yet bootstrap its own data directory:
`install` uses C PostgreSQL 18's `initdb` (from the PGDG package, which
also provides `psql`) and then runs the pgrust server against that
datadir.
- **Build provenance.** `install` downloads the official published v0.2
release binary for the machine's architecture
(<https://pgrust.com/downloads/v0.2/>, sha256-verified). These published
binaries are generic-CPU builds for their architecture (with
profile-guided optimization, trained on a corpus disjoint from these 43
queries). The results submitted here come from that published binary,
i.e. exactly what `./benchmark.sh` reproduces.
- **Required settings.** `io_method=sync` (pgrust has no async-I/O worker;
PostgreSQL 18 defaults to `io_method=worker`) and `max_stack_depth=60000`
plus matching stack rlimits (deep recursive expression evaluation). These
are requirements, not tuning. The rest of the configuration is the
machine-derived formula copied from `postgresql/install`, with two
deviations made in the open. **`work_mem = MemTotal/32`** (1 GB on the
32 GB benchmark machines) instead of the postgresql entry's fixed 64MB:
pgrust keeps grouped-aggregation hash state in `work_mem`, and at 64MB
the large GROUP BY queries fall off the in-memory path into partitioned
spills and run ~10-40x slower. **`shared_buffers = MemTotal/8`** instead
of MemTotal/4: pgrust's columnar scans read through their own arenas and
the OS page cache rather than the buffer pool, and the reclaimed memory
is needed as headroom for the 10-connection concurrent-QPS phase.
`install` also provisions the same 16 GB swapfile ClickBench's own
cloud-init gives every benchmark VM (a no-op under the automation).
Everything else is the shared formula.
- **`pgrust.condition_cache = on` — enabled for parity with ClickHouse,
and disclosed.** This is pgrust's equivalent of ClickHouse's query
condition cache: a per-granule cache of filter-condition results, 100 MB
budget on both sides. ClickHouse ships it **default-on since 25.4**
(`use_query_condition_cache = true`, `src/Core/Settings.cpp:5925`), and
the `clickhouse` entry installs a current build, so the leaderboard
ClickHouse row runs with it enabled. pgrust's is off by default in v0.2;
enabling it here puts both systems on the same footing. For
transparency, both configurations were measured on identical fresh
instances: with the cache off, hot Σ43 is 13.18 s (vs 11.91 s on), and
the entry scores ~4% ahead of ClickHouse's published c8g.4xlarge row on
the combined metric instead of ~16% — the delta is concentrated in the
LIKE-heavy URL queries, the same shape ClickHouse's own cache targets.
- **Load path: parquet.** The dataset is loaded from the single
as-published `hits.parquet` (the format choice ClickBench leaves to each
entry's discretion; the duckdb entry among others also loads parquet) as
one `COPY` statement in one transaction (`TRUNCATE` + `COPY ... FREEZE`,
then `VACUUM ANALYZE`), matching the shape of `postgresql/load`.
`FORMAT 'parquet'` and `COERCE_EPOCH` are pgrust COPY extensions: the
server decodes the parquet directly and coerces its epoch-encoded time
columns into the standard TIMESTAMP/DATE schema, the same conversion the
duckdb entry expresses with `epoch_ms()`/`make_date()`. `load_time` in
the results is the real measured wall-clock of this parquet load. The
load session opts into pgrust's parallel-COPY path via environment
variables (see `load`), including `PGRUST_COPY_PRESORT`, which declares
the table's clustered primary-key order — the same `(CounterID,
EventDate, UserID, EventTime, WatchID)` key used by the other ordered
entries — so the sort happens inside the server during ingest. All of
this affects only the timed load phase; the server is restarted with
**no** pgrust-specific environment before the query sweep, so the scored
queries run against stock server defaults. (Loading from `hits.tsv` with
a plain `COPY hits FROM ... WITH (FREEZE)` also works in v0.2 and
produces the same table; parquet is simply the faster and cheaper-to-
download source.)
- **Cold runs are true cold runs.** The shared driver stops the server,
drops the page cache, and restarts before each query's first try (no
`lukewarm-cold-run` tag).
- **Results are submitted for c8g.4xlarge (arm64) only.** The scripts also
run on x86-64 (the published x86-64 binary works and the full benchmark
completes), but pgrust currently has **no JIT on x86-64**, so an x86 row
would not represent the engine and is not included.
- **Known defect visible in the concurrent-QPS numbers.** Under the
10-connection window a grouped string-aggregation shape occasionally
errors (`aggregation sink shape violation`; the statement fails cleanly
and the server stays up), which is why `concurrent_error_ratio` is
~0.005 rather than 0. It is a known v0.2 defect tracked on the pgrust
side.

## History

An earlier attempt to add pgrust (v0.1) to ClickBench ([PR
#983](https://github.com/ClickHouse/ClickBench/pull/983)) failed: v0.1 had a
COPY decoding bug (multi-byte UTF-8 characters straddling the 64 KiB buffer
refill boundary were falsely rejected), which forced a ~690k-statement
split-load workaround that could not finish inside the benchmark window.
v0.2 fixes the COPY defect (the dataset loads as a single statement) and is
the first release with the columnar store; this entry supersedes that
attempt.

## Usage

```bash
./benchmark.sh
```
3 changes: 3 additions & 0 deletions pgrust/benchmark.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash
export BENCH_DOWNLOAD_SCRIPT="download-hits-parquet-single"
exec ../lib/benchmark-common.sh
5 changes: 5 additions & 0 deletions pgrust/check
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash
set -e
. "$(dirname "$0")/env.sh"

$PSQL -tAc 'SELECT 1' >/dev/null
109 changes: 109 additions & 0 deletions pgrust/create.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
CREATE ACCESS METHOD cbstore TYPE TABLE HANDLER heap_tableam_handler;
CREATE TABLE hits
(
WatchID BIGINT NOT NULL,
JavaEnable SMALLINT NOT NULL,
Title TEXT NOT NULL,
GoodEvent SMALLINT NOT NULL,
EventTime TIMESTAMP NOT NULL,
EventDate Date NOT NULL,
CounterID INTEGER NOT NULL,
ClientIP INTEGER NOT NULL,
RegionID INTEGER NOT NULL,
UserID BIGINT NOT NULL,
CounterClass SMALLINT NOT NULL,
OS SMALLINT NOT NULL,
UserAgent SMALLINT NOT NULL,
URL TEXT NOT NULL,
Referer TEXT NOT NULL,
IsRefresh SMALLINT NOT NULL,
RefererCategoryID SMALLINT NOT NULL,
RefererRegionID INTEGER NOT NULL,
URLCategoryID SMALLINT NOT NULL,
URLRegionID INTEGER NOT NULL,
ResolutionWidth SMALLINT NOT NULL,
ResolutionHeight SMALLINT NOT NULL,
ResolutionDepth SMALLINT NOT NULL,
FlashMajor SMALLINT NOT NULL,
FlashMinor SMALLINT NOT NULL,
FlashMinor2 TEXT NOT NULL,
NetMajor SMALLINT NOT NULL,
NetMinor SMALLINT NOT NULL,
UserAgentMajor SMALLINT NOT NULL,
UserAgentMinor VARCHAR(255) NOT NULL,
CookieEnable SMALLINT NOT NULL,
JavascriptEnable SMALLINT NOT NULL,
IsMobile SMALLINT NOT NULL,
MobilePhone SMALLINT NOT NULL,
MobilePhoneModel TEXT NOT NULL,
Params TEXT NOT NULL,
IPNetworkID INTEGER NOT NULL,
TraficSourceID SMALLINT NOT NULL,
SearchEngineID SMALLINT NOT NULL,
SearchPhrase TEXT NOT NULL,
AdvEngineID SMALLINT NOT NULL,
IsArtifical SMALLINT NOT NULL,
WindowClientWidth SMALLINT NOT NULL,
WindowClientHeight SMALLINT NOT NULL,
ClientTimeZone SMALLINT NOT NULL,
ClientEventTime TIMESTAMP NOT NULL,
SilverlightVersion1 SMALLINT NOT NULL,
SilverlightVersion2 SMALLINT NOT NULL,
SilverlightVersion3 INTEGER NOT NULL,
SilverlightVersion4 SMALLINT NOT NULL,
PageCharset TEXT NOT NULL,
CodeVersion INTEGER NOT NULL,
IsLink SMALLINT NOT NULL,
IsDownload SMALLINT NOT NULL,
IsNotBounce SMALLINT NOT NULL,
FUniqID BIGINT NOT NULL,
OriginalURL TEXT NOT NULL,
HID INTEGER NOT NULL,
IsOldCounter SMALLINT NOT NULL,
IsEvent SMALLINT NOT NULL,
IsParameter SMALLINT NOT NULL,
DontCountHits SMALLINT NOT NULL,
WithHash SMALLINT NOT NULL,
HitColor CHAR NOT NULL,
LocalEventTime TIMESTAMP NOT NULL,
Age SMALLINT NOT NULL,
Sex SMALLINT NOT NULL,
Income SMALLINT NOT NULL,
Interests SMALLINT NOT NULL,
Robotness SMALLINT NOT NULL,
RemoteIP INTEGER NOT NULL,
WindowName INTEGER NOT NULL,
OpenerName INTEGER NOT NULL,
HistoryLength SMALLINT NOT NULL,
BrowserLanguage TEXT NOT NULL,
BrowserCountry TEXT NOT NULL,
SocialNetwork TEXT NOT NULL,
SocialAction TEXT NOT NULL,
HTTPError SMALLINT NOT NULL,
SendTiming INTEGER NOT NULL,
DNSTiming INTEGER NOT NULL,
ConnectTiming INTEGER NOT NULL,
ResponseStartTiming INTEGER NOT NULL,
ResponseEndTiming INTEGER NOT NULL,
FetchTiming INTEGER NOT NULL,
SocialSourceNetworkID SMALLINT NOT NULL,
SocialSourcePage TEXT NOT NULL,
ParamPrice BIGINT NOT NULL,
ParamOrderID TEXT NOT NULL,
ParamCurrency TEXT NOT NULL,
ParamCurrencyID SMALLINT NOT NULL,
OpenstatServiceName TEXT NOT NULL,
OpenstatCampaignID TEXT NOT NULL,
OpenstatAdID TEXT NOT NULL,
OpenstatSourceID TEXT NOT NULL,
UTMSource TEXT NOT NULL,
UTMMedium TEXT NOT NULL,
UTMCampaign TEXT NOT NULL,
UTMContent TEXT NOT NULL,
UTMTerm TEXT NOT NULL,
FromTag TEXT NOT NULL,
HasGCLID SMALLINT NOT NULL,
RefererHash BIGINT NOT NULL,
URLHash BIGINT NOT NULL,
CLID INTEGER NOT NULL
) USING cbstore WITH (codec = 'lz4');
6 changes: 6 additions & 0 deletions pgrust/data-size
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
set -eu
. "$(dirname "$0")/env.sh"

# Whole datadir, including WAL and catalogs.
sudo du -bs "$PGDATA_DIR" | awk '{print $1}'
26 changes: 26 additions & 0 deletions pgrust/env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Shared paths for the pgrust ClickBench entry. Sourced by the sibling
# scripts; not executable on its own.

PGRUST_VERSION=0.2
# The server binary, installed by ./install (sha256-verified download).
PGRUST_BIN=/usr/local/bin/pgrust-postgres
# The data directory. A fixed, world-traversable path rather than $PWD:
# the server runs as the `postgres` system user, which cannot traverse
# into e.g. /root if the checkout lives there.
PGDATA_DIR=/var/lib/pgrust/data
SERVER_LOG=/var/lib/pgrust/server.log
# Unix socket directory (the server does not listen on TCP).
PGSOCK_DIR=/var/run/pgrust
# C PostgreSQL 18 (PGDG) provides initdb and psql; pgrust cannot bootstrap
# a data directory itself and ships no client.
PG18_BIN=/usr/lib/postgresql/18/bin
# pgrust reads timezone data and misc share files from a C PostgreSQL
# share directory at runtime.
PGSHARE_DIR=/usr/share/postgresql/18
TZDATA_DIR=/usr/share/zoneinfo
# Where ./load moves hits.parquet before the server-side COPY. /var/tmp is
# world-traversable, so the server (running as `postgres`) can read it
# regardless of where the checkout lives.
HITS_FILE=/var/tmp/hits.parquet

PSQL="psql -h $PGSOCK_DIR -p 5432 -U postgres"
122 changes: 122 additions & 0 deletions pgrust/install
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/bin/bash
set -eu
. "$(dirname "$0")/env.sh"

export DEBIAN_FRONTEND=noninteractive
sudo apt-get update -y
sudo apt-get install -y curl ca-certificates gnupg

# --- C PostgreSQL 18 client tools (PGDG apt repo) --------------------------
# pgrust v0.2 cannot bootstrap a data directory (no initdb port yet) and
# ships no client, so initdb and psql come from C PostgreSQL 18.
# Source: https://wiki.postgresql.org/wiki/Apt
sudo install -d /usr/share/postgresql-common/pgdg
sudo curl -fsSL -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
https://www.postgresql.org/media/keys/ACCC4CF8.asc
echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] http://apt.postgresql.org/pub/repos/apt $(. /etc/os-release && echo $VERSION_CODENAME)-pgdg main" \
| sudo tee /etc/apt/sources.list.d/pgdg.list
sudo apt-get update -y
sudo apt-get install -y postgresql-18 postgresql-client-18

# The Debian package auto-creates and starts a stock C PostgreSQL cluster.
# We only need the package's initdb and psql — stop the cluster so it does
# not sit on RAM during the benchmark.
sudo systemctl disable --now postgresql || true
sudo systemctl disable --now postgresql@18-main 2>/dev/null || true

# --- the pgrust server binary ----------------------------------------------
# Official published v0.2 release build (generic CPU baseline for the
# architecture, i.e. NOT -Ctarget-cpu=native; see README.md). Verified
# against the published sha256 before install.
case "$(uname -m)" in
aarch64) PLATFORM=linux-aarch64 ;;
x86_64) PLATFORM=linux-x86_64 ;;
*) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;;
esac
tmp=$(mktemp -d)
curl -fL -o "$tmp/pgrust-$PGRUST_VERSION-$PLATFORM" \
"https://pgrust.com/downloads/v$PGRUST_VERSION/pgrust-$PGRUST_VERSION-$PLATFORM"
curl -fL -o "$tmp/pgrust-$PGRUST_VERSION-$PLATFORM.sha256" \
"https://pgrust.com/downloads/v$PGRUST_VERSION/pgrust-$PGRUST_VERSION-$PLATFORM.sha256"
(cd "$tmp" && sha256sum -c "pgrust-$PGRUST_VERSION-$PLATFORM.sha256")
sudo install -m 755 "$tmp/pgrust-$PGRUST_VERSION-$PLATFORM" "$PGRUST_BIN"
rm -rf "$tmp"

# --- data directory --------------------------------------------------------
# initdb from C PostgreSQL 18; pgrust then runs against this datadir.
sudo rm -rf "$PGDATA_DIR"
sudo install -d -o postgres -g postgres "$(dirname "$PGDATA_DIR")"
sudo -u postgres "$PG18_BIN/initdb" -D "$PGDATA_DIR" \
--no-locale --encoding=UTF8 -U postgres -A trust >/dev/null

# Pin timezones so timestamp-dependent queries cannot vary with the host.
sudo -u postgres sed -i \
"s/^log_timezone = .*/log_timezone = 'GMT'/; s/^timezone = .*/timezone = 'GMT'/" \
"$PGDATA_DIR/postgresql.conf"

# --- swap parity with the benchmark automation ------------------------------
# ClickBench's own cloud-init provisions a 16 GB swapfile on every benchmark
# VM (32 GB machines OOM row stores during load otherwise). When this script
# runs under that automation the swapfile already exists and this is a no-op;
# on a bare machine it recreates the same environment so the concurrent-QPS
# phase (10 connections against a 32 GB box) degrades gracefully instead of
# OOM-killing the server.
if [ ! -f /swapfile ] && [ "$(awk '/SwapTotal/ {print $2}' /proc/meminfo)" = "0" ]; then
sudo fallocate -l 16G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile >/dev/null
sudo swapon /swapfile
fi

# --- configuration ---------------------------------------------------------
# The machine-derived formula below is copied from this repo's
# postgresql/install so this entry is configured the same way the other
# PostgreSQL-family entries are, with TWO deviations, in the open (both
# documented in README.md):
#
# work_mem = MemTotal/32 (1 GB on a 32 GB machine) instead of the
# postgresql entry's fixed 64MB. pgrust's grouped-aggregation execution
# keeps per-query hash state in work_mem; at 64MB the large GROUP BY
# queries fall off the in-memory path into partitioned spills and run
# ~10-40x slower.
#
# shared_buffers = MemTotal/8 instead of MemTotal/4. pgrust's columnar
# scans read through their own arenas and the OS page cache rather than
# the buffer pool; a 25% buffer pool is dead weight that starves the
# concurrent-QPS phase (10 connections x ~2 GB of transient aggregation
# state) into the OOM killer.
memory=$(awk '/MemTotal/ {print $2}' /proc/meminfo)
threads=$(nproc)
cpus=$(($threads / 2))
shared_buffers=$(($memory / 8))
effective_cache_size=$(($memory - ($memory / 4)))
max_worker_processes=$(($threads + 15))
work_mem=$(($memory / 32))

sudo -u postgres tee -a "$PGDATA_DIR/postgresql.conf" >/dev/null <<CONF
shared_buffers=${shared_buffers}kB
max_worker_processes=${max_worker_processes}
max_parallel_workers=${threads}
max_parallel_maintenance_workers=${cpus}
max_parallel_workers_per_gather=${cpus}
max_wal_size=32GB
work_mem=${work_mem}kB
effective_cache_size = ${effective_cache_size}kB

# Required by pgrust (not tuning): PostgreSQL 18 defaults io_method=worker,
# and pgrust has no async-I/O worker; it requires synchronous I/O. It also
# requires an enlarged stack (deep recursive expression evaluation paths;
# ./start raises the rlimit to match).
io_method=sync
max_stack_depth=60000

# Parity with ClickHouse, not a shortcut: pgrust.condition_cache is pgrust's
# equivalent of ClickHouse's query condition cache (a per-granule cache of
# filter-condition results, 100 MB budget on both sides). ClickHouse has
# shipped it DEFAULT-ON since 25.4 (use_query_condition_cache = true,
# src/Core/Settings.cpp:5925), and the ClickBench clickhouse entry installs
# a current build, so the leaderboard ClickHouse row runs with it enabled.
# pgrust's is off by default at v0.2; enabling it here puts both systems on
# the same footing. Disclosed in README.md.
pgrust.condition_cache = on
CONF
Loading