Skip to content

Optimization tips #40

@wmertens

Description

@wmertens

Most we already have, but:

https://x.com/meln1k/status/1813314113705062774?t=six_z6z7w7S-sqQnhIji9g&s=19

how I squeeze 60K RPS out of SQLite on a $5 VPS:

There are tons of tutorials on how to setup SQLite, and sometimes they contradict each other. Here is what worked for me.

  1. Configuring PRAGMAs. We need to send the following PRAGMA commands right after opening the connection:

PRAGMA journal_mode = WAL;

  • enables write-ahead log so that your reads do not block writes and vice-versa.

PRAGMA busy_timeout = 5000;

  • sqlite will wait 5 seconds to obtain a lock before returning SQLITE_BUSY errors, which will significantly reduce them.

PRAGMA synchronous = NORMAL;

  • sqlite will sync less frequently and be more performant, still safe to use because of the enabled WAL mode.

PRAGMA cache_size = -20000;

  • negative number means kilobytes, in this case 20MB of memory for cache.

PRAGMA foreign_keys = true;

  • because of historical reasons foreign keys are disabled by default, we should manually enable them.

PRAGMA temp_store = memory;

  • moves temporary tables from disk into RAM, speeds up performance a lot.

Do NOT use cache=shared! Some tutorials recommend configuring it, but this is how you get nasty SQLITE_BUSY errors. It is disabled by default, so you don't have to do anything extra.

  1. Use immediate transactions
    If you know that transaction can possibly do a write, always use BEGIN IMMEDIATE or you can a get SQLITE_BUSY error. Check your framework, you should be able to set this at the connection level.

  2. Open two connection pools
    Another trick is to open 2 connection pools, one for reads only and another for reads/writes. Set the connection limit of write pool to 1, and the connection limit of the read pool to some reasonably high number, e.g. number of your CPU cores.

  3. Bonus: how I configure sqlite with Go
    Here is the code in go I use to configure the sqlite connections:

func SQLiteDbString(file string, readonly bool) string {

connectionParams := make(url.Values)
connectionParams.Add("_journal_mode", "WAL")
connectionParams.Add("_busy_timeout", "5000")
connectionParams.Add("_synchronous", "NORMAL")
connectionParams.Add("_cache_size", "-20000")
connectionParams.Add("_foreign_keys", "true")
if readonly {
connectionParams.Add("mode", "ro")
} else {
connectionParams.Add("_txlock", "IMMEDIATE")
connectionParams.Add("mode", "rwc")
}

return "file:" + file + "?" + connectionParams.Encode()
}

func OpenSqliteDatabase(file string, readonly bool) (*sql.DB, error) {

dbString := SQLiteDbString(file, readonly)
db, err := sql .Open("sqlite3", dbString)

pragmasToSet := []string{
"temp_store=memory",
}

for _, pragma := range pragmasToSet {
_, err = db.Exec("PRAGMA " + pragma + ";")
if err != nil {
return nil, err
}
}

if readonly {
db.SetMaxOpenConns(max(4, runtime.NumCPU()))
} else {
db.SetMaxOpenConns(1)
}

return db, nil
}

Important note: if you use synchronous = NORMAL with WAL your database won't be corrupted, but during a power loss the latest transactions could be rolled back.

To fix that use synchronous = FULL, you will lose around 30% in write performance, but sqlite will sync the WAL after every commit, so no committed transaction will ever disappear.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions