How AI is applied across API Evangelist and APIs.io. Read my AI disclosure →
API Evangelist API Evangelist
Discovery
Learnings
Guidance
Toolbox
Alignment
API Evangelist LLC

Presto Benchmarking Tutorial – TPC-H & TPC-DS on Iceberg with Google Cloud Storage (GCS)

calendar_today May 3, 2026 person saurabh domain presto-foundation

TL;DR: What You Will Build

In this comprehensive guide, you will deploy a Presto benchmarking setup using Docker Compose. We will construct a cloud-native Data Lakehouse by mapping raw data into Apache Iceberg tables stored on Google Cloud Storage (GCS). Finally, we will execute the industry-standard TPC-H and TPC-DS benchmark suites using PBench, and visualize our query latencies in real-time using a persistent MySQL and Grafana observability stack.

Whether you are stress-testing JVM garbage collection, validating Iceberg partitioning, or evaluating Presto query performance at scale, this tutorial provides the blueprint.

Prerequisites

ComponentRole
Docker & Docker ComposeThe orchestration layer that provides a consistent, isolated environment for all services.
Presto (Java)The distributed SQL query engine. The Coordinator handles query planning and metadata retrieval, while the Workers perform the actual data processing.
Hive Metastore (HMS)Acts as the Catalog Service. It stores the mapping between Presto table names and their physical locations on GCS.
PostgreSQLThe persistent storage for the Hive Metastore, ensuring that your table schemas and metadata survive container restarts.
Google Cloud StorageAn object storage, where your raw or Iceberg/Parquet data resides.
PBenchThe benchmark orchestrator. It executes the TPC-H/DS suites and captures the fine-grained execution metrics for each query. Download it from here.
MySQL 8.0A dedicated database used to archive benchmark results, enabling long-term performance trend analysis.
GrafanaThe visualization layer that transforms raw MySQL metrics into real-time, interactive performance dashboards.

Important
GCS Service Account: Download JSON key file (gcs-key.json) with Storage Admin permissions and save inside root directory.

Infrastructure Deployment

1. Define the Services

Create a docker-compose.yml that maps the Presto cluster to your local environment, ensuring the MySQL metrics database is mapped to a non-standard port (e.g., 3307) to avoid host conflicts.

services:

  metastore-db-presto:
    image: postgres:15
    container_name: metastore-db-presto
    hostname: metastore-db-presto
    environment:
      POSTGRES_DB: metastore
      POSTGRES_USER: hive
      POSTGRES_PASSWORD: hive_password
    volumes:
      - metastore-db-data-presto:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U hive -d metastore"]
      interval: 5s
      timeout: 5s
      retries: 10
    networks:
      - presto-network

  hive-metastore-presto:
    image: apache/hive:3.1.3
    container_name: hive-metastore-presto
    hostname: hive-metastore-presto
    platform: linux/amd64
    depends_on:
      metastore-db-presto:
        condition: service_healthy
    environment:
      SERVICE_NAME: metastore
      DB_DRIVER: postgres
      IS_RESUME: "true"

    volumes:
      - ./hive-metastore/hive-site.xml:/opt/hive/conf/hive-site.xml:ro
      - ./hive-metastore/core-site.xml:/opt/hive/conf/core-site.xml:ro
      - ./gcs-key.json:/opt/hive/conf/gcs-key.json:ro
      - ./hive-metastore/postgresql-42.7.2.jar:/opt/hive/lib/postgresql-42.7.2.jar:ro
      - ./hive-metastore/gcs-connector-hadoop3-shaded.jar:/opt/hive/lib/gcs-connector-hadoop3-shaded.jar:ro
    ports:
      - "9083:9083"
    healthcheck:
      test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/localhost/9083"]
      interval: 10s
      timeout: 10s
      retries: 15
      start_period: 30s
    networks:
      - presto-network

  presto-coordinator:
    image: prestodb/presto:latest
    container_name: presto-coordinator
    hostname: presto-coordinator
    depends_on:
      hive-metastore-presto:
        condition: service_healthy
    volumes:
      - ./coordinator/etc:/opt/presto-server/etc:ro
      - ./gcs-key.json:/etc/presto/gcs-key.json:ro
      - ./hive-metastore/core-site.xml:/etc/presto/core-site.xml:ro
      - ./presto-spill:/tmp/presto-spill
    ports:
      - "8080:8080"
    networks:
      - presto-network

  presto-worker:
    image: prestodb/presto:latest
    container_name: presto-worker
    hostname: presto-worker
    depends_on:
      - presto-coordinator
    environment:
      - GOOGLE_APPLICATION_CREDENTIALS=/etc/presto/gcs-key.json
    volumes:
      - ./worker/etc:/opt/presto-server/etc:ro
      - ./gcs-key.json:/etc/presto/gcs-key.json:ro
      - ./hive-metastore/core-site.xml:/etc/presto/core-site.xml:ro
      - ./presto-spill:/tmp/presto-spill
    networks:
      - presto-network

  mysql:
    image: mysql:8.0
    container_name: mysql
    hostname: mysql
    environment:
      - MYSQL_DATABASE=pbench
      - MYSQL_USER=pbench
      - MYSQL_PASSWORD=pbench_password
      - MYSQL_ROOT_PASSWORD=root_password
    ports:
      - "3307:3306"
    networks:
      - presto-network
    healthcheck:
      test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 10

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    hostname: grafana
    depends_on:
      mysql:
        condition: service_healthy
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
    networks:
      - presto-network

