쿼리 실행
쿼리 실행 (Run Queries, Java/JDBC)
개요 (Overview)
Java 애플리케이션은 익숙한 Statement, PreparedStatement, ResultSet 인터페이스를 통해 쿼리를 보내고 결과 집합을 읽어요. 이 페이지는 쿼리 전송과 DuckDB의 중첩·복합 타입 읽기를 다룬다. 이들이 실행되는 Connection을 여는 방법은 [Define Connections]({% link docs/current/clients/java/connecting.md %})를 참고해요.
출처: 문서
본문
쿼리 전송 (Sending Queries)
DuckDB는 쿼리를 보내고 결과 집합을 검색하는 표준 JDBC 메서드를 지원해요. 먼저 Connection에서 Statement 객체를 만들어야 하고, 이 객체로 execute()와 executeQuery()를 사용해 쿼리를 보낼 수 있어요. execute()는 [CREATE TABLE]({% link docs/current/sql/statements/create_table.md %})이나 [UPDATE]({% link docs/current/sql/statements/update.md %})처럼 결과가 예상되지 않는 쿼리용이고, executeQuery()는 결과를 생성하는 쿼리(예: [SELECT]({% link docs/current/sql/statements/select.md %}))용이에요. 아래에 두 예시가 있어요. JDBC Statement와 ResultSet 문서도 참고해요.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
Connection conn = DriverManager.getConnection("jdbc:duckdb:");
// 테이블 생성
Statement stmt = conn.createStatement();
stmt.execute("CREATE TABLE items (item VARCHAR, value DECIMAL(10, 2), count INTEGER)");
// 테이블에 두 항목 삽입
stmt.execute("INSERT INTO items VALUES ('jeans', 20.0, 1), ('hammer', 42.2, 2)");
try (ResultSet rs = stmt.executeQuery("SELECT * FROM items")) {
while (rs.next()) {
System.out.println(rs.getString(1));
System.out.println(rs.getInt(3));
}
}
stmt.close();
jeans
1
hammer
2
DuckDB는 JDBC API에 따라 prepared statements도 지원해요:
import java.sql.PreparedStatement;
try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO items VALUES (?, ?, ?);")) {
stmt.setString(1, "chainsaw");
stmt.setDouble(2, 500.0);
stmt.setInt(3, 42);
stmt.execute();
// execute() 추가 호출 가능
}
경고: DuckDB에 대량의 데이터를 삽입하는 데 prepared statements는 사용하지 마세요. 더 나은 옵션은 [데이터 가져오기 문서]({% link docs/current/data/overview.md %})를 참고해요.
중첩 및 복합 타입 읽기
DuckDB는 LIST, STRUCT, MAP, UNION, ENUM 같은 중첩 및 복합 컬럼 타입을 지원해요. JDBC 드라이버는 각각을 관용적인 Java 객체로 매핑해요. getObject로 ResultSet에서 값을 읽는데, 다음 타입들을 반환해요:
| DuckDB 타입 | getObject가 반환하는 Java 타입 |
|---|---|
LIST, ARRAY |
org.duckdb.DuckDBArray (구현: java.sql.Array) |
STRUCT |
org.duckdb.DuckDBStruct (구현: java.sql.Struct) |
MAP |
java.util.LinkedHashMap |
UNION |
해석된 멤버 값 |
ENUM |
String |
try (ResultSet rs = stmt.executeQuery("SELECT [1, 2, 3] AS l, {'a': 1, 'b': 2} AS s")) {
while (rs.next()) {
DuckDBArray list = (DuckDBArray) rs.getObject(1);
Object[] values = (Object[]) list.getArray();
DuckDBStruct struct = (DuckDBStruct) rs.getObject(2);
Map<String, Object> fields = struct.getMap();
}
}
DuckDB 결과 집합은 JDBC API에 대한 확장으로 getArray(int), getStruct(int), getUuid(int), getHugeint(int), getJsonObject(int)를 포함한 타입 접근자도 노출해요. DuckDBStruct.getMap()은 struct 필드를 이름으로 반환해요.
DuckDBArray.getResultSet()은 리스트 요소를 org.duckdb.DuckDBArrayResultSet으로 노출해요. INDEX(요소의 1-기반 위치)와 VALUE(요소 자체) 두 컬럼을 가진 읽기 전용 ResultSet이에요. 다른 결과 집합처럼 반복해요:
try (ResultSet rs = stmt.executeQuery("SELECT [10, 20, 30] AS l")) {
rs.next();
DuckDBArray list = (DuckDBArray) rs.getObject(1);
try (ResultSet elements = list.getResultSet()) {
while (elements.next()) {
int index = elements.getInt("INDEX"); // 1-기반 위치
int value = elements.getInt("VALUE"); // 요소
System.out.println(index + " -> " + value);
}
}
}
JSON 컬럼은 getObject(int)(및 getJsonObject(int))가 org.duckdb.JsonNode로 반환하는데, 원시 JSON 텍스트의 가벼운 래퍼예요. isArray(), isObject(), isString(), isNumber(), isBoolean(), isNull()로 타입을 테스트하고, toString()으로 JSON 원본을 복구해요:
try (ResultSet rs = stmt.executeQuery("SELECT '[1, 2, 3]'::JSON AS j")) {
rs.next();
JsonNode json = (JsonNode) rs.getObject(1);
if (json.isArray()) {
System.out.println(json); // [1, 2, 3]
}
}
중첩 및 복합 파라미터 바인딩
LIST/ARRAY, STRUCT, MAP 값을 prepared-statement 파라미터로 바인딩하려면 Connection에서 그것을 만들고 setObject()로 전달해요. 각 팩토리 메서드는 드라이버가 값을 마샬링하는 데 필요한 SQL 타입 이름을 붙여요.
createArrayOf(typeName, elements)— 표준 JDBC 메서드 —LIST또는ARRAY에 대해org.duckdb.user.DuckDBUserArray(ajava.sql.Array)를 반환.typeName은 요소 타입.createStruct(typeName, attributes)— 표준 JDBC 메서드 —STRUCT에 대해org.duckdb.user.DuckDBUserStruct(ajava.sql.Struct)를 반환.typeName은STRUCT타입이고attributes는 선언 순서의 필드 값.createMap(typeName, map)—DuckDBConnection의 DuckDB 확장 —MAP에 대해org.duckdb.user.DuckDBMap(ajava.util.Map)을 반환.typeName은MAP(VARCHAR, INTEGER)같은 전체 맵 타입.
import java.sql.Array;
import java.sql.Struct;
import java.util.Map;
import org.duckdb.DuckDBConnection;
DuckDBConnection conn = (DuckDBConnection) DriverManager.getConnection("jdbc:duckdb:");
// LIST / ARRAY
Array list = conn.createArrayOf("INTEGER", new Object[] {1, 2, 3});
try (PreparedStatement stmt = conn.prepareStatement("SELECT ?::INTEGER[]")) {
stmt.setObject(1, list);
stmt.execute();
}
// STRUCT
Struct point = conn.createStruct("STRUCT(x DOUBLE, y DOUBLE)", new Object[] {1.0, 2.0});
// MAP
Map<String, Integer> counts = conn.createMap("MAP(VARCHAR, INTEGER)", Map.of("a", 1, "b", 2));
공간 값 바인딩
[spatial 확장]({% link docs/current/core_extensions/spatial/overview.md %})은 STRUCT와 LIST 타입에서 geometry 타입을 만드므로, 같은 팩토리 메서드로 바인딩돼요. POINT_2D는 두 DOUBLE 필드의 struct이고, LINESTRING은 그러한 포인트들의 리스트예요. 확장을 로드한 후 createStruct()(또는 createArrayOf())로 값을 만들고 파라미터를 GEOMETRY로 캐스트해요. GEOMETRY 결과는 getBlob()을 통해 Well-Known Binary로 돌아와요:
try (Statement stmt = conn.createStatement()) {
stmt.execute("INSTALL spatial");
stmt.execute("LOAD spatial");
}
// POINT_2D는 STRUCT(x DOUBLE, y DOUBLE)
Struct point = conn.createStruct("POINT_2D", new Object[] {41.1, 42.2});
try (PreparedStatement stmt = conn.prepareStatement("SELECT ?::POINT_2D::GEOMETRY")) {
stmt.setObject(1, point);
try (ResultSet rs = stmt.executeQuery()) {
rs.next();
Blob wkb = rs.getBlob(1); // geometry를 Well-Known Binary로
}
}
// LINESTRING은 POINT_2D struct의 LIST
Struct p1 = conn.createStruct("POINT_2D", new Object[] {0.0, 0.0});
Struct p2 = conn.createStruct("POINT_2D", new Object[] {1.0, 1.0});
Array line = conn.createArrayOf("POINT_2D", new Object[] {p1, p2});
시간 파라미터 바인딩
날짜와 시간 파라미터는 같은 setObject() 경로를 따라요. java.time이나 java.sql 시간 값을 바인딩하면 드라이버가 그 값을 일치하는 내부 홀더에 감싸요 — java.sql.Date/LocalDate는 org.duckdb.DuckDBDate가 되고, Timestamp/LocalDateTime은 DuckDBTimestamp가, OffsetDateTime은 DuckDBTimestampTZ가, Time/LocalTime은 DuckDBTime이 되며 — 해당 DuckDB 타입으로 마샬링해요:
import java.time.LocalDate;
import java.time.LocalDateTime;
try (PreparedStatement stmt = conn.prepareStatement("SELECT ?, ?")) {
stmt.setObject(1, LocalDate.of(2024, 1, 15));
stmt.setObject(2, LocalDateTime.of(2024, 1, 15, 12, 30));
stmt.execute();
}
각 홀더는 값을 Unix 에폭(1970-01-01)에 상대적으로 유지해요. DuckDBDate는 getDaysSinceEpoch()로 전체 일수를 세고, DuckDBTimestamp, DuckDBTimestampTZ, DuckDBTime은 getMicrosEpoch()로 마이크로초를 보고해요 (DuckDBTime은 자정 이후 마이크로초):
import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.OffsetDateTime;
import org.duckdb.DuckDBDate;
import org.duckdb.DuckDBTime;
import org.duckdb.DuckDBTimestamp;
import org.duckdb.DuckDBTimestampTZ;
long days = new DuckDBDate(Date.valueOf("2024-01-15")).getDaysSinceEpoch(); // 19737
long micros = new DuckDBTimestamp(Timestamp.valueOf("2024-01-15 12:30:00")).getMicrosEpoch();
long zonedMicros = new DuckDBTimestampTZ(OffsetDateTime.parse("2024-01-15T12:30:00Z")).getMicrosEpoch();
long timeMicros = new DuckDBTime(Time.valueOf("12:30:00")).getMicrosEpoch();
메타데이터 검사 (Inspecting Metadata)
드라이버는 표준 JDBC 메타데이터 인터페이스를 구현하므로, 애플리케이션이 카탈로그 쿼리를 손으로 실행하지 않고 데이터베이스, 결과 집합, prepared statement의 파라미터를 내부검사할 수 있어요.
Connection.getMetaData()는org.duckdb.DuckDBDatabaseMetaData를 반환하는데, 데이터베이스 전반 능력을 보고하고getTables(),getColumns(),getSchemas()같은 카탈로그 객체를 DuckDB의 시스템 테이블에서 만든 결과 집합으로 노출하는java.sql.DatabaseMetaData예요.ResultSet.getMetaData()는 결과 컬럼을 설명하는org.duckdb.DuckDBResultSetMetaData를 반환해요 —getColumnCount(),getColumnName(int),getColumnType(int),getColumnTypeName(int),getPrecision(int),getScale(int).PreparedStatement.getParameterMetaData()는 statement의 파라미터(1-기반)를 설명하는org.duckdb.DuckDBParameterMetaData를 반환해요.getParameterCount(),getParameterType(int),getParameterTypeName(int)포함.
try (PreparedStatement stmt = conn.prepareStatement("SELECT * FROM items WHERE value > ?")) {
stmt.setDouble(1, 10.0);
ResultSetMetaData rsMeta = stmt.getMetaData();
for (int i = 1; i <= rsMeta.getColumnCount(); i++) {
System.out.println(rsMeta.getColumnName(i) + " : " + rsMeta.getColumnTypeName(i));
}
System.out.println("parameters: " + stmt.getParameterMetaData().getParameterCount());
}
DuckDB 확장으로서 DuckDBResultSetMetaData.getReturnType()는 statement가 무엇을 반환하는지 org.duckdb.StatementReturnType으로 보고해요 — QUERY_RESULT, CHANGED_ROWS, 또는 NOTHING:
import org.duckdb.DuckDBResultSetMetaData;
import org.duckdb.StatementReturnType;
DuckDBResultSetMetaData meta = (DuckDBResultSetMetaData) rs.getMetaData();
StatementReturnType returnType = meta.getReturnType(); // QUERY_RESULT
DECIMAL 컬럼의 경우 getPrecision(int)와 getScale(int)가 DuckDBColumnTypeMetaData에서 DuckDB가 타입에 기록한 너비와 스케일을 보고해요. items 테이블의 value DECIMAL(10, 2) 컬럼에 대해 10과 2를 반환해요:
try (ResultSet rs = stmt.executeQuery("SELECT value FROM items")) {
ResultSetMetaData rsMeta = rs.getMetaData();
int precision = rsMeta.getPrecision(1); // 10
int scale = rsMeta.getScale(1); // 2
System.out.println("DECIMAL(" + precision + ", " + scale + ")");
}
더 알아보기 (Learn more)
- [Handle Results]({% link docs/current/clients/java/result_handling.md %}) — 표준
ResultSet을 넘어서는 Apache Arrow 교환, 결과 스트리밍, 청크 결과. - [Import Data]({% link docs/current/clients/java/data_import.md %}) — 벌크 삽입에 prepared statements의 권장 대안인 Appender와 batch writer.
- [Prepared Statements]({% link docs/current/sql/query_syntax/prepared_statements.md %}) — 여기서 사용한 파라미터화 쿼리에 대한 DuckDB의 SQL 수준 지원.
- [Define Connections]({% link docs/current/clients/java/connecting.md %}) — 이 구문들이 실행되는
Connection열기.