Appender로 대량 데이터 넣기

Appender로 대량 데이터 넣기

Appender는 DuckDB 데이터베이스에 대량의 데이터를 한꺼번에 밀어 넣을 때 쓰는 장치예요. C, C++, Go, Java, Julia, Node.js, Rust 클라이언트에서 사용할 수 있죠. 이 문서에서는 각 클라이언트에서 Appender가 어떻게 동작하는지와 최소 예시를 보여줄게요. 전체 API에 대한 자세한 내용은 아래의 Client APIs 링크를 따라가면 됩니다.

출처: 공식문서

동작 방식 (How It Works)

Appender는 하나의 연결(connection)에 묶이고, 항상 데이터베이스 파일 안의 단일 테이블에만 추가합니다. 이때 그 연결의 트랜잭션 컨텍스트를 사용해요.

성능을 위해 Appender에 추가한 값들은 실제로 DB에 들어가기 전에 캐시에 먼저 쌓입니다. 그래서 추가하는 도중에는 그 행들이 시스템에서 바로 보이지 않을 수 있어요. 캐시는 Appender가 닫히거나 스코프를 벗어나면 자동으로 비워지고, 수동으로 비울 수도 있습니다. Appender가 flush되거나 닫히고 나면 모든 데이터가 데이터베이스에 기록된 상태예요.

NOT NULL, PRIMARY KEY, UNIQUE 같은 제약조건은 행을 추가할 때마다가 아니라 캐시가 flush될 때 검사됩니다. 그래서 행이 받아들여졌다가 flush 시점에서 실패할 수도 있어요. 자세한 내용은 Handling Constraint Violations를 보세요.

Appender 만들기

각 클라이언트는 연결과 대상 테이블로 Appender를 만들고, 행을 추가한 뒤 flush합니다. 아래 예시들은 정수 컬럼과 문자열 컬럼을 가진 테이블에 데이터를 넣는 경우예요.

C++

AppendRow 함수가 데이터를 추가하는 가장 쉬운 방법입니다. 재귀 템플릿을 사용해서 한 번의 함수 호출로 한 행의 모든 값을 넣을 수 있어요.

DuckDB db;
Connection con(db);
con.Query("CREATE TABLE people (id INTEGER, name VARCHAR)");
Appender appender(con, "people");
appender.AppendRow(1, "Mark");

행을 하나씩 만들려면 BeginRow, EndRow, Append 메서드를 쓸 수도 있습니다. AppendRow가 내부적으로 이 방식으로 동작하므로 성능 특성은 동일해요.

appender.BeginRow();
appender.Append<int32_t>(2);
appender.Append<string>("Hannes");
appender.EndRow();

자세한 내용은 C++ client를 참고하세요.

Java (JDBC)

Appender는 DuckDBConnection에서 createAppender()로 만들고, 행은 beginRow(), append(), endRow()로 구성합니다. try-with-resources를 쓰면 스코프가 끝날 때 자동으로 flush되고 닫혀요.

try (var appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "tbl")) {
    appender.beginRow();
    appender.append(10);
    appender.append("hello");
    appender.endRow();
}

자세한 내용은 Java Appender API를 참고하세요.

Go

NewAppenderFromConn()에 연결을 넘겨 Appender를 얻고, AppendRow()로 행을 추가합니다.

appender, err := NewAppenderFromConn(conn, "", "test")
defer appender.Close()

err = appender.AppendRow(1, "hello")

// Optional, if you want to access the appended rows immediately.
err = appender.Flush()

자세한 내용은 Go Appender API를 참고하세요.

Rust

Connection에서 appender()로 Appender를 만들고, params! 매크로로 만든 행을 push하면 됩니다.

let mut app = conn.appender("foo")?;
app.append_row(params![1, "hello"])?;
app.flush()?;

자세한 내용은 Rust Appender API를 참고하세요.

C

자세한 내용은 C Appender API를 참고하세요.

날짜, 시간, 타임스탬프

숫자와 문자열은 말할 필요도 없지만, 날짜·시간·타임스탬프는 설명이 조금 필요해요. C++ API에서는 duckdb::Date, duckdb::Time 또는 duckdb::Timestamp가 제공하는 메서드로 직접 추가할 수 있습니다. 내부 duckdb::Value 타입으로도 추가할 수 있으나, 그 경우 오버헤드가 늘어나므로 가능하면 피하는 게 좋아요.

짧은 예시를 보여드릴게요.

con.Query("CREATE TABLE dates (d DATE, t TIME, ts TIMESTAMP)");
Appender appender(con, "dates");

// construct the values using the Date/Time/Timestamp types
// (this is the most efficient approach)
appender.AppendRow(
    Date::FromDate(1992, 1, 1),
    Time::FromTime(1, 1, 1, 0),
    Timestamp::FromDatetime(Date::FromDate(1992, 1, 1), Time::FromTime(1, 1, 1, 0))
);
// construct duckdb::Value objects
appender.AppendRow(
    Value::DATE(1992, 1, 1),
    Value::TIME(1, 1, 1, 0),
    Value::TIMESTAMP(1992, 1, 1, 1, 1, 1, 0)
);

커밋 주기 (Commit Frequency)

기본적으로 Appender는 204,800행마다 커밋합니다. 이 값을 바꾸고 싶다면 transactions을 명시적으로 사용해서, 추가할 행 배치를 BEGIN TRANSACTIONCOMMIT 문으로 감싸면 돼요.

제약조건 위반 처리

Appender가 PRIMARY KEY 충돌이나 UNIQUE 제약조건 위반을 만나면 실패하고 다음 오류를 반환합니다.

Constraint Error:
PRIMARY KEY or UNIQUE constraint violated: duplicate key "..."

이 경우 전체 추가 연산이 실패하고 어떤 행도 삽입되지 않습니다.

Client APIs

각 클라이언트 문서에는 전체 Appender API가 담겨 있습니다. 비기본 스키마·카탈로그용 생성자와 클라이언트별 기능도 포함돼요.

더 알아보기 (Learn more)

  • DuckDB Appender는 대량 적재(bulk load) 전용이에요. 일반적인 INSERT 문보다 훨씬 빠르게 데이터를 넣을 수 있지만, 행 단위 검증은 flush 시점에 이뤄진다는 점을 기억하세요.
  • 다양한 클라이언트 언어의 API 문서를 함께 보면 세부 옵션을 파악하기 좋습니다.