volumes:
  metastore-db-data-presto:
  mysql-data:

networks:
  presto-network:
    driver: bridge

Tip
If you would like to install Presto manually then follow this guide

To configure coordinator, create coordinator folder inside root directory and etc folder inside coordinator and add following configurations:

./coordinator/etc/config.properties

coordinator=true
node-scheduler.include-coordinator=false
discovery-server.enabled=true
discovery.uri=http://presto-coordinator:8080
http-server.http.port=8080
query.max-memory=6GB
query.max-memory-per-node=5GB
query.max-total-memory-per-node=6GB
experimental.spill-enabled=true
experimental.query-max-spill-per-node=100GB
experimental.spiller-spill-path=/tmp/presto-spill

./coordinator/etc/jvm.config

-server
-Xmx12G
-XX:+UseG1GC
-XX:G1HeapRegionSize=32M
-XX:+ExplicitGCInvokesConcurrent
-XX:+HeapDumpOnOutOfMemoryError
-XX:+ExitOnOutOfMemoryError
-XX:ReservedCodeCacheSize=512M
-Djdk.attach.allowAttachSelf=true

# Java 17 module system opens (required for Presto internals)
--add-opens=java.base/java.lang=ALL-UNNAMED
--add-opens=java.base/java.lang.invoke=ALL-UNNAMED
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED
--add-opens=java.base/java.io=ALL-UNNAMED
--add-opens=java.base/java.net=ALL-UNNAMED
--add-opens=java.base/java.nio=ALL-UNNAMED
--add-opens=java.base/java.util=ALL-UNNAMED
--add-opens=java.base/java.util.concurrent=ALL-UNNAMED
--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED
--add-opens=java.base/sun.nio.ch=ALL-UNNAMED
--add-opens=java.base/sun.nio.cs=ALL-UNNAMED
--add-opens=java.base/sun.security.action=ALL-UNNAMED
--add-opens=java.base/sun.util.calendar=ALL-UNNAMED
--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED

./coordinator/etc/node.properties

node.environment=docker
node.id=coordinator-001
node.data-dir=/var/presto/data
node.location=/docker/coordinator

Create catalog folder inside etc and add following configurations:

./coordinator/etc/catalog/hive.properties

connector.name=hive-hadoop2
hive.metastore.uri=thrift://hive-metastore-presto:9083
hive.config.resources=/etc/presto/core-site.xml
hive.gcs.json-key-file-path=/etc/presto/gcs-key.json
hive.allow-drop-table=true
hive.allow-rename-table=true
hive.allow-add-column=true
hive.allow-drop-column=true

./coordinator/etc/catalog/iceberg.properties

connector.name=iceberg
iceberg.catalog.type=hive
hive.metastore.uri=thrift://hive-metastore-presto:9083
hive.config.resources=/etc/presto/core-site.xml
hive.gcs.json-key-file-path=/etc/presto/gcs-key.json
iceberg.file-format=PARQUET
iceberg.max-partitions-per-writer=5000

