diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2026-04-16 20:48:48 +0200 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2026-04-16 20:48:48 +0200 |
| commit | a646924f1f474aeb5acd3d753cfcb2379dca5210 (patch) | |
| tree | 4ea79637b707b90e37439ac34f77d1bb39b66b41 /query.go | |
| parent | 84dc446e7ca631212aa6c279b1eede759fa8ecc0 (diff) | |
| download | go-oblast-a646924f1f474aeb5acd3d753cfcb2379dca5210.tar.gz | |
add PrepareThreshold tuning parameter
Diffstat (limited to 'query.go')
| -rw-r--r-- | query.go | 64 |
1 files changed, 49 insertions, 15 deletions
@@ -4,11 +4,24 @@ package oblast import ( + "database/sql" "errors" "fmt" "reflect" ) +// PrepareThreshold is a tuning parameter for the strategy used by all methods of [Store] operating on batches of records provided by the caller +// (specifically, [Store.Insert], [Store.Update] and [Store.Delete]). +// +// For large amounts of records, it is obviously advantageous to build a prepared statement for the one query that will be used repeatedly on all of them. +// However, building a prepared statement is associated with some amount of bookkeeping on the level of the database/sql library. +// When operating on individual records or small amounts of records at a time (that is, in OLTP rather than OLAP workloads), this overhead becomes a measurable performance burden. +// +// This tuning parameter defines the minimum number of records that will justify maintaining a prepared statement. +// Our benchmarking with the mattn/go-sqlite3 driver (and last checked with Go 1.26.2 on x86_64) indicates that this becomes a worthwhile investment at 8 or more records, so this is our default. +// If your benchmarking indicates a different tradeoff depending on your choice of Go version or SQL driver, you may adjust this variable accordingly. +var PrepareThreshold int = 8 + // Insert executes an SQL INSERT statement for each of the provided records. // // Fields that are declared with the "auto" tag will not be written into the DB, @@ -35,13 +48,17 @@ func (s Store[R]) Insert(db Handle, records ...R) (returnedRecords []R, returned scanSlots = make([]any, len(scanIndexes)) } - stmt, err := db.Prepare(s.plan.Insert.Query) - if err != nil { - return nil, fmt.Errorf("during Prepare(): %w", err) + var stmt *sql.Stmt + if len(records) >= PrepareThreshold { + var err error + stmt, err = db.Prepare(s.plan.Insert.Query) + if err != nil { + return nil, fmt.Errorf("during Prepare(): %w", err) + } + defer func() { + returnedError = mergeCloseError("Stmt", returnedError, stmt.Close()) + }() } - defer func() { - returnedError = mergeCloseError("Stmt", returnedError, stmt.Close()) - }() for idx := range records { v := reflect.ValueOf(&records[idx]).Elem() @@ -50,7 +67,15 @@ func (s Store[R]) Insert(db Handle, records ...R) (returnedRecords []R, returned } if s.dialect.UsesLastInsertID() { - result, err := stmt.Exec(argumentSlots...) + var ( + result sql.Result + err error + ) + if stmt == nil { + result, err = db.Exec(s.plan.Insert.Query, argumentSlots...) + } else { + result, err = stmt.Exec(argumentSlots...) + } if err != nil { return nil, fmt.Errorf("during Exec() for record with idx = %d: %w", idx, err) } @@ -80,7 +105,7 @@ func (s Store[R]) Insert(db Handle, records ...R) (returnedRecords []R, returned return records, nil } -// TODO: update +// TODO: Store.Update // Delete executes an SQL DELETE statement for each of the provided records, using their primary keys to locate the respective table rows. // @@ -99,20 +124,29 @@ func (s Store[R]) Delete(db Handle, records ...R) (returnedError error) { argumentSlots = make([]any, len(argumentIndexes)) ) - stmt, err := db.Prepare(s.plan.Delete.Query) - if err != nil { - return fmt.Errorf("during Prepare(): %w", err) + var stmt *sql.Stmt + if len(records) >= PrepareThreshold { + var err error + stmt, err = db.Prepare(s.plan.Delete.Query) + if err != nil { + return fmt.Errorf("during Prepare(): %w", err) + } + defer func() { + returnedError = mergeCloseError("Stmt", returnedError, stmt.Close()) + }() } - defer func() { - returnedError = mergeCloseError("Stmt", returnedError, stmt.Close()) - }() for idx, r := range records { v := reflect.ValueOf(&r).Elem() for idx, index := range argumentIndexes { argumentSlots[idx] = v.FieldByIndex(index).Addr().Interface() } - _, err := stmt.Exec(argumentSlots...) + var err error + if stmt == nil { + _, err = db.Exec(s.plan.Delete.Query, argumentSlots...) + } else { + _, err = stmt.Exec(argumentSlots...) + } if err != nil { return fmt.Errorf("during Exec() for record with idx = %d: %w", idx, err) } |
