Database Testing with JDBC
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. |
The reference material consulted while preparing these pages is listed in the section bibliography.
This page was written against Apache JMeter 5.6.3 (latest release on 2026-10-14; it runs on Java 8 and later, Java 17
or later is recommended), with the differences planned for JMeter 6.0 noted where they matter. JMeter 6.0 is not
released: its planned changes come from xdocs/changes.xml on the master branch, need Java 17 or later (Java 21
recommended) and are described as planned, not as shipped behaviour. The examples use the PostgreSQL JDBC driver
(pgjdbc) 42.7.14, the latest release on Maven Central on the same date, against a local PostgreSQL server. The books
on the market target JMeter 2.11 to 3.0; the database elements changed a lot since then (the connection pool moved from
Excalibur to Apache Commons DBCP 2 in 3.0, and most of the pool and result-set options described below arrived in 3.3
and the 5.x line), so every field, property and default on this page is taken from the official component reference,
the database tutorial, the properties reference and, where the documentation is silent, the 5.6.3 source code, which
is said explicitly.
JMeter’s HTTP samplers test a database indirectly: every Bookshelf search runs SQL somewhere behind the API. The JDBC elements let a test plan talk to the database directly, as the application does. That is useful in two quite different ways: to load-test the database itself (how many searches per second can PostgreSQL serve, does the new index help, where does the lock contention start), and, far more often, as a utility inside an API load test (seeding and cleaning test data, reading valid ids to feed the HTTP requests, checking that a loan really reached the table). This page covers both, starting with when each is appropriate.
Testing the database directly or through the API
A JDBC Request measures the time from asking the connection pool for a connection to the end of reading the result set. It does not see the API’s connection pool, its object-relational mapping, its caches, its serialisation, its authentication or the network hop from the user. A database test answers "how does this SQL behave under concurrency", not "how fast is Bookshelf for a reader".
| Question | Through the API (HTTP samplers) | Directly (JDBC samplers) |
|---|---|---|
What is measured |
What a user experiences: application code, the application’s pool, ORM, caches, serialisation, network. |
The SQL statements alone, plus the JDBC driver and JMeter’s pool. |
Realism of the load |
High, if the workload model is right: the API decides which queries run, how many connections are used and how transactions are scoped. |
Only as realistic as the SQL you copy from the application and the concurrency you choose. It is easy to send queries the application never sends, or far more concurrent connections than its pool allows. |
Best for |
Acceptance criteria, capacity of the whole service, regressions between builds. |
Comparing indexes or query rewrites, sizing the database server, reproducing a slow query or a lock contention seen in an API test, testing a database shared by several applications. |
Isolating bottlenecks |
Harder: a slow search could be the API, the pool or the database. |
Easy: only the database and the driver are involved. |
Risks |
Low, as long as the target is a test environment. |
Writing statements bypass every application rule; never aim them at a production database, and use a disposable copy of the data. |
Recommended split for Bookshelf. The main plan, bookshelf-load.jmx, tests through the API and uses JDBC only for
test-data utilities (later sections). A separate plan, bookshelf-db.jmx, replays the two hottest statements found in
the API test (the book search and the loan insert) against a disposable copy of the database, to compare indexes and
find the database’s own limit. Finding those statements is a database task: PostgreSQL’s statistics views and slow
query logging are covered in Diagnosing and Fixing Slow Queries.
Match the application’s concurrency. The database never sees the API’s 50 virtual users; it sees the API’s pool, for example ten connections. A JDBC test with 50 threads and one connection each (the JMeter default) puts five times more concurrent sessions on the server than the application ever could. Use the application’s pool size as the number of JMeter threads when the goal is to reproduce what the application does, and go beyond it only when the goal is to find the database’s limit.
The Bookshelf database
The examples use the Bookshelf PostgreSQL database that the Bookshelf API runs on, published by the local Docker Compose
stack on localhost:5432 with a database and a role named bookshelf. Only two tables matter here. The schema below is
the one this guide assumes; adapt the column names to your own database.
CREATE TABLE books (
id BIGSERIAL PRIMARY KEY,
isbn VARCHAR(17) NOT NULL UNIQUE,
title VARCHAR(300) NOT NULL,
author VARCHAR(200) NOT NULL,
available_copies INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE loans (
id BIGSERIAL PRIMARY KEY,
book_id BIGINT NOT NULL REFERENCES books (id),
username VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX loans_username_idx ON loans (username);
-- Used by the callable-statement example
CREATE FUNCTION count_active_loans(p_username VARCHAR) RETURNS INTEGER
LANGUAGE sql STABLE
AS $$ SELECT count(*)::INTEGER FROM loans WHERE username = p_username AND status = 'ACTIVE' $$;
The test users are the reader0001-style accounts of users.csv (see
Parameterization & Test Data), so every row a test creates can
be found and removed by its username. Connection settings are passed as run properties, like threads and
rampup elsewhere in this guide: db_host, db_port, db_name, db_user and db_password. The password has a
placeholder value and is never written into the .jmx file.
Adding the JDBC driver
JMeter does not ship any JDBC driver. The driver jar of your database must be on JMeter’s classpath, and the easiest
place for it is the lib directory (not lib/ext, which is for JMeter components and plugins).
-
Download the driver from its vendor. For PostgreSQL, the pgjdbc jar is published on Maven Central with a SHA-1 checksum next to it.
-
Copy it to
$JMETER_HOME/lib. -
Restart JMeter. Jars are only scanned at start-up.
-
In the JDBC Connection Configuration, pick or type the driver class (
org.postgresql.Driver).
cd "$JMETER_HOME/lib"
curl -fLO https://repo1.maven.org/maven2/org/postgresql/postgresql/42.7.14/postgresql-42.7.14.jar
curl -fL https://repo1.maven.org/maven2/org/postgresql/postgresql/42.7.14/postgresql-42.7.14.jar.sha1; echo
shasum -a 1 postgresql-42.7.14.jar
Output (the two hashes must be identical):
0e7c94ce80cb1dd9875ff44ffe6a40a8aaa3eefd
0e7c94ce80cb1dd9875ff44ffe6a40a8aaa3eefd postgresql-42.7.14.jar
Other ways to add the jar, all described on
Installation & Configuration: the user.classpath property
(a directory outside the JMeter installation), or Add directory or jar to classpath on the Test Plan element, which
adds it for that plan only. In distributed mode the driver must be present on every load generator, and the Maven
plugin used in CI needs it declared as a test-plan library (covered in the CI/CD page of this section).
Source: Getting Started: JDBC Driver (User Manual), Getting Started: Classpath (User Manual) and Initializing the Driver (pgjdbc)
Driver classes and URLs
The JDBC Driver class drop-down offers a list of common classes, and the Validation Query drop-down a list of common
validation queries. Both lists come from properties (jdbc.config.jdbc.driver.class and jdbc.config.check.query),
so they can be extended in user.properties; any other class name can be typed in.
| Database | Driver class (5.6.3 list) | URL form |
|---|---|---|
PostgreSQL |
|
|
MySQL |
|
|
MariaDB |
|
per the MariaDB Connector/J documentation |
Microsoft SQL Server |
|
|
Oracle |
|
|
H2, HSQLDB, Derby, DB2, Firebird, SQLite, Exasol, jTDS, Ingres |
listed in the drop-down |
per vendor documentation |
If JMeter reports No suitable driver, either the class was not found (the log then shows
DataSourceElement: Could not load driver with a ClassNotFoundException), or the class was found but does not
accept the URL (a typo in the URL, or the wrong class). If the server is down or unreachable, the error is a
java.net.ConnectException.
The MySQL driver class and JMeter 6.0
MySQL renamed the class of its driver in Connector/J 8.0: the class implementing java.sql.Driver changed from
com.mysql.jdbc.Driver to com.mysql.cj.jdbc.Driver, and the old name is deprecated. JMeter 5.6.3 still uses the old
name everywhere: in the default value of jdbc.config.jdbc.driver.class, in the component reference, and in the
database tutorial, whose example also names the long-gone mysql-connector-java-X.X.X-bin.jar.
The 6.0.0 section of changes.xml on master lists, under Changes / General, "Change references to old MySQL driver
to new class com.mysql.cj.jdbc.Driver`" (pull request 6274). It is not in the list of incompatible changes: it changes
what JMeter suggests and documents, not what a test plan may contain. A plan with the old class name keeps whatever
behaviour the Connector/J version in `lib gives that deprecated name. Typing the new name now works in 5.6.3 too,
and removes the dependency on a deprecated alias:
# user.properties: put the current MySQL class first in the 5.6.3 drop-down (6.0 is planned to do this itself)
jdbc.config.jdbc.driver.class=com.mysql.cj.jdbc.Driver|org.postgresql.Driver|org.mariadb.jdbc.Driver|\
com.microsoft.sqlserver.jdbc.SQLServerDriver|oracle.jdbc.OracleDriver|org.h2.Driver
JDBC Connection Configuration
The JDBC Connection Configuration creates a pool of database connections, through Apache Commons DBCP 2
(BasicDataSource), and binds it to a name. Every JDBC element (Request, PreProcessor, PostProcessor) names the pool it
uses; nothing else connects it to the configuration element. Several configurations can exist side by side, each with
its own name, for example one for the Bookshelf database and one for a reporting replica.
Add → Config Element → JDBC Connection Configuration (test class JDBCDataSource, a TestBean element, so its panel is
generated from its properties and grouped into the four sections below).
Where it sits and when it runs. The pool is set up once, when the test starts (testStarted in the 5.6.3 source),
before any thread runs, and samplers find it by name. Its settings are read at that moment, so they can use properties
and functions (${__P(db_host,localhost)}) but not variables set by threads, such as CSV Data Set Config variables.
The element accepts no children. Put it directly under the Test Plan, or under the Thread Group that uses it, and keep
the name unique across the plan: with two configurations of the same name, only one is kept, and JMeter logs
JDBC data source already defined for: ....
Every field
Defaults and JMX property names are from the 5.6.3 source (DataSourceElementBeanInfo and its resource bundle);
descriptions follow the component reference.
| Field | Meaning | Default | JMX property |
|---|---|---|---|
Variable Name Bound to Pool |
|||
Variable Name for created pool |
The name the pool is bound to. Every JDBC Request names it in Variable Name of Pool declared in JDBC Connection Configuration. Required; must be unique. |
(empty) |
|
Connection Pool Configuration |
|||
Max Number of Connections |
Maximum number of connections in the pool. The manual’s advice: in most cases set it to |
|
|
Max Wait (ms) |
How long a thread waits for a free connection before the pool throws an error (DBCP |
|
|
Time Between Eviction Runs (ms) |
Sleep time of DBCP’s idle-connection evictor thread; non-positive means no evictor. In 5.6.3 it is only applied when Test While Idle is on. |
|
|
Auto Commit |
Whether connections commit every statement automatically. Turn it off only for explicit transactions with the Commit and Rollback query types. |
|
|
Transaction Isolation |
The isolation level set on each connection: |
|
|
Pool Prepared Statements |
Prepared-statement pooling per connection. |
|
|
Preinit Pool |
Open the pool immediately at test start instead of on the first request. With |
|
|
Init SQL statements separated by new line |
SQL statements run once on every new physical connection, when the pool creates it, for example session settings. Since 5.1. |
(empty) |
|
Connection Validation by Pool |
|||
Test While Idle |
Let the evictor validate idle connections with the validation query. If off, Soft Min Evictable Idle Time and Validation Query are ignored. |
|
|
Soft Min Evictable Idle Time(ms) |
How long a connection may sit idle before the evictor may close it, as long as at least |
|
|
Validation Query |
A simple query that proves the database still answers. Empty (default since 3.3) means the driver’s |
(empty) |
|
Database Connection Configuration |
|||
Database URL |
JDBC connection string, defined by the driver vendor. |
(empty) |
|
JDBC Driver class |
Fully qualified driver class; the jar must be on JMeter’s classpath. |
(empty) |
|
Username |
Database user. If empty, no user or password is passed. |
(empty) |
|
Password |
Password. The manual warns that it is stored unencrypted in the test plan; pass it as a property instead. |
(empty) |
|
Connection Properties |
Driver properties set when a connection is established, in DBCP’s format |
(empty) |
|
Source: JDBC Connection Configuration (Component Reference), BasicDataSource Configuration (Apache Commons DBCP) and Connection parameters (pgjdbc)
Two properties tune the drop-downs and the sampler globally:
| Property | Meaning | Default |
|---|---|---|
|
Validation queries offered in the drop-down, separated by a vertical bar. |
the list above |
|
Driver classes offered in the drop-down, separated by a vertical bar. |
the list above |
|
The string that means SQL |
|
|
Maximum bytes kept from a |
|
Per-thread connections or a shared pool
The choice of Max Number of Connections changes what the test measures.
0 (default): one connection per thread |
N > 0: one shared pool of N connections |
|
|---|---|---|
Connections opened on the database |
One per JMeter thread that uses the pool, opened lazily on its first request. |
Up to N, created at start if Preinit Pool is on, otherwise on demand. |
Waiting |
Never: each thread owns its connection. |
A thread waits up to Max Wait when all N are busy; that wait is part of the sample time. |
Transactions over several requests |
Work: every request of the thread uses the same connection. |
Unsafe: each request borrows a connection and returns it, so Autocommit(false), the statements and Commit may run on different connections. |
Models |
Clients that each hold a connection (batch jobs, desktop clients). |
An application server with its own pool, such as the Bookshelf API with ten connections. |
Worked numbers. With the canonical load profile of this guide (50 threads, 60 s ramp-up, 660 s duration) and the
default 0, PostgreSQL receives up to 50 new sessions during the first minute, about one every 1.2 s, each a separate
server process. PostgreSQL’s max_connections is typically 100, and the Bookshelf API holds ten of them, so 50 JMeter
threads fit; 120 would make connection attempts fail once the server’s limit is reached. To model the API instead, use
poolMax = 10 with 50 threads: at most ten statements run at once, and the other threads queue in JMeter’s pool for up
to 10 s, as requests do in the API’s pool.
Example: the Bookshelf pool
Add → Config Element → JDBC Connection Configuration under the Test Plan, then:
<JDBCDataSource guiclass="TestBeanGUI" testclass="JDBCDataSource" testname="Bookshelf DB" enabled="true">
<stringProp name="dataSource">bookshelfDb</stringProp>
<stringProp name="poolMax">0</stringProp>
<stringProp name="timeout">10000</stringProp>
<stringProp name="trimInterval">60000</stringProp>
<boolProp name="autocommit">true</boolProp>
<stringProp name="transactionIsolation">DEFAULT</stringProp>
<stringProp name="poolPreparedStatements">-1</stringProp>
<boolProp name="preinit">false</boolProp>
<stringProp name="initQuery">SET statement_timeout = '5s'</stringProp>
<boolProp name="keepAlive">true</boolProp>
<stringProp name="connectionAge">5000</stringProp>
<stringProp name="checkQuery"></stringProp>
<stringProp name="dbUrl">jdbc:postgresql://${__P(db_host,localhost)}:${__P(db_port,5432)}/${__P(db_name,bookshelf)}</stringProp>
<stringProp name="driver">org.postgresql.Driver</stringProp>
<stringProp name="username">${__P(db_user,bookshelf)}</stringProp>
<stringProp name="password">${__P(db_password,)}</stringProp>
<stringProp name="connectionProperties">ApplicationName=jmeter-bookshelf</stringProp>
</JDBCDataSource>
<hashTree/>
statement_timeout makes PostgreSQL cancel any statement running longer than five seconds, and ApplicationName labels
the JMeter sessions in pg_stat_activity, so they can be told apart from the API’s sessions while the test runs. The
password property has no default; it is supplied at run time:
jmeter -n -t bookshelf-db.jmx -Jthreads=10 -Jrampup=10 -Jduration=300 -Jdb_host=localhost -Jdb_password=change-me -l results/db.jtl
A -J value is visible in the shell history and the process list; for anything but a local disposable database, put
db_password in a properties file that is not committed and pass it with -q.
JDBC Request
The JDBC Request sampler sends one SQL statement through a named pool and records the result as a sample. Its fields are also the fields of the JDBC PreProcessor and PostProcessor.
Add → Sampler → JDBC Request (test class JDBCSampler, a TestBean element).
Every field
| Field | Meaning | Default | JMX property |
|---|---|---|---|
Name |
Shown in the tree and used as the sample label in results. |
|
|
Variable Name of Pool declared in JDBC Connection Configuration |
Must match the Variable Name for created pool of a JDBC Connection Configuration. Required. |
(empty) |
|
Query Type |
The kind of statement, see the next table. Required. |
|
|
SQL Query |
The statement. No trailing semicolon. For callable statements |
(empty) |
|
Parameter values |
For prepared and callable statements: comma-separated values, one per |
(empty) |
|
Parameter types |
One type per placeholder: a field name of |
(empty) |
|
Variable Names |
Comma-separated variable names, one per column of the result (skip a column with an empty name), or one per OUT parameter of a callable statement, in order. |
(empty) |
|
Result Variable Name |
If set, an object variable holding the whole result as a |
(empty) |
|
Query timeout(s) |
Statement timeout in seconds. Empty means |
(empty), meaning 0 |
|
Limit ResultSet |
Maximum number of rows to read. Empty means |
(empty), meaning -1 |
|
Handle ResultSet |
What to do with a |
|
|
Query types
| Query Type | JDBC call and use | Response data |
|---|---|---|
Select Statement |
|
A header line of column labels and one line per row, values separated by tabs. |
Update Statement |
|
|
Callable Statement |
|
Each result set and update count, then |
Prepared Select Statement |
|
Same as Select Statement. |
Prepared Update Statement |
|
|
Commit, Rollback |
|
|
AutoCommit(false), AutoCommit(true) |
|
|
Edit |
A variable reference that evaluates to one of the types above. |
As the resolved type. |
The queryType value in the .jmx file is the English label (Prepared Select Statement, AutoCommit(false) and so
on); the source warns that these strings must not change because plans store them.
Prefer the prepared types. A Select Statement with ${term} pasted into the SQL text is sent to the database as
a new statement text on every request: the database parses and plans each one, which is not what an application using
prepared statements does, and a term containing a quote breaks the SQL. With Prepared Select Statement, the SQL text
is constant and the values travel as parameters.
What the sample records
From the 5.6.3 source (JDBCSampler):
-
Elapsed time runs from the request for a connection to the end of reading the whole result set into text.
-
Connect time is the time to obtain a connection from the pool: near zero with a warm pool, the full TCP and authentication handshake on the first request of a per-thread pool, and the queueing time with a busy shared pool.
-
Latency ends when the statement returns, before the result set is converted to text. (The component reference still says that latency is set from the time to acquire a connection; since 3.1, connect time and latency are recorded separately.)
-
Response code is
200and the messageOKon success. On anSQLException, the code is the SQLState followed by the vendor error code, the message is the exception text, and the sample fails. Any other exception gives code000. -
Response headers hold a description of the connection; response data the text shown in the table above, encoded as UTF-8.
A JDBC sample fails only when the statement throws. A query that returns no rows, or an update that changes nothing, is a success; assertions are needed to catch those (see below).
Source: JDBC Request (Component Reference) and Changes history (3.1: "JDBC Request … computes latency and connect time")
Variables from a result
With Variable Names set, every row of a select (plain or prepared) sets numbered variables, and a count variable
ends in _#. For a search that returns two rows of id, title, author and the variable list book_id,,author:
| Variable | Value |
|---|---|
|
|
|
the |
|
the same for the third column |
The second column has an empty name, so it is skipped. If the query returns no rows, the count variables are 0 and
no numbered variables are set. Numbered variables left over from an earlier, longer result are removed, so
book_id_3 does not survive from a previous iteration. The count variable is what a ForEach Controller or an If
Controller reads (see Logic Controllers & Transactions).
With Result Variable Name = bookRows, the same result is also stored as a list of maps, for a script:
// JSR223 PostProcessor, child of the JDBC Request
def rows = vars.getObject('bookRows') // List<Map<String, Object>>
if (rows) {
def pick = rows[new Random().nextInt(rows.size())]
vars.put('bookId', pick.get('id').toString())
}
Example: the Bookshelf search as a prepared select
Menu path: select the Thread Group, Add → Sampler → JDBC Request, and fill in:
| Field | Value |
|---|---|
Name |
|
Variable Name of Pool |
|
Query Type |
Prepared Select Statement |
SQL Query |
|
Parameter values |
|
Parameter types |
|
Variable Names |
|
Query timeout(s) |
|
Limit ResultSet |
|
<JDBCSampler guiclass="TestBeanGUI" testclass="JDBCSampler" testname="DB search books" enabled="true">
<stringProp name="dataSource">bookshelfDb</stringProp>
<stringProp name="queryType">Prepared Select Statement</stringProp>
<stringProp name="query">SELECT id, title, author FROM books WHERE title ILIKE ? ORDER BY title LIMIT 20</stringProp>
<stringProp name="queryArguments">"%${term}%"</stringProp>
<stringProp name="queryArgumentsTypes">VARCHAR</stringProp>
<stringProp name="variableNames">book_id,book_title,book_author</stringProp>
<stringProp name="resultVariable"></stringProp>
<stringProp name="queryTimeout">5</stringProp>
<stringProp name="resultSetMaxRows">20</stringProp>
<stringProp name="resultSetHandler">Store as String</stringProp>
</JDBCSampler>
<hashTree/>
The term variable comes from the same search-terms.csv CSV Data Set Config as the HTTP plan. The response data
shown in the View Results Tree during a one-thread check looks like this (titles illustrative):
id title author
17 Dune Frank Herbert
42 Dune Messiah Frank Herbert
Example: create a loan and read back its id
PostgreSQL returns generated values with RETURNING, so the insert runs as a Prepared Select Statement and its
result sets a variable like any query:
| Field | Value |
|---|---|
Name |
|
Query Type |
Prepared Select Statement |
SQL Query |
|
Parameter values |
|
Parameter types |
|
Variable Names |
|
After the sample, loanId_1 holds the new id and loanId_# is 1. With Auto Commit on, the insert is committed
when the statement completes. A plain Prepared Update Statement would also work, but only returns 1 updates..
Example: a callable statement with an OUT value
The count_active_loans function returns the number of active loans of a reader. With the JDBC escape syntax for a
function result, the first placeholder is the OUT value; it still needs a parameter value and a type:
| Field | Value |
|---|---|
Name |
|
Query Type |
Callable Statement |
SQL Query |
|
Parameter values |
|
Parameter types |
|
Variable Names |
|
The response data ends with Output variables by position: followed by [1] 3, and activeLoans is 3. For OUT
parameters, the variable names are matched to the OUT parameters in order, without the _1 suffix.
Example: an explicit transaction
Borrowing a book in the database is two writes that must succeed together: insert the loan and decrement
available_copies. With auto commit on, each statement commits on its own. The simplest way to get a real
transaction is a second pool whose connections do not auto-commit, used only by the transactional requests, with
the whole unit inside a Transaction Controller so it is also measured:
Test Plan
├── JDBC Connection Configuration "Bookshelf DB (tx)" Variable Name: bookshelfTx, Max Number of Connections: 0,
│ Auto Commit: False, other fields as bookshelfDb
└── Thread Group Action after a Sampler error: Start Next Thread Loop
└── Transaction Controller "DB borrow book" Generate parent sample: off, Include timers: off
├── JDBC Request "reset" Query Type: Rollback
├── JDBC Request "insert loan" Prepared Update Statement
│ INSERT INTO loans (book_id, username, status) VALUES (?, ?, 'ACTIVE')
│ values: ${book_id_1},${username} types: BIGINT,VARCHAR
├── JDBC Request "take copy" Prepared Update Statement
│ UPDATE books SET available_copies = available_copies - 1 WHERE id = ? AND available_copies > 0
│ values: ${book_id_1} types: BIGINT
└── JDBC Request "commit" Query Type: Commit
<TransactionController guiclass="TransactionControllerGui" testclass="TransactionController" testname="DB borrow book">
<boolProp name="TransactionController.parent">false</boolProp>
<boolProp name="TransactionController.includeTimers">false</boolProp>
</TransactionController>
<hashTree>
<JDBCSampler guiclass="TestBeanGUI" testclass="JDBCSampler" testname="reset">
<stringProp name="dataSource">bookshelfTx</stringProp>
<stringProp name="queryType">Rollback</stringProp>
<stringProp name="query"></stringProp>
</JDBCSampler>
<hashTree/>
<JDBCSampler guiclass="TestBeanGUI" testclass="JDBCSampler" testname="insert loan">
<stringProp name="dataSource">bookshelfTx</stringProp>
<stringProp name="queryType">Prepared Update Statement</stringProp>
<stringProp name="query">INSERT INTO loans (book_id, username, status) VALUES (?, ?, 'ACTIVE')</stringProp>
<stringProp name="queryArguments">${book_id_1},${username}</stringProp>
<stringProp name="queryArgumentsTypes">BIGINT,VARCHAR</stringProp>
</JDBCSampler>
<hashTree/>
<JDBCSampler guiclass="TestBeanGUI" testclass="JDBCSampler" testname="take copy">
<stringProp name="dataSource">bookshelfTx</stringProp>
<stringProp name="queryType">Prepared Update Statement</stringProp>
<stringProp name="query">UPDATE books SET available_copies = available_copies - 1 WHERE id = ? AND available_copies > 0</stringProp>
<stringProp name="queryArguments">${book_id_1}</stringProp>
<stringProp name="queryArgumentsTypes">BIGINT</stringProp>
</JDBCSampler>
<hashTree/>
<JDBCSampler guiclass="TestBeanGUI" testclass="JDBCSampler" testname="commit">
<stringProp name="dataSource">bookshelfTx</stringProp>
<stringProp name="queryType">Commit</stringProp>
<stringProp name="query"></stringProp>
</JDBCSampler>
<hashTree/>
</hashTree>
Three conditions make this correct:
-
One connection per thread. The transactional pool must use Max Number of Connections =
0. With a shared pool, each request borrows a connection and returns it, so the four requests can run on different connections (this follows from the 5.6.3 source, where every sample gets a connection and closes it). -
Discard a half-finished transaction. If insert loan fails, Start Next Thread Loop skips the rest of the iteration and leaves the transaction open on the thread’s connection; JMeter’s pool does not roll back when a connection is returned (the 5.6.3 source sets DBCP’s
rollbackOnReturnto false). The reset request at the start of the next iteration rolls it back. On a clean connection it is an empty rollback, cheap but still a sample; give it a label that the analysis can filter out. -
Set Include timers explicitly. The plan writes
TransactionController.includeTimersasfalse, as everywhere in this guide, because a missing property is read as true by 5.6.3.
The AutoCommit(false) and AutoCommit(true) query types do the same on a pool that auto-commits, but switching back to auto commit with a transaction still open commits it, so a failed iteration is harder to clean up than with a dedicated pool.
Source: JDBC Request (Component Reference), Transaction Controller (Component Reference), Thread Group (Component Reference) and Connection.setAutoCommit (Java SE 21 API)
Isolation levels and what each guarantees are explained in Transactions in the SQL section.
Checking JDBC results
Because an empty result is a success, add the cheapest assertion that proves the answer is useful:
-
Rows returned. A Response Assertion with Apply to: JMeter Variable Name to use =
book_id_#, Field to Test = Text Response, Not + Equals, pattern0, fails the search when no book matched. -
Row written. A Response Assertion on the Text Response of a Prepared Update Statement, Substring, pattern
1 updates, fails take copy when no copy was available. -
Duration. A Duration Assertion with
${__P(search_sla_ms,800)}works on JDBC samples exactly as on HTTP samples.
The assertion types and their costs are described in Assertions.
JDBC PreProcessor and JDBC PostProcessor
The JDBC PreProcessor runs an SQL statement just before the sampler it is attached to; the JDBC PostProcessor runs one
just after. The component reference gives their purpose: the PreProcessor for data a sample needs and that cannot be
prepared in a setUp Thread Group, the PostProcessor to put back the state a sample changed. The official demo plan
JDBC-Pre-Post-Processor.jmx creates a row with a stored procedure in a PreProcessor and deletes it in a PostProcessor.
Add → Pre Processors → JDBC PreProcessor and Add → Post Processors → JDBC PostProcessor (test classes
JDBCPreProcessor and JDBCPostProcessor). Their fields are exactly those of the JDBC Request, from Variable Name of
Pool to Handle ResultSet, with the same JMX property names, and they can set variables in the same way.
Behaviour that differs from a sampler (5.6.3 source, AbstractJDBCProcessor):
-
They produce no sample: their time is not recorded, and it does not count in the sampler’s elapsed time. A Transaction Controller includes it only when Include duration of timer and pre-post processors in generated sample is ticked.
-
Errors do not fail anything. An
SQLExceptionis logged as a warning (SQL Problem in ...) and the test goes on. Checkjmeter.logafter a run that uses them, or verify their effect with a sampler. -
They follow the usual scoping: as a child of a sampler they run for that sampler only; at Thread Group level they run around every sampler in scope, which is rarely intended.
Source: JDBC PreProcessor (Component Reference), JDBC PostProcessor (Component Reference) and Test Plan using JDBC Pre/Post Processor (demo)
Example: remove the loan that an HTTP request created
In the Bookshelf API journey, POST /api/loans creates a loan and the JSON Extractor stores its id in loanId (see
Correlation & Extractors). A JDBC PostProcessor on the last request
of the journey, GET /api/loans/{id}, deletes it again, so a long soak test does not fill the loans table and slow
every later query:
<JDBCPostProcessor guiclass="TestBeanGUI" testclass="JDBCPostProcessor" testname="Delete test loan" enabled="true">
<stringProp name="dataSource">bookshelfDb</stringProp>
<stringProp name="queryType">Prepared Update Statement</stringProp>
<stringProp name="query">DELETE FROM loans WHERE id = ? AND username = ?</stringProp>
<stringProp name="queryArguments">${loanId},${username}</stringProp>
<stringProp name="queryArgumentsTypes">BIGINT,VARCHAR</stringProp>
<stringProp name="variableNames"></stringProp>
<stringProp name="resultVariable"></stringProp>
<stringProp name="queryTimeout">5</stringProp>
<stringProp name="resultSetMaxRows"></stringProp>
<stringProp name="resultSetHandler">Store as String</stringProp>
</JDBCPostProcessor>
<hashTree/>
The username condition makes sure the statement can only delete a row of a test user. The trade-off: the delete
adds one database round trip per journey that real users do not cause, and it runs while the load test is measuring.
At the canonical 15.6 requests per second, with five requests per journey, that is at most about three extra deletes
per second. That is negligible for Bookshelf, but for a database close to its limit, prefer a single clean-up in a
tearDown Thread Group:
-- tearDown Thread Group, JDBC Request "Clean test loans", Query Type: Update Statement
DELETE FROM loans WHERE username LIKE 'reader%'
Example: feed the HTTP plan with real book ids
Instead of a hand-written list of ids that drifts from the data, a setUp Thread Group can read available books before the load starts and write them to a CSV file for the main Thread Group. Variables do not cross Thread Groups, so the file is the hand-over.
-
Add → Threads (Users) → setUp Thread Group, one thread, one loop, Action to be taken after a Sampler error: Stop Test.
-
Under it, Add → Sampler → JDBC Request "Read available books": Prepared Select Statement, SQL
SELECT id FROM books WHERE available_copies > ? ORDER BY id, values0, typesINTEGER, Result Variable NamebookRows, Limit ResultSet500. -
Under the request, Add → Post Processors → JSR223 PostProcessor (Groovy, cache compiled script ticked):
import org.apache.jmeter.services.FileServer def rows = vars.getObject('bookRows') ?: [] if (rows.isEmpty()) { prev.setSuccessful(false) prev.setResponseMessage('No available books: seed the database first') return } def base = new File(FileServer.getFileServer().getBaseDir()) def target = new File(base, 'data/generated/book-ids.csv') target.parentFile.mkdirs() target.withWriter('UTF-8') { w -> w << 'bookId\n' rows.each { w << it.get('id') << '\n' } } log.info("wrote ${rows.size()} book ids") -
In the main Thread Group, a CSV Data Set Config reads
data/generated/book-ids.csvintobookId.
The query runs once, before any measured traffic, so it costs the test nothing.
The database tutorial adapted to Bookshelf
The user manual’s Building a Database Test Plan creates 50 users that each send two SQL requests 100 times, 10,000
JDBC requests in total, against a MySQL database, and reads the results in a Summary Report. The same steps, with the
Bookshelf PostgreSQL database and the conventions of this guide, give bookshelf-db.jmx.
Step 1: add users
Select the Test Plan, Add → Threads (Users) → Thread Group, and name it JDBC Users. The tutorial uses 50 users, a
10 s ramp-up (one new user every 0.2 s) and a loop count of 100. The Bookshelf version keeps the run properties of this
guide, so the same plan serves smoke and load runs:
| Field | Tutorial | Bookshelf |
|---|---|---|
Name |
JDBC Users |
JDBC Users |
Number of Threads (users) |
50 |
|
Ramp-up period (seconds) |
10 |
|
Loop Count |
100 |
Infinite, with Specify Thread lifetime ticked |
Duration (seconds) |
(none) |
|
Step 2: add the connection and the requests
-
Select JDBC Users, Add → Config Element → JDBC Connection Configuration, and fill it in as in the Bookshelf pool example above (variable name
bookshelfDb,org.postgresql.Driver, URL built fromdb_host,db_port,db_name). Where the tutorial types a user name and password, the Bookshelf plan uses${__P(db_user,bookshelf)}and${__P(db_password,)}. -
Add → Config Element → CSV Data Set Config for
search-terms.csv(variableterm). -
Add → Sampler → JDBC Request "DB search books", as in the prepared-select example.
-
Add → Sampler → JDBC Request "DB loans of a reader": Prepared Select Statement,
SELECT id, book_id, status FROM loans WHERE username = ? ORDER BY created_at DESC LIMIT 10, values${username}fromusers.csv, typesVARCHAR. It replaces the tutorial’s second query, which differed from the first only by its parameter value.
The tutorial’s two requests are both prepared selects with one VARCHAR parameter; Bookshelf keeps that shape but
reads two different tables, so the dashboard shows the search and the loan lookup separately.
Step 3: add a listener and run
The tutorial adds a Summary Report and runs from the GUI. For a measured run, keep the GUI for a one-thread check and run the load in CLI mode, writing a results file:
# smoke: one thread, 30 s
jmeter -n -t bookshelf-db.jmx -Jthreads=1 -Jrampup=1 -Jduration=30 -Jdb_password=change-me -l results/db-smoke.jtl
# load: ten threads (the size of the API's pool), five minutes
jmeter -n -t bookshelf-db.jmx -Jthreads=10 -Jrampup=10 -Jduration=300 -Jdb_password=change-me -l results/db-load.jtl -e -o results/db-dashboard
The summariser prints lines of this form (numbers illustrative):
summary + 29870 in 00:00:28 = 1066.8/s Avg: 8 Min: 1 Max: 154 Err: 0 (0.00%) Active: 10 Started: 10 Finished: 0
summary + 32514 in 00:00:30 = 1083.8/s Avg: 9 Min: 1 Max: 201 Err: 0 (0.00%) Active: 10 Started: 10 Finished: 0
The dashboard is written to results/db-dashboard/index.html.
Worked numbers: closed loop or a fixed rate
The plan above has no timer, so each of the 10 threads sends its next query as soon as the previous one returns. With
an average of 9 ms per sample, one thread does about 1 / 0.009 ≈ 111 queries per second and ten threads about 1,100,
which is what the illustrative summariser shows: the database is driven as hard as ten connections can drive it. That
is the right shape for finding the database’s limit with a given concurrency (raise threads step by step and watch
when throughput stops growing and the average rises).
To reproduce the load the API puts on the database instead, measure the statement rates during an API test (for example from PostgreSQL’s statement statistics) and pace the JDBC plan to them with a Constant Throughput Timer. If the API test at 15.6 requests per second produced 6 searches per second, a timer with a target of 360 samples per minute, Calculate Throughput based on: all active threads in current thread group (shared), placed as a child of "DB search books", reproduces it. By Little’s Law the database then holds on average 6 × 0.009 ≈ 0.05 search statements at a time, which is why a database that looks idle under the API test can still be saturated by an unthrottled JDBC test. Timers and the throughput timers are covered in Timers & Think Time and Open Model & Throughput Control.
Cost on the load generator
JDBC samplers are cheap compared with HTTP requests with large bodies, but three things grow with the load:
-
Result text. Every sample converts the whole result set to text in memory and keeps it as the response data until the sample is processed. A
SELECT *over a large table at a few hundred samples per second can use more heap than everything else in the plan. Select only the needed columns, addLIMITin the SQL, and set Limit ResultSet, which since 5.6.3 also stops the driver from fetching extra rows. The pgjdbc documentation notes that the driver fetches all rows of a result at once by default. -
Result variables. Result Variable Name keeps every row as objects for the thread, in addition to the text, and Store as Object keeps result sets alive. Use them only for small results, and never in the measured part of a load test without a reason.
-
Connections. Each per-thread connection holds a socket and buffers on the JMeter side and a server process on the PostgreSQL side. Hundreds of threads mean hundreds of sessions, which usually tests the database’s connection handling more than its queries.
As for every protocol, do not keep the View Results Tree in a load run; it holds every response.
Source: Best Practices (User Manual), Changes (5.6.3: "Pass JDBCSampler.maxRows to Statement.setMaxRows") and Issuing a Query and Processing the Result (pgjdbc)
Pitfalls
-
Connection time in the first samples. With per-thread pools, each thread’s pool is created by its first JDBC request, so that sample includes connecting and authenticating. Preinit Pool does not help here (in the 5.6.3 source it only takes effect for a shared pool, which is created at test start). Exclude the ramp-up from the analysis, or start each thread with a warm-up
select 1request under a Once Only Controller and leave its label out of the results. -
No pool found named: '...'. The request’s pool name does not match any configuration’s Variable Name for created pool, or the configuration is disabled. Names are case-sensitive. -
Pool names that clash with variables. The pool is stored as a JMeter variable. A CSV column or User Defined Variable with the same name replaces it, and the sampler reports that the object stored under the name is not a data source.
-
CSV variables in the connection. The URL, user and password are read when the test starts, before any CSV line is read, so they can only use properties and functions.
-
Too many parameters or types. The counts of parameter values, parameter types and
?placeholders must be equal, OUT parameters included. A missing value or type fails the sample. -
Commas in values.
%${term}%without quotes splits into two parameters as soon as a term contains a comma. Quote values that come from data. -
Trailing semicolons. Some drivers reject them; the manual says not to write them.
-
Write tests that leave data behind. Inserts accumulate during long runs and change the query plans and timings the test is supposed to measure. Clean up with a PostProcessor or a tearDown Thread Group, or restore the database from a snapshot between runs.
-
Auto commit off with a shared pool. Uncommitted work stays on a connection that another thread may borrow next. Use per-thread connections for explicit transactions.
-
Silent PreProcessor and PostProcessor failures. They only log a warning.
-
Password in the plan. A literal password is saved unencrypted in the
.jmxfile and ends up in version control. Use${__P(db_password,)}.
What changed between versions
| Area | JMeter 2.x and 3.0 (the books' era) | JMeter 5.6.3, and 6.0 as planned |
|---|---|---|
Connection pool |
Excalibur datasource until 2.13; 3.0 replaced it with Apache Commons DBCP 2 and asked users to check their validation query. |
DBCP 2. 3.1 uses the driver’s |
Pool options |
Max connections, max wait, eviction, auto commit, isolation (2.6), keep-alive. |
Init SQL statements (5.1), Preinit Pool (5.2), Connection Properties (5.2.1), Pool Prepared Statements (5.5). |
Sampler |
Select, update, callable, prepared, commit, rollback, auto commit; Handle ResultSet (2.13); query timeout (2.10). |
Separate connect time and latency (3.1); drivers without query-timeout support ( |
Pre- and post-processors |
JDBC PreProcessor and PostProcessor since 2.6. |
Unchanged. |
MySQL driver |
|
5.6.3 still lists |
Runtime |
Java 7 or 8. |
5.6.3 on Java 8 or later (17 recommended). Planned for 6.0: Java 17 or later, and commons-pool2 2.13.1 among the updated libraries. |
Source: Changes history, Changes and changes.xml on master
References
-
JDBC Connection Configuration, JDBC Request, JDBC PreProcessor and JDBC PostProcessor (Component Reference)
-
setUp Thread Group, tearDown Thread Group, Transaction Controller, Constant Throughput Timer, Response Assertion, JSR223 PostProcessor and CSV Data Set Config (Component Reference)
-
Getting Started (User Manual): JDBC driver, classpath and command-line options
-
Changes, Changes history and changes.xml on master (planned 6.0 changes)
-
Apache JMeter source (5.6.3
DataSourceElement,DataSourceElementBeanInfo,JDBCSampler,AbstractJDBCTestElement,AbstractJDBCProcessor,bin/saveservice.propertiesandbin/templates/jdbc.jmx) -
Initializing the Driver and connection parameters, Issuing a Query and Processing the Result and Calling Stored Functions and Procedures (pgjdbc documentation)
-
Connections and Authentication, Client Connection Defaults, CREATE TABLE and INSERT (PostgreSQL documentation)