SQLite es uno de los sistemas de gestión de bases de datos más utilizados en el mundo de la programación. Su simplicidad, portabilidad y ausencia de configuración lo convierten en la opción perfecta para aplicaciones pequeñas y medianas, prototipos e incluso para aprender SQL. En esta guía completa, aprenderás a utilizar SQLite con Python para crear, manipular y consultar bases de datos de manera eficiente.
La biblioteca sqlite3 viene incluida en la biblioteca estándar de Python, lo que significa que no necesitas instalar nada más que Python para empezar a trabajar con bases de datos SQLite. Esto hace que SQLite sea la opción ideal para quien está comenzando en el mundo de las bases de datos relacionales.
🔧 ¿Qué es SQLite y por qué usarlo con Python?
SQLite es una base de datos relacional embebida, lo que significa que no requiere un servidor separado para funcionar. A diferencia de bases de datos como MySQL, PostgreSQL o SQL Server, SQLite almacena toda la base de datos en un único archivo en tu sistema de archivos. Esto trae varias ventajas importantes:
En primer lugar, la simplicidad de configuración es una de las mayores fortalezas de SQLite. No es necesario instalar servidores, configurar usuarios o gestionar permisos complejos. Simplemente creas un archivo y empiezas a usarlo. Además, SQLite es extremadamente ligero - la biblioteca completa tiene solo unos pocos megabytes, lo que la convierte en ideal para aplicaciones móviles, programas de escritorio e incluso navegadores web.
Otra ventaja significativa es la portabilidad. Un archivo de base de datos SQLite puede moverse entre diferentes sistemas operativos sin ninguna modificación. Puedes crear la base de datos en Windows, moverla a Linux o Mac y seguir trabajando normalmente. Por último, SQLite ofrece velocidad impresionante para operaciones de lectura y escritura, superando frecuentemente a las bases de datos cliente-servidor en escenarios de uso local.
Para aprender más sobre los fundamentos de SQLite, te recomendamos consultar la documentación oficial de SQLite.
📦 Conectando a la Base de Datos SQLite
El primer paso para trabajar con SQLite en Python es establecer una conexión con la base de datos. Para esto, usamos la biblioteca estándar sqlite3. Veamos cómo hacerlo de diferentes formas:
import sqlite3
Forma más simple de conexión
conexion = sqlite3.connect('mi_base_datos.db')
Para bases de datos en memoria (útil para pruebas)
conexion_memoria = sqlite3.connect(':memory:')
Cerrar la conexión cuando ya no necesites
conexion.close()
Cuando llamas a sqlite3.connect(), Python crea automáticamente el archivo de la base de datos si no existe. Si el archivo ya existe, se abre normalmente. Es importante siempre cerrar la conexión cuando termines de usar la base de datos para evitar fugas de recursos y posibles corrupciones en los datos.
Una práctica muy importante es siempre usar el gestor de contexto (with) para garantizar que las conexiones se cierren correctamente, incluso si ocurren errores durante la ejecución:
import sqlite3
Usando gestor de contexto (recomendado)
with sqlite3.connect('mi_base_datos.db') as conexion:
Tu código aquí
pass
La conexión se cierra automáticamente al salir del bloque
Para entender mejor cómo gestionar conexiones de bases de datos en Python, puedes consultar la documentación oficial del módulo sqlite3.
📋 Creando Tablas y Estructura de la Base de Datos
Ahora que sabes cómo conectarte a la base de datos, el siguiente paso es crear las tablas que almacenarán tus datos. Las tablas son la estructura fundamental de cualquier base de datos relacional y definen cómo se organizarán tus datos.
Para crear tablas, usamos el comando SQL CREATE TABLE. Vamos a crear una tabla de ejemplo para almacenar información de usuarios:
import sqlite3
with sqlite3.connect('usuarios.db') as conexion:
cursor = conexion.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS usuarios (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
edad INTEGER,
fecha_registro TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
print("¡Tabla creada exitosamente!")</code></pre>
En este ejemplo, creamos una tabla con los siguientes campos: id como clave primaria con autoincremento, nombre como texto obligatorio, email como texto único y obligatorio, edad como número entero y fecha_registro que usa el valor predeterminado de SQLite con la fecha y hora actual.
Agreguemos otra tabla para ejemplificar una relación:
cursor.execute("""
CREATE TABLE IF NOT EXISTS publicaciones (
id INTEGER PRIMARY KEY AUTOINCREMENT,
usuario_id INTEGER NOT NULL,
titulo TEXT NOT NULL,
contenido TEXT,
fecha_publicacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (usuario_id) REFERENCES usuarios (id)
)
""")
Esta tabla publicaciones tiene una clave foránea (FOREIGN KEY) que relaciona cada publicación con un usuario de la tabla usuarios. Esto demuestra cómo crear relaciones entre tablas en una base de datos relacional.
W3Schools ofrece un excelente tutorial sobre SQL para quien está comenzando y desea aprender más sobre comandos SQL.
➕ Insertando Datos (Operación CREATE)
Con las tablas creadas, el siguiente paso es aprender a insertar datos en ellas. En SQLite con Python, usamos el comando SQL INSERT INTO para agregar nuevos registros. Veamos diferentes formas de hacer esto:
import sqlite3
with sqlite3.connect('usuarios.db') as conexion:
cursor = conexion.cursor()
#Inserción simple de un registro
cursor.execute("""
INSERT INTO usuarios (nombre, email, edad)
VALUES ('Juan Pérez', '[email protected]', 28)
""")
#Inserción con parámetros (más seguro contra inyección SQL)
nombre = 'María López'
email = '[email protected]'
edad = 32
cursor.execute("""
INSERT INTO usuarios (nombre, email, edad)
VALUES (?, ?, ?)
""", (nombre, email, edad))
#Inserción múltiple de una vez
usuarios = [
('Pedro García', '[email protected]', 25),
('Ana Martínez', '[email protected]', 29),
('Carlos Rodríguez', '[email protected]', 35)
]
cursor.executemany("""
INSERT INTO usuarios (nombre, email, edad)
VALUES (?, ?, ?)
""", usuarios)
#Importante: confirmar los cambios
conexion.commit()
print("¡Datos insertados exitosamente!")</code></pre>
Es fundamental entender el concepto de commit. En SQLite, las operaciones de inserción, actualización y exclusión necesitan ser confirmadas con conexion.commit() para que los cambios se guarden permanentemente en la base de datos. Si no llamas a commit(), los cambios se perderán al cerrar la conexión.
Una observación muy importante es siempre usar parámetros parametrizados (como ? o :nombre) en lugar de interpolar cadenas directamente en tus consultas. Esto previene ataques de Inyección SQL, una de las vulnerabilidades más comunes en aplicaciones que usan bases de datos.
Para más información sobre seguridad en bases de datos, consulta la Inyección SQL de OWASP.
📖 Consultando Datos (Operación READ)
La recuperación de datos es una de las operaciones más importantes en cualquier base de datos. SQLite ofrece varias formas de consultar datos usando el comando SELECT. Exploremos las principales técnicas:
import sqlite3
with sqlite3.connect('usuarios.db') as conexion:
cursor = conexion.cursor()
#Consulta simple - obtener todos los registros
cursor.execute("SELECT * FROM usuarios")
resultados = cursor.fetchall()
for usuario in resultados:
print(usuario)
print("\n" + "="*50 + "\n")
#Consulta con filtro WHERE
cursor.execute("SELECT * FROM usuarios WHERE edad > 30")
mayores = cursor.fetchall()
print("Usuarios mayores de 30 años:")
for usuario in mayores:
print(f" {usuario[1]} - {usuario[2]}")
print("\n" + "="*50 + "\n")
#Consulta retornando solo algunas columnas
cursor.execute("SELECT nombre, email FROM usuarios")
print("Nombres y emails:")
for usuario in cursor.fetchall():
print(f" {usuario[0]}: {usuario[1]}")
print("\n" + "="*50 + "\n")
#Usando LIMIT y OFFSET para paginación
cursor.execute("SELECT * FROM usuarios LIMIT 2 OFFSET 1")
print("Registros 2 y 3:")
for usuario in cursor.fetchall():
print(f" ID: {usuario[0]}, Nombre: {usuario[1]}")</code></pre>
El método fetchall() retorna todos los resultados de la consulta como una lista de tuplas. Para grandes volúmenes de datos, puedes usar fetchone() para obtener un registro a la vez, o fetchmany(tamaño) para obtener una cantidad específica de registros.
Ahora veamos cómo hacer consultas más complejas con ordenación y condiciones:
#Ordenando resultados (ORDER BY)
cursor.execute("SELECT * FROM usuarios ORDER BY nombre ASC")
for usuario in cursor.fetchall():
print(usuario[1])
print("\n")
Usando LIKE para búsqueda parcial
cursor.execute("SELECT * FROM usuarios WHERE nombre LIKE '%Pérez%'")
print("Búsqueda por 'Pérez':")
for usuario in cursor.fetchall():
print(f" {usuario[1]}")
print("\n")
Usando BETWEEN para intervalos
cursor.execute("SELECT * FROM usuarios WHERE edad BETWEEN 25 AND 35")
print("Usuarios entre 25 y 35 años:")
for usuario in cursor.fetchall():
print(f" {usuario[1]}: {usuario[3]} años")
print("\n")
Usando IN para múltiples valores
cursor.execute("SELECT * FROM usuarios WHERE id IN (1, 3, 5)")
print("Usuarios con ID 1, 3 o 5:")
for usuario in cursor.fetchall():
print(f" {usuario[0]}: {usuario[1]}")
Para aprender más sobre consultas SQL avanzadas, el sitio GeeksforGeeks ofrece tutoriales completos y detallados.
✏️ Actualizando Datos (Operación UPDATE)
La actualización de datos existentes es otra operación fundamental. El comando UPDATE de SQL permite modificar registros ya existentes en la base de datos. Es crucial usar la cláusula WHERE para especificar qué registros deben actualizarse, de lo contrario puedes terminar modificando todos los registros de la tabla accidentalmente.
import sqlite3
with sqlite3.connect('usuarios.db') as conexion:
cursor = conexion.cursor()
#Actualización simple
cursor.execute("""
UPDATE usuarios
SET edad = 30
WHERE id = 1
""")
#Actualización con múltiples campos
cursor.execute("""
UPDATE usuarios
SET nombre = 'Juan Pérez Santos', edad = 29
WHERE email = '[email protected]'
""")
#Actualización basada en cálculos
cursor.execute("""
UPDATE usuarios
SET edad = edad + 1
WHERE edad IS NOT NULL
""")
conexion.commit()
print("¡Datos actualizados exitosamente!")
#Verificando los cambios
cursor.execute("SELECT * FROM usuarios WHERE id = 1")
usuario = cursor.fetchone()
print(f"Usuario actualizado: {usuario}")</code></pre>
Siempre que sea posible, usa la clave primaria (id) o campos únicos en la cláusula WHERE para garantizar que estás actualizando exactamente el registro deseado. Esto previene comportamientos inesperados y garantiza la integridad de tus datos.
🗑️ Excluyendo Datos (Operación DELETE)
La exclusión de datos debe realizarse con mucho cuidado. El comando DELETE de SQL elimina registros permanentemente de la base de datos, y una operación mal ejecutada puede resultar en la pérdida de datos importantes. Al igual que UPDATE, la cláusula WHERE es esencial para especificar qué registros serán eliminados.
#Exclusión simple - eliminar un registro específico
cursor.execute("DELETE FROM usuarios WHERE id = 5")
Exclusión basada en condición
cursor.execute("DELETE FROM usuarios WHERE edad < 18")
Exclusión de todos los registros (¡cuidado!)
cursor.execute("DELETE FROM usuarios") # ¡Esto lo borra todo!
conexion.commit()
print("¡Datos eliminados exitosamente!")
Una práctica de seguridad muy importante es siempre hacer un SELECT antes de un DELETE para confirmar qué registros serán afectados. Esto ayuda a evitar eliminaciones accidentales de datos importantes.
Para operaciones de DELETE en gran escala, considera usar transacciones para garantizar la integridad de los datos. Puedes aprender más sobre esto en la documentación de transacciones de SQLite.
🔄 Transacciones y Control de Datos
Las transacciones son un concepto fundamental en bases de datos relacionales que permiten agrupar múltiples operaciones en una única unidad lógica de trabajo. SQLite soporta transacciones ACID (Atomicidad, Consistencia, Aislamiento, Durabilidad), garantizando que tus operaciones se ejecuten de forma segura.
import sqlite3
from sqlite3 import Error
def crear_conexion(db_archivo):
try:
conexion = sqlite3.connect(db_archivo)
return conexion
except Error as e:
print(f"Error al conectar: {e}")
return None
def ejecutar_transaccion(conexion):
try:
cursor = conexion.cursor()
#Iniciar transacción explícitamente (opcional en SQLite)
# conexion.isolation_level = None #Modo autocommit
#Insertar nuevo usuario
cursor.execute("""
INSERT INTO usuarios (nombre, email, edad)
VALUES ('Nuevo Usuario', '[email protected]', 25)
""")
#Insertar publicaciones para este usuario
usuario_id = cursor.lastrowid
cursor.executemany("""
INSERT INTO publicaciones (usuario_id, titulo, contenido)
VALUES (?, ?, ?)
""", [
(usuario_id, 'Primera Publicación', 'Contenido de la primera publicación'),
(usuario_id, 'Segunda Publicación', 'Contenido de la segunda publicación')
])
#Confirmar la transacción
conexion.commit()
print("¡Transacción completada exitosamente!")
except Error as e:
#En caso de error, deshacer todas las operaciones
conexion.rollback()
print(f"Error en la transacción: {e}")
Usando savepoint para transacciones parciales
with sqlite3.connect('usuarios.db') as conexion:
cursor = conexion.cursor()
cursor.execute("SAVEPOINT inicio")
try:
cursor.execute("DELETE FROM usuarios WHERE id = 1")
#Si algo sale mal aquí, podemos volver al savepoint
# cursor.execute("ROLLBACK TO SAVEPOINT inicio")
conexion.commit()
except Error as e:
cursor.execute("ROLLBACK TO SAVEPOINT inicio")
conexion.commit()</code></pre>
El uso de commit() confirma todas las operaciones dentro de la transacción, mientras que rollback() deshace todos los cambios si algo sale mal. SQLite también soporta SAVEPOINT para crear puntos de recuperación dentro de una transacción mayor.
📊 Funciones de Agregación y Group By
SQLite ofrece varias funciones de agregación que permiten realizar cálculos en conjuntos de datos. Estas funciones son extremadamente útiles para análisis e informes. Exploremos las principales:
#Conteo de registros
cursor.execute("SELECT COUNT(*) FROM usuarios")
total = cursor.fetchone()[0]
print(f"Total de usuarios: {total}")
Suma de valores
cursor.execute("SELECT SUM(edad) FROM usuarios")
suma_edades = cursor.fetchone()[0]
print(f"Suma de las edades: {suma_edades}")
Promedio
cursor.execute("SELECT AVG(edad) FROM usuarios")
promedio_edad = cursor.fetchone()[0]
print(f"Promedio de edad: {promedio_edad:.2f}")
Menor y mayor valor
cursor.execute("SELECT MIN(edad), MAX(edad) FROM usuarios")
min_max = cursor.fetchone()
print(f"Menor edad: {min_max[0]}, Mayor edad: {min_max[1]}")
Agrupando resultados con GROUP BY
cursor.execute("""
SELECT edad, COUNT(*) as total
FROM usuarios
WHERE edad IS NOT NULL
GROUP BY edad
ORDER BY total DESC
""")
print("\nUsuarios por edad:")
for fila in cursor.fetchall():
print(f" Edad {fila[0]}: {fila[1]} usuario(s)")
Filtrando grupos con HAVING
cursor.execute("""
SELECT edad, COUNT() as total
FROM usuarios
GROUP BY edad
HAVING COUNT() > 1
""")
print("\nEdades con más de 1 usuario:")
for fila in cursor.fetchall():
print(f" Edad {fila[0]}: {fila[1]} usuarios")
Las funciones GROUP BY y HAVING son herramientas poderosas para el análisis de datos. Mientras que WHERE filtra filas antes de la agregación, HAVING filtra grupos después de la agregación. Esto permite crear informes complejos y resumidos.
Para más detalles sobre funciones de agregación, visita la documentación oficial de SQLite sobre funciones de agregación.
🔗 Consultas con JOIN
Una de las funcionalidades más poderosas de las bases de datos relacionales es la capacidad de combinar datos de múltiples tablas usando JOIN. Veamos cómo hacerlo con SQLite y Python:
#INNER JOIN - retorna solo registros con correspondencia en ambas tablas
cursor.execute("""
SELECT
u.nombre,
u.email,
p.titulo,
p.fecha_publicacion
FROM usuarios u
INNER JOIN publicaciones p ON u.id = p.usuario_id
""")
print("Publicaciones con información del autor:")
for fila in cursor.fetchall():
print(f" {fila[0]} ({fila[1]}): {fila[2]} - {fila[3]}")
LEFT JOIN - retorna todos los registros de la tabla izquierda
cursor.execute("""
SELECT
u.nombre,
COUNT(p.id) as total_publicaciones
FROM usuarios u
LEFT JOIN publicaciones p ON u.id = p.usuario_id
GROUP BY u.id, u.nombre
""")
print("\nCantidad de publicaciones por usuario:")
for fila in cursor.fetchall():
print(f" {fila[0]}: {fila[1]} publicación(es)")
JOIN con múltiples condiciones
cursor.execute("""
SELECT
u.nombre,
p.titulo
FROM usuarios u
JOIN publicaciones p ON u.id = p.usuario_id
WHERE u.edad > 25
ORDER BY p.fecha_publicacion DESC
""")
print("\nPublicaciones de usuarios mayores de 25 años:")
for fila in cursor.fetchall():
print(f" {fila[0]}: {fila[1]}")
El INNER JOIN retorna solo registros que existen en ambas tablas relacionadas. El LEFT JOIN retorna todos los registros de la tabla izquierda, incluso si no tienen correspondencia en la tabla derecha. Elegir el tipo correcto de JOIN depende de tu necesidad específica de negocio.
Para profundizar tus conocimientos sobre JOINs, el tutorial de DigitalOcean sobre JOINs es una excelente fuente.
⚠️ Manejo de Errores y Buenas Prácticas
Al trabajar con bases de datos, es esencial implementar un manejo de errores robusto para garantizar la integridad de los datos y la estabilidad de la aplicación. Veamos cómo hacer esto correctamente:
import sqlite3
from sqlite3 import Error
def crear_tabla_con_errores(conexion, sql):
try:
cursor = conexion.cursor()
cursor.execute(sql)
print("¡Tabla creada exitosamente!")
except Error as e:
print(f"Error al crear tabla: {e}")
def insertar_usuario_seguro(conexion, nombre, email, edad):
try:
cursor = conexion.cursor()
cursor.execute("""
INSERT INTO usuarios (nombre, email, edad)
VALUES (?, ?, ?)
""", (nombre, email, edad))
conexion.commit()
return cursor.lastrowid
except Error as e:
conexion.rollback()
print(f"Error al insertar usuario: {e}")
return None
Manejo con verificación de datos
def insertar_usuario_validado(conexion, nombre, email, edad):
Validación de datos antes de la inserción
if not nombre or not email:
print("¡Error: Nombre y email son obligatorios!")
return None
if edad is not None and (edad < 0 or edad > 150):
print("¡Error: Edad inválida!")
return None
return insertar_usuario_seguro(conexion, nombre, email, edad)
Ejemplo de uso con manejo de errores
try:
with sqlite3.connect('usuarios.db') as conexion:
usuario_id = insertar_usuario_validado(conexion, "Prueba", "[email protected]", 25)
if usuario_id:
print(f"Usuario insertado con ID: {usuario_id}")
except Error as e:
print(f"Error de base de datos: {e}")
Siempre usa manejo de errores para capturar y tratar excepciones que pueden ocurrir durante operaciones de base de datos. Usa try/except para capturar errores específicos e implementa rollback() cuando una operación falle para evitar datos parcialmente insertados o corruptos.
🛠️ Creando Clases de Acceso a Datos (DAO)
Para aplicaciones más grandes, es una buena práctica crear una capa de abstracción para operaciones de base de datos. Esto facilita el mantenimiento, las pruebas y la reutilización de código. Creemos una clase DAO simple:
import sqlite3
from sqlite3 import Error
from typing import List, Optional, Tuple
class UsuarioDAO:
def init(self, db_ruta: str):
self.db_ruta = db_ruta
def _conectar(self) -> sqlite3.Connection:
return sqlite3.connect(self.db_ruta)
def crear_tabla(self):
with self._conectar() as conexion:
cursor = conexion.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS usuarios (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
edad INTEGER
)
""")
conexion.commit()
def insertar(self, nombre: str, email: str, edad: Optional[int] = None) -> Optional[int]:
try:
with self._conectar() as conexion:
cursor = conexion.cursor()
cursor.execute("""
INSERT INTO usuarios (nombre, email, edad)
VALUES (?, ?, ?)
""", (nombre, email, edad))
conexion.commit()
return cursor.lastrowid
except Error as e:
print(f"Error al insertar: {e}")
return None
def buscar_por_id(self, id: int) -> Optional[Tuple]:
with self._conectar() as conexion:
cursor = conexion.cursor()
cursor.execute("SELECT * FROM usuarios WHERE id = ?", (id,))
return cursor.fetchone()
def buscar_todos(self) -> List[Tuple]:
with self._conectar() as conexion:
cursor = conexion.cursor()
cursor.execute("SELECT * FROM usuarios ORDER BY nombre")
return cursor.fetchall()
def actualizar(self, id: int, nombre: str, email: str, edad: Optional[int]) -> bool:
try:
with self._conectar() as conexion:
cursor = conexion.cursor()
cursor.execute("""
UPDATE usuarios
SET nombre = ?, email = ?, edad = ?
WHERE id = ?
""", (nombre, email, edad, id))
conexion.commit()
return cursor.rowcount > 0
except Error as e:
print(f"Error al actualizar: {e}")
return False
def eliminar(self, id: int) -> bool:
try:
with self._conectar() as conexion:
cursor = conexion.cursor()
cursor.execute("DELETE FROM usuarios WHERE id = ?", (id,))
conexion.commit()
return cursor.rowcount > 0
except Error as e:
print(f"Error al eliminar: {e}")
return False
Ejemplo de uso
dao = UsuarioDAO('usuarios.db')
dao.crear_tabla()
Insertar
id = dao.insertar('Juan', '[email protected]', 28)
print(f"Usuario insertado con ID: {id}")
Buscar todos
usuarios = dao.buscar_todos()
for u in usuarios:
print(f"ID: {u[0]}, Nombre: {u[1]}, Email: {u[2]}, Edad: {u[3]}")
Actualizar
dao.actualizar(id, 'Juan Pérez', '[email protected]', 29)
Eliminar
dao.eliminar(id)
Esta estructura de DAO (Data Access Object) es muy utilizada en aplicaciones Python para separar la lógica de acceso a datos de la lógica de negocio. Esto hace que el código sea más modular, testeable y fácil de mantener.
🚀 Conclusión y Próximos Pasos
En esta guía completa, has aprendido desde los conceptos básicos de SQLite hasta técnicas más avanzadas como transacciones, JOINs y creación de clases de acceso a datos. SQLite con Python es una combinación poderosa para el desarrollo de aplicaciones que necesitan una base de datos ligera y eficiente.
Ahora que dominas lo básico, se recomienda explorar temas avanzados como: migraciones de base de datos usando herramientas como Alembic, ORMs como SQLAlchemy para abstraer completamente el SQL, y consultas asíncronas con librerías como aiosqlite para aplicaciones que necesitan alto rendimiento.
Recuerda siempre seguir buenas prácticas de seguridad, como usar parámetros parametrizados para prevenir inyección SQL, hacer respaldos regulares de tus bases de datos e implementar manejo de errores adecuado en todas las operaciones de base de datos.
Para continuar aprendiendo, puedes explorar otros artículos de nuestro blog sobre Python, como Python Requests: Guía Completa de Solicitudes HTTP que enseña cómo hacer solicitudes HTTP, o Python DateTime: Manipulación de Fechas para trabajar con fechas y horarios.