Подключение к SQLite — это просто открытый файл
У SQLite нет сервера. Нет демона, слушающего порт, нет хоста, к которому надо коннектиться, нет логина и пароля. «Подключение к SQLite» — это когда драйвер открывает файл на диске и начинает читать и писать в него страницы. Вот и вся модель в голове.
В каждом языке есть свой драйвер, который оборачивает сишную библиотеку SQLite. Обёртки выглядят по-разному, но детали везде одни и те же: путь к файлу базы, вызов открытия, дескриптор, на котором выполняются запросы, и закрытие, когда всё сделано.
-- Концептуально, каждый драйвер делает следующее:
-- 1. Открыть или создать файл по заданному пути.
-- 2. Получить дескриптор (handle).
-- 3. Выполнить SQL через подготовленные выражения (prepared statements).
-- 4. Закрыть дескриптор.
Дальше на странице — как это выглядит в реальном коде и какие пара настроек стоит выставить ещё до первого запроса.
Python: модуль sqlite3 из стандартной библиотеки
В Python модуль sqlite3 идёт «из коробки» — ставить ничего не нужно. Базовый каркас подключения к SQLite выглядит так:
-- 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 (?)", ("первая заметка",))
conn.commit()
for row in conn.execute("SELECT id, body FROM notes"):
print(row)
conn.close()
Несколько важных моментов:
sqlite3.connect("app.db")создаёт файл, если его ещё нет. Передайте":memory:", чтобы база жила только в оперативке.sqlite3.connect("file:app.db?mode=ro", uri=True)открывает базу только на чтение через URI-форму.?в SQL — это плейсхолдер. Всегда используйте привязку параметров, никогда не склеивайте строки. Подробнее об этом — в следующей главе.conn.commit()обязателен, если только вы не используете контекстный менеджер (with conn:) — он коммитит сам.
В долгоживущем приложении задайте busy timeout, чтобы конкурентные пишущие соединения ждали, а не падали с ошибкой:
-- Python
conn.execute("PRAGMA busy_timeout = 5000") -- ждать до 5 секунд
conn.execute("PRAGMA journal_mode = WAL") -- лучшая параллельность
Node.js: better-sqlite3
В экосистеме Node вариантов несколько, но большинство команд выбирают именно better-sqlite3. Он синхронный — и хотя для Node это звучит как ересь, на практике для SQLite так получается быстрее: запросы отрабатывают за микросекунды.
-- 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("первая заметка");
const rows = db.prepare("SELECT id, body FROM notes").all();
console.log(rows);
db.close();
db.prepare(...) возвращает объект подготовленного запроса, который можно переиспользовать. .run() — для записи, .all() отдаёт все строки, .get() — одну. Шаблон знакомый по большинству SQL-драйверов.
Прагмы выставляем при старте:
-- Node.js
db.pragma("journal_mode = WAL");
db.pragma("busy_timeout = 5000");
db.pragma("foreign_keys = ON"); -- по умолчанию выключено, почти всегда нужно включать
foreign_keys = ON стоит упомянуть отдельно: SQLite не проверяет внешние ключи, пока вы явно не попросите — причём для каждого подключения отдельно. Забудете включить — и ваши REFERENCES превращаются в чистую декорацию.
Go: database/sql и драйвер для SQLite
Стандартный пакет database/sql в Go не привязан к конкретному драйверу. Для SQLite обычно берут один из двух: modernc.org/sqlite (чистый Go, без CGO) либо github.com/mattn/go-sqlite3 (с 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 (?)", "первая заметка")
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)
}
Строка запроса после имени файла — это способ, которым драйвер передаёт прагмы при подключении. Формат зависит от конкретного драйвера, так что загляните в документацию того, что выберете.
sql.Open на самом деле не открывает соединение — оно устанавливается при первом запросе. db здесь — это пул соединений. Для SQLite обычно достаточно небольшого пула, а под нагрузкой на запись часто разумно вообще выставить db.SetMaxOpenConns(1).
Java: подключение SQLite через JDBC
Стандартом де-факто считается драйвер org.xerial:sqlite-jdbc. JDBC-строка подключения выглядит как jdbc:sqlite:<path>:
-- 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, "первая заметка");
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:. Только для чтения: добавьте ?open_mode=1 либо воспользуйтесь объектом SQLiteConfig.
PHP: PDO
В PDO строка подключения к SQLite (DSN) выглядит как 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(["первая заметка"]);
foreach ($db->query("SELECT id, body FROM notes") as $row) {
echo $row["id"] . " " . $row["body"] . "\n";
}
sqlite::memory: — это база данных в памяти. Обязательно выставляйте ATTR_ERRMODE в режим исключений: молчаливые ошибки потом крайне неприятно отлаживать.
Строка подключения и пути к файлам
В разных драйверах строка подключения к SQLite встречается в двух вариантах:
- Обычный путь:
app.db,./data/app.db,/var/lib/myapp/app.db. Относительные пути считаются от текущей рабочей директории процесса — а это редко то, что нужно на проде. Лучше используйте абсолютные пути. - URI-формат:
file:app.db?mode=rwc&cache=shared. Здесь можно задать флаги:mode=ro(только чтение),mode=rwc(чтение-запись-создание, по умолчанию),cache=shared,nolock=1.
Особые значения, которые точно встретятся:
:memory:— приватная база в оперативной памяти. У каждого подключения своя.file::memory:?cache=shared— база в памяти, которую могут совместно использовать несколько подключений в рамках одного процесса.""(пустая строка) — приватная временная база на диске, удаляется при закрытии.
В JDBC к URI добавляется префикс jdbc:sqlite:. PDO использует sqlite:. Драйверы для Go и модуль sqlite3 в Python принимают путь или URI напрямую.
А как же пулы соединений?
SQLite — однопользовательская на запись база. В любой момент времени блокировкой на запись владеет ровно одно подключение, остальные ждут своей очереди. Пул из кучи писателей не ускорит запись — вы просто получите больше претендентов на один и тот же лок.
И всё же небольшой пул бывает полезен:
- Для параллельных чтений в режиме WAL, где читатели не блокируют ни друг друга, ни писателя.
- Чтобы избежать head-of-line blocking — ситуации, когда один медленный запрос тормозит всё приложение.
Разумные настройки по умолчанию для веб-приложения:
- Включённый WAL-режим.
busy_timeoutв несколько секунд, чтобы при конкуренции запросы вежливо ждали, а не падали с ошибкой.- Размер пула: 1 писатель + N читателей, либо вообще одно общее подключение, если трафик невелик.
- Внешние ключи включены на каждом подключении.
-- Применяйте это для каждого нового соединения:
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA foreign_keys = ON;
PRAGMA synchronous = NORMAL; -- безопасно с WAL; быстрее, чем FULL
synchronous = NORMAL — это типичный спутник WAL: переживает падения приложения, чуть менее строг при падении ОС и заметно быстрее, чем дефолтный FULL.
Закрытие подключений (и почему это важно)
В каждом драйвере есть свой метод закрытия: conn.close(), db.Close(), db.close(). Если его не вызывать, утекают файловые дескрипторы, а WAL-файл может бесконтрольно разрастаться.
В долгоживущих сервисах гораздо разумнее держать одно подключение (или пул) на всё время жизни процесса, а не открывать и закрывать его вокруг каждого запроса. Само подключение к SQLite стоит дёшево, но вот заново прогонять PRAGMA при каждом открытии — это лишняя работа, про которую к тому же легко забыть.
-- Python — соединение на процесс, переиспользуется между запросами
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")
Если говорить о Python, то параметр check_same_thread=False понадобится, когда соединение используется из нескольких потоков, — и тогда же стоит задуматься о блокировке или пуле, чтобы вызовы шли по очереди.
Чеклист перед выкаткой в прод
Прежде чем направлять реальный трафик в базу SQLite, пройдитесь по списку:
- Указывайте абсолютный путь к файлу базы.
- Включите режим WAL (
PRAGMA journal_mode = WAL). - Выставьте
busy_timeoutв диапазоне 2–10 секунд. - Включайте внешние ключи на каждом соединении.
- Используйте подготовленные запросы с биндингом параметров — никакой склейки строк.
- Убедитесь, что каталог с базой доступен процессу на запись (в режиме WAL рядом с основным файлом SQLite создаёт файлы
-walи-shm). - Подумайте о бэкапах заранее, а не когда уже припекло, — про
VACUUM INTOи команду.backupпоговорим дальше.
Дальше: миграции
Подключиться — это самое простое. Сложнее эволюционировать схему со временем, не правя продовую базу руками. Миграции — это как раз способ превратить ALTER TABLE в повторяемый процесс под контролем версий. Об этом — на следующей странице.
Часто задаваемые вопросы
Как подключиться к базе SQLite из кода?
Достаточно указать драйверу путь к файлу. В Python это sqlite3.connect('app.db'), в Node — new Database('app.db') через better-sqlite3, в Go — sql.Open("sqlite", "app.db"). Сервера у SQLite нет, поэтому «подключение» по сути сводится к открытию файла — если файла нет, SQLite создаст его сам.
Как выглядит строка подключения к SQLite?
Большинство драйверов принимают либо обычный путь к файлу (./data/app.db), либо URI-форму (file:app.db?mode=rwc&cache=shared). URI удобен тем, что через него можно задать флаги: режим только для чтения, общий кэш или базу :memory: в оперативке. JDBC использует формат jdbc:sqlite:app.db, PDO — sqlite:app.db.
Нужен ли SQLite пул соединений?
Обычно не в том смысле, как для Postgres или MySQL. SQLite сериализует запись на уровне всей базы, так что несколько писателей в пуле всё равно не дадут прироста. Небольшой пул имеет смысл для параллельных чтений, особенно в режиме WAL. Многим приложениям хватает одного общего соединения с PRAGMA journal_mode=WAL и адекватным busy_timeout.
Как избавиться от ошибки 'database is locked'?
Поставьте busy timeout, чтобы драйвер ждал, а не падал сразу: PRAGMA busy_timeout = 5000 (миллисекунды). Включите режим WAL через PRAGMA journal_mode=WAL — тогда читатели не блокируют писателей. И главное: держите транзакции короткими и не оставляйте открытую транзакцию записи, пока выполняете долгие операции вне базы.