Skip to content
Open
97 changes: 97 additions & 0 deletions hyrise/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
FROM ubuntu:25.04 AS build

ENV DEBIAN_FRONTEND=noninteractive
ENV HYRISE_HEADLESS_SETUP=1

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
autoconf \
bash-completion \
bc \
ca-certificates \
clang-19 \
clang-20 \
cmake \
curl \
dos2unix \
g++-13 \
g++-15 \
gcc-13 \
gcc-15 \
git \
libboost-all-dev \
libhwloc-dev \
libncurses-dev \
libnuma-dev \
libnuma1 \
libpq-dev \
libreadline-dev \
libsqlite3-dev \
libtbb-dev \
lld-20 \
llvm-20 \
lsb-release \
make \
ninja-build \
parallel \
postgresql-server-dev-all \
python3 \
python3-pip \
software-properties-common \
sudo \
&& update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 90 --slave /usr/bin/g++ g++ /usr/bin/g++-15 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

# Pin a specific Hyrise revision for reproducibility (defaults to master).
ARG HYRISE_REF=master
# LTO is off by default: GCC's `-flto=auto` link of libhyrise_impl.so spawns
# one lto1 per core, and on the 32 GB c6a.4xlarge runner 16 of them exhaust
# RAM+swap and get killed (earlyoom SIGTERM → "Terminated signal terminated
# program lto1"). Set NO_LTO=FALSE on machines with ≥4 GB/core to re-enable.
ARG NO_LTO=TRUE

WORKDIR /opt
RUN git clone https://github.com/hyrise/hyrise.git \
&& cd hyrise \
&& git checkout "${HYRISE_REF}" \
&& git submodule update --jobs 8 --init --recursive --depth 1

# hyriseConsole is shipped alongside the server because its `export` command
# reads tables via GetTable without the MVCC Validate wrapper — SQL `COPY TO`
# validates, and BinaryWriter materializes the resulting ReferenceSegments as
# unencoded value segments, which would make the snapshot raw-sized and make
# it re-import unencoded. See ./load.
WORKDIR /opt/hyrise/cmake-build-release
RUN cmake -GNinja -DCMAKE_BUILD_TYPE=Release -DNO_LTO=${NO_LTO} .. \
&& ninja hyriseServer hyriseConsole

FROM ubuntu:25.04
ENV DEBIAN_FRONTEND=noninteractive
# libboost-container/date-time: libhyrise_impl.so links them dynamically
# (they are not dependencies of boost-system/thread, so they must be listed).
# libreadline8t64 + libncurses6: hyriseConsole.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libboost-container1.83.0 \
libboost-date-time1.83.0 \
libboost-system1.83.0 \
libboost-thread1.83.0 \
libhwloc15 \
libncurses6 \
libnuma1 \
libreadline8t64 \
libsqlite3-0 \
libtbb12 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

COPY --from=build /opt/hyrise/cmake-build-release/hyriseServer /usr/local/bin/hyriseServer
COPY --from=build /opt/hyrise/cmake-build-release/hyriseConsole /usr/local/bin/hyriseConsole
COPY --from=build /opt/hyrise/cmake-build-release/lib/libhyrise_impl.so /usr/local/lib/
COPY --from=build /opt/hyrise/cmake-build-release/third_party/jemalloc/lib/libjemalloc.so.2 /usr/local/lib/
RUN ldconfig

EXPOSE 5432
ENTRYPOINT ["/usr/local/bin/hyriseServer"]
CMD ["5432"]
14 changes: 14 additions & 0 deletions hyrise/benchmark.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash
# Thin shim — actual flow is in lib/benchmark-common.sh.
#
# Hyrise has no on-disk persistence: every restart resurfaces with an empty
# catalog and the dataset must be re-loaded into RAM. BENCH_DURABLE=no makes
# the driver re-run ./load on every cold cycle (and roll that wall-clock into
# the cold-try timing) so each "cold" number genuinely measures
# load+query against a fresh in-memory dataset. The first ./load converts the
# CSV into an encoded binary snapshot; the per-cycle reloads import that
# snapshot, which is the closest Hyrise equivalent of a durable system
# re-reading its own on-disk format after a restart.
export BENCH_DOWNLOAD_SCRIPT="download-hits-csv"
export BENCH_DURABLE=no
exec ../lib/benchmark-common.sh
4 changes: 4 additions & 0 deletions hyrise/check
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash
set -e

psql -h 127.0.0.1 -p 5432 -U postgres -c 'SELECT 1' >/dev/null
10 changes: 10 additions & 0 deletions hyrise/data-size
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash
set -eu

# Hyrise has no on-disk persistence; report the total in-memory segment
# size from meta_segments. Strip any whitespace so the driver's strict
# digits-only regex check passes.
psql -h 127.0.0.1 -p 5432 -U postgres -t -A \
-c "SELECT SUM(estimated_size_in_bytes) FROM meta_segments WHERE table_name = 'hits';" \
| tr -d '[:space:]'
echo
116 changes: 116 additions & 0 deletions hyrise/hits.csv.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
{
"config": {
"rfc_mode": true,
"separator": ",",
"quote": "\"",
"escape": "\"",
"delimiter": "\n"
},
"columns": [
{"name": "WatchID", "type": "long", "nullable": false},
{"name": "JavaEnable", "type": "int", "nullable": false},
{"name": "Title", "type": "string", "nullable": false},
{"name": "GoodEvent", "type": "int", "nullable": false},
{"name": "EventTime", "type": "string", "nullable": false},
{"name": "EventDate", "type": "string", "nullable": false},
{"name": "CounterID", "type": "int", "nullable": false},
{"name": "ClientIP", "type": "int", "nullable": false},
{"name": "RegionID", "type": "int", "nullable": false},
{"name": "UserID", "type": "long", "nullable": false},
{"name": "CounterClass", "type": "int", "nullable": false},
{"name": "OS", "type": "int", "nullable": false},
{"name": "UserAgent", "type": "int", "nullable": false},
{"name": "URL", "type": "string", "nullable": false},
{"name": "Referer", "type": "string", "nullable": false},
{"name": "IsRefresh", "type": "int", "nullable": false},
{"name": "RefererCategoryID", "type": "int", "nullable": false},
{"name": "RefererRegionID", "type": "int", "nullable": false},
{"name": "URLCategoryID", "type": "int", "nullable": false},
{"name": "URLRegionID", "type": "int", "nullable": false},
{"name": "ResolutionWidth", "type": "int", "nullable": false},
{"name": "ResolutionHeight", "type": "int", "nullable": false},
{"name": "ResolutionDepth", "type": "int", "nullable": false},
{"name": "FlashMajor", "type": "int", "nullable": false},
{"name": "FlashMinor", "type": "int", "nullable": false},
{"name": "FlashMinor2", "type": "string", "nullable": false},
{"name": "NetMajor", "type": "int", "nullable": false},
{"name": "NetMinor", "type": "int", "nullable": false},
{"name": "UserAgentMajor", "type": "int", "nullable": false},
{"name": "UserAgentMinor", "type": "string", "nullable": false},
{"name": "CookieEnable", "type": "int", "nullable": false},
{"name": "JavascriptEnable", "type": "int", "nullable": false},
{"name": "IsMobile", "type": "int", "nullable": false},
{"name": "MobilePhone", "type": "int", "nullable": false},
{"name": "MobilePhoneModel", "type": "string", "nullable": false},
{"name": "Params", "type": "string", "nullable": false},
{"name": "IPNetworkID", "type": "int", "nullable": false},
{"name": "TraficSourceID", "type": "int", "nullable": false},
{"name": "SearchEngineID", "type": "int", "nullable": false},
{"name": "SearchPhrase", "type": "string", "nullable": false},
{"name": "AdvEngineID", "type": "int", "nullable": false},
{"name": "IsArtifical", "type": "int", "nullable": false},
{"name": "WindowClientWidth", "type": "int", "nullable": false},
{"name": "WindowClientHeight", "type": "int", "nullable": false},
{"name": "ClientTimeZone", "type": "int", "nullable": false},
{"name": "ClientEventTime", "type": "string", "nullable": false},
{"name": "SilverlightVersion1", "type": "int", "nullable": false},
{"name": "SilverlightVersion2", "type": "int", "nullable": false},
{"name": "SilverlightVersion3", "type": "int", "nullable": false},
{"name": "SilverlightVersion4", "type": "int", "nullable": false},
{"name": "PageCharset", "type": "string", "nullable": false},
{"name": "CodeVersion", "type": "int", "nullable": false},
{"name": "IsLink", "type": "int", "nullable": false},
{"name": "IsDownload", "type": "int", "nullable": false},
{"name": "IsNotBounce", "type": "int", "nullable": false},
{"name": "FUniqID", "type": "long", "nullable": false},
{"name": "OriginalURL", "type": "string", "nullable": false},
{"name": "HID", "type": "int", "nullable": false},
{"name": "IsOldCounter", "type": "int", "nullable": false},
{"name": "IsEvent", "type": "int", "nullable": false},
{"name": "IsParameter", "type": "int", "nullable": false},
{"name": "DontCountHits", "type": "int", "nullable": false},
{"name": "WithHash", "type": "int", "nullable": false},
{"name": "HitColor", "type": "string", "nullable": false},
{"name": "LocalEventTime", "type": "string", "nullable": false},
{"name": "Age", "type": "int", "nullable": false},
{"name": "Sex", "type": "int", "nullable": false},
{"name": "Income", "type": "int", "nullable": false},
{"name": "Interests", "type": "int", "nullable": false},
{"name": "Robotness", "type": "int", "nullable": false},
{"name": "RemoteIP", "type": "int", "nullable": false},
{"name": "WindowName", "type": "int", "nullable": false},
{"name": "OpenerName", "type": "int", "nullable": false},
{"name": "HistoryLength", "type": "int", "nullable": false},
{"name": "BrowserLanguage", "type": "string", "nullable": false},
{"name": "BrowserCountry", "type": "string", "nullable": false},
{"name": "SocialNetwork", "type": "string", "nullable": false},
{"name": "SocialAction", "type": "string", "nullable": false},
{"name": "HTTPError", "type": "int", "nullable": false},
{"name": "SendTiming", "type": "int", "nullable": false},
{"name": "DNSTiming", "type": "int", "nullable": false},
{"name": "ConnectTiming", "type": "int", "nullable": false},
{"name": "ResponseStartTiming", "type": "int", "nullable": false},
{"name": "ResponseEndTiming", "type": "int", "nullable": false},
{"name": "FetchTiming", "type": "int", "nullable": false},
{"name": "SocialSourceNetworkID", "type": "int", "nullable": false},
{"name": "SocialSourcePage", "type": "string", "nullable": false},
{"name": "ParamPrice", "type": "long", "nullable": false},
{"name": "ParamOrderID", "type": "string", "nullable": false},
{"name": "ParamCurrency", "type": "string", "nullable": false},
{"name": "ParamCurrencyID", "type": "int", "nullable": false},
{"name": "OpenstatServiceName", "type": "string", "nullable": false},
{"name": "OpenstatCampaignID", "type": "string", "nullable": false},
{"name": "OpenstatAdID", "type": "string", "nullable": false},
{"name": "OpenstatSourceID", "type": "string", "nullable": false},
{"name": "UTMSource", "type": "string", "nullable": false},
{"name": "UTMMedium", "type": "string", "nullable": false},
{"name": "UTMCampaign", "type": "string", "nullable": false},
{"name": "UTMContent", "type": "string", "nullable": false},
{"name": "UTMTerm", "type": "string", "nullable": false},
{"name": "FromTag", "type": "string", "nullable": false},
{"name": "HasGCLID", "type": "int", "nullable": false},
{"name": "RefererHash", "type": "long", "nullable": false},
{"name": "URLHash", "type": "long", "nullable": false},
{"name": "CLID", "type": "int", "nullable": false}
]
}
17 changes: 17 additions & 0 deletions hyrise/install
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash
set -eu

sudo apt-get update -y
sudo apt-get install -y docker.io postgresql-client gzip

# Hyrise has no upstream binary distribution and the build requires a
# recent toolchain (gcc-15 / clang-20). Build inside Docker on top of
# Ubuntu 25.04, then ship just the binary and its runtime libs in a slim
# image.
sudo docker build -t clickbench-hyrise .

# The bind-mounted /data dir must exist before docker run (created here
# instead of in ./load so ./start can be called before the first ./load
# without erroring on a missing mount source).
mkdir -p data
chmod 777 data
57 changes: 57 additions & 0 deletions hyrise/load
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/bin/bash
set -eu

# ./load runs on every cold cycle (BENCH_DURABLE=no). The first call turns
# hits.csv into an encoded binary snapshot (data/hits.bin); every call then
# imports that snapshot into the freshly started server.
#
# Why not one big `COPY hits FROM '/data/hits.csv'`: Hyrise's CSV import
# reads the entire file into a single std::string and materializes every
# column as unencoded ValueSegments before dictionary-encoding them —
# roughly 200 GB peak for the 75 GB hits.csv. `COPY ... FROM` into an
# existing table appends chunks, so importing 5M-row pieces sequentially
# caps the transient overhead at one piece on top of the growing encoded
# table. The pieces are imported in file order, which keeps the original
# EventTime ordering and thus meaningful chunk-pruning statistics.
#
# Why the snapshot is written by hyriseConsole and not by SQL `COPY TO`:
# a server-side SQL statement wraps the stored table in an MVCC Validate
# operator, and BinaryWriter materializes the resulting ReferenceSegments
# as unencoded value segments — the snapshot would be raw-sized and would
# re-import unencoded (binary imports skip the encoding pass). The
# console's `export` command reads the table via GetTable without
# validation, so the dictionary-encoded segments are written — and later
# re-imported, chunk by chunk — as-is. The console is a separate process
# with its own catalog, so it performs the piecewise CSV import itself;
# the server never sees the pieces, only the snapshot. Importing the
# snapshot regenerates full-table statistics on add_table.
if [ ! -f data/hits.bin ]; then
# hits.csv was delivered into cwd by download-hits-csv. Guarded so a
# retry after a partial first load resumes from the existing pieces.
if [ -f hits.csv ]; then
split -l 5000000 --numeric-suffixes=1 --additional-suffix=.csv \
hits.csv data/hits_part_
rm hits.csv
fi

# Each piece needs its own CSV meta file: Hyrise resolves it as
# "<filename>.json" next to the data file.
for piece in data/hits_part_*.csv; do
cp hits.csv.json "${piece}.json"
echo "COPY hits FROM '/data/$(basename "$piece")' WITH (FORMAT CSV);"
done > data/make_snapshot.script
echo "export hits /data/hits.bin" >> data/make_snapshot.script

# The console exits 0 even when the script aborts on an error, so the
# snapshot file itself is the success signal.
sudo docker exec hyrise hyriseConsole /data/make_snapshot.script
if [ ! -f data/hits.bin ]; then
echo "hyriseConsole did not produce data/hits.bin" >&2
exit 1
fi
rm -f data/hits_part_*.csv data/hits_part_*.csv.json data/make_snapshot.script
fi

psql -h 127.0.0.1 -p 5432 -U postgres -v ON_ERROR_STOP=1 -q \
-c "COPY hits FROM '/data/hits.bin' WITH (FORMAT BINARY);"
sync
Loading