⚠️ This hands-on training is designed for desktop — you’ll need a keyboard and a terminal. Feel free to skim here, but plan to run it from a computer.
1 Welcome to VoltDB
Try in Sandbox instead — skip the local install. This launches a real VoltDB cluster in a live container in your browser, so you can run every step for real.

Welcome to the VoltDB tutorial! VoltDB is an in-memory relational database designed for applications that require real-time, high-throughput transactional processing. It uses standard SQL, so if you've worked with any relational database before, you'll feel right at home.

Prerequisites

  • VoltDB 14.1+ running (standalone or inside the training Docker container)
  • Access to sqlcmd (VoltDB command-line SQL tool)
  • Terminal access for CLI tools like csvloader and voltadmin

VoltDB is already running inside the Docker container. This guide walks you through 14 progressive steps covering tables, queries, partitioning, durability, and stored procedures.

You'll use two tools throughout this tutorial:

  • SQL Console / sqlcmd — Run SQL queries and stored procedures directly against VoltDB
  • Terminal — A bash shell for running command-line tools like csvloader, voltadmin, and sqlcmd

You're ready to go — VoltDB supports standard SQL DDL (CREATE, ALTER, DROP) and DML (SELECT, INSERT, UPDATE, DELETE). Let's start by creating a table.

The SQL Console supports both SQL queries and exec commands (e.g., exec @Statistics TABLE 0). For advanced CLI tools like csvloader, use a terminal.
2 Create Your First Table

VoltDB uses standard SQL DDL (Data Definition Language) to define database schema. Just like other relational databases, you use CREATE TABLE to define tables with typed columns.

Let's create a simple table for storing information about towns:

SQL
CREATE TABLE towns (
  town VARCHAR(64),
  county VARCHAR(64),
  state VARCHAR(2)
);

VoltDB supports the standard SQL data types including VARCHAR, INTEGER, TINYINT, SMALLINT, BIGINT, FLOAT, DECIMAL, TIMESTAMP, and VARBINARY.

You can also load DDL from a file using the FILE directive in sqlcmd: FILE mytables.sql;
Read more: Designing the Database Schema
3 Insert Data

Use standard SQL INSERT statements to add records to the table. Let's add a few towns from the northeastern United States:

SQL
INSERT INTO towns VALUES ('Billerica','Middlesex','MA');
INSERT INTO towns VALUES ('Buffalo','Erie','NY');
INSERT INTO towns VALUES ('Bay View','Erie','OH');

Each INSERT should return a confirmation with a modified row count of 1.

4 Query the Data

Now let's verify our data using SQL queries. VoltDB supports the full range of SQL query features: WHERE clauses, ORDER BY, GROUP BY, JOIN, subqueries, and window functions.

First, count the total number of records:

SQL
SELECT count(*) AS total FROM towns;
Expected Output
TOTAL ----- 3 (Returned 1 rows in 0.01s)

Now list all towns sorted alphabetically:

SQL
SELECT town, state FROM towns ORDER BY town;
Expected Output
TOWN STATE ---------- ----- Bay View OH Billerica MA Buffalo NY (Returned 3 rows in 0.01s)

You can also filter with WHERE clauses. Try finding all towns in a specific state:

SQL
SELECT town, county FROM towns WHERE state = 'MA';
Read more: SQL Statement Reference
5 Schema Evolution

One of VoltDB's strengths is the ability to modify the schema of a running database without downtime. You can add or remove columns, add indexes, and create new tables — all while the database continues to serve requests.

Let's add an elevation column to our towns table:

SQL
ALTER TABLE towns ADD COLUMN elevation INTEGER;

Now update the existing records with elevation data (in feet):

SQL
UPDATE towns SET elevation = 50 WHERE town = 'Billerica';
UPDATE towns SET elevation = 600 WHERE town = 'Buffalo';
UPDATE towns SET elevation = 700 WHERE town = 'Bay View';

Query the data to see the new column, sorted by elevation (highest first):

SQL
SELECT town, state, elevation FROM towns ORDER BY elevation DESC;
Expected Output
TOWN STATE ELEVATION ---------- ----- --------- Bay View OH 700 Buffalo NY 600 Billerica MA 50 (Returned 3 rows in 0.01s)
You cannot add new unique constraints to a table that already has data. If you need unique constraints, define them when creating the table or drop and recreate it.
Read more: Modifying the Schema
6 Extended Schema

For more interesting queries, we need a richer schema and more data. We'll use geographic data from the U.S. Geological Survey's Geographic Names Information Service (GNIS), which contains information about populated places across the country.

The data includes numeric state and county codes (FIPS codes), which will serve as our partitioning keys later. Let's create a schema that matches this data format.

