쿼리 실행
쿼리 실행 (Run Queries, Go)
개요 (Overview)
Go 클라이언트는 database/sql을 구현하므로 쿼리는 표준 메서드를 통해 실행돼요:
Exec와ExecContext는INSERT나 DDL처럼 행을 반환하지 않는 구문을 보내고, 영향을 받은 행 수를 보고해요.Query,QueryContext,QueryRow,QueryRowContext는 행을 반환하는 구문을 실행하고*sql.Rows(또는 단일*sql.Row)를 돌려주어 Go 값으로 스캔할 수 있게 해줘요.
Context 변형은 첫 인자로 context.Context를 받는데, 쿼리를 취소하거나 타임아웃할 수 있게 해주므로 권장 형태예요. 아래 섹션이 구문 전송, 파라미터 바인딩, prepared statements, 트랜잭션, 결과 스캔을 다룬다. 이들이 실행되는 데이터베이스를 여는 방법은 [Connect]({% link docs/current/clients/go/connecting.md %})를 참고해요.
출처: 문서
본문
구문 전송 (Sending Statements)
행을 반환하지 않는 구문(예: [INSERT]({% link docs/current/sql/statements/insert.md %})나 [UPDATE]({% link docs/current/sql/statements/update.md %}))에는 Exec(또는 ExecContext)를 사용해요. sql.Result를 반환하며, 그 RowsAffected()가 몇 행이 변경되었는지 보고해요:
_, err := db.ExecContext(ctx, `CREATE TABLE users (name VARCHAR, age INTEGER)`)
if err != nil {
log.Fatal(err)
}
res, err := db.ExecContext(ctx, `INSERT INTO users VALUES ('marc', 99)`)
if err != nil {
log.Fatal(err)
}
n, _ := res.RowsAffected()
log.Printf("inserted %d rows", n)
파라미터 바인딩 (Binding Parameters)
값은 SQL 문자열에 포맷되는 대신 구문의 플레이스홀더에 바인딩되어, SQL 인젝션을 피하고 DuckDB가 플랜을 재사용하게 해줘요. DuckDB는 위치(?)와 번호($1, $2) 플레이스홀더를 받아요. 값은 순서대로 후행 인자로 전달해요:
rows, err := db.QueryContext(ctx, `
SELECT name, age
FROM users
WHERE (name = ? OR name = ?) AND age > ?`,
"macgyver", "marc", 30,
)
$name 같은 명명된 플레이스홀더에는 각 인자를 sql.Named로 감싸요:
row := db.QueryRowContext(ctx,
"SELECT $age >= 18 AND $name = 'Alice'",
sql.Named("age", minAge),
sql.Named("name", name),
)
경고: DuckDB에 대량의 데이터를 삽입하는 데 prepared statements는 사용하지 마세요. 벌크 삽입에 훨씬 빠른 Appender는 [Import Data]({% link docs/current/clients/go/data_import.md %})를 참고해요.
파라미터 타입 강제
클라이언트는 바인딩된 값의 DuckDB 타입을 Go 타입에서 추론해요. 그 추론이 원하는 것이 아닐 때 duckdb.Typed()로 값을 감싸 DuckDB [논리 타입]({% link docs/current/sql/data_types/overview.md %})을 고정해요. 이는 타임스탬프에서 가장 중요해요. 맨 time.Time은 TIMESTAMP_TZ로 바인딩되므로, TIMESTAMP_NS 컬럼에 바인딩하려면 힌트가 필요해요:
start := time.Date(2024, time.April, 5, 0, 0, 0, 0, time.UTC)
end := time.Date(2024, time.April, 6, 0, 0, 0, 0, time.UTC)
row := db.QueryRow(`
SELECT count(*)
FROM (VALUES (TIMESTAMP_NS '2024-04-05 12:00:00.000000001')) events(ts)
WHERE ts >= ? AND ts < ?`,
duckdb.Typed(start, duckdb.TYPE_TIMESTAMP_NS),
duckdb.Typed(end, duckdb.TYPE_TIMESTAMP_NS),
)
duckdb.Typed()는 스칼라 바인딩 힌트예요. DuckDB는 파라미터가 바인딩될 때 요청된 타입에 대해 값을 검증해요. 클라이언트가 time.Time을 DuckDB의 타임스탬프 타입에 어떻게 매핑하는지는 [Troubleshoot]({% link docs/current/clients/go/troubleshoot.md %}#timestamp-versus-timestamp_tz)를 참고해요.
Prepared Statements
db.PrepareContext()는 구문을 한 번 컴파일해 여러 번 실행할 수 있게 해주며, 루프에서 이득이 돼요. 반환된 *sql.Stmt는 자체 Exec와 Query 메서드를 가지며, 더 이상 필요 없을 때 닫아야 해요:
stmt, err := db.PrepareContext(ctx, `INSERT INTO users VALUES (?, ?)`)
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
for _, u := range users {
if _, err := stmt.ExecContext(ctx, u.name, u.age); err != nil {
log.Fatal(err)
}
}
트랜잭션 (Transactions)
db.BeginTx()는 트랜잭션을 시작하고, 그 Exec와 Query 메서드가 트랜잭션 안에서 실행되는 *sql.Tx를 반환해요. tx.Commit()으로 커밋하거나 tx.Rollback()으로 롤백해요:
tx, err := db.BeginTx(ctx, nil)
if err != nil {
log.Fatal(err)
}
if _, err := tx.ExecContext(ctx, `INSERT INTO users VALUES ('gru', 25)`); err != nil {
tx.Rollback()
log.Fatal(err)
}
if err := tx.Commit(); err != nil {
log.Fatal(err)
}
결과 스캔 (Scanning Results)
QueryContext는 *sql.Rows 커서를 반환해요. Next()로 진행하고, Scan()으로 현재 행의 컬럼을 포인터로 읽고, 루프 후 항상 Err()을 확인하고 커서를 닫아야 해요. Scan은 컬럼을 위치로 대상 Go 타입에 매치해요:
rows, err := db.QueryContext(ctx, `SELECT name, age FROM users WHERE age > ?`, 30)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var (
name string
age int
)
if err := rows.Scan(&name, &age); err != nil {
log.Fatal(err)
}
log.Printf("%s is %d years old", name, age)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
컬럼 집합을 미리 알 수 없을 때는 rows.Columns()가 컬럼 이름을 반환하고, any 슬라이스로 스캔하면 각 컬럼을 driver.Value로 읽어요. 스캔된 값은 SQL NULL의 경우 nil일 수 있다는 점을 기억해요.
중첩 및 복합 타입 읽기
DuckDB의 [중첩 타입]({% link docs/current/sql/data_types/overview.md %}#nested--composite-types)(LIST, STRUCT, MAP 등)과 [JSON]({% link docs/current/data/json/overview.md %}) 타입은 일반적인 duckdb.Composite[T] 래퍼로 스캔돼요. SQL 형태에 맞는 Go 타입 파라미터를 선택한 다음 Get()으로 디코딩된 값을 읽어요:
// 슬라이스로 읽은 JSON (또는 LIST) 값.
var arr duckdb.Composite[[]any]
row := db.QueryRow(`SELECT json_array('foo', 'bar')`)
if err := row.Scan(&arr); err != nil {
log.Fatal(err)
}
log.Printf("first element: %s", arr.Get()[0])
// 맵으로 읽은 JSON 객체 (또는 STRUCT) 값.
var obj duckdb.Composite[map[string]any]
row = db.QueryRow(`SELECT '{"family": "anatidae", "coolness": 42.42}'::JSON`)
if err := row.Scan(&obj); err != nil {
log.Fatal(err)
}
log.Printf("family: %s", obj.Get()["family"])
이것은 클라이언트의 json 예시를 따르는. 행 단위가 아니라 결과 집합 전체를 열 지향 배치로 읽으려면 [Handle Results]({% link docs/current/clients/go/result_handling.md %})를 참고해요.
더 알아보기 (Learn more)
- [Handle Results]({% link docs/current/clients/go/result_handling.md %}) — 행 단위가 아니라 Apache Arrow record batch로 결과 읽기.
- [Import Data]({% link docs/current/clients/go/data_import.md %}) — 벌크 삽입에 prepared statements의 권장 대안인 Appender.
- [Prepared Statements]({% link docs/current/sql/query_syntax/prepared_statements.md %}) — 여기서 사용한 파라미터화 쿼리에 대한 DuckDB의 SQL 수준 지원.
- [Connect]({% link docs/current/clients/go/connecting.md %}) — 이 구문들이 실행되는 데이터베이스 열기.