./coordinator/etc/catalog/tpcds.properties

connector.name=tpcds
tpcds.use-varchar-type=true

./coordinator/etc/catalog/tpch.properties

connector.name=tpch
tpch.use-varchar-type=true

To configure workers, create worker folder inside root directory and etc folder inside worker and add following configurations:

./worker/etc/config.properties

coordinator=false
discovery.uri=http://presto-coordinator:8080
http-server.http.port=8080
query.max-memory=6GB
query.max-memory-per-node=5GB
query.max-total-memory-per-node=6GB
experimental.spill-enabled=true
experimental.query-max-spill-per-node=100GB
experimental.spiller-spill-path=/tmp/presto-spill

jvm.config (Identical to the Coordinator specifications)

./worker/etc/node.properties

node.environment=docker
node.id=worker-001
node.data-dir=/var/presto/data
node.location=/docker/worker

./worker/etc/catalog/hive.properties

connector.name=hive-hadoop2
hive.metastore.uri=thrift://hive-metastore-presto:9083
hive.config.resources=/etc/presto/core-site.xml
hive.gcs.json-key-file-path=/etc/presto/gcs-key.json

./worker/etc/catalog/iceberg.properties

connector.name=iceberg
iceberg.catalog.type=hive
hive.metastore.uri=thrift://hive-metastore-presto:9083
hive.config.resources=/etc/presto/core-site.xml
iceberg.file-format=PARQUET
hive.gcs.json-key-file-path=/etc/presto/gcs-key.json

The tpch and tpcds catalog configurations are identical to those on the Coordinator node.

2. Configure GCS Connectivity

Presto requires a core-site.xml to handle gs:// URIs. This file must be mounted into the Presto and Hive Metastore containers. Create a metastore folder inside root directory to hold core-site.xml, hive-site.xml and required JARs.

<!--core-site.xml-->

<configuration>
<!-- Register GCS as a Hadoop filesystem -->
<property>
<name>fs.gs.impl</name>
<value>com.google.cloud.hadoop.fs.gcs.GoogleHadoopFileSystem</value>
</property>
<property>
<name>fs.AbstractFileSystem.gs.impl</name>
<value>com.google.cloud.hadoop.fs.gcs.GoogleHadoopFS</value>
</property>

<!-- Service Account Authentication -->
<property>
<name>google.cloud.auth.service.account.enable</name>
<value>true</value>
</property>
<property>
<name>google.cloud.auth.service.account.json.keyfile</name>
<value>/opt/hive/conf/gcs-key.json</value>
</property>
</configuration>
<!--hive-site.xml-->

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
<property>
<name>javax.jdo.option.ConnectionURL</name>
<value>jdbc:postgresql://metastore-db-presto:5432/metastore</value>
</property>
<property>
<name>javax.jdo.option.ConnectionDriverName</name>
<value>org.postgresql.Driver</value>
</property>
<property>
<name>javax.jdo.option.ConnectionUserName</name>
<value>hive</value>
</property>
<property>
<name>javax.jdo.option.ConnectionPassword</name>
<value>hive_password</value>
</property>
<property>
<name>hive.metastore.uris</name>
<value>thrift://0.0.0.0:9083</value>
</property>
<property>
<name>hive.metastore.warehouse.dir</name>
<value>/opt/hive/warehouse</value>
</property>
<property>
<name>datanucleus.autoCreateSchema</name>
<value>false</value>
</property>
<property>
<name>datanucleus.fixedDatastore</name>
<value>true</value>
</property>
<property>
<name>fs.gs.impl</name>
<value>com.google.cloud.hadoop.fs.gcs.GoogleHadoopFileSystem</value>
</property>
<property>
<name>fs.AbstractFileSystem.gs.impl</name>
<value>com.google.cloud.hadoop.fs.gcs.GoogleHadoopFS</value>
</property>
<property>
<name>fs.gs.project.id</name>
<value>Your Google Cloud Project ID</value>
</property>
<property>
<name>google.cloud.auth.service.account.enable</name>
<value>true</value>
</property>
<property>
<name>google.cloud.auth.service.account.json.keyfile</name>
<value>/opt/hive/conf/gcs-key.json</value>
</property>
<property>
<name>metastore.storage.schema.reader.impl</name>
<value>org.apache.hadoop.hive.metastore.SerDeStorageSchemaReader</value>
</property>
</configuration>

Info
Hive Metastore image doesn’t come with the plugins required to talk to PostgreSQL or Google Cloud Storage. You need 2 JAR files (for PostgreSQL and GCS Connectors), download them from here

3. Mapping Schemas & External Tables

To query raw TPC-H/DS data stored as .tbl or .dat files, we must first “bridge” the physical storage to Presto’s SQL engine. We do this by creating a Raw Staging Layer using the Hive connector. This layer maps the existing files as external tables without moving or duplicating any data. Raw data has been generated through official TPC tools using dbgen and dsdgen.

We use a dedicated schema with the _raw suffix to clearly separate our staging metadata from our optimized production data.

-- Create a persistent schema for raw staging metadata
CREATE SCHEMA IF NOT EXISTS hive.tpch_sf10_raw
WITH (location = 'gs://data_benchmarking/raw/tpch_sf10_raw/');

We create an External Table that points directly to the GCS bucket.

Note: In this staging layer, we define all columns as VARCHAR. We will perform strict type casting later during the migration to Iceberg.

-- Create an external table pointing to your GCS location
CREATE TABLE hive.tpch_sf10_raw.nation_raw (
    n_nationkey  VARCHAR,
    n_name       VARCHAR,
    n_regionkey  VARCHAR,
    n_comment    VARCHAR
)
WITH (
    format = 'CSV',
    csv_separator = '|',
    external_location = 'gs://data_benchmarking/raw/tpch_sf10/nation/'
);

4. The Iceberg Layer

Once our raw data is queryable, we migrate it into Apache Iceberg. This transforms the data into an optimized, columnar Parquet format, adds ACID compliance, and enables Presto’s advanced performance features like hidden partitioning and time-travel.

Unlike the raw layer, this schema uses the Iceberg connector and will store its metadata in the Hive Metastore.

-- Create the Iceberg schema
CREATE SCHEMA IF NOT EXISTS iceberg.tpch_sf10
WITH (location = 'gs://data_benchmarking/iceberg/tpch_sf10/');

Now we define the table with the actual data types (Integers, Decimals, etc.) required for high-speed analytical joins.

-- Migrate to Iceberg (with Casting)

CREATE TABLE iceberg.tpch_sf10.nation
WITH (format = 'PARQUET')
AS SELECT 
    CAST(n_nationkey AS BIGINT) AS n_nationkey,
    n_name,
    CAST(n_regionkey AS BIGINT) AS n_regionkey,
    n_comment
FROM hive.tpch_sf10_raw.nation_raw;

Scanning flat tables at SF-10, SF-100 or SF-1000 will result in massive GCS costs and slow queries. To prevent this, we use Iceberg Partitioning directly in the WITH clause using a bucketing strategy.

-- Example
CREATE TABLE iceberg.tpch_sf10.lineitem
WITH (
    format = 'PARQUET',
    location = 'gs://data_benchmarking/iceberg/tpch_sf10/lineitem/',
    partitioning = ARRAY['month(l_shipdate)']
)
AS
SELECT 
    CAST(l_orderkey AS BIGINT) AS l_orderkey,
    CAST(l_partkey AS BIGINT) AS l_partkey,
    CAST(l_suppkey AS BIGINT) AS l_suppkey,
    CAST(l_linenumber AS INTEGER) AS l_linenumber,
    CAST(l_quantity AS DECIMAL(15,2)) AS l_quantity,
    CAST(l_extendedprice AS DECIMAL(15,2)) AS l_extendedprice,
    CAST(l_discount AS DECIMAL(15,2)) AS l_discount,
    CAST(l_tax AS DECIMAL(15,2)) AS l_tax,
    l_returnflag,
    l_linestatus,
    CAST(l_shipdate AS DATE) AS l_shipdate,
    CAST(l_commitdate AS DATE) AS l_commitdate,
    CAST(l_receiptdate AS DATE) AS l_receiptdate,
    l_shipinstruct,
    l_shipmode,
    l_comment