Replace the table

First, drop the existing towns table and recreate it with the extended schema. The column order matches our source data file to simplify loading:

SQL
DROP TABLE towns IF EXISTS;
SQL
CREATE TABLE towns (
  town VARCHAR(64),
  state VARCHAR(2),
  state_num TINYINT NOT NULL,
  county VARCHAR(64),
  county_num SMALLINT NOT NULL,
  elevation INTEGER
);

The state_num and county_num columns hold FIPS numeric codes. These are ideal partition keys because they distribute data evenly and enable efficient joins.

We'll add partitioning in a later step. For now, this table is "replicated" — every record is available on all partitions. This is VoltDB's default behavior for tables without a PARTITION statement.
7 Load Data with csvloader

Manually inserting records one at a time is fine for testing, but impractical for loading real datasets. VoltDB provides the csvloader utility for high-speed bulk loading from delimited text files.

csvloader works by automatically calling the default INSERT procedure that VoltDB creates for every table. It handles batching, parallelism, and error logging — you just point it at your file.

Download the tutorial data

Open a terminal and download the official VoltDB tutorial data files:

Shell
cd /tmp
wget http://downloads.voltactivedata.com/technologies/other/tutorial_files_51.zip
unzip -o tutorial_files_51.zip

This archive contains data/towns.txt — a pipe-delimited file with ~193,000 populated places from the U.S. Geological Survey's Geographic Names Information Service (GNIS).

Load the data

Run csvloader from the terminal (this is a CLI tool, not SQL):

Shell
csvloader --separator "|" --skip 1 --file data/towns.txt towns

The csvloader flags explained:

  • --separator "|" — specifies the delimiter character (pipe instead of comma)
  • --skip 1 — skips the first line (header row)
  • --file <path> — path to the data file
  • towns — the target table name

csvloader generates three log files in the current directory: one for errors, one for records that couldn't be loaded, and one for performance statistics including rows loaded and throughput.

csvloader is a command-line tool — run it in a terminal, not in the SQL console.
Read more: csvloader Reference

Verify the data loaded by running this query:

SQL
SELECT COUNT(*) AS total FROM towns;
Expected Output
TOTAL ------ 193297 (Returned 1 rows in 0.01s)
8 Explore the Data

Now that we have a real dataset loaded, let's run some more interesting queries to explore it.

Find the highest-elevation towns in our dataset:

SQL
SELECT town, state, elevation
  FROM towns
  ORDER BY elevation DESC
  LIMIT 5;

Count how many towns are in each state:

SQL
SELECT state, COUNT(*) AS num_towns
  FROM towns
  GROUP BY state
  ORDER BY num_towns DESC;
Expected Output (sample)
STATE NUM_TOWNS ----- --------- OH 5 MA 4 NY 3 ...

Find the average elevation by state:

SQL
SELECT state, CAST(AVG(elevation) AS INTEGER) AS avg_elev,
       MIN(elevation) AS min_elev,
       MAX(elevation) AS max_elev
  FROM towns
  GROUP BY state
  ORDER BY avg_elev DESC;

Search for a specific town:

SQL
SELECT town, county, state, elevation
  FROM towns
  WHERE town LIKE 'B%'
  ORDER BY town;

These are basic examples, but VoltDB supports the full SQL standard including multi-table JOINs, subqueries, CASE expressions, and window functions.

9 Partitioning

Partitioning is one of VoltDB's most important features and the key to its performance. It distributes data across the cluster so that queries can be processed in parallel across multiple CPU cores.

How partitioning works

VoltDB's approach differs from traditional database sharding in three important ways:

  • Automatic distribution — VoltDB partitions table data automatically based on a column you specify. You don't need to manually manage which data goes where.
  • Multiple partitions per server — Multiple partitions can exist on a single server, each running on its own CPU core. This improves both scalability and throughput.
  • Data AND processing — VoltDB partitions both the data and the processing. When a stored procedure is partitioned, it runs entirely within one partition without any cross-partition coordination. This single-partition execution is the foundation of VoltDB's sub-millisecond latency.
Add partitioning to our table

We'll partition on state_num so that all towns in the same state are stored together. This is efficient because queries often filter by state, and the numeric FIPS code distributes evenly across partitions.

VoltDB cannot partition a table that already contains data. We need to clear the table first, add partitioning, then reload:

SQL
DELETE FROM towns;
SQL
PARTITION TABLE towns ON COLUMN state_num;

Now reload the data from the terminal. Loading into a partitioned table is significantly faster — the tutorial reports a 3x speedup:

Shell
csvloader --separator "|" --skip 1 --file data/towns.txt towns
In production, you would define partitioning at table creation time by placing PARTITION TABLE right after CREATE TABLE in your DDL file. That way the table is partitioned before any data is loaded.
Partitioned vs. replicated tables

By default, VoltDB tables are replicated — every record is stored on every partition. This is useful for small lookup tables that are frequently joined but rarely updated. Examples include reference data like state names, country codes, or configuration settings.

Partitioned tables split data across partitions based on the partition column. This is essential for large tables with high write throughput. As a rule of thumb: partition your large, frequently-written tables and replicate your small, read-mostly reference tables.

Read more: Partitioning Tables
10 Durability & Snapshots

VoltDB is an in-memory database, but that doesn't mean your data is lost when the server stops. VoltDB provides two mechanisms for data persistence:

Command logging (Enterprise)

Command logging records all database activity — including schema changes and data modifications — to disk in real time. When the database restarts, it automatically replays the log to restore the exact state. This is the recommended approach for production deployments.

Snapshots (Community & Enterprise)

Snapshots are point-in-time copies of the entire database written to disk. You can create snapshots manually or have VoltDB create them automatically. On restart, VoltDB checks for snapshot files and restores from the most recent one.

You can save a snapshot at any time without stopping the database:

Shell
voltadmin save /tmp/voltdb-snapshots mybackup

To shut down cleanly and save a final snapshot, use voltadmin:

Shell
voltadmin shutdown --save

When VoltDB starts again, it automatically detects and restores the saved snapshot — all your tables, data, and schema come back exactly as they were.

Restart VoltDB after shutdown

If you ran the shutdown command above, VoltDB is now stopped. Restart it from the terminal:

Shell
cd /data/voltdb && voltdb start --background --host=localhost --ignore=thp

Wait a few seconds for VoltDB to come up, then verify your data was restored:

SQL
SELECT COUNT(*) AS total FROM towns;
In production, you would use command logging or automatic snapshots for continuous durability. Command logging records every transaction in real time, providing the strongest durability guarantee.
Read more: Saving & Restoring the Database
Read more: Automatic Snapshots
Read more: Command Logging and Recovery
11 Create a Stored Procedure

Stored procedures are the primary way to interact with VoltDB in production applications. Rather than sending ad-hoc SQL over the wire, you define procedures that encapsulate your database logic and call them by name with parameters.

Stored procedures offer several advantages over ad-hoc SQL:

  • Performance — The SQL is pre-planned, avoiding query planning overhead on every call
  • Partitioning — Procedures can be partitioned to run on a single partition, enabling maximum parallelism
  • Transactions — Each procedure call is a complete ACID transaction — it either succeeds entirely or rolls back completely
  • Reusability — Define the logic once, call it from any client language
Simple SQL stored procedures

For single-statement queries, VoltDB lets you define a stored procedure using pure SQL with CREATE PROCEDURE. Question marks (?) serve as placeholders for parameters that are supplied at runtime.

Let's create a procedure that counts towns in a given state. The PARTITION ON clause tells VoltDB to route each call directly to the partition containing that state's data:

SQL
CREATE PROCEDURE towncount
  PARTITION ON TABLE towns COLUMN state_num
AS
  SELECT COUNT(*) AS town_count
    FROM towns
    WHERE state_num = ?;

The key elements are:

  • towncount — The procedure name (used to call it later)
  • PARTITION ON TABLE towns COLUMN state_num — Routes calls to the correct partition based on the first parameter
  • ? — Placeholder for the state_num argument supplied at runtime
Because this procedure is partitioned, it runs entirely within one partition with zero cross-partition coordination. This is why partitioned procedures in VoltDB achieve sub-millisecond latency at scale.
Read more: Designing Stored Procedures
12 Execute Stored Procedures

Call stored procedures using the EXEC command. The first argument after the procedure name corresponds to the first ? placeholder, the second to the second ?, and so on.

Count towns in Massachusetts (FIPS state code 25):

SQL
EXEC towncount 25;
Expected Output
TOWN_COUNT ---------- 4 (Returned 1 rows in 0.00s)

Count towns in New York (FIPS state code 36):

SQL
EXEC towncount 36;
Expected Output
TOWN_COUNT ---------- 3 (Returned 1 rows in 0.00s)

Count towns in Ohio (FIPS state code 39):

SQL
EXEC towncount 39;
Expected Output
TOWN_COUNT ---------- 5 (Returned 1 rows in 0.00s)
Default CRUD procedures

VoltDB automatically generates default INSERT, UPDATE, DELETE, and SELECT procedures for every table. csvloader uses the default INSERT procedure under the hood. You can call these too:

