데이터 가져오기
데이터 가져오기 (Import Data)
JDBC 드라이버에는 데이터를 DuckDB에 벌크로 넣는 두 가지 방식이 있어요. 고성능 Appender와 JDBC 배치 작성기(batch writer)예요. 이 페이지에서 둘 다와 각각 언제 쓰면 좋은지 설명해 드릴게요.
출처: 문서
본문
Appender
[Appender]({% link docs/current/data/appender.md %})는 org.duckdb.DuckDBAppender 클래스를 통해 DuckDB JDBC 드라이버에서 사용할 수 있어요. Appender는 DuckDBConnection의 createAppender() 메서드로 만들며, 테이블 이름과 선택적으로 적용할 스키마·카탈로그를 넘겨요. Appender는 close() 메서드가 호출될 때 플러시돼요.
예시:
import java.sql.DriverManager;
import org.duckdb.DuckDBConnection;
DuckDBConnection conn = (DuckDBConnection) DriverManager.getConnection("jdbc:duckdb:");
try (var stmt = conn.createStatement()) {
stmt.execute("CREATE TABLE tbl (x INTEGER, y DOUBLE, s VARCHAR)");
// using try-with-resources to automatically close the appender at the end of the scope
try (var appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "tbl")) {
appender.beginRow();
appender.append(10);
appender.append(3.2);
appender.append("hello");
appender.endRow();
appender.beginRow();
appender.append(20);
appender.append(-8.1);
appender.append("world");
appender.endRow();
}
}
Appender는 기본 스키마용 createAppender(tableName), 특정 카탈로그 대상용 createAppender(catalogName, schemaName, tableName)으로도 만들 수 있어요. 버퍼링된 행은 appender가 닫힐 때, 또는 더 일찍 flush()를 호출할 때 테이블에 쓰여요. 행 안에서 appendNull()은 NULL을, appendDefault()는 열의 DEFAULT 값을 써요.
중첩·컬렉션 타입도 행 안에서 추가돼요. STRUCT 열의 경우 beginStruct()를 호출하고 각 필드 값을 선언 순서대로 추가한 뒤 endStruct()를 호출해요. UNION 열의 경우 beginUnion(tag) — 여기서 tag는 대상 UNION 멤버 이름 — 을 호출하고 그 멤버의 값을 추가한 뒤 endUnion()을 호출해요. 다음 행은 CREATE TABLE nested_tbl (point STRUCT(x INTEGER, y INTEGER), value UNION(num INTEGER, str VARCHAR))로 선언된 테이블을 대상으로 해요.
try (var appender = conn.createAppender("nested_tbl")) {
appender.beginRow();
appender.beginStruct(); // enter the STRUCT column
appender.append(1); // field x
appender.append(2); // field y
appender.endStruct();
appender.beginUnion("str"); // select the UNION member named "str"
appender.append("hello"); // its VARCHAR value
appender.endUnion();
appender.endRow();
}
LIST 또는 ARRAY 열의 경우 Java 배열을 단일 값으로 넘겨요. 선택적으로 Boolean null 마스크를 넘겨 true인 요소를 NULL로 표시할 수 있어요.
try (var appender = conn.createAppender("list_tbl")) {
appender.beginRow();
appender.append(new int[] {1, 2, 3}); // a LIST value
appender.append(new int[] {4, 5}, new boolean[] {false, true}); // second element is NULL
appender.endRow();
}
MAP 열의 경우 Java Map을 단일 값으로 넘겨요. 각 항목은 키/값 쌍으로 쓰이고, null 맵은 SQL NULL로 추가돼요. 다음 행은 CREATE TABLE map_tbl (m MAP(VARCHAR, INTEGER))로 선언된 테이블을 대상으로 해요.
try (var appender = conn.createAppender("map_tbl")) {
appender.beginRow();
appender.append(Map.of("a", 1, "b", 2)); // a MAP value
appender.endRow();
}
Appender 성능 극대화
Appender는 이미 벌크 로드의 가장 빠른 방법이지만, 어떻게 동작하는지의 몇 가지 속성이 처리량을 결정해요.
- 행 배칭은 고정·내부적. Appender는 2,048행의 고정 크기 배치(하나의 DuckDB 벡터)로 행을 쓰고, 배치가 차자마자 자동으로 플러시해요. 설정 가능한 플러시 카운트가 없으므로 Appender 위에 애플리케이션 레벨 배치 크기를 추가해도 성능이 좋아지지 않아요.
- 복합 타입보다 원시 열을 선호하세요.
STRUCT,LIST,UNION같은 중첩 타입은 평평한 원시 열보다 값당 비용이 더 들어요. 원시 열로 만든 스키마가 가장 빠르게 추가돼요. - 행마다 Java 객체 할당을 피하세요. 값을 박싱하거나 행마다 새 객체를 만드는 것은 할당·GC 부담을 늘려요. 원시
append()오버로드를 선호하고 가능하면 버퍼를 재사용하세요. - 가능하면 문자열을 짧게 유지하세요. DuckDB는 최대 12바이트 문자열을 인라인으로 저장해요. 더 긴 문자열은 아웃오브라인으로 저장되어 값당 약간의 오버헤드를 추가해요.
단일 값 Appender (레거시)
이전 appender인 org.duckdb.DuckDBSingleValueAppender는 DuckDBConnection.createSingleValueAppender()로 만들며, beginRow()와 endRow() 사이에서 타입 있는 append() 오버로드로 한 번에 한 값을 써요. 클래스와 팩토리 메서드 둘 다 deprecated이며 하위 호환성 위해서만 유지돼요.
Warning 새 코드는 위에서 설명한
DuckDBAppender를 사용하세요. 더 빠르고 중첩·컬렉션 타입을 추가할 수 있어요.DuckDBSingleValueAppender는 기존 애플리케이션만을 위해 남아 있어요.
배치 작성기 (Batch Writer)
DuckDB JDBC 드라이버는 배치 쓰기 기능을 제공해요. 배치 작성기는 쿼리 파싱 오버헤드를 줄이기 위해 준비된 문을 지원해요.
벌크 삽입의 선호 방법은 성능이 더 높은 Appender를 사용하는 거예요. 다만 Appender를 사용할 수 없을 때 배치 작성기를 대안으로 쓸 수 있어요.
준비된 문이 있는 배치 작성기
준비된 문은 배치 작성기를 사용하는 권장 방식이에요. 문을 한 번 파싱하고 모든 행에 재사용하기 때문이에요. 각 행의 파라미터를 바인딩하고 addBatch()를 호출해 행을 대기열에 넣고, executeBatch()를 호출해 대기 중인 모든 행을 단일 작업으로 써요.
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import org.duckdb.DuckDBConnection;
DuckDBConnection conn = (DuckDBConnection) DriverManager.getConnection("jdbc:duckdb:");
PreparedStatement stmt = conn.prepareStatement("INSERT INTO test (x, y, z) VALUES (?, ?, ?);");
stmt.setObject(1, 1);
stmt.setObject(2, 2);
stmt.setObject(3, 3);
stmt.addBatch();
stmt.setObject(1, 4);
stmt.setObject(2, 5);
stmt.setObject(3, 6);
stmt.addBatch();
stmt.executeBatch();
stmt.close();
일반 문이 있는 배치 작성기
배치 작성기는 값을 인라인으로 담은 일반 SQL 문도 받아들여요(파라미터 없이). addBatch(String)으로 각 문을 대기열에 넣고 executeBatch()로 함께 제출해요. 이렇게 하면 파라미터 바인딩을 피하지만 준비된 문이 제공하는 재사용의 이점은 없어요.
import java.sql.DriverManager;
import java.sql.Statement;
import org.duckdb.DuckDBConnection;
DuckDBConnection conn = (DuckDBConnection) DriverManager.getConnection("jdbc:duckdb:");
Statement stmt = conn.createStatement();
stmt.execute("CREATE TABLE test (x INTEGER, y INTEGER, z INTEGER)");
stmt.addBatch("INSERT INTO test (x, y, z) VALUES (1, 2, 3);");
stmt.addBatch("INSERT INTO test (x, y, z) VALUES (4, 5, 6);");
stmt.executeBatch();
stmt.close();
더 알아보기 (Learn more)
- [Appender]({% link docs/current/data/appender.md %}) — Java
DuckDBAppender가 감싸는 엔진 레벨 Appender 인터페이스. - [Data Import]({% link docs/current/data/overview.md %}) — CSV, Parquet, JSON 파일에서 직접 읽는 것을 포함한 DuckDB의 다른 벌크 로딩 옵션.
- [Run Queries]({% link docs/current/clients/java/querying.md %}) — 배치 작성기가 기반으로 하는 [
INSERT]({% link docs/current/sql/statements/insert.md %})와 [CREATE TABLE]({% link docs/current/sql/statements/create_table.md %}) 문 보내기. - [Define Connections]({% link docs/current/clients/java/connecting.md %}) — Appender가 만들어지는
DuckDBConnection얻기.