Statement
The Statement interface is your basic tool for sending raw SQL to a database once a Connection is open. It handles queries, inserts, updates, deletes, and even multi-statement batches — all with a clean, straightforward API.
What Is a Statement?
java.sql.Statement is an interface in the java.sql package. You obtain an instance from a live Connection, supply your SQL as a plain String, and call one of its execute* methods. The driver compiles and sends the SQL to the database on each call.
Connection conn = DriverManager.getConnection(url, user, password);
Statement stmt = conn.createStatement();
Note:
Statementsends SQL as-is, every single time you call it. For repeated or parameterised SQL, prefer PreparedStatement — it is safer and faster.
The Three Execute Methods
Statement offers three distinct execution methods, each suited to a different kind of SQL.
| Method | Use for | Returns |
|---|---|---|
executeQuery(sql) | SELECT statements | ResultSet |
executeUpdate(sql) | INSERT, UPDATE, DELETE, DDL | int (rows affected) |
execute(sql) | Unknown or mixed SQL | boolean |
executeQuery — Reading Data
Use executeQuery whenever you expect rows back. It returns a ResultSet you iterate with next().
import java.sql.*;
public class ReadExample {
public static void main(String[] args) throws Exception {
String url = "jdbc:mysql://localhost:3306/school";
try (Connection conn = DriverManager.getConnection(url, "root", "secret");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT id, name FROM students")) {
while (rs.next()) {
System.out.println(rs.getInt("id") + " -> " + rs.getString("name"));
}
}
}
}
Output:
1 -> Alice
2 -> Bob
3 -> Carol
The try-with-resources block guarantees that ResultSet, Statement, and Connection are all closed in reverse order, even if an exception is thrown.
executeUpdate — Writing Data
executeUpdate is for any SQL that modifies the database. It returns the number of rows the statement affected, or 0 for DDL statements like CREATE TABLE.
import java.sql.*;
public class WriteExample {
public static void main(String[] args) throws Exception {
String url = "jdbc:mysql://localhost:3306/school";
try (Connection conn = DriverManager.getConnection(url, "root", "secret");
Statement stmt = conn.createStatement()) {
int inserted = stmt.executeUpdate(
"INSERT INTO students (name, grade) VALUES ('Diana', 'A')"
);
System.out.println("Rows inserted: " + inserted);
int updated = stmt.executeUpdate(
"UPDATE students SET grade = 'B' WHERE name = 'Bob'"
);
System.out.println("Rows updated: " + updated);
}
}
}
Output:
Rows inserted: 1
Rows updated: 1
execute — When You Are Not Sure
execute returns true if the SQL produced a ResultSet, or false if it produced an update count. You then call getResultSet() or getUpdateCount() to retrieve the result. This is most useful for dynamic SQL tooling, not everyday application code.
boolean hasResultSet = stmt.execute("SELECT * FROM students");
if (hasResultSet) {
ResultSet rs = stmt.getResultSet();
// process rows...
} else {
int count = stmt.getUpdateCount();
System.out.println("Rows affected: " + count);
}
Useful Statement Options
You can tune Statement behaviour before executing SQL.
Query Timeout
Prevent a long-running query from hanging your application:
stmt.setQueryTimeout(10); // cancel if not done within 10 seconds
The driver throws a SQLTimeoutException if the limit is reached.
Max Rows
Cap how many rows the driver fetches, useful for previewing large tables:
stmt.setMaxRows(100); // stop after 100 rows
ResultSet rs = stmt.executeQuery("SELECT * FROM logs ORDER BY ts DESC");
Fetch Size
Hint to the driver about how many rows to pull from the database at a time (controls network round-trips):
stmt.setFetchSize(50); // fetch 50 rows per network trip
Tip: Tuning fetch size can dramatically cut memory usage when you iterate millions of rows. The default (often 10 or 0) is rarely optimal for bulk reads.
Batch Execution
Sending many individual statements in a loop is slow — each call involves a network round-trip. Batching groups them into one trip:
import java.sql.*;
public class BatchExample {
public static void main(String[] args) throws Exception {
String url = "jdbc:mysql://localhost:3306/school";
try (Connection conn = DriverManager.getConnection(url, "root", "secret");
Statement stmt = conn.createStatement()) {
conn.setAutoCommit(false); // wrap the batch in a transaction
stmt.addBatch("INSERT INTO students (name, grade) VALUES ('Eve', 'A')");
stmt.addBatch("INSERT INTO students (name, grade) VALUES ('Frank', 'B')");
stmt.addBatch("INSERT INTO students (name, grade) VALUES ('Grace', 'A')");
int[] results = stmt.executeBatch(); // sends all three at once
conn.commit();
System.out.println("Batch done, statements executed: " + results.length);
}
}
}
Output:
Batch done, statements executed: 3
Each element of the returned int[] is the update count for the corresponding statement. If any statement fails, a BatchUpdateException is thrown and conn.rollback() should be called.
Warning: Always wrap batches in a transaction (
setAutoCommit(false)+commit()/rollback()). Without a transaction, a failure midway through leaves partial data committed — which can be very hard to clean up.
Retrieving Auto-Generated Keys
When you insert a row into a table with an auto-increment primary key, you often need the generated key immediately:
int rows = stmt.executeUpdate(
"INSERT INTO students (name, grade) VALUES ('Hank', 'C')",
Statement.RETURN_GENERATED_KEYS
);
try (ResultSet keys = stmt.getGeneratedKeys()) {
if (keys.next()) {
long newId = keys.getLong(1);
System.out.println("New student ID: " + newId);
}
}
Pass Statement.RETURN_GENERATED_KEYS as the second argument. The database returns the new key in a special single-column ResultSet.
Under the Hood
When you call stmt.executeQuery(sql), the driver sends the raw SQL string over the network to the database server. The server parses it, plans an execution strategy, runs it, and streams rows back. Because this happens on every call, Statement incurs full parse-and-plan cost each time.
Contrast this with PreparedStatement, which sends the SQL once for the server to compile into a reusable execution plan. For any SQL you run more than once — especially in loops — PreparedStatement wins on both performance and safety.
Statement is also the entry point for SQL injection if you concatenate user input directly into the SQL string:
// DANGEROUS — never do this with user input
String name = request.getParameter("name");
ResultSet rs = stmt.executeQuery("SELECT * FROM users WHERE name = '" + name + "'");
// Attacker input: ' OR '1'='1 -> leaks entire table
Use PreparedStatement with parameter placeholders (?) any time external data is involved.
Statement vs PreparedStatement vs CallableStatement
| Feature | Statement | PreparedStatement | CallableStatement |
|---|---|---|---|
| SQL type | Static, no params | Parameterised SQL | Stored procedures |
| Compiled once? | No — every call | Yes — on prepare() | Yes |
| SQL injection safe? | No | Yes | Yes |
| Best for | One-off DDL, admin | App queries with params | DB stored procedures |
For everyday application queries, prefer PreparedStatement. Use plain Statement for DDL (CREATE, DROP, ALTER) and quick one-time administrative queries.
Quick Reference
// Create
Statement stmt = conn.createStatement();
// Read
ResultSet rs = stmt.executeQuery("SELECT ...");
// Write
int count = stmt.executeUpdate("INSERT / UPDATE / DELETE ...");
// Batch
stmt.addBatch("INSERT ...");
stmt.addBatch("INSERT ...");
int[] results = stmt.executeBatch();
// Options
stmt.setQueryTimeout(10); // seconds
stmt.setMaxRows(500);
stmt.setFetchSize(50);
// Auto-generated keys
stmt.executeUpdate(sql, Statement.RETURN_GENERATED_KEYS);
ResultSet keys = stmt.getGeneratedKeys();
Related Topics
- PreparedStatement — parameterised SQL that prevents injection and compiles once for better performance
- CallableStatement — execute stored procedures and retrieve OUT parameters
- ResultSet — navigate and read the rows returned by
executeQuery - Connection Interface — the object that creates
Statementinstances and manages transactions - Transaction Management — commit, rollback, and savepoints to keep your data consistent
- Batch Processing — deep dive into high-throughput bulk inserts and updates