SQL
EXEC TOWNS.insert Portland ME 23 Cumberland 5 62;

Verify the insert worked:

SQL
SELECT town, state, elevation FROM towns WHERE town = 'Portland';
13 Java Stored Procedures

For complex operations that require multiple SQL statements, conditional logic, or data transformation, VoltDB supports Java-based stored procedures. These extend the VoltProcedure class and can contain arbitrary Java logic within a transactional context.

Project structure
Project Layout
hello-world-procedures/
  pom.xml                          # Maven build file
  load-procedures.sql               # SQL script to load JAR + create table + register procedure
  src/main/java/helloworld/
    UpdatePeople.java               # The stored procedure
Understanding the procedure

Every Java stored procedure follows a consistent pattern:

  • Import VoltDB classes (org.voltdb.*)
  • Extend VoltProcedure
  • Declare SQL statements as SQLStmt fields
  • Implement a run() method that queues and executes SQL

The UpdatePeople procedure looks up a county and either updates or inserts a population record:

Java
package helloworld;

import org.voltdb.*;

public class UpdatePeople extends VoltProcedure {

  public final SQLStmt findCurrent = new SQLStmt(
    "SELECT population FROM people"
    + " WHERE state_num=? AND county_num=?"
    + " ORDER BY population;");

  public final SQLStmt updateExisting = new SQLStmt(
    "UPDATE people SET population=?"
    + " WHERE state_num=? AND county_num=?;");

  public final SQLStmt addNew = new SQLStmt(
    "INSERT INTO people VALUES (?,?,?,?,?);");

  public VoltTable[] run(byte state_num,
                         short county_num,
                         String state,
                         String county,
                         long population)
      throws VoltAbortException {

    voltQueueSQL(findCurrent, state_num, county_num);
    VoltTable[] results = voltExecuteSQL();

    if (results[0].getRowCount() > 0) {
      voltQueueSQL(updateExisting, population,
                   state_num, county_num);
    } else {
      voltQueueSQL(addNew, state_num, county_num,
                   state, county, population);
    }
    return voltExecuteSQL(true);
  }
}

Key points about Java stored procedures:

  • voltQueueSQL() — Queues a SQL statement for execution. You can queue multiple statements before executing them as a batch.
  • voltExecuteSQL() — Executes all queued SQL statements and returns results. Pass true for the final batch.
  • VoltAbortException — Throw this to explicitly abort the transaction and roll back all changes.
  • Each procedure call is a complete ACID transaction — if any statement fails, all changes are rolled back.
Build and load

Build the JAR with Maven and load it into VoltDB:

Shell
cd /opt/trainingUI/data/hello-world-procedures
mvn clean package
sqlcmd < load-procedures.sql
Test the procedure

Try it out:

SQL
exec UpdatePeople 25 17 'MA' 'Middlesex' 1500000;
SQL
SELECT * FROM people WHERE state_num = 25;
Java stored procedures can also be used with csvloader via the -p flag: csvloader --file data.csv -p helloworld.UpdatePeople. This lets you use custom upsert logic during bulk loading.
Read more: Procedure Anatomy
14 Client Applications & Next Steps

In production, applications interact with VoltDB by calling stored procedures through client libraries. VoltDB provides official clients for Java, Python, Go, C++, and a language-agnostic JSON/HTTP API.

The VoltDB JSON API

The simplest way to call stored procedures from any language is the JSON API. Try it in the terminal:

Shell
curl -s --globoff "http://localhost:8081/api/2.0/?Procedure=towncount&Parameters=[25]" | python3 -m json.tool
High-performance Java clients

For maximum throughput, Java applications use the asynchronous Client2 API. This allows thousands of in-flight requests and handles backpressure automatically:

Java
Client2Config config = new Client2Config();
Client2 client = ClientFactory.createClient(config);
client.connectSync("localhost");

// Asynchronous call with callback
client.callProcedureAsync("towncount", stateNum)
  .whenComplete((response, error) -> {
    // Process result
  });

VoltDB routinely achieves hundreds of thousands of transactions per second with sub-millisecond latency using this pattern.

Where to go from here

Congratulations! You've covered the core concepts of VoltDB: tables, queries, schema evolution, partitioning, durability, and stored procedures. Here are some next steps:

  • Try the other tutorials in this Training Center (VWAP, DDoS, Toll Collection)
  • Read the full VoltDB documentation at docs.voltdb.com
  • Explore VoltDB Topics for streaming data out of VoltDB
  • Try VoltSP for building streaming pipelines from Kafka into VoltDB
Read more: Designing Client Applications
Read more: VoltDB Topics (Streaming Data)