Eine Verbindung ist einfach eine geöffnete Datei
SQLite hat keinen Server. Da läuft kein Daemon auf einem Port, es gibt keinen Host, den du anwählen müsstest, und auch keine Zugangsdaten, die ausgehandelt werden. Eine SQLite-Verbindung aufzubauen bedeutet schlicht: Dein Treiber öffnet eine Datei auf der Festplatte und fängt an, deren Pages zu lesen und zu schreiben. Mehr steckt nicht dahinter.
Für jede Sprache gibt es einen Treiber, der die SQLite-C-Bibliothek umhüllt. Die Form unterscheidet sich, aber die beweglichen Teile sind überall dieselben: ein Pfad zur Datenbankdatei, ein Open-Aufruf, ein Handle, über das du deine Statements ausführst, und ein Close-Aufruf, wenn du fertig bist.
-- Konzeptionell macht jeder Treiber das Folgende:
-- 1. Datei am angegebenen Pfad öffnen oder erstellen.
-- 2. Ein Handle beschaffen.
-- 3. SQL über vorbereitete Statements ausführen.
-- 4. Das Handle schließen.
Im Rest dieser Seite zeige ich dir, wie das in echtem Code aussieht – und welche wenigen Einstellungen sich lohnen, bevor du deine erste Query absetzt.
Python: sqlite3 aus der Standardbibliothek
Python bringt sqlite3 von Haus aus mit – keine Installation nötig. So sieht das Grundgerüst aus:
-- Python
import sqlite3
conn = sqlite3.connect("app.db")
conn.execute("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)")
conn.execute("INSERT INTO notes (body) VALUES (?)", ("erste Notiz",))
conn.commit()
for row in conn.execute("SELECT id, body FROM notes"):
print(row)
conn.close()
Ein paar Dinge, die du wissen solltest:
sqlite3.connect("app.db")legt die Datei automatisch an, falls sie noch nicht existiert. Mit":memory:"bekommst du eine Datenbank, die nur im RAM lebt.sqlite3.connect("file:app.db?mode=ro", uri=True)öffnet die Datenbank über die URI-Form im Read-Only-Modus.- Das
?im SQL ist ein Platzhalter — nutze immer Parameter-Binding, niemals String-Konkatenation. Im nächsten Kapitel gehen wir genauer darauf ein. conn.commit()ist Pflicht, es sei denn, du verwendest einen Context Manager (with conn:), der automatisch committet.
Für eine länger laufende Anwendung solltest du ein Busy-Timeout setzen, damit parallele Schreibzugriffe warten, statt mit einem Fehler abzubrechen:
-- Python
conn.execute("PRAGMA busy_timeout = 5000") -- bis zu 5s warten
conn.execute("PRAGMA journal_mode = WAL") -- bessere Nebenläufigkeit
Node.js: better-sqlite3
Im Node-Ökosystem gibt es mehrere Optionen, aber zu better-sqlite3 greifen die meisten Teams. Die Library arbeitet synchron – was sich in Node erstmal falsch anhört, für SQLite aber tatsächlich schneller ist, weil Queries innerhalb von Mikrosekunden zurückkommen.
-- Node.js
const Database = require("better-sqlite3");
const db = new Database("app.db");
db.exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)");
const insert = db.prepare("INSERT INTO notes (body) VALUES (?)");
insert.run("erste Notiz");
const rows = db.prepare("SELECT id, body FROM notes").all();
console.log(rows);
db.close();
db.prepare(...) liefert ein wiederverwendbares Statement-Objekt zurück. .run() ist für schreibende Zugriffe gedacht, .all() gibt alle Zeilen zurück und .get() nur eine einzige. Dasselbe Muster wie bei den meisten SQL-Treibern.
Pragmas beim Start setzen:
-- Node.js
db.pragma("journal_mode = WAL");
db.pragma("busy_timeout = 5000");
db.pragma("foreign_keys = ON"); -- standardmäßig aus, fast immer erwünscht
foreign_keys = ON verdient eine eigene Erwähnung: SQLite erzwingt Fremdschlüssel nämlich nur, wenn du es ausdrücklich verlangst – und zwar pro Verbindung. Vergisst du das, sind deine REFERENCES-Klauseln reine Deko.
Go: database/sql mit einem SQLite Go Treiber
Das Standardpaket database/sql in Go ist treiberunabhängig. Für SQLite haben sich zwei Treiber etabliert: modernc.org/sqlite (reines Go, ohne CGO) und github.com/mattn/go-sqlite3 (mit CGO).
-- Go
import (
"database/sql"
_ "modernc.org/sqlite"
)
db, err := sql.Open("sqlite", "app.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)")
if err != nil { panic(err) }
defer db.Close()
_, err = db.Exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)")
_, err = db.Exec("INSERT INTO notes (body) VALUES (?)", "erste Notiz")
rows, _ := db.Query("SELECT id, body FROM notes")
defer rows.Close()
for rows.Next() {
var id int; var body string
rows.Scan(&id, &body)
fmt.Println(id, body)
}
Der Query-String hinter dem Dateinamen ist hier der Weg, über den dieser Treiber beim Verbindungsaufbau Pragmas mitgibt — das genaue Format hängt vom Treiber ab, also schau am besten in die Doku des Treibers, den du verwendest.
sql.Open baut übrigens noch gar keine Verbindung auf — das passiert erst bei der ersten Query. db ist ein Connection Pool. Für SQLite fährst du in der Regel mit einem kleinen Pool gut, bei schreiblastigen Workloads sogar mit db.SetMaxOpenConns(1).
Java: SQLite JDBC Treiber
Der Standard ist hier der Treiber org.xerial:sqlite-jdbc. JDBC-URLs haben das Format jdbc:sqlite:<pfad>:
-- Java
import java.sql.*;
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:app.db")) {
try (Statement st = conn.createStatement()) {
st.execute("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)");
st.execute("PRAGMA journal_mode = WAL");
st.execute("PRAGMA busy_timeout = 5000");
}
try (PreparedStatement ps = conn.prepareStatement("INSERT INTO notes (body) VALUES (?)")) {
ps.setString(1, "erste Notiz");
ps.executeUpdate();
}
try (PreparedStatement ps = conn.prepareStatement("SELECT id, body FROM notes");
ResultSet rs = ps.executeQuery()) {
while (rs.next()) System.out.println(rs.getInt(1) + " " + rs.getString(2));
}
}
In-Memory: jdbc:sqlite::memory:. Nur lesend: entweder ?open_mode=1 anhängen oder ein SQLiteConfig-Objekt verwenden.
PHP: SQLite-Verbindung mit PDO
Der SQLite-DSN für PDO hat die Form sqlite:<path>:
-- PHP
$db = new PDO("sqlite:app.db");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)");
$db->exec("PRAGMA journal_mode = WAL");
$db->exec("PRAGMA busy_timeout = 5000");
$stmt = $db->prepare("INSERT INTO notes (body) VALUES (?)");
$stmt->execute(["erste Notiz"]);
foreach ($db->query("SELECT id, body FROM notes") as $row) {
echo $row["id"] . " " . $row["body"] . "\n";
}
sqlite::memory: für eine In-Memory-Datenbank. Setze ATTR_ERRMODE immer auf Exceptions – stille Fehler sind beim Debuggen die Hölle.
SQLite Connection String und Dateipfade
Quer durch alle Treiber begegnen dir zwei Varianten von "Connection String":
- Einfacher Pfad:
app.db,./data/app.db,/var/lib/myapp/app.db. Relative Pfade beziehen sich auf das Arbeitsverzeichnis des Prozesses – und das ist im Produktivbetrieb selten das, was du eigentlich willst. Greif lieber zu absoluten Pfaden. - URI-Form:
file:app.db?mode=rwc&cache=shared. Damit kannst du Flags setzen wiemode=ro(nur lesend),mode=rwc(lesen, schreiben, anlegen – Standard),cache=sharedodernolock=1.
Ein paar Sonderwerte, die dir über den Weg laufen:
:memory:– eine private In-Memory-Datenbank. Jede Verbindung bekommt ihre eigene.file::memory:?cache=shared– eine In-Memory-Datenbank, die sich mehrere Verbindungen innerhalb desselben Prozesses teilen können.""(leerer String) – eine private, temporäre Datenbank auf der Platte, die beim Schließen gelöscht wird.
JDBC stellt der URI ein jdbc:sqlite: voran, PDO nutzt sqlite:. Go-Treiber und Pythons sqlite3 akzeptieren den Pfad oder die URI direkt.
Was ist mit Connection Pools?
SQLite ist eine Single-Writer-Datenbank. Zu jedem Zeitpunkt hält genau eine Verbindung das Schreib-Lock, alle anderen warten. Viele Writer in einen Pool zu packen macht Schreibvorgänge nicht schneller – du hast einfach nur mehr Kandidaten, die sich um dasselbe Lock streiten.
Trotzdem ist ein kleiner Pool nützlich für:
- Parallele Lesezugriffe im WAL-Modus, wo sich Reader weder gegenseitig noch den Writer blockieren.
- Vermeidung von Head-of-Line-Blocking, damit nicht eine einzige langsame Query die ganze App ausbremst.
Vernünftige Defaults für eine Web-App:
- WAL-Modus an.
busy_timeoutvon ein paar Sekunden, damit bei Konflikten höflich gewartet wird, statt direkt einen Fehler zu werfen.- Eine Pool-Größe von 1 Writer + N Readern – oder einfach eine einzige geteilte Verbindung, wenn der Traffic überschaubar ist.
- Foreign Keys auf jeder Verbindung aktiviert.
-- Bei jeder neuen Verbindung anwenden:
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA foreign_keys = ON;
PRAGMA synchronous = NORMAL; -- sicher mit WAL; schneller als FULL
synchronous = NORMAL ist die übliche Ergänzung zu WAL – haltbar bei App-Abstürzen, etwas weniger streng bei OS-Crashes, dafür spürbar schneller als der Default FULL.
Verbindungen schließen (und warum das wichtig ist)
Jeder Treiber hat seinen eigenen Schließbefehl: conn.close(), db.Close(), db.close(). Wer das nicht macht, riskiert undichte File Descriptors und eine WAL-Datei, die immer weiter wächst.
In langlaufenden Services ist das übliche Muster: eine Verbindung (oder ein Pool) für die gesamte Lebensdauer des Prozesses – statt bei jedem Request neu zu öffnen und wieder zu schließen. Eine SQLite-Verbindung aufzubauen ist zwar günstig, aber die Pragmas jedes Mal aufs Neue zu setzen ist Verschwendung und wird schnell mal vergessen.
-- Python — eine Verbindung pro Prozess, über Anfragen hinweg wiederverwendet
DB = sqlite3.connect("app.db", check_same_thread=False)
DB.execute("PRAGMA journal_mode = WAL")
DB.execute("PRAGMA busy_timeout = 5000")
DB.execute("PRAGMA foreign_keys = ON")
Speziell bei Python brauchst du check_same_thread=False, sobald du die Verbindung aus mehreren Threads heraus benutzt – und dazu am besten einen Lock oder einen Pool, damit die Aufrufe sauber serialisiert werden.
Checkliste vor dem Go-Live
Bevor du echten Traffic auf eine SQLite-Datenbank loslässt:
- Verwende einen absoluten Pfad zur Datenbankdatei.
- Aktiviere den WAL-Modus (
PRAGMA journal_mode = WAL). - Setze einen
busy_timeoutzwischen 2 und 10 Sekunden. - Schalte Foreign Keys ein – und zwar bei jeder Verbindung neu.
- Nutze Prepared Statements mit Parameter-Binding – niemals String-Interpolation.
- Stell sicher, dass das Verzeichnis der Datenbank für den Prozess schreibbar ist (SQLite legt im WAL-Modus zusätzlich eine
-wal- und eine-shm-Datei neben der Hauptdatei an). - Kümmere dich frühzeitig um Backups –
VACUUM INTOund den.backup-Befehl schauen wir uns später noch an.
Als Nächstes: Migrationen
Das Verbinden ist der einfache Teil. Schwieriger wird es, wenn du dein Schema über die Zeit weiterentwickeln willst, ohne von Hand an der Produktionsdatenbank herumzuschrauben. Genau dafür gibt es Migrationen: Sie machen aus ALTER TABLE einen wiederholbaren, versionierten Prozess – darum geht es auf der nächsten Seite.
Häufig gestellte Fragen
Wie verbinde ich mich aus dem Code mit einer SQLite-Datenbank?
Du gibst dem Treiber einfach einen Dateipfad. In Python ist das sqlite3.connect('app.db'), in Node.js mit better-sqlite3 schreibst du new Database('app.db'), in Go nutzt du sql.Open("sqlite", "app.db"). SQLite hat keinen Server — die „Verbindung" ist im Grunde nur das Öffnen einer Datei. Existiert sie noch nicht, legt SQLite sie automatisch an.
Wie sieht ein Connection String für SQLite aus?
Die meisten Treiber akzeptieren entweder einen schlichten Dateipfad (./data/app.db) oder die URI-Form (file:app.db?mode=rwc&cache=shared). Über die URI-Variante setzt du Flags wie Read-only, Shared Cache oder eine :memory:-Datenbank. JDBC erwartet jdbc:sqlite:app.db, PDO entsprechend sqlite:app.db.
Brauche ich bei SQLite einen Connection Pool?
In den meisten Fällen nicht — zumindest nicht so wie bei Postgres oder MySQL. SQLite serialisiert Schreibvorgänge auf Datenbankebene, ein Pool aus Schreibern bringt also nichts. Für parallele Lesezugriffe kann ein kleiner Pool sinnvoll sein, vor allem im WAL-Modus. Viele Apps fahren völlig problemlos mit einer einzigen geteilten Connection plus PRAGMA journal_mode=WAL und einem vernünftigen busy_timeout.
Wie vermeide ich „database is locked"-Fehler?
Setz einen Busy-Timeout, damit der Treiber wartet statt sofort abzubrechen: PRAGMA busy_timeout = 5000 (in Millisekunden). Aktiviere zusätzlich den WAL-Modus mit PRAGMA journal_mode=WAL, dann blockieren Leser und Schreiber sich nicht mehr gegenseitig. Halte Transaktionen kurz und lass keine offene Schreibtransaktion liegen, während dein Code noch andere langsame Dinge erledigt.