쿼리 실행

쿼리 실행 (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 StatementResultSet 문서도 참고해요.

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 객체로 매핑해요. getObjectResultSet에서 값을 읽는데, 다음 타입들을 반환해요:

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(a java.sql.Array)를 반환. typeName은 요소 타입.
  • createStruct(typeName, attributes) — 표준 JDBC 메서드 — STRUCT에 대해 org.duckdb.user.DuckDBUserStruct(a java.sql.Struct)를 반환. typeNameSTRUCT 타입이고 attributes는 선언 순서의 필드 값.
  • createMap(typeName, map)DuckDBConnection의 DuckDB 확장 — MAP에 대해 org.duckdb.user.DuckDBMap(a java.util.Map)을 반환. typeNameMAP(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 %})은 STRUCTLIST 타입에서 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/LocalDateorg.duckdb.DuckDBDate가 되고, Timestamp/LocalDateTimeDuckDBTimestamp가, OffsetDateTimeDuckDBTimestampTZ가, Time/LocalTimeDuckDBTime이 되며 — 해당 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)에 상대적으로 유지해요. DuckDBDategetDaysSinceEpoch()로 전체 일수를 세고, DuckDBTimestamp, DuckDBTimestampTZ, DuckDBTimegetMicrosEpoch()로 마이크로초를 보고해요 (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) 컬럼에 대해 102를 반환해요:

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 열기.