FROM hive.tpch_sf10_raw.lineitem_raw;

The final, most important step for benchmarking is to collect statistics. This allows Presto’s Cost-Based Optimizer (CBO) to choose the most efficient join order.

-- Collect row counts and column distributions
ANALYZE iceberg.tpch_sf10.nation;

Info
Repeat Steps 3 and 4 for all the remaining tables in TPC-H and TPC-DS

5. Automated Benchmarking with PBench

To get repeatable, professional results, we use PBench, a high-performance benchmark orchestrator that automates query execution and captures detailed performance metrics. PBench doesn’t just print results to the console; it persists them in a MySQL database. This allows us to compare “Run A” vs “Run B” weeks later.

Download the PBench tar file and unpack it into your local project root directory.

Create mysql.json file inside pbench directory and paste following config

{
  "username": "pbench",
  "password": "pbench_password",
  "server": "127.0.0.1:3307",
  "database": "pbench"
}

Next, we create a JSON manifest that tells PBench which catalog to use, which schema to target, and which SQL files to execute. Notice that we also enable save_json and save_output for deep debugging.

Example tpch_sf10_iceberg.json:

{
  "name": "tpch_sf10_iceberg",
  "description": "TPC-H SF-10 Benchmarking on Iceberg",
  "catalog": "iceberg",
  "schema": "tpch_sf10",
  "runs": 1,
  "save_json": true,
  "save_output": true,
  "query_files": [
    "benchmarks/tpch/queries/query_01.sql",
    "benchmarks/tpch/queries/query_02.sql"
    // ... continues through query_22.sql
  ]
}

Example tpcds_sf10_iceberg.json:

{
  "name": "tpcds_sf10_iceberg",
  "description": "TPC-DS SF-10 Benchmarking on Iceberg",
  "catalog": "iceberg",
  "schema": "tpcds_sf10",
  "runs": 1,
  "save_json": true,
  "save_output": true,
  "query_files": [
    "benchmarks/tpc-ds/queries/query_01.sql",
    "benchmarks/tpc-ds/queries/query_02.sql",
	// ... continues through query_99.sql
  ]
}

Run the following command to start the suite. PBench will sequentially execute every query, measure the wall time, and stream the results directly to your MySQL container.

#For TPC-H Benchmarking
./pbench run --mysql mysql.json tpch_sf10_iceberg.json
#For TPC-DS Benchmarking
./pbench run --mysql mysql.json tpcds_sf10_iceberg.json

pbench will also save the raw data results and the execution statistics locally into a new folder (e.g., pbench/tpcds_sf10_iceberg_260429-XXXXXX/).

Important
Always discard the results of the first run. The JVM needs time to warm up and JIT-compile the execution paths. Change the JSON to "runs": 3 or "runs": 5

6. Real-Time Observability with Grafana

Because pbench automatically streams our execution metrics into MySQL, we can use Grafana to visualize our cluster’s performance in real-time. Our Docker Compose stack automatically provisions a Grafana instance alongside our MySQL metrics database.

  • Navigate to http://localhost:3000 (Default login: admin / admin).
  • Go to Connections > Data Sources and add a new MySQL datasource.
  • Configure the connection using the internal Docker network:
    • Hostmysql:3306
    • Databasepbench
    • Userpbench
    • Passwordpbench_password

Success
Benchmarking stats have been recorded successfully

7. Troubleshooting

When running complex analytical queries at scale, you are bound to hit the physical limits of your hardware or data anomalies. Here is how you can solve the biggest challenges in this architecture:

TPC-DS Query 72 involves massive multi-way joins that can easily exceed physical RAM.

  • The Fix: We enabled disk spilling. By mounting a /tmp/presto-spill volume to our Docker containers and adding experimental.spill-enabled=true to our config.properties, Presto safely offloads intermediate join data to disk rather than crashing.

Legacy TPC-DS data generators often produce sparse data that results in a DIVISION_BY_ZERO SQL error on Query 90.

  • The Fix: Instead of failing the benchmark, we wrapped the mathematical operations in the SQL file with a safe division wrapper: COALESCE(NULLIF(value, 0), 1).

8. Resources

Follow Us

open_in_new Read original post