Files
stash/pkg/sqlite/tx.go
WithoutPants 0fd7a2ac20 SQL performance improvements (#6378)
* Change queryStruct to use tx.Get instead of queryFunc

Using queryFunc meant that the performance logging was inaccurate due to the query actually being executed during the call to Scan.

* Only add join args if join was added

* Omit joins that are only used for sorting when skipping sorting

Should provide some marginal improvement on systems with a lot of items.

* Make all calls to the database pass context.

This means that long queries can be cancelled by navigating to another page. Previously the query would continue to run, impacting on future queries.
2025-12-08 08:08:31 +11:00

150 lines
3.7 KiB
Go

package sqlite
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/logger"
)
const (
slowLogTime = time.Millisecond * 200
)
type dbReader interface {
Get(dest interface{}, query string, args ...interface{}) error
GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error)
}
type stmt struct {
*sql.Stmt
query string
}
func logSQL(start time.Time, query string, args ...interface{}) {
since := time.Since(start)
if since >= slowLogTime {
logger.Debugf("SLOW SQL [%v]: %s, args: %v", since, query, args)
} else {
logger.Tracef("SQL [%v]: %s, args: %v", since, query, args)
}
}
type dbWrapperType struct{}
var dbWrapper = dbWrapperType{}
func sqlError(err error, sql string, args ...interface{}) error {
if err == nil {
return nil
}
return fmt.Errorf("error executing `%s` [%v]: %w", sql, args, err)
}
func (*dbWrapperType) Get(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
tx, err := getDBReader(ctx)
if err != nil {
return sqlError(err, query, args...)
}
start := time.Now()
err = tx.GetContext(ctx, dest, query, args...)
logSQL(start, query, args...)
return sqlError(err, query, args...)
}
func (*dbWrapperType) Select(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
tx, err := getDBReader(ctx)
if err != nil {
return sqlError(err, query, args...)
}
start := time.Now()
err = tx.SelectContext(ctx, dest, query, args...)
logSQL(start, query, args...)
return sqlError(err, query, args...)
}
func (*dbWrapperType) Queryx(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) {
tx, err := getDBReader(ctx)
if err != nil {
return nil, sqlError(err, query, args...)
}
start := time.Now()
ret, err := tx.QueryxContext(ctx, query, args...)
logSQL(start, query, args...)
return ret, sqlError(err, query, args...)
}
func (*dbWrapperType) QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) {
return dbWrapper.Queryx(ctx, query, args...)
}
func (*dbWrapperType) NamedExec(ctx context.Context, query string, arg interface{}) (sql.Result, error) {
tx, err := getTx(ctx)
if err != nil {
return nil, sqlError(err, query, arg)
}
start := time.Now()
ret, err := tx.NamedExecContext(ctx, query, arg)
logSQL(start, query, arg)
return ret, sqlError(err, query, arg)
}
func (*dbWrapperType) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
tx, err := getTx(ctx)
if err != nil {
return nil, sqlError(err, query, args...)
}
start := time.Now()
ret, err := tx.ExecContext(ctx, query, args...)
logSQL(start, query, args...)
return ret, sqlError(err, query, args...)
}
// Prepare creates a prepared statement.
func (*dbWrapperType) Prepare(ctx context.Context, query string, args ...interface{}) (*stmt, error) {
tx, err := getTx(ctx)
if err != nil {
return nil, sqlError(err, query, args...)
}
// nolint:sqlclosecheck
ret, err := tx.PrepareContext(ctx, query)
if err != nil {
return nil, sqlError(err, query, args...)
}
return &stmt{
query: query,
Stmt: ret,
}, nil
}
func (*dbWrapperType) ExecStmt(ctx context.Context, stmt *stmt, args ...interface{}) (sql.Result, error) {
_, err := getTx(ctx)
if err != nil {
return nil, sqlError(err, stmt.query, args...)
}
start := time.Now()
ret, err := stmt.ExecContext(ctx, args...)
logSQL(start, stmt.query, args...)
return ret, sqlError(err, stmt.query, args...)
}