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
  1. Por qué una base de datos y no un CSV
  2. Crear la base y cargar los datos
  3. Consultar: SELECT, WHERE y ORDER BY
  4. Resumir: GROUP BY
  5. Relacionar tablas: JOIN
  6. Parámetros: la regla de seguridad número uno
  7. Modificar datos: UPDATE y DELETE
  8. De SQL a pandas
  9. Resumen
Lo que lograrás 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.
📥 Practica esta lección

Usarás el mismo archivo de ventas de la lección 11.

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 TABLE define las columnas y su tipo. PRIMARY KEY da a cada fila un identificador único y NOT NULL impide 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.
Ejecuta la carga una sola vez

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 total > 2000
  • HAVING total > 2000
  • ORDER BY total > 2000

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?

  • INNER JOIN
  • LEFT JOIN, con ventas como primera tabla
  • Ninguno: hay que borrar sus ventas.

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?

  • Porque las f-strings no funcionan con SQLite.
  • Para que los valores se envíen separados del SQL y no puedan alterar la consulta (inyección SQL).
  • Para que la consulta sea más corta.

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:

  1. Unidades e ingresos por producto, de mayor a menor ingreso.
  2. La venta más grande (en dinero) de cada vendedor.
  3. 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_sql lleva 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.

🎓 Guarda tu avance y obtén tu certificado gratis

Crea tu cuenta en 30 segundos, marca tus lecciones y descarga el certificado al terminar.

Inscribirme gratis