JDBC & Databases

Executing Statements

15 min Lesson 3 of 13

Executing Statements

Once you hold a live Connection, the next step is to send SQL to the database. JDBC gives you the Statement interface for that job and two primary execution methods: executeQuery for reads and executeUpdate for writes. Understanding the difference — and knowing when each applies — is the foundation of every database interaction you will write.

Creating a Statement

A Statement is created from the connection object. It is also a resource that must be closed after use, so use try-with-resources:

import java.sql.Connection; import java.sql.DriverManager; import java.sql.Statement; public class StatementDemo { public static void main(String[] args) throws Exception { String url = "jdbc:mysql://localhost:3306/shop"; try (Connection conn = DriverManager.getConnection(url, "root", "secret"); Statement stmt = conn.createStatement()) { // use stmt here } // conn and stmt are both closed automatically } }
Statement is not thread-safe. Never share a single Statement instance across threads. Each thread should create its own from its own Connection.

executeQuery — Reading Data

executeQuery(String sql) is for SQL statements that return a result set — almost always a SELECT. It returns a ResultSet that you iterate over to read rows. The method throws SQLException if the SQL is malformed or the database rejects it, and it also throws if you accidentally pass a DML statement (an UPDATE, for instance) instead of a query.

import java.sql.*; public class QueryDemo { public static void main(String[] args) throws Exception { String url = "jdbc:mysql://localhost:3306/shop"; String sql = "SELECT id, name, price FROM products WHERE price < 100"; try (Connection conn = DriverManager.getConnection(url, "root", "secret"); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { while (rs.next()) { int id = rs.getInt("id"); String name = rs.getString("name"); double price = rs.getDouble("price"); System.out.printf("%-3d %-20s %.2f%n", id, name, price); } } } }

Key points about executeQuery:

  • Returns a ResultSet — never null, but may be empty (zero rows).
  • The ResultSet cursor starts before the first row; call rs.next() to advance.
  • The ResultSet is also a resource — wrap it in try-with-resources or close it explicitly.
  • Closing the Statement closes the associated ResultSet automatically.

executeUpdate — Writing Data

executeUpdate(String sql) is for any statement that modifies data or structure: INSERT, UPDATE, DELETE, CREATE TABLE, DROP TABLE, and so on. It returns an int — the update count, meaning the number of rows affected. For DDL statements (like CREATE) the return value is always 0.

import java.sql.*; public class UpdateDemo { public static void main(String[] args) throws Exception { String url = "jdbc:mysql://localhost:3306/shop"; try (Connection conn = DriverManager.getConnection(url, "root", "secret"); Statement stmt = conn.createStatement()) { // INSERT — returns 1 if one row was inserted int inserted = stmt.executeUpdate( "INSERT INTO products (name, price) VALUES ('Widget', 9.99)" ); System.out.println("Rows inserted: " + inserted); // UPDATE — returns the number of matching rows that were changed int updated = stmt.executeUpdate( "UPDATE products SET price = 8.99 WHERE name = 'Widget'" ); System.out.println("Rows updated: " + updated); // DELETE — returns the number of deleted rows int deleted = stmt.executeUpdate( "DELETE FROM products WHERE price < 1.00" ); System.out.println("Rows deleted: " + deleted); } } }
Check the return value of executeUpdate. If you expected to update one row but the count is 0, your WHERE clause matched nothing. Ignoring the return value silently swallows that logic error.

The execute Method — the Universal Fallback

There is a third method, execute(String sql), that handles any SQL statement regardless of whether it returns rows. It returns a boolean: true means the first result is a ResultSet, false means it is an update count. You then call stmt.getResultSet() or stmt.getUpdateCount() accordingly.

boolean hasResultSet = stmt.execute("SELECT 1"); if (hasResultSet) { try (ResultSet rs = stmt.getResultSet()) { // process rows } } else { int count = stmt.getUpdateCount(); System.out.println("Affected rows: " + count); }

In practice execute is used when you are running dynamic or user-supplied SQL whose type you do not know at compile time — for example, a database admin tool that accepts arbitrary queries. For regular application code, prefer executeQuery or executeUpdate because the intent is explicit and the compiler will catch you if you use the wrong one.

Why Not Mix Them?

Calling executeQuery with a non-SELECT statement, or executeUpdate with a SELECT, results in a SQLException at runtime (the exact behaviour is driver-specific, but relying on it is wrong). The distinction also communicates intent to every developer who reads your code: this branch reads, that branch writes. It pairs well with connection pooling and read-replica routing, where reads can go to a replica and writes must go to the primary.

Never build SQL strings by concatenating user input. The code in this lesson uses literals to stay focused on the API. In the next lesson you will learn PreparedStatement, which prevents SQL injection and should be used for any query that involves external data.

Retrieving Auto-Generated Keys

When you INSERT into a table with an auto-increment primary key you often need the generated ID immediately. Pass the flag Statement.RETURN_GENERATED_KEYS to executeUpdate, then read the key from a special ResultSet:

int rows = stmt.executeUpdate( "INSERT INTO products (name, price) VALUES ('Gadget', 29.99)", Statement.RETURN_GENERATED_KEYS ); try (ResultSet keys = stmt.getGeneratedKeys()) { if (keys.next()) { long newId = keys.getLong(1); System.out.println("New product ID: " + newId); } }

Summary

  • executeQuery — SELECT statements; returns a ResultSet.
  • executeUpdate — INSERT / UPDATE / DELETE / DDL; returns a row count.
  • execute — universal fallback when the SQL type is not known at compile time.
  • Always close Statement and ResultSet with try-with-resources.
  • Use RETURN_GENERATED_KEYS to retrieve auto-increment IDs after an INSERT.
  • Move to PreparedStatement the moment user data enters the SQL — never concatenate.