ResultSetMetaData
ResultSetMetaData is a JDBC interface that describes the structure of a ResultSet — how many columns it has, what each column is named, what SQL type it holds, whether values can be NULL, and more. It is especially powerful when you do not know the query shape ahead of time, such as when building a generic database browser, a CSV exporter, or a dynamic reporting tool.
Getting a ResultSetMetaData Object
Every ResultSet carries metadata. Call getMetaData() right after executing your query:
import java.sql.*;
public class MetaDataBasic {
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, grade FROM students")) {
ResultSetMetaData meta = rs.getMetaData();
int columnCount = meta.getColumnCount();
System.out.println("Number of columns: " + columnCount);
for (int i = 1; i <= columnCount; i++) {
System.out.printf("Column %d: %-15s type: %s%n",
i,
meta.getColumnName(i),
meta.getColumnTypeName(i));
}
}
}
}
Output:
Number of columns: 3
Column 1: id type: INT
Column 2: name type: VARCHAR
Column 3: grade type: CHAR
Note: Column indices in
ResultSetMetaDataare 1-based, just likeResultSetgetters.
Core Methods at a Glance
Here are the methods you will reach for most often:
| Method | Returns | What it tells you |
|---|---|---|
getColumnCount() | int | Total number of columns in the result |
getColumnName(i) | String | The column name as defined in the table |
getColumnLabel(i) | String | Alias from AS clause, or column name if none |
getColumnTypeName(i) | String | Database-specific type name (VARCHAR, INT, …) |
getColumnType(i) | int | JDBC type code from java.sql.Types |
getColumnDisplaySize(i) | int | Max characters needed to display the value |
getPrecision(i) | int | Decimal digits (numeric types) or char length |
getScale(i) | int | Digits to the right of the decimal point |
getTableName(i) | String | Table the column came from (driver-dependent) |
getSchemaName(i) | String | Schema name (driver-dependent) |
getCatalogName(i) | String | Catalog/database name (driver-dependent) |
isNullable(i) | int | 0 = no null, 1 = nullable, 2 = unknown |
isAutoIncrement(i) | boolean | Auto-increment / identity column? |
isCaseSensitive(i) | boolean | Is string comparison case-sensitive? |
isReadOnly(i) | boolean | Cannot be updated through the ResultSet? |
isWritable(i) | boolean | May be updated through the ResultSet? |
isSigned(i) | boolean | Holds signed numeric values? |
getColumnName vs getColumnLabel
These two trip people up when using AS aliases in SQL:
ResultSet rs = stmt.executeQuery(
"SELECT id AS student_id, name AS full_name FROM students");
ResultSetMetaData meta = rs.getMetaData();
System.out.println(meta.getColumnName(1)); // id (original column)
System.out.println(meta.getColumnLabel(1)); // student_id (alias)
Output:
id
student_id
Tip: Always prefer
getColumnLabel()when building UI headers or CSV files — it respects anyASaliases your query defines, which is what the end user expects to see.
Checking Nullability
The isNullable(i) method returns one of three integer constants defined in ResultSetMetaData itself:
import java.sql.*;
public class NullabilityCheck {
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, grade FROM students")) {
ResultSetMetaData meta = rs.getMetaData();
for (int i = 1; i <= meta.getColumnCount(); i++) {
int nullable = meta.isNullable(i);
String status = switch (nullable) {
case ResultSetMetaData.columnNoNulls -> "NOT NULL";
case ResultSetMetaData.columnNullable -> "NULLABLE";
case ResultSetMetaData.columnNullableUnknown -> "UNKNOWN";
default -> "?";
};
System.out.println(meta.getColumnLabel(i) + " => " + status);
}
}
}
}
Output:
id => NOT NULL
name => NULLABLE
grade => NULLABLE
Real-World Use: Generic ResultSet Printer
Here is a practical utility that prints any query result as a neatly aligned table — without knowing the schema in advance:
import java.sql.*;
public class GenericResultPrinter {
public static void printResult(ResultSet rs) throws SQLException {
ResultSetMetaData meta = rs.getMetaData();
int cols = meta.getColumnCount();
// Print header row
for (int i = 1; i <= cols; i++) {
System.out.printf("%-20s", meta.getColumnLabel(i));
}
System.out.println();
System.out.println("-".repeat(20 * cols));
// Print data rows
while (rs.next()) {
for (int i = 1; i <= cols; i++) {
Object val = rs.getObject(i);
System.out.printf("%-20s", val == null ? "NULL" : val);
}
System.out.println();
}
}
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, grade FROM students")) {
printResult(rs);
}
}
}
Output:
id name grade
------------------------------------------------------------
1 Alice A
2 Bob B
3 Carol A
Because printResult is driven entirely by ResultSetMetaData, it works with any query you pass — tables, joins, views, stored procedure outputs.
Mapping JDBC Types to Java Types
getColumnType(i) returns an integer constant from java.sql.Types. You can use it to pick the right getter at runtime:
import java.sql.*;
public class TypeMapper {
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, score FROM students")) {
ResultSetMetaData meta = rs.getMetaData();
while (rs.next()) {
for (int i = 1; i <= meta.getColumnCount(); i++) {
int type = meta.getColumnType(i);
Object value = switch (type) {
case Types.INTEGER, Types.SMALLINT, Types.TINYINT -> rs.getInt(i);
case Types.BIGINT -> rs.getLong(i);
case Types.DOUBLE, Types.FLOAT -> rs.getDouble(i);
case Types.DECIMAL, Types.NUMERIC -> rs.getBigDecimal(i);
case Types.BOOLEAN -> rs.getBoolean(i);
default -> rs.getString(i);
};
System.out.printf("%s = %s ", meta.getColumnLabel(i), value);
}
System.out.println();
}
}
}
}
This pattern is the foundation of ORMs, query builders, and testing frameworks that need to handle arbitrary schemas.
Building a CSV Exporter
ResultSetMetaData makes writing a generic CSV exporter straightforward:
import java.sql.*;
import java.io.*;
public class CsvExporter {
public static void exportToCsv(ResultSet rs, PrintWriter writer) throws SQLException {
ResultSetMetaData meta = rs.getMetaData();
int cols = meta.getColumnCount();
// Write header
for (int i = 1; i <= cols; i++) {
writer.print(meta.getColumnLabel(i));
if (i < cols) writer.print(",");
}
writer.println();
// Write rows
while (rs.next()) {
for (int i = 1; i <= cols; i++) {
String val = rs.getString(i);
writer.print(val == null ? "" : val.replace(",", ";"));
if (i < cols) writer.print(",");
}
writer.println();
}
}
}
Pass any ResultSet and a PrintWriter wrapping your output file, and this method handles the rest — headers and all.
Under the Hood
When your JDBC driver executes a query, the database server sends back a result set descriptor before the first data row. This descriptor lists every column: its name, SQL type code, precision, scale, nullability flag, table origin, and more. The driver wraps this wire-level descriptor in an object that implements ResultSetMetaData.
Key points for experienced readers:
- No extra round-trip. The metadata arrives with the query response. Calling
rs.getMetaData()is essentially free — it reads from in-memory data the driver already received. - Driver quality matters. Methods like
getTableName(),getSchemaName(), andgetCatalogName()are marked optional in the JDBC spec. Some drivers return an empty string; others return accurate data. Always test with your specific driver. getColumnType()vsgetColumnTypeName(). The integer code fromTypesis portable across databases. The string fromgetColumnTypeName()is database-specific — MySQL returns"INT", PostgreSQL returns"int4"for the same concept. Use theTypesconstant when you need cross-database logic.- Prepared statement metadata. You can also call
PreparedStatement.getMetaData()before executing, if the driver supports it. This lets you inspect expected result columns at preparation time, not just at execution time. ParameterMetaData. For PreparedStatement inputs (the?parameters), there is a sibling interfaceParameterMetaDataretrieved viaps.getParameterMetaData(). It tells you the type and nullability of each input parameter, which is useful for generic query-building frameworks.
Warning: Never cache a
ResultSetMetaDatareference after closing theResultSet. The underlying data may be released, and the reference becomes invalid.
Related Topics
- ResultSet — navigate and read the actual data rows;
ResultSetMetaDatadescribes their shape - DatabaseMetaData — database-level metadata: tables, schemas, supported features, and driver capabilities
- PreparedStatement — execute parameterised SQL safely; exposes
getMetaData()andgetParameterMetaData() - Statement — the base interface for executing queries that produce a
ResultSet - JDBC Steps — the end-to-end process for connecting and querying a database
- Reflection API — another layer of runtime introspection, this time for Java classes rather than SQL schemas