Skip to content
Java jdbc 6 min read

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, and Connection are 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 typeGetter by nameGetter by index
intgetInt("col")getInt(1)
longgetLong("col")getLong(2)
doublegetDouble("col")getDouble(3)
StringgetString("col")getString(4)
booleangetBoolean("col")getBoolean(5)
DategetDate("col")getDate(6)
TimestampgetTimestamp("col")getTimestamp(7)
BigDecimalgetBigDecimal("col")getBigDecimal(8)
byte[]getBytes("col")getBytes(9)
ObjectgetObject("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 constantCursor movementSees live changes?
TYPE_FORWARD_ONLY (default)Forward onlyDriver-dependent
TYPE_SCROLL_INSENSITIVEAny directionNo — snapshot
TYPE_SCROLL_SENSITIVEAny directionYes — 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 methodDescription
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 ResultSet requires 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 the ResultSet is closed.
  • TYPE_FORWARD_ONLY is cheapest: the driver can discard rows from memory once the cursor passes them.
  • TYPE_SCROLL_INSENSITIVE fetches 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 ResultSet objects as soon as you are done. An open ResultSet holds a cursor on the server, consuming resources. Using try-with-resources makes this automatic.

Common Mistakes

  • Calling a getter before next() — you’ll get a SQLException because 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 namers.getString("name").
  • Keeping a ResultSet open across long-running business logic — tie up server cursors and connection pool slots unnecessarily.
  • Forgetting that getDate() returns java.sql.Date, not java.util.Date. Use toLocalDate() to convert for modern code.
Last updated June 13, 2026
Was this helpful?