lección 4
Python habla con bases de datos: conectar, consultar, guardar
El patrón fundamental Python↔BD con sqlite3 (incluido en Python): conectar, consultar con SQL, traer resultados a un DataFrame con pd.read_sql y escribir con df.to_sql.
⏱ 50 min
### El puente entre dos mundos
Hasta ahora has usado SQL en un cliente de base de datos y Python en scripts separados. Pero en un pipeline real, necesitas que Python HABLE con la base de datos: leer datos, transformarlos y escribir los resultados de vuelta. Ese puente lo construyes con un conector de base de datos (aquí, la librería sqlite3 que ya viene con Python) y con pandas.read_sql.
Antes de escribir una sola línea, aclaremos una posible confusión, porque hasta ahora has tocado varias bases de datos y conviene poner cada una en su sitio. Aprendiste a hablar SQL con DuckDB (Skills 7 y 8) porque es cero-setup y está pensado para análisis: perfecto para practicar SELECT, JOIN y window functions sobre archivos. Pero en una empresa, la base de datos que guarda el día a día del negocio (los pedidos que entran, los usuarios que se registran) suele ser una base operacional como PostgreSQL, esa que levantaste con Docker en la Skill 6. Son mundos distintos: DuckDB es el analista, PostgreSQL es el operario.
Y aquí, en esta lección, vamos a usar una tercera: sqlite3. ¿Por qué una más? Porque lo que quiero que aprendas no es una base de datos concreta, sino el PATRÓN universal: Python se conecta a una base de datos, ejecuta SQL, trae los resultados a un DataFrame y escribe resultados de vuelta. Ese patrón es idéntico en sqlite3, en PostgreSQL y en cualquier otra. sqlite3 tiene una ventaja brutal para aprender: viene incluida en Python (no instalas nada) y no necesita levantar ningún servidor. Practicas el concepto sin fricción, y luego lo trasladas tal cual a PostgreSQL cambiando solo la forma de conectar.
La analogía: piensa en un restaurante. SQL es el idioma que habla el chef (la base de datos). Python es el camarero (tu programa). El conector de la base de datos es el protocolo de comunicación entre el comedor y la cocina: el camarero escribe el pedido en una comanda (query), se la pasa al chef, y recoge los platos (resultados). Sin ese conector, el camarero no podría comunicarse con el chef. sqlite3, psycopg2 (PostgreSQL) o SQLAlchemy son distintas marcas de ese mismo servicio de comandas.
### sqlite3 ya viene con Python: cero instalación
SQLite es una base de datos completa que vive en un solo archivo (o incluso solo en memoria). No hay servidor que arrancar, ni usuario, ni contraseña, ni puerto. La librería sqlite3 forma parte de la biblioteca estándar de Python desde hace más de una década, así que si tienes Python, ya tienes sqlite3. No hay pip install que valga: simplemente lo importas.
1import sqlite323# Opción A: base de datos en un archivo (persiste en disco)4# conn = sqlite3.connect('mi_base.db')56# Opción B: base de datos en memoria (desaparece al cerrar el programa)7# Perfecta para aprender y para tests: siempre empieza limpia8conn = sqlite3.connect(':memory:')910# Verificar que la conexión funciona11cursor = conn.execute('SELECT sqlite_version()')12print('Conectado a SQLite version:', cursor.fetchone()[0])1314conn.close()
Tu "hola mundo" con sqlite3: conectar y preguntar la versión
Consejo de senior: usa :memory: para practicar y para tus tests unitarios. Cada ejecución arranca con una base de datos vacía, así que tus pruebas son deterministas y no arrastran basura de ejecuciones anteriores. Cuando quieras que los datos sobrevivan, cambia :memory: por un nombre de archivo. Ese es el único cambio.
### Crear una tabla e insertar datos de ejemplo
Para practicar el patrón necesitamos una base de datos con algo dentro. Vamos a crear una tabla y meter unas filas, todo con el SQL que ya conoces. Fíjate en dos detalles nuevos: el commit (confirmar los cambios) y el cursor (el objeto que ejecuta las sentencias).
1import sqlite323conn = sqlite3.connect(':memory:')45# Crear una tabla (mismo SQL que en PostgreSQL o DuckDB)6conn.execute('''7 CREATE TABLE clientes (8 id INTEGER PRIMARY KEY,9 nombre TEXT NOT NULL,10 ciudad TEXT,11 total_gastado REAL12 )13''')1415# Insertar datos de ejemplo (varias filas de una vez con executemany)16clientes = [17 (1, 'Ana', 'Madrid', 5400.0),18 (2, 'Carlos', 'Barcelona', 1200.0),19 (3, 'Lucía', 'Madrid', 8900.0),20 (4, 'Pedro', 'Sevilla', 320.0),21 (5, 'María', 'Barcelona', 6100.0),22]23conn.executemany(24 'INSERT INTO clientes (id, nombre, ciudad, total_gastado) VALUES (?, ?, ?, ?)',25 clientes26)2728# CONFIRMAR los cambios: sin commit, no se guardan29conn.commit()3031# Comprobar cuántas filas hay32n = conn.execute('SELECT COUNT(*) FROM clientes').fetchone()[0]33print(f'Insertados {n} clientes')3435conn.close()
CREATE TABLE + INSERT con sqlite3: el SQL es el mismo que ya dominas
El error más común con sqlite3: olvidar conn.commit(). Si insertas o actualizas datos y no haces commit, al cerrar la conexión los cambios se esfuman sin avisar. Regla: toda operación que MODIFICA datos (INSERT, UPDATE, DELETE, CREATE) debe terminar en un commit. Las lecturas (SELECT) no necesitan commit.
### Leer datos a un DataFrame: pandas.read_sql()
Aquí es donde todo se une: escribes SQL (que ya dominas) y Pandas te devuelve un DataFrame. Es como tener lo mejor de ambos mundos: la potencia de SQL para filtrar y agregar en la base de datos, y la flexibilidad de Pandas para transformar en Python. pd.read_sql recibe una query y una conexión, y te devuelve la tabla ya montada en un DataFrame.
1import sqlite32import pandas as pd34conn = sqlite3.connect(':memory:')5conn.execute('''6 CREATE TABLE clientes (7 id INTEGER PRIMARY KEY, nombre TEXT, ciudad TEXT, total_gastado REAL8 )9''')10conn.executemany(11 'INSERT INTO clientes VALUES (?, ?, ?, ?)',12 [13 (1, 'Ana', 'Madrid', 5400.0),14 (2, 'Carlos', 'Barcelona', 1200.0),15 (3, 'Lucía', 'Madrid', 8900.0),16 (4, 'Pedro', 'Sevilla', 320.0),17 (5, 'María', 'Barcelona', 6100.0),18 ]19)20conn.commit()2122# Leer una tabla completa a un DataFrame23clientes = pd.read_sql('SELECT * FROM clientes', conn)24print(f'Clientes: {len(clientes)} filas')2526# Leer con filtros y agregación (la base de datos hace el trabajo pesado)27resumen = pd.read_sql('''28 SELECT ciudad,29 COUNT(*) AS num_clientes,30 SUM(total_gastado) AS gasto_total31 FROM clientes32 GROUP BY ciudad33 HAVING SUM(total_gastado) > 100034 ORDER BY gasto_total DESC35''', conn)3637print(resumen)3839conn.close()
read_sql combina SQL + Pandas: filtra y agrega en la DB, transforma en Python
Consejo de senior: filtra en la base de datos, transforma en Pandas. No hagas SELECT * de una tabla de 10M filas para luego filtrar en Python. Deja que el motor SQL haga lo que hace bien (filtrar, agregar, ordenar) y usa Pandas para lo que SQL no puede (limpieza, pivots complejos, merge con archivos locales). Y cuando el orden importe, pon SIEMPRE un ORDER BY: es la única forma de que el resultado sea reproducible.
### Escribir datos: DataFrame.to_sql()
El camino inverso: tomas un DataFrame transformado y lo guardas en la base de datos. Esto es fundamental para pipelines ETL: lees datos crudos, los limpias en Python y los escribes en una tabla "limpia" que consulta el equipo de BI. Con sqlite3 el método es exactamente el mismo que usarías contra PostgreSQL: df.to_sql.
1import sqlite32import pandas as pd34conn = sqlite3.connect(':memory:')56# DataFrame con datos ya procesados7resumen = pd.DataFrame({8 'mes': ['2024-01', '2024-02', '2024-03'],9 'total_ventas': [125000, 148000, 132000],10 'num_pedidos': [450, 520, 480],11 'ticket_medio': [277.8, 284.6, 275.0]12})1314# Escribir el DataFrame en una tabla de la base de datos15resumen.to_sql(16 'resumen_mensual', # nombre de la tabla17 conn, # la conexión sqlite318 if_exists='replace', # 'fail', 'replace', 'append'19 index=False # no guardar el índice de Pandas como columna20)2122print('Datos escritos correctamente')2324# Verificar leyendo de vuelta (ORDER BY para salida determinista)25check = pd.read_sql('SELECT * FROM resumen_mensual ORDER BY mes', conn)26print(check)2728conn.close()
to_sql escribe un DataFrame directamente en una tabla de la base de datos
- if_exists="fail" — Error si la tabla ya existe (por defecto). Seguro pero poco práctico.
- if_exists="replace" — Borra la tabla y la recrea. Útil para tablas de resumen que se regeneran.
- if_exists="append" — Añade filas a la tabla existente. Cuidado con duplicados.
### Consultas parametrizadas: nunca concatenes strings
Cuando una query depende de un valor variable (una ciudad, un importe mínimo, un id que llega de fuera), la tentación es construir el SQL pegando strings. NO lo hagas: es la puerta de entrada al SQL injection, uno de los agujeros de seguridad más antiguos y peligrosos. La forma correcta es dejar huecos (placeholders) y pasar los valores por separado, para que el motor los trate como datos y nunca como código.
1import sqlite32import pandas as pd34conn = sqlite3.connect(':memory:')5conn.execute('CREATE TABLE clientes (id INTEGER, nombre TEXT, ciudad TEXT, total_gastado REAL)')6conn.executemany('INSERT INTO clientes VALUES (?, ?, ?, ?)', [7 (1, 'Ana', 'Madrid', 5400.0),8 (2, 'Carlos', 'Barcelona', 1200.0),9 (3, 'Lucía', 'Madrid', 8900.0),10])11conn.commit()1213# CORRECTO: placeholders ? y los valores en una tupla aparte14ciudad = 'Madrid'15minimo = 2000.016query = '''17 SELECT nombre, total_gastado18 FROM clientes19 WHERE ciudad = ? AND total_gastado >= ?20 ORDER BY total_gastado DESC21'''22df = pd.read_sql(query, conn, params=(ciudad, minimo))23print(df)2425# INCORRECTO (NO lo hagas): f"... WHERE ciudad = '{ciudad}'"26# Si 'ciudad' viniera de un formulario, alguien podría inyectar SQL malicioso.2728conn.close()
sqlite3 usa ? como placeholder; los valores van en params, nunca en el string
SQL injection: si construyes SQL con f-strings metiendo valores de fuera (un formulario, una API, un CSV), un atacante puede colar comandos que borren tablas o roben datos. La regla es absoluta: los valores SIEMPRE van como parámetros (? en sqlite3, :nombre en SQLAlchemy), NUNCA concatenados en el string de la query. Sin excepciones.
### Patrón completo: ETL con Python + SQL
Aquí tienes el patrón completo que usarás en pipelines reales. Es un mini-ETL: Extraer de la base, Transformar en Python, Cargar de vuelta. Lo montamos entero con sqlite3 para que sea ejecutable de principio a fin, pero fíjate: la estructura es exactamente la que aplicarías contra PostgreSQL.
1import sqlite32import pandas as pd34def pipeline_resumen_ventas(conn):5 """Mini-ETL: calcula un resumen de ventas por producto desde datos crudos."""67 # EXTRACT: leer datos crudos (aquí filtramos lo que nos interesa)8 ventas = pd.read_sql('''9 SELECT producto_id, cantidad, precio_unitario10 FROM ventas_raw11 ''', conn)12 print(f'Extraídas {len(ventas)} ventas')1314 # TRANSFORM: limpiar y agregar en Pandas15 ventas['importe'] = ventas['cantidad'] * ventas['precio_unitario']16 resumen = ventas.groupby('producto_id').agg(17 total_ventas=('importe', 'sum'),18 num_transacciones=('importe', 'count'),19 ticket_medio=('importe', 'mean')20 ).reset_index()2122 # LOAD: escribir el resultado en una tabla nueva23 resumen.to_sql('resumen_ventas', conn, if_exists='replace', index=False)24 print(f'Cargadas {len(resumen)} filas en resumen_ventas')25 return resumen2627# --- Preparar datos de ejemplo y ejecutar ---28conn = sqlite3.connect(':memory:')29conn.execute('CREATE TABLE ventas_raw (producto_id INTEGER, cantidad INTEGER, precio_unitario REAL)')30conn.executemany('INSERT INTO ventas_raw VALUES (?, ?, ?)', [31 (1, 2, 10.0), (1, 3, 10.0), (2, 1, 50.0), (2, 4, 50.0), (3, 5, 8.0),32])33conn.commit()3435resultado = pipeline_resumen_ventas(conn)36print(resultado.sort_values('producto_id').to_string(index=False))37conn.close()
Mini-ETL completo con sqlite3: Extract → Transform → Load, ejecutable de principio a fin
Este script es el embrión de lo que acabará siendo un DAG de Airflow (Skill 15). La lógica es la misma, solo cambia la orquestación. Aprende el patrón ETL ahora y todo lo que venga después será una extensión natural.
### En producción: SQLAlchemy + PostgreSQL
Todo lo que has hecho con sqlite3 se traslada casi literalmente a una base de datos operacional como PostgreSQL. La diferencia es que PostgreSQL es un servidor (con host, puerto, usuario y contraseña), así que necesitas dos piezas más: SQLAlchemy (una capa que unifica el acceso a muchas bases de datos) y un driver (psycopg2 para PostgreSQL). En vez de una conexión a un archivo, creas un "engine" con una URL. El SQL, el pd.read_sql y el df.to_sql son idénticos. Este bloque es de referencia: no lo ejecutes aquí (necesita el PostgreSQL que levantaste con Docker en la Skill 6), pero guárdalo, porque es el que usarás en un trabajo real.
1import os2import pandas as pd3from sqlalchemy import create_engine, text45# 1) Crear el engine: la URL sustituye a la ruta del archivo de sqlite6# formato: dialecto+driver://usuario:contraseña@host:puerto/base7db_url = os.environ.get(8 'DATABASE_URL',9 'postgresql+psycopg2://postgres:postgres@localhost:5432/tienda'10)11engine = create_engine(db_url)1213# 2) Leer: EXACTAMENTE la misma llamada que con sqlite314clientes = pd.read_sql('SELECT * FROM clientes', engine)1516# 3) Escribir: igual que con sqlite317clientes.to_sql('clientes_backup', engine, if_exists='replace', index=False)1819# 4) Parametrizadas: PostgreSQL/SQLAlchemy usa :nombre + diccionario20# (sqlite3 usaba ? + tupla; el concepto es el mismo)21query = text('SELECT * FROM clientes WHERE ciudad = :ciudad AND total_gastado >= :minimo')22df = pd.read_sql(query, engine, params={'ciudad': 'Madrid', 'minimo': 2000})23print(df)
El mismo patrón contra PostgreSQL: cambia la conexión (engine) y el estilo de placeholder, nada más
## ejercicios
Extraer clientes VIP de la base de datos
El CEO quiere la lista de clientes VIP: los que han gastado más de 5000€ en total. Crea una base de datos sqlite en memoria con las tablas clientes y pedidos (te damos los datos), y escribe una query con JOIN + GROUP BY + HAVING que calcule el gasto total por cliente y filtre los que superan 5000€. Ordena por gasto descendente e imprime el DataFrame.
💡 Resultado esperado
nombre total_gastado 0 Lucía 8500.0 1 Ana 5500.0
Escribir un resumen calculado en la DB
Lee la tabla "pedidos" de la base sqlite (te la damos), calcula un resumen mensual con tres métricas (total_ventas, num_pedidos, ticket_medio), y escribe el resultado en una tabla "ventas_mensuales" con if_exists="replace". Al final, lee esa tabla ordenada por mes e imprímela.
💡 Resultado esperado
mes total_ventas num_pedidos ticket_medio 0 2024-01 300.0 2 150.0 1 2024-02 400.0 2 200.0 2 2024-03 300.0 1 300.0
Consulta parametrizada segura
Escribe una función buscar_pedidos(conn, ciudad, importe_minimo) que use parámetros de sqlite3 (los placeholders ?, NO concatenación de strings) para buscar pedidos de una ciudad con importe mínimo, ordenados por importe descendente. Te damos la base de datos de ejemplo.
💡 Resultado esperado
id nombre ciudad importe 0 1 Ana Madrid 300.0 1 4 Lucía Madrid 150.0 id nombre ciudad importe
Regístrate para guardar tu progreso.
## comentarios
Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.
Inicia sesión para comentar y responder.
cargando comentarios...