Una conexión es simplemente un archivo abierto
SQLite no tiene servidor. No hay un demonio escuchando en un puerto, ni un host al que llamar, ni credenciales que negociar. "Conectarse" significa que tu driver abre un archivo en disco y empieza a leer y escribir sus páginas. Ese es todo el modelo mental.
Cada lenguaje tiene un driver que envuelve la librería de C de SQLite. Las formas cambian, pero las piezas son las mismas: una ruta al archivo de la base de datos, una llamada para abrirlo, un handle sobre el que ejecutas sentencias y una llamada para cerrarlo cuando terminas.
-- Conceptualmente, cada driver hace esto:
-- 1. Abrir o crear el archivo en la ruta indicada.
-- 2. Adquirir un handle.
-- 3. Ejecutar SQL mediante sentencias preparadas.
-- 4. Cerrar el handle.
El resto de esta página es justo eso traducido a código real, junto con los pocos ajustes que conviene tocar antes de lanzar tu primera consulta.
Python: sqlite3 en la librería estándar
Python ya trae sqlite3 de fábrica, así que no hace falta instalar nada. Esta es la forma básica de conectar a SQLite desde Python:
-- 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 (?)", ("primera nota",))
conn.commit()
for row in conn.execute("SELECT id, body FROM notes"):
print(row)
conn.close()
Algunas cosas que conviene tener en cuenta:
sqlite3.connect("app.db")crea el archivo si no existe. Pasa":memory:"para tener una base de datos que viva solo en RAM.sqlite3.connect("file:app.db?mode=ro", uri=True)abre la base en modo solo lectura usando la forma URI.- El
?dentro del SQL es un marcador (placeholder) — usa siempre parámetros vinculados, nunca concatenación de strings. En el próximo capítulo profundizamos en esto. conn.commit()es obligatorio, salvo que uses un context manager (with conn:), que hace commit automáticamente.
En una aplicación de larga duración, conviene configurar un busy timeout para que las escrituras concurrentes esperen en lugar de fallar:
-- Python
conn.execute("PRAGMA busy_timeout = 5000") -- esperar hasta 5s
conn.execute("PRAGMA journal_mode = WAL") -- mejor concurrencia
Node.js: better-sqlite3
Dentro del ecosistema de Node hay varias opciones, pero better-sqlite3 es la que la mayoría de los equipos termina usando. Es síncrona (lo cual suena raro en Node, pero en realidad es más rápido para SQLite, ya que las consultas se resuelven en microsegundos).
-- 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("primera nota");
const rows = db.prepare("SELECT id, body FROM notes").all();
console.log(rows);
db.close();
db.prepare(...) devuelve un objeto statement reutilizable. Usa .run() para escrituras, .all() para obtener todas las filas y .get() para una sola. Es el mismo patrón que verás en la mayoría de drivers SQL.
Configura los pragmas al arrancar:
-- Node.js
db.pragma("journal_mode = WAL");
db.pragma("busy_timeout = 5000");
db.pragma("foreign_keys = ON"); -- desactivado por defecto, casi siempre se quiere activar
foreign_keys = ON merece mención aparte: SQLite no aplica las claves foráneas a menos que se lo pidas explícitamente, y hay que hacerlo en cada conexión. Si se te olvida, tus cláusulas REFERENCES son puro adorno.
Go: database/sql con un driver
El paquete estándar database/sql de Go es agnóstico al driver. Para SQLite, las opciones habituales son modernc.org/sqlite (Go puro, sin CGO) y github.com/mattn/go-sqlite3 (con 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 (?)", "primera nota")
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)
}
La query string que va después del nombre de archivo es la forma en que este driver pasa los pragmas al momento de conectar — el formato cambia según el driver, así que revisa la documentación del que vayas a usar.
sql.Open en realidad no abre una conexión; lo hace recién la primera consulta. db es un pool de conexiones. Para SQLite, un pool pequeño (o directamente db.SetMaxOpenConns(1) si tu carga es de muchas escrituras) suele ser lo más acertado.
Java: conectar SQLite con JDBC
El driver estándar es org.xerial:sqlite-jdbc. Las URLs de JDBC tienen la forma jdbc:sqlite:<ruta>:
-- 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, "primera nota");
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));
}
}
En memoria: jdbc:sqlite::memory:. Solo lectura: añade ?open_mode=1 o usa un objeto SQLiteConfig.
PHP: PDO
El DSN de SQLite en PDO tiene la forma sqlite:<ruta>:
-- 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(["primera nota"]);
foreach ($db->query("SELECT id, body FROM notes") as $row) {
echo $row["id"] . " " . $row["body"] . "\n";
}
sqlite::memory: para una base de datos en memoria. Configura siempre ATTR_ERRMODE en modo excepciones — los fallos silenciosos son una pesadilla a la hora de depurar.
Cadenas de conexión y rutas de archivos
Da igual el driver que uses: vas a encontrarte con dos sabores de "cadena de conexión":
- Ruta simple:
app.db,./data/app.db,/var/lib/myapp/app.db. Las rutas relativas se resuelven respecto al directorio de trabajo del proceso, que casi nunca es lo que quieres en producción. Usa rutas absolutas siempre que puedas. - Formato URI:
file:app.db?mode=rwc&cache=shared. Te permite pasar flags comomode=ro(solo lectura),mode=rwc(lectura, escritura y creación, el valor por defecto),cache=sharedynolock=1.
Algunos valores especiales con los que te vas a topar:
:memory:— una base de datos privada en memoria. Cada conexión obtiene la suya.file::memory:?cache=shared— una base de datos en memoria que varias conexiones del mismo proceso pueden compartir.""(cadena vacía) — una base de datos temporal y privada en disco que se elimina al cerrarla.
JDBC antepone jdbc:sqlite: a la URI. PDO usa sqlite:. Los drivers de Go y el módulo sqlite3 de Python aceptan directamente la ruta o la URI.
¿Qué pasa con los pools de conexiones?
SQLite es una base de datos de un solo escritor. En cualquier instante, exactamente una conexión tiene el lock de escritura; las demás esperan. Tener un pool con muchos escritores no acelera las escrituras — solo te da más candidatos peleándose por el mismo lock.
Dicho esto, un pool pequeño sí resulta útil para:
- Lecturas concurrentes en modo WAL, donde los lectores no se bloquean entre sí ni bloquean al escritor.
- Evitar el head-of-line blocking, esa situación en la que una consulta lenta deja toda la app colgada.
Valores por defecto razonables para una app web:
- Modo WAL activado.
busy_timeoutde unos segundos, para que la contención espere educadamente en vez de fallar.- Un pool de 1 escritor + N lectores, o simplemente una conexión compartida si el tráfico es bajo.
- Foreign keys activadas, en cada conexión.
-- Aplica esto en cada nueva conexión:
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA foreign_keys = ON;
PRAGMA synchronous = NORMAL; -- seguro con WAL; más rápido que FULL
synchronous = NORMAL es el acompañante habitual del modo WAL: aguanta caídas de la aplicación, es algo más laxo ante caídas del sistema operativo y resulta bastante más rápido que el FULL por defecto.
Cerrar conexiones (y por qué es importante)
Todos los drivers tienen su método de cierre: conn.close(), db.Close(), db.close(). Si no cierras, se filtran descriptores de archivo y el archivo WAL puede seguir creciendo sin control.
En servicios de larga duración, el patrón más común es una conexión (o un pool) durante toda la vida del proceso, en lugar de abrir y cerrar en cada petición. Abrir una conexión a SQLite es barato, pero volver a aplicar los pragmas cada vez es un desperdicio y, además, es fácil que se te pase.
-- Python — conexión por proceso, reutilizada entre solicitudes
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")
En Python concretamente, necesitarás check_same_thread=False si vas a usar la conexión desde varios hilos — y conviene añadir un lock o un pool para serializar las llamadas.
Checklist antes de pasar a producción
Antes de dirigir tráfico real contra una base de datos SQLite:
- Usa una ruta absoluta para el archivo de la base de datos.
- Activa el modo WAL (
PRAGMA journal_mode = WAL). - Configura un
busy_timeoutde entre 2 y 10 segundos. - Habilita las claves foráneas en cada conexión.
- Usa sentencias preparadas con parámetros vinculados — nunca interpolación de strings.
- Asegúrate de que el directorio que contiene la base de datos tenga permisos de escritura para el proceso (en modo WAL, SQLite crea los archivos
-waly-shmjunto al principal). - Piensa en los backups antes de necesitarlos — más adelante veremos
VACUUM INTOy el comando.backup.
Siguiente paso: migraciones
Conectarse es lo fácil. Lo difícil viene después: hacer evolucionar el esquema con el tiempo sin tener que editar a mano la base de datos en producción. Las migraciones son la forma de convertir un ALTER TABLE en un proceso repetible y versionado — justo lo que veremos en la próxima página.
Preguntas frecuentes
¿Cómo me conecto a una base de datos SQLite desde código?
Le pasas al driver una ruta de archivo y listo. En Python sería sqlite3.connect('app.db'); en Node, new Database('app.db') con better-sqlite3; en Go, sql.Open("sqlite", "app.db"). SQLite no tiene servidor, así que la "conexión" en realidad es abrir un archivo: si no existe, SQLite lo crea por ti.
¿Qué pinta tiene una cadena de conexión de SQLite?
La mayoría de drivers aceptan o bien una ruta normal (./data/app.db) o el formato URI (file:app.db?mode=rwc&cache=shared). El formato URI te permite activar flags como modo solo lectura, caché compartida o bases en memoria con :memory:. JDBC usa jdbc:sqlite:app.db; PDO usa sqlite:app.db.
¿Necesito un pool de conexiones con SQLite?
Normalmente no, al menos no como en Postgres o MySQL. SQLite serializa las escrituras a nivel de base de datos, así que tener varios writers en paralelo no acelera nada. Un pool pequeño sí ayuda con lecturas concurrentes, sobre todo en modo WAL. Muchas aplicaciones funcionan perfectamente con una única conexión compartida, PRAGMA journal_mode=WAL y un busy_timeout razonable.
¿Cómo evito los errores de 'database is locked'?
Configura un busy timeout para que el driver espere en lugar de fallar al instante: PRAGMA busy_timeout = 5000 (en milisegundos). Activa el modo WAL con PRAGMA journal_mode=WAL para que las lecturas no bloqueen las escrituras. Mantén las transacciones cortas y nunca dejes una transacción de escritura abierta mientras haces trabajo lento que no toca la base de datos.