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.
sqlcmd (VoltDB command-line SQL tool)csvloader and voltadminVoltDB 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:
csvloader, voltadmin, and sqlcmdYou'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.
exec @Statistics TABLE 0). For advanced CLI tools like csvloader, use a terminal.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:
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.
FILE mytables.sql;Use standard SQL INSERT statements to add records to the table. Let's add a few towns from the northeastern United States:
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.
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:
SELECT count(*) AS total FROM towns;
Now list all towns sorted alphabetically:
SELECT town, state FROM towns ORDER BY town;
You can also filter with WHERE clauses. Try finding all towns in a specific state:
SELECT town, county FROM towns WHERE state = 'MA';
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:
ALTER TABLE towns ADD COLUMN elevation INTEGER;
Now update the existing records with elevation data (in feet):
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):
SELECT town, state, elevation FROM towns ORDER BY elevation DESC;
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.
First, drop the existing towns table and recreate it with the extended schema. The column order matches our source data file to simplify loading:
DROP TABLE towns IF EXISTS;
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.
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.
Open a terminal and download the official VoltDB tutorial data files:
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).
Run csvloader from the terminal (this is a CLI tool, not SQL):
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 filetowns — the target table namecsvloader 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.
Verify the data loaded by running this query:
SELECT COUNT(*) AS total FROM towns;
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:
SELECT town, state, elevation FROM towns ORDER BY elevation DESC LIMIT 5;
Count how many towns are in each state:
SELECT state, COUNT(*) AS num_towns FROM towns GROUP BY state ORDER BY num_towns DESC;
Find the average elevation by state:
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:
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.
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.
VoltDB's approach differs from traditional database sharding in three important ways:
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:
DELETE FROM towns;
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:
csvloader --separator "|" --skip 1 --file data/towns.txt towns
PARTITION TABLE right after CREATE TABLE in your DDL file. That way the table is partitioned before any data is loaded.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 TablesVoltDB 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 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 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:
voltadmin save /tmp/voltdb-snapshots mybackup
To shut down cleanly and save a final snapshot, use voltadmin:
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.
If you ran the shutdown command above, VoltDB is now stopped. Restart it from the terminal:
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:
SELECT COUNT(*) AS total FROM towns;
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:
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:
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 runtimeCall 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):
EXEC towncount 25;
Count towns in New York (FIPS state code 36):
EXEC towncount 36;
Count towns in Ohio (FIPS state code 39):
EXEC towncount 39;
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:
EXEC TOWNS.insert Portland ME 23 Cumberland 5 62;
Verify the insert worked:
SELECT town, state, elevation FROM towns WHERE town = 'Portland';
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.
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
Every Java stored procedure follows a consistent pattern:
org.voltdb.*)VoltProcedureSQLStmt fieldsrun() method that queues and executes SQLThe UpdatePeople procedure looks up a county and either updates or inserts a population record:
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.Build the JAR with Maven and load it into VoltDB:
cd /opt/trainingUI/data/hello-world-procedures mvn clean package sqlcmd < load-procedures.sql
Try it out:
exec UpdatePeople 25 17 'MA' 'Middlesex' 1500000;
SELECT * FROM people WHERE state_num = 25;
-p flag: csvloader --file data.csv -p helloworld.UpdatePeople. This lets you use custom upsert logic during bulk loading.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 simplest way to call stored procedures from any language is the JSON API. Try it in the terminal:
curl -s --globoff "http://localhost:8081/api/2.0/?Procedure=towncount&Parameters=[25]" | python3 -m json.tool
For maximum throughput, Java applications use the asynchronous Client2 API. This allows thousands of in-flight requests and handles backpressure automatically:
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.
Congratulations! You've covered the core concepts of VoltDB: tables, queries, schema evolution, partitioning, durability, and stored procedures. Here are some next steps: