Saltar al contenido

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 sqlite3
2
3# Opción A: base de datos en un archivo (persiste en disco)
4# conn = sqlite3.connect('mi_base.db')
5
6# Opción B: base de datos en memoria (desaparece al cerrar el programa)
7# Perfecta para aprender y para tests: siempre empieza limpia
8conn = sqlite3.connect(':memory:')
9
10# Verificar que la conexión funciona
11cursor = conn.execute('SELECT sqlite_version()')
12print('Conectado a SQLite version:', cursor.fetchone()[0])
13
14conn.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 sqlite3
2
3conn = sqlite3.connect(':memory:')
4
5# 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 REAL
12 )
13''')
14
15# 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 clientes
26)
27
28# CONFIRMAR los cambios: sin commit, no se guardan
29conn.commit()
30
31# Comprobar cuántas filas hay
32n = conn.execute('SELECT COUNT(*) FROM clientes').fetchone()[0]
33print(f'Insertados {n} clientes')
34
35conn.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 sqlite3
2import pandas as pd
3
4conn = sqlite3.connect(':memory:')
5conn.execute('''
6 CREATE TABLE clientes (
7 id INTEGER PRIMARY KEY, nombre TEXT, ciudad TEXT, total_gastado REAL
8 )
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()
21
22# Leer una tabla completa a un DataFrame
23clientes = pd.read_sql('SELECT * FROM clientes', conn)
24print(f'Clientes: {len(clientes)} filas')
25
26# 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_total
31 FROM clientes
32 GROUP BY ciudad
33 HAVING SUM(total_gastado) > 1000
34 ORDER BY gasto_total DESC
35''', conn)
36
37print(resumen)
38
39conn.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 sqlite3
2import pandas as pd
3
4conn = sqlite3.connect(':memory:')
5
6# DataFrame con datos ya procesados
7resumen = 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})
13
14# Escribir el DataFrame en una tabla de la base de datos
15resumen.to_sql(
16 'resumen_mensual', # nombre de la tabla
17 conn, # la conexión sqlite3
18 if_exists='replace', # 'fail', 'replace', 'append'
19 index=False # no guardar el índice de Pandas como columna
20)
21
22print('Datos escritos correctamente')
23
24# Verificar leyendo de vuelta (ORDER BY para salida determinista)
25check = pd.read_sql('SELECT * FROM resumen_mensual ORDER BY mes', conn)
26print(check)
27
28conn.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 sqlite3
2import pandas as pd
3
4conn = 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()
12
13# CORRECTO: placeholders ? y los valores en una tupla aparte
14ciudad = 'Madrid'
15minimo = 2000.0
16query = '''
17 SELECT nombre, total_gastado
18 FROM clientes
19 WHERE ciudad = ? AND total_gastado >= ?
20 ORDER BY total_gastado DESC
21'''
22df = pd.read_sql(query, conn, params=(ciudad, minimo))
23print(df)
24
25# INCORRECTO (NO lo hagas): f"... WHERE ciudad = '{ciudad}'"
26# Si 'ciudad' viniera de un formulario, alguien podría inyectar SQL malicioso.
27
28conn.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.

Extract con read_sql, Transform en Pandas, Load con to_sql: idéntico en toda base de datos
1import sqlite3
2import pandas as pd
3
4def pipeline_resumen_ventas(conn):
5 """Mini-ETL: calcula un resumen de ventas por producto desde datos crudos."""
6
7 # EXTRACT: leer datos crudos (aquí filtramos lo que nos interesa)
8 ventas = pd.read_sql('''
9 SELECT producto_id, cantidad, precio_unitario
10 FROM ventas_raw
11 ''', conn)
12 print(f'Extraídas {len(ventas)} ventas')
13
14 # TRANSFORM: limpiar y agregar en Pandas
15 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()
21
22 # LOAD: escribir el resultado en una tabla nueva
23 resumen.to_sql('resumen_ventas', conn, if_exists='replace', index=False)
24 print(f'Cargadas {len(resumen)} filas en resumen_ventas')
25 return resumen
26
27# --- 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()
34
35resultado = 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 os
2import pandas as pd
3from sqlalchemy import create_engine, text
4
5# 1) Crear el engine: la URL sustituye a la ruta del archivo de sqlite
6# formato: dialecto+driver://usuario:contraseña@host:puerto/base
7db_url = os.environ.get(
8 'DATABASE_URL',
9 'postgresql+psycopg2://postgres:postgres@localhost:5432/tienda'
10)
11engine = create_engine(db_url)
12
13# 2) Leer: EXACTAMENTE la misma llamada que con sqlite3
14clientes = pd.read_sql('SELECT * FROM clientes', engine)
15
16# 3) Escribir: igual que con sqlite3
17clientes.to_sql('clientes_backup', engine, if_exists='replace', index=False)
18
19# 4) Parametrizadas: PostgreSQL/SQLAlchemy usa :nombre + diccionario
20# (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

[01]

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
Cargando editor...
[02]

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
Cargando editor...
[03]

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
Cargando editor...

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...