Skip to main content

Reproducing the pgbench benchmark

The two charts on the homepage compare OrioleDB-beta18 against a stock PostgreSQL 18.6 on the same hardware, running the same load. This page is a literal, step-by-step recipe for regenerating those numbers from scratch.

Hardware​

An c5d.metal EC2 instance:

  • 96 vCPU (48 physical cores × 2 threads), 2 sockets, Intel Xeon Platinum 8275CL @ 3.00 GHz
  • 188 GB RAM
  • 1 × local NVMe (938 GB) mounted at /mnt, ext4, noatime

If you use a different instance type or another cloud, only the absolute TPS numbers change; the shape of the curves and the ratio between the engines stay representative.

OS setup​

# format the local NVMe and mount at /mnt
sudo mkfs.ext4 -q -F -E nodiscard /dev/nvme1n1
sudo mount -o noatime /dev/nvme1n1 /mnt
sudo chown ubuntu:ubuntu /mnt

# 100 GiB of 2 MiB huge pages -- PostgreSQL uses them for shared_buffers
sudo sysctl -w vm.nr_hugepages=51200
echo "vm.nr_hugepages=51200" | sudo tee /etc/sysctl.d/99-oriole.conf

# raised file-descriptor limit
sudo tee /etc/security/limits.d/99-oriole.conf <<'EOF'
* soft nofile 65535
* hard nofile 65535
EOF

# core dumps go to /mnt so they don't fill the root filesystem
sudo sh -c 'echo "/mnt/%t_%p.core" > /proc/sys/kernel/core_pattern'

Log out and back in for the ulimit change to take effect.

Build dependencies​

sudo apt-get -qq -y install \
build-essential clang bison flex pkg-config git \
libicu-dev libssl-dev libreadline-dev zlib1g-dev liblz4-dev libzstd-dev \
libxml2-dev libxslt1-dev libperl-dev python3-dev python3-venv \
libcurl4-openssl-dev

libcurl4-openssl-dev is required by OrioleDB for the decoupled-storage backends; skipping it makes the OrioleDB build fail.

Build A — OrioleDB beta18 on the patched PostgreSQL​

mkdir -p ~/oriole-bench/pgbin
git clone https://github.com/orioledb/postgres.git ~/oriole-bench/postgres-oriole
git clone https://github.com/orioledb/orioledb.git ~/oriole-bench/orioledb

# beta18 requires the patches18_3 patch set
cd ~/oriole-bench/postgres-oriole && git checkout patches18_3
cd ~/oriole-bench/orioledb && git checkout beta18

PRE=~/oriole-bench/pgbin/beta18
cd ~/oriole-bench/postgres-oriole
./configure --enable-debug --disable-cassert --with-icu --with-ssl=openssl \
--prefix=$PRE CC=clang CFLAGS="-O3 -fno-omit-frame-pointer"
make -j$(nproc) && make install
make -C contrib -j$(nproc) install

cd ~/oriole-bench/orioledb
PATH=$PRE/bin:$PATH make -j$(nproc) USE_PGXS=1 ORIOLEDB_PATCHSET_VERSION=patches18_3
PATH=$PRE/bin:$PATH make install USE_PGXS=1 ORIOLEDB_PATCHSET_VERSION=patches18_3

Build B — stock upstream PostgreSQL 18.6​

To make the comparison fair, PostgreSQL is built from the plain upstream REL_18_6 tag (no OrioleDB patches). Loading shared_preload_libraries = 'orioledb' is turned off for this build.

git clone --branch REL_18_6 https://git.postgresql.org/git/postgresql.git \
~/postgres-stock
PRE=~/oriole-bench/pgbin/pg18-stock
cd ~/postgres-stock
./configure --enable-debug --disable-cassert --with-icu --with-ssl=openssl \
--prefix=$PRE CC=clang CFLAGS="-O3 -fno-omit-frame-pointer"
make -j$(nproc) && make install
make -C contrib -j$(nproc) install

Driver harness​

The sweep is driven by ci/pgbench.py from the OrioleDB source tree. It uses testgres to spin up a cluster, run pgbench at every client count, and collect resource samples.

python3 -m venv ~/refbench-venv
~/refbench-venv/bin/pip install testgres psutil matplotlib

# `ci/pgbench.py` imports `telegram` unconditionally to (optionally) push status
# messages; stub it out if the bot isn't configured.
SITE=$(~/refbench-venv/bin/python -c "import site; print(site.getsitepackages()[0])")
cat > "$SITE/telegram.py" <<'PY'
class Bot:
def __init__(self, *a, **k): pass
def send_message(self, *a, **k): pass
def send_document(self, *a, **k): pass
def send_photo(self, *a, **k): pass
PY

Running the sweep​

ci/pgbench.py builds the read-only-9 and read-write-proc pgbench scripts internally and runs them at every client count in the --clients list.

The OrioleDB arm​

PRE=~/oriole-bench/pgbin/beta18
mkdir -p /mnt/results-oriole

~/refbench-venv/bin/python \
~/oriole-bench/orioledb/ci/pgbench.py \
--shared_buffers=32GB --max_wal_size=4GB \
--clients=1,5,10,20,40,60,80,100,120,140,160,180,200,220,240,260,280 \
--max_connections=300 --time=300 \
--engines=orioledb --tests=read-only-9,read-write-proc \
--scale=1000 --base_dir=/mnt/refbench-oriole \
--results_dir=/mnt/results-oriole

The heap arm — against stock PostgreSQL​

Point PATH at the stock build and pass --engines=builtin; the harness will skip shared_preload_libraries and use only the public schema.

PRE=~/oriole-bench/pgbin/pg18-stock
mkdir -p /mnt/results-heap

~/refbench-venv/bin/python \
~/oriole-bench/orioledb/ci/pgbench.py \
--shared_buffers=32GB --max_wal_size=4GB \
--clients=1,5,10,20,40,60,80,100,120,140,160,180,200,220,240,260,280 \
--max_connections=300 --time=300 \
--engines=builtin --tests=read-only-9,read-write-proc \
--scale=1000 --base_dir=/mnt/refbench-heap \
--results_dir=/mnt/results-heap

Each arm takes about three and a half hours: it initialises a scale-1000 cluster (roughly 15 minutes) once, then runs pgbench -M prepared -T 300 across all 17 client counts for both tests, 34 measurements in total.

What the two tests do​

Both scripts are defined in ci/pgbench.py, are used with -M prepared, and run for 300 seconds per client count.

read-only-9 — a single-statement transaction that selects nine random accounts by primary key:

\set aid1 random(1, 100000 * :scale)
\set aid2 random(1, 100000 * :scale)
-- ... aid3 … aid9
SELECT abalance FROM pgbench_accounts
WHERE aid IN (:aid1, :aid2, :aid3, :aid4, :aid5, :aid6, :aid7, :aid8, :aid9);

read-write-proc — the classic pgbench transaction wrapped in a plpgsql function, so each client sends a single round-trip that performs four updates and an insert:

CREATE FUNCTION pgbench_transaction(_aid int, _bid int, _tid int, _delta int)
RETURNS void AS $$
BEGIN
UPDATE pgbench_accounts SET abalance = abalance + _delta WHERE aid = _aid;
PERFORM abalance FROM pgbench_accounts WHERE aid = _aid;
UPDATE pgbench_tellers SET tbalance = tbalance + _delta WHERE tid = _tid;
UPDATE pgbench_branches SET bbalance = bbalance + _delta WHERE bid = _bid;
INSERT INTO pgbench_history (tid, bid, aid, delta, mtime)
VALUES (_tid, _bid, _aid, _delta, CURRENT_TIMESTAMP);
END;
$$ LANGUAGE plpgsql;

For OrioleDB the harness also adds PRIMARY KEY (bid, mtime, tid, aid, delta) to pgbench_history, so inserts are spread across the tree rather than serialised on the rightmost page.

Configuration summary​

settingvaluewhy
shared_buffers32 GBboth engines get the same total engine cache (OrioleDB splits the 32 GB into 1 GB shared_buffers for catalogs plus 31 GB orioledb.main_buffers)
max_wal_size4 GBmatches the reference; small enough that heap sees frequent checkpoints, large enough that OrioleDB does not run out of WAL
max_connections300headroom above the widest client count (280)
scale1000dataset roughly 15 GB, fits in shared_buffers; the workload is CPU-bound, not I/O-bound
--time=3005-minute runsshorter runs overshoot at low client counts and miss steady state at high ones
-M preparedalwaysotherwise the workload is dominated by query planning