Set of scripts and configs to make a detailed SQL DBs performance tests and comparisons - both locally and on remote machines!
Requirements:
- Docker
- Bash & Python 3 for scripts
- To run remotely: DigitalOcean account or similar remote machine access through SSH keys
Detailed instructions for preparation & runs on different environments:
Local Tests
Preparation
DBs
To run tests, DBs with expected schemas are required. Currently, supported DBs are:
- MySQL
- PostgreSQL
- MariaDB
To build and run them in Docker, execute:
./build_and_run_mysql.bash
./build_and_run_postgresql.bash
./build_and_run_mariadb.bash
Check out those scripts if you want to customize their runtime and various config options a bit; for DB versions and schemas inspect db folders respectively.
We have the following schema - postgresql/schema.sql example:
CREATE TABLE "user" ( id BIGSERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP ); CREATE TABLE "order" ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP ); CREATE INDEX order_user_id ON "order"(user_id); CREATE TABLE "item" ( id BIGSERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL UNIQUE, description TEXT, price NUMERIC(10, 2) NOT NULL, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP ); CREATE TABLE "order_item" ( id BIGSERIAL PRIMARY KEY, order_id BIGINT NOT NULL REFERENCES "order"(id) ON DELETE CASCADE, item_id BIGINT NOT NULL REFERENCES "item"(id) ON DELETE CASCADE ); CREATE INDEX order_item_order_id ON "order_item"(order_id); CREATE INDEX order_item_item_id ON "order_item"(item_id);
Tests
All tests cases are defined in a single tests/src/main/java/SqlDbPerformanceTests.java file. Java 25 & compatible Maven is required to build them, but it's all hidden in Docker:
./build_performance_tests.bash
Once this image is prepared, we can use another script, run_test.py, to choose a particular test case from the available options (Python 3 required).
Execution
After going through the preparation, to run test cases locally against a particular database, just execute the run_test.py script:
./run_test.py
Choose test case. Available options:
1 - INSERT_USERS
2 - INSERT_ITEMS_IN_BATCHES
3 - INSERT_ORDERS_IN_BATCHES
4 - INSERT_ORDER_ITEMS_IN_BATCHES
5 - SELECT_USERS_BY_ID
6 - SELECT_USERS_BY_EMAIL
7 - SELECT_SORTED_BY_ID_USER_PAGES
8 - SELECT_ORDERS_JOINED_WITH_USERS
9 - SELECT_ORDERS_JOINED_WITH_ITEMS
10 - SELECT_USERS_WITH_ORDERS_STATS_BY_ID
11 - UPDATE_USER_EMAILS_BY_ID
12 - UPDATE_USER_UPDATED_ATS_BY_ID
13 - UPDATE_USER_MULTIPLE_COLUMNS_BY_ID
14 - DELETE_ORDERS_BY_ID
15 - DELETE_ORDERS_IN_BATCHES_BY_ID
16 - INSERT_USERS_AND_ORDERS_WITH_ITEMS_IN_TRANSACTIONS
17 - INSERT_UPDATE_DELETE_AND_SELECT_USERS_BY_ID
1
Chosen test case: INSERT_USERS
Choose db. Available options:
1 - MySQL
2 - PostgreSQL
3 - MariaDB
2
Running with PostgreSQL
sql-db-performance-tests
Starting DB Performance Tests, connecting to POSTGRESQL data source with a pool of 64 connections...
[main] INFO com.zaxxer.hikari.HikariDataSource - POSTGRESQL - Starting...
[main] INFO com.zaxxer.hikari.pool.HikariPool - POSTGRESQL - Added connection org.postgresql.jdbc.PgConnection@4bec1f0c
POSTGRESQL data source connected, about to run INSERT_USERS test case.
[main] INFO com.zaxxer.hikari.HikariDataSource - POSTGRESQL - Start completed.
The following test case specification will be executed: TestCaseSpec[queriesToExecute=500000, queriesRate=10000, queryGroups=[QueryGroup[id=insert-users, tables=["user"]]]]
Tables count before test:
"user": 0
2026-01-05T06:11:51.609, 10000/500000 queries were issued, waiting 1s before sending next query batch...
2026-01-05T06:11:53.065, 20000/500000 queries were issued, waiting 1s before sending next query batch...
2026-01-05T06:11:54.080, 30000/500000 queries were issued, waiting 1s before sending next query batch...
...
2026-01-05T06:12:40.324, 470000/500000 queries were issued, waiting 1s before sending next query batch...
2026-01-05T06:12:41.381, 480000/500000 queries were issued, waiting 1s before sending next query batch...
2026-01-05T06:12:42.444, 490000/500000 queries were issued, waiting 1s before sending next query batch...
...
Test case INSERT_USERS with POSTGRESQL data source finished! It had queries: [QueryGroup[id=insert-users, tables=["user"]]]
Tables count after test:
"user": 500000
Some stats...
Total test duration: PT53.043S
Queries duration: PT53.043S
Executed queries: 500000
Wanted queries rate: 10000/s
Actual queries rate: 9426/s
Min: 1.066 ms
Max: 69.944 ms
Mean: 2.383 ms
Percentile 50 (Median): 2.358 ms
Percentile 75: 2.668 ms
Percentile 90: 2.905 ms
Percentile 99: 3.795 ms
Percentile 99.9: 13.348 ms
After running this particular test case, we might examine our local Postgres instance:
docker exec -it postgresql-performance psql -U postgres -d performance
psql (18.1 (Debian 18.1-1.pgdg13+2))
Type "help" for help.
performance=# \dt
List of tables
Schema | Name | Type | Owner
--------+------------+-------+----------
public | item | table | postgres
public | order | table | postgres
public | order_item | table | postgres
public | user | table | postgres
(4 rows)
performance=# select count(*) from "user";
count
--------
500000
(1 row)
The same could be done for other DBs and test cases:
docker exec -it mariadb-performance mariadb --database performance -p
docker exec -it mysql-performance mysql --database performance -p
Remote Tests
Preparation
Infra
First things first, we need to have a few virtual machines. As said, we're going to use DigitalOcean to set the infrastructure up. You might also run this on any Linux-based machines that have network access to each other on db-related ports; but in order to do that, you would have to customize scripts a bit.
With that in mind, let's start; from the remote-scripts dir, set the Python env up with just the requests dependency:
./init_python_env.bash
source venv/bin/activate
Now, the following may take a while, since we are creating quite a few machines; three for running tests and three for DBs:
export DO_API_TOKEN="<your DigitalOcean API key>"
export SSH_KEY_FINGERPRINT="<fingerprint of your ssh key, uploaded to DigitalOcean, giving you ssh access to created machines>"
./prepare_infra.py
Again, if you don't want to run this on DigitalOcean, you must set these machines up on your own.
After the script succeeds, we have six ready-to-be-used machines for our performance tests.
DBs
Now we will build, package and deploy databases - all three of them.
First, let's build & package - all in Docker; it might take a while, especially when executed for the first time. From the remote-scripts, run:
./build_and_package_dbs.bash
It builds docker images of all dbs and packages them into tar archives, to-be-deployed to our remote machines.
If you are curious and/or want to customize db configs a bit, check out build_and_package.bash scripts, located in each db dir.
To deploy dbs, we must get public ip addresses of the target machines - each db having its own, dedicated one. Then, we simply run:
export MYSQL_HOST=<mysql machine public ip>
export POSTGRESQL_HOST=<postgresql machine public ip>
export MARIADB_HOST=<mariadb machine public ip>
./deploy_dbs.bash
This can also take a while, since we are sending full-blown docker images here - but fortunately, it must be done only once! Once the scripts finishes, dbs are ready and running:
ssh ops@$MYSQL_HOST "docker ps"
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
de987e27e98e mysql-performance:latest "docker-entrypoint.s…" 8 minutes ago Up 8 minutes mysql-performance
ssh ops@$POSTGRESQL_HOST "docker ps"
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
baafa551cf83 postgresql-performance:latest "docker-entrypoint.s…" 7 minutes ago Up 7 minutes postgresql-performance
ssh ops@$MARIADB_HOST "docker ps"
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3faba51eeba5 mariadb-performance:latest "docker-entrypoint.s…" 7 minutes ago Up 7 minutes mariadb-performance
DBs are then ready and steady - time to prepare performance tests.
Tests
Similar to dbs, we build & package tests in Docker (Java 25 & Maven app):
./build_and_package_tests.bash
Then we similarly deploy it; with a caveat that the docker images are just sent and loaded on the target tests machines - they are not being run just yet:
export MYSQL_TESTS_HOST=<mysql tests machine public ip>
export POSTGRESQL_TESTS_HOST=<postgresql tests machine public ip>
export MARIADB_TESTS_HOST=<mariadb tests machine public ip>
./deploy_tests.bash
Together with tests, the run_test.py script is deployed to each tests machine - we will now use it to finally run the tests!
Execution
After going through the preparation, to run test cases remotely against a particular database, we just execute the run_test.py script on each machine:
ssh ops@<tests machine public ip>
cd deploy/tests
export DB_HOST=<db machine PRIVATE ip>
./run_test.py
Choose test case. Available options:
1 - INSERT_USERS
2 - INSERT_ITEMS_IN_BATCHES
3 - INSERT_ORDERS_IN_BATCHES
4 - INSERT_ORDER_ITEMS_IN_BATCHES
5 - SELECT_USERS_BY_ID
6 - SELECT_USERS_BY_EMAIL
7 - SELECT_SORTED_BY_ID_USER_PAGES
8 - SELECT_ORDERS_JOINED_WITH_USERS
9 - SELECT_ORDERS_JOINED_WITH_ITEMS
10 - SELECT_USERS_WITH_ORDERS_STATS_BY_ID
11 - UPDATE_USER_EMAILS_BY_ID
12 - UPDATE_USER_UPDATED_ATS_BY_ID
13 - UPDATE_USER_MULTIPLE_COLUMNS_BY_ID
14 - DELETE_ORDERS_BY_ID
15 - DELETE_ORDERS_IN_BATCHES_BY_ID
16 - INSERT_USERS_AND_ORDERS_WITH_ITEMS_IN_TRANSACTIONS
17 - INSERT_UPDATE_DELETE_AND_SELECT_USERS_BY_ID
1
Chosen test case: INSERT_USERS
Choose db. Available options:
1 - MySQL
2 - PostgreSQL
3 - MariaDB
1
Running with MySQL
Starting DB Performance Tests, connecting to MYSQL data source with a pool of 128 connections...
[main] INFO com.zaxxer.hikari.HikariDataSource - MYSQL - Starting...
[main] INFO com.zaxxer.hikari.pool.HikariPool - MYSQL - Added connection com.mysql.cj.jdbc.ConnectionImpl@757277dc
[main] INFO com.zaxxer.hikari.HikariDataSource - MYSQL - Start completed.
MYSQL data source connected, about to run INSERT_USERS test case.
The following test case specification will be executed: TestCaseSpec[queriesToExecute=500000, queriesRate=10000, queryGroups=[QueryGroup[id=insert-users, tables=[`user`]]]]
Tables count before test:
`user`: 0
2026-01-12T09:39:05.477, 10000/500000 queries were issued, waiting 1s before sending next query batch...
2026-01-12T09:39:08.601, 20000/500000 queries were issued, waiting 1s before sending next query batch...
2026-01-12T09:39:10.087, 30000/500000 queries were issued, waiting 1s before sending next query batch...
...
2026-01-12T09:40:03.453, 470000/500000 queries were issued, waiting 1s before sending next query batch...
2026-01-12T09:40:04.664, 480000/500000 queries were issued, waiting 1s before sending next query batch...
2026-01-12T09:40:06.044, 490000/500000 queries were issued, waiting 1s before sending next query batch...
...
Test case INSERT_USERS with MYSQL data source finished! It had queries: [QueryGroup[id=insert-users, tables=[`user`]]]
Tables count after test:
`user`: 500000
Some stats...
Total test duration: PT1M3.299S
Queries duration: PT1M3.299S
Executed queries: 500000
Wanted queries rate: 10000/s
Actual queries rate: 7899/s
Min: 2.818 ms
Max: 213.591 ms
Mean: 12.469 ms
Percentile 50 (Median): 10.67 ms
Percentile 75: 12.905 ms
Percentile 90: 16.104 ms
Percentile 99: 79.42 ms
Percentile 99.9: 112.038 ms
Tests have finished running, exporting results to a file...
Results exported to to the insert_users_mysql.txt file
As the output says - there are lots of test cases and the output of a chosen one is saved in the file.
Customization
You can customize each test case a bit without changing the code, using environment variables:
export QUERIES_TO_EXECUTE=<how much queries to execute in total>
export QUERIES_RATE=<queries per second rate>
export DATA_SOURCE_CONNECTION_POOL_SIZE=<connection pool size - different default values for different DBs>
export DB_HOST=<if different than localhost - for remote tests mostly>
Results
If you care just about the numbers, in the /results there are results of my executions, together with _env_.md information in each case.
You can run them yourself and compare the results :)