ResultSet
Every time you run a SELECT statement through JDBC, the database hands you back a ResultSet — a table-shaped object you can walk through row by row to read the returned data. Understanding ResultSet well is the difference between fragile database code and confident, production-ready queries.
What Is a ResultSet?
ResultSet is an interface in the java.sql package. It represents the result of a SQL query and maintains an internal cursor that points to the current row. Initially the cursor sits before the first row; you call next() to move it forward one row at a time.
import java.sql.*;
public class BasicResultSet {
public static void main(String[] args) throws Exception {
String url = "jdbc:mysql://localhost:3306/school";
try (Connection conn = DriverManager.getConnection(url, "root", "password");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT id, name, grade FROM students")) {
while (rs.next()) { // move cursor to next row
int id = rs.getInt("id");
String name = rs.getString("name");
double grade = rs.getDouble("grade");
System.out.println(id + " | " + name + " | " + grade);
}
}
}
}
Output:
1 | Alice | 92.5
2 | Bob | 87.0
3 | Carol | 95.0
Tip: Always use try-with-resources so the
ResultSet,Statement, andConnectionare closed automatically — even if an exception is thrown.
Reading Column Values
ResultSet provides typed getter methods for every major SQL type. You can identify columns by name (preferred for readability) or by index (1-based, slightly faster).
| Java type | Getter by name | Getter by index |
|---|---|---|
int | getInt("col") | getInt(1) |
long | getLong("col") | getLong(2) |
double | getDouble("col") | getDouble(3) |
String | getString("col") | getString(4) |
boolean | getBoolean("col") | getBoolean(5) |
Date | getDate("col") | getDate(6) |
Timestamp | getTimestamp("col") | getTimestamp(7) |
BigDecimal | getBigDecimal("col") | getBigDecimal(8) |
byte[] | getBytes("col") | getBytes(9) |
Object | getObject("col") | getObject(10) |
// Reading mixed types
while (rs.next()) {
String name = rs.getString("name");
Date enrolled = rs.getDate("enrolled_on"); // java.sql.Date
boolean active = rs.getBoolean("is_active");
System.out.println(name + " enrolled " + enrolled + " active=" + active);
}
Handling NULL Values
SQL columns can be NULL. If you call getInt() on a NULL column you get 0 — not an exception. Always check wasNull() right after if that matters:
int score = rs.getInt("score");
if (rs.wasNull()) {
System.out.println("Score not recorded yet");
} else {
System.out.println("Score: " + score);
}
Warning:
wasNull()only reports on the column read by the immediately preceding getter call. Read one column, check immediately, then read the next.
ResultSet Types
When you create a Statement or PreparedStatement, you can request a specific ResultSet type:
Statement stmt = conn.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE, // scrollable
ResultSet.CONCUR_READ_ONLY // read-only
);
| Type constant | Cursor movement | Sees live changes? |
|---|---|---|
TYPE_FORWARD_ONLY (default) | Forward only | Driver-dependent |
TYPE_SCROLL_INSENSITIVE | Any direction | No — snapshot |
TYPE_SCROLL_SENSITIVE | Any direction | Yes — live |
Scrollable ResultSet
With a scrollable type you can move the cursor freely:
ResultSet rs = stmt.executeQuery("SELECT * FROM students ORDER BY name");
rs.last();
System.out.println("Total rows: " + rs.getRow()); // last row number
rs.first(); // jump to first row
System.out.println(rs.getString("name"));
rs.absolute(3); // jump to row 3
System.out.println(rs.getString("name"));
rs.relative(-1); // move one row back
System.out.println(rs.getString("name"));
| Navigation method | Description |
|---|---|
next() | Move to next row; returns false at end |
previous() | Move to previous row (scrollable only) |
first() | Jump to first row |
last() | Jump to last row |
absolute(n) | Jump to row n (1-based; negative counts from end) |
relative(n) | Move n rows from current position |
beforeFirst() | Move before the first row |
afterLast() | Move after the last row |
getRow() | Current row number (0 = not on a row) |
Note: Not all JDBC drivers fully support scrollable result sets, especially with
TYPE_SCROLL_SENSITIVE. Always check your driver’s documentation.
Updatable ResultSet
Using CONCUR_UPDATABLE, you can modify rows directly through the ResultSet without writing a separate UPDATE statement:
Statement stmt = conn.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATABLE
);
ResultSet rs = stmt.executeQuery("SELECT id, grade FROM students WHERE id = 1");
if (rs.next()) {
rs.updateDouble("grade", 98.5); // stage the change
rs.updateRow(); // push to database
System.out.println("Grade updated.");
}
You can also insert a new row:
rs.moveToInsertRow(); // move to special insert buffer
rs.updateString("name", "Dave");
rs.updateDouble("grade", 88.0);
rs.insertRow(); // insert into the database
rs.moveToCurrentRow(); // move back to where you were
And delete the current row:
rs.next();
rs.deleteRow();
Warning: Updatable
ResultSetrequires the query to select from a single table and include the primary key. Complex joins often force the driver to fall back to read-only mode.
Reading ResultSet Metadata
ResultSetMetaData tells you everything about the columns at runtime — column count, names, types, and more. This is especially useful when you don’t know the query shape ahead of time.
ResultSetMetaData meta = rs.getMetaData();
int cols = meta.getColumnCount();
for (int i = 1; i <= cols; i++) {
System.out.printf("%-20s %-15s%n",
meta.getColumnName(i),
meta.getColumnTypeName(i));
}
Output:
id INT
name VARCHAR
grade DOUBLE
See the dedicated ResultSetMetaData page for deeper coverage.
Practical Patterns
Mapping Rows to Objects
A clean pattern is to map each row to a plain Java object inside the loop:
List<Student> students = new ArrayList<>();
while (rs.next()) {
Student s = new Student(
rs.getInt("id"),
rs.getString("name"),
rs.getDouble("grade")
);
students.add(s);
}
Counting Rows Without SELECT COUNT
With a scrollable ResultSet you can count without a separate query:
rs.last();
int rowCount = rs.getRow();
rs.beforeFirst(); // reset cursor before iterating
Limiting Results in Code
If you can’t add LIMIT to the SQL (e.g., the query is fixed), call setMaxRows on the Statement before executing:
stmt.setMaxRows(50); // driver discards anything beyond row 50
ResultSet rs = stmt.executeQuery("SELECT * FROM logs");
Under the Hood
When you call executeQuery(), the JDBC driver sends SQL to the database server. The server executes the query and returns rows over the network. The ResultSet object on the Java side is a client-side cursor — rows are typically buffered in memory as you fetch them.
- Fetch size controls how many rows the driver retrieves per network round-trip. The default is driver-specific (often 10–100). For large result sets call
rs.setFetchSize(500)to reduce round-trips at the cost of more memory per batch. - Streaming mode (MySQL:
stmt.setFetchSize(Integer.MIN_VALUE)) tells the driver to stream rows one at a time, avoiding an out-of-memory error on huge tables — but it keeps the connection busy until theResultSetis closed. TYPE_FORWARD_ONLYis cheapest: the driver can discard rows from memory once the cursor passes them.TYPE_SCROLL_INSENSITIVEfetches all rows into memory up front, making scrolling fast but potentially expensive for large queries.- Internally, column access by index (
getInt(1)) skips a string lookup and is marginally faster than by name — relevant only in tight loops processing millions of rows.
Tip: Always close
ResultSetobjects as soon as you are done. An openResultSetholds a cursor on the server, consuming resources. Using try-with-resources makes this automatic.
Common Mistakes
- Calling a getter before
next()— you’ll get aSQLExceptionbecause the cursor hasn’t moved to any row yet. - Accessing a column by a name that doesn’t match the alias in the query. Use the SQL alias:
SELECT first_name AS name→rs.getString("name"). - Keeping a
ResultSetopen across long-running business logic — tie up server cursors and connection pool slots unnecessarily. - Forgetting that
getDate()returnsjava.sql.Date, notjava.util.Date. UsetoLocalDate()to convert for modern code.
Related Topics
- Statement — execute plain SQL queries that return a ResultSet
- PreparedStatement — parameterised queries and safer SQL execution
- ResultSetMetaData — inspect column names, types, and table metadata at runtime
- CallableStatement — call stored procedures that return result sets
- Transaction Management — controlling commits and rollbacks around your queries
- JDBC Steps — the full connection-to-close workflow in one place