Lección 13 de 16 · Nivel 4: Experto
Bases de datos con Python: SQLite y SQL desde cero
- ⏱ 10 min de lectura
- ❓ 3 preguntas de repaso
- ✍️ Ejercicio con solución
- 📥 Plantilla descargable
En esta lección
- Crear una base de datos SQLite desde Python, sin instalar nada.
- Escribir consultas SQL: SELECT, WHERE, ORDER BY, GROUP BY y JOIN.
- Insertar y modificar datos de forma segura con parámetros (y evitar la inyección SQL).
- Pasar los resultados de una consulta a pandas para analizarlos.
Usarás el mismo archivo de ventas de la lección 11.
- ventas.csv · 10 ventas de ejemplo
Bienvenido al Nivel 4: Experto. Ya sabes programar, analizar datos con pandas y conectarte a APIs. Ahora sumarás cuatro habilidades que piden casi todas las ofertas de trabajo de datos y desarrollo: bases de datos con SQL, gráficos, extracción de datos de páginas web y una introducción al aprendizaje automático. Empezamos por SQL, el lenguaje con el que se consultan las bases de datos de casi todas las empresas del mundo.
Por qué una base de datos y no un CSV
Un CSV está bien para intercambiar datos, pero se queda corto cuando los datos crecen o cuando varios programas los usan a la vez: hay que leerlo entero para buscar una fila, no impide errores (como texto donde va un número) y no relaciona tablas. Una base de datos guarda los datos con tipos definidos, busca rápido aunque haya millones de filas y permite consultas potentes con SQL.
SQLite es una base de datos completa que vive en un solo archivo y viene incluida en Python con el módulo sqlite3. La usan aplicaciones como navegadores y teléfonos, y el SQL que aprendas aquí es casi idéntico al de PostgreSQL, MySQL o SQL Server.
Crear la base y cargar los datos
import csv
import sqlite3
con = sqlite3.connect("tienda.db") # crea el archivo si no existe
cur = con.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS ventas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
fecha TEXT NOT NULL,
region TEXT NOT NULL,
vendedor TEXT NOT NULL,
producto TEXT NOT NULL,
unidades INTEGER NOT NULL,
precio REAL NOT NULL
)
""")
with open("ventas.csv", encoding="utf-8") as f:
filas = [(r["fecha"], r["region"], r["vendedor"], r["producto"],
int(r["unidades"]), float(r["precio"])) for r in csv.DictReader(f)]
cur.executemany(
"INSERT INTO ventas (fecha, region, vendedor, producto, unidades, precio) "
"VALUES (?, ?, ?, ?, ?, ?)",
filas,
)
con.commit()
print(cur.execute("SELECT COUNT(*) FROM ventas").fetchone()[0], "ventas cargadas")
10 ventas cargadas
CREATE TABLEdefine las columnas y su tipo.PRIMARY KEYda a cada fila un identificador único yNOT NULLimpide dejar datos vacíos.- Los
?son parámetros: los valores van aparte, en la tupla. Más abajo verás por qué esto es tan importante. commit()guarda los cambios. Sin él, al cerrar el programa se pierden.
Si vuelves a correr el programa, las 10 ventas se insertan de nuevo y tendrás 20. Para empezar de cero, borra el archivo tienda.db o agrega cur.execute("DELETE FROM ventas") antes de insertar.
Consultar: SELECT, WHERE y ORDER BY
for fila in cur.execute(
"SELECT fecha, vendedor, unidades FROM ventas "
"WHERE region = 'Norte' ORDER BY fecha"
):
print(fila)
('2026-01-05', 'Ana', 120)
('2026-01-20', 'Ana', 95)
('2026-02-21', 'Ana', 40)
('2026-03-18', 'Ana', 55)
La estructura de una consulta siempre sigue este orden:
SELECT columnas o cálculos
FROM tabla
WHERE filtro de filas
GROUP BY agrupación
HAVING filtro de grupos
ORDER BY orden
LIMIT cantidad máxima de filas
Resumir: GROUP BY
Es el equivalente de una tabla dinámica, o del groupby de pandas:
consulta = """
SELECT region,
SUM(unidades * precio) AS total,
COUNT(*) AS n_ventas
FROM ventas
GROUP BY region
ORDER BY total DESC
"""
for region, total, n in cur.execute(consulta):
print(f"{region:<8}{total:>10,.2f}{n:>4}")
Norte 4,112.50 4
Sur 2,520.00 3
Centro 1,687.50 2
875.00 1
¿Y esa última fila sin nombre? Es la venta con la región vacía que ya encontraste al limpiar datos en la lección 11. SQL tiene funciones para tratarla al consultar: COALESCE(NULLIF(region, ''), 'Sin región') convierte el texto vacío en "Sin región".
1. ¿Qué cláusula usas para quedarte solo con las regiones cuyo total supera 2.000, después de agrupar?
WHERE filtra filas antes de agrupar; HAVING filtra los grupos ya calculados, como los totales por región.
Relacionar tablas: JOIN
La comisión de cada vendedor está en otra tabla. Con JOIN las combinas por la columna en común, como hiciste con BUSCARV en Excel:
cur.execute("CREATE TABLE IF NOT EXISTS vendedores "
"(nombre TEXT PRIMARY KEY, comision REAL NOT NULL)")
cur.executemany("INSERT OR REPLACE INTO vendedores VALUES (?, ?)",
[("Ana", 0.05), ("Luis", 0.04), ("Eva", 0.06)])
con.commit()
consulta = """
SELECT v.vendedor,
SUM(v.unidades * v.precio) AS total,
SUM(v.unidades * v.precio) * d.comision AS comision
FROM ventas AS v
JOIN vendedores AS d ON d.nombre = v.vendedor
GROUP BY v.vendedor
ORDER BY comision DESC
"""
for nombre, total, comision in cur.execute(consulta):
print(f"{nombre:<6}{total:>10,.2f}{comision:>9,.2f}")
Ana 4,112.50 205.62
Eva 2,562.50 153.75
Luis 2,520.00 100.80
JOIN (o INNER JOIN) solo devuelve las filas que tienen pareja en ambas tablas. Si un vendedor no estuviera en la tabla vendedores, sus ventas desaparecerían del resultado. Para conservarlas, usa LEFT JOIN: mantiene todas las filas de la primera tabla y deja vacío lo que no encuentra.
2. Una vendedora nueva, Rosa, tiene ventas pero no está en la tabla de comisiones. ¿Qué JOIN mantiene sus ventas en el informe?
LEFT JOIN conserva todas las filas de la tabla de la izquierda (ventas) y pone NULL en la comisión de Rosa, que luego puedes detectar.
Parámetros: la regla de seguridad número uno
Nunca construyas una consulta pegando texto que viene del usuario:
# ❌ PELIGROSO: el texto del usuario se mezcla con el SQL
nombre = input("Vendedor: ")
cur.execute(f"SELECT * FROM ventas WHERE vendedor = '{nombre}'")
Si alguien escribe Luis' OR '1'='1, la consulta se convierte en "trae todas las ventas", y con otras entradas podría borrar tablas. Se llama inyección SQL y es una de las vulnerabilidades más explotadas de la historia. Con parámetros, el valor nunca se interpreta como SQL:
# ✅ SEGURO
def ventas_de(vendedor):
return cur.execute(
"SELECT COUNT(*), SUM(unidades) FROM ventas WHERE vendedor = ?",
(vendedor,),
).fetchone()
print(ventas_de("Luis"))
print(ventas_de("Luis' OR '1'='1"))
(3, 280)
(0, None)
Fíjate en la coma de (vendedor,): los parámetros siempre van en una tupla, aunque sea uno solo.
Modificar datos: UPDATE y DELETE
with con: # hace commit al terminar (o deshace todo si hay un error)
con.execute("UPDATE ventas SET region = 'Centro' WHERE region = ''")
La venta sin región era de Eva, que trabaja en Centro, así que la corregimos en la base. Revisa siempre el WHERE antes de ejecutar un UPDATE o un DELETE: sin él, el cambio se aplica a todas las filas de la tabla. Un buen hábito es ejecutar primero un SELECT con el mismo WHERE para ver qué filas se van a modificar.
De SQL a pandas
Lo mejor de ambos mundos: SQL para filtrar y resumir en la base, pandas para seguir analizando o graficar:
import pandas as pd
df = pd.read_sql(
"SELECT region, SUM(unidades * precio) AS total FROM ventas GROUP BY region",
con,
)
print(df)
con.close()
region total
0 Centro 2562.5
1 Norte 4112.5
2 Sur 2520.0
Para guardar un DataFrame como tabla nueva, usa df.to_sql("resumen", con, if_exists="replace", index=False).
3. ¿Por qué se usan los signos ? en las consultas en lugar de f-strings?
Con parámetros, el motor trata el valor solo como dato. Es la defensa básica contra la inyección SQL.
Ejercicio práctico: consultas de negocio
Con la base tienda.db ya cargada, escribe consultas que respondan:
- Unidades e ingresos por producto, de mayor a menor ingreso.
- La venta más grande (en dinero) de cada vendedor.
- Todas las ventas de más de 1.000, ordenadas de mayor a menor.
Ver solución
print(cur.execute("""
SELECT producto, SUM(unidades), SUM(unidades * precio) AS ingresos
FROM ventas GROUP BY producto ORDER BY ingresos DESC
""").fetchall())
# [('Café', 420, 5250.0), ('Té', 280, 2520.0), ('Chocolate', 95, 1425.0)]
print(cur.execute("""
SELECT vendedor, MAX(unidades * precio) FROM ventas GROUP BY vendedor
""").fetchall())
# [('Ana', 1500.0), ('Eva', 937.5), ('Luis', 990.0)]
for fila in cur.execute("""
SELECT fecha, vendedor, producto, unidades * precio AS importe
FROM ventas WHERE unidades * precio > 1000 ORDER BY importe DESC
"""):
print(fila)
# ('2026-01-05', 'Ana', 'Café', 1500.0)
# ('2026-01-20', 'Ana', 'Café', 1187.5)
Si tus resultados no coinciden, comprueba que no hayas cargado las ventas dos veces: SELECT COUNT(*) FROM ventas debe dar 10.
Resumen
- SQLite viene con Python: una base de datos completa en un archivo, con el módulo
sqlite3. - SELECT … FROM … WHERE … GROUP BY … HAVING … ORDER BY es la estructura de casi toda consulta.
- JOIN combina tablas por una columna común; LEFT JOIN conserva las filas sin pareja.
- Usa siempre parámetros (
?) para los valores: evita la inyección SQL. pd.read_sqllleva el resultado de una consulta a pandas.
En la próxima lección convertirás estos resultados en gráficos claros con matplotlib: barras, líneas y un pequeño panel listo para un informe.
Crea tu cuenta en 30 segundos, marca tus lecciones y descarga el certificado al terminar.