# Tema 3 · 10 · Ejercicio adicional C: estaciones de bicicleta en DynamoDB (clave compuesta) Presentación del Tema 3: «Ejercicio C» de «Ejercicios adicionales» (DynamoDB con clave de partición + clave de ordenación). Duración: unos 20 minutos, más ~9 minutos de espera si creas el índice secundario global. Coste: céntimos (tabla on-demand de 600 ítems pequeños). > **¿Dudas con un script?** Todos los scripts de esta carpeta traen su propia ayuda: `python3 genera-estaciones.py --ayuda` (también vale `--help` o `-h`) explica los pasos y las opciones. Este `README.md` es el paso a paso de la actividad: tenlo a mano y consúltalo antes de preguntar. > **Etiquetas de lo que creas.** Todo recurso que crees a mano (máquina, volumen, bucket, base de datos, tabla, función...) ponle estas etiquetas, al crearlo (apartado *Tags* / *Etiquetas*) o después desde su pestaña de etiquetas: `curso` = `G214`, `universidad` = `CUNEF` y `actividad` = `03.10-estacionesdynamodb`. Si algún día hay que borrar recursos a mano, se localizan todos de golpe con `aws resourcegroupstaggingapi get-resources --region eu-north-1 --tag-filters Key=actividad,Values=03.10-estacionesdynamodb`. Más detalle (cómo etiquetar con la CLI, cómo buscar y qué hacer en una emergencia): [README principal](../../README.md), sección «Etiquetas». ## Qué vas a hacer Vas a generar 600 lecturas simuladas de estaciones de bicicleta pública, cargarlas en una tabla de DynamoDB con **clave compuesta** (`estacion_id` + `ts`) y comprobar con `--return-consumed-capacity` la diferencia de coste entre `query` (lee solo lo que pides) y `scan` (lee toda la tabla). Al final creas un índice secundario global para resolver la pregunta «¿qué estaciones no tienen bicis ahora mismo?». ``` genera-estaciones.py ──> batch_estaciones_00.json … _23.json (24 ficheros de 25 ítems) aws dynamodb batch-write-item ──> tabla Estaciones (estacion_id HASH, ts RANGE) ──> query / scan / índice estado-ts-index ``` **Qué se crea en AWS:** la tabla `Estaciones` (modo `PAY_PER_REQUEST`) y, si haces la última parte, el índice secundario global `estado-ts-index`. Todo se hace en CloudShell (región `eu-north-1`). Las lecturas reales de estaciones en tiempo real no son de descarga libre (Barcelona exige un token y Madrid solo publica información de las estaciones), por eso se generan simuladas. ## Qué contiene esta carpeta | Fichero | Para qué sirve | En qué paso se usa | |---|---|---| | `genera-estaciones.py` | Genera 600 lecturas de estaciones de bicicleta (20 estaciones `E01`..`E20` × 30 lecturas, una cada 5 minutos desde las 17:00 del 2026-03-14) con `bicis_libres`, `anclajes_libres` y `estado` (`NORMAL`, `SIN_BICIS` o `LLENA`), en ficheros `batch_estaciones_00.json`... de **25 ítems** (el máximo de `batch-write-item`; con 30 ítems en una llamada da `Member must have length less than or equal to 25`). Solo usa la librería estándar | Paso 2 | | `README.md` | Esta guía (incluye los comandos de la AWS CLI) | Siempre | ## Antes de empezar - [ ] Cuenta de AWS propia, región de la consola `eu-north-1` (elígela ANTES de abrir CloudShell). - [ ] Sabes abrir AWS CloudShell (icono `>_`). Esta actividad **no** necesita la RDS ni Adminer. - [ ] Idea previa: la presentación (DynamoDB, ver [03.07](../03.07-dynamodbcli/README.md)): los números viajan como cadenas dentro de `{"N": "..."}`. Comprobación previa en CloudShell: `aws sts get-caller-identity` y `echo "$AWS_REGION"` (debe imprimir `eu-north-1`; si no, cierra CloudShell, cambia la región de la consola y ábrelo de nuevo). ## Cómo llevar los ficheros a donde toque Se hace en **CloudShell**. Comprime **esta carpeta** (`03.10-estacionesdynamodb`) en un zip, abre CloudShell en `eu-north-1`, **Actions** > **Upload file** y sube el zip. Después: ```bash unzip -o 03.10-estacionesdynamodb.zip && cd 03.10-estacionesdynamodb && chmod -R u+rwX . sed -i 's/\r$//' *.py chmod +x *.py ls ``` > Si ya subiste `g214-materiales.zip` (README principal), no necesitas este zip: entra con `cd ~/g214-materiales/tema3/03.10-estacionesdynamodb` y, si algún fichero da `Permission denied`, ejecuta allí `chmod -R u+rwX . && chmod +x *.py`. Qué verás: `ls` muestra `README.md` y `genera-estaciones.py`. (`sed` quita los finales de línea de Windows y `chmod +x` da permiso de ejecución.) Ejecuta todo desde dentro de esta carpeta. También puedes ejecutarlo en tu PC con la AWS CLI configurada; en PowerShell, ver el paso 3. ## Paso a paso **Paso 1. Crear la tabla `Estaciones`.** Con el comando de la presentación: `create-table` con `estacion_id` como clave de partición (HASH), `ts` como clave de ordenación (RANGE) y `PAY_PER_REQUEST`. Equivalente en CLI (sin probar tal cual es la referencia): ```bash aws dynamodb create-table --table-name Estaciones \ --attribute-definitions AttributeName=estacion_id,AttributeType=S AttributeName=ts,AttributeType=S \ --key-schema AttributeName=estacion_id,KeyType=HASH AttributeName=ts,KeyType=RANGE \ --billing-mode PAY_PER_REQUEST --region eu-north-1 aws dynamodb wait table-exists --table-name Estaciones --region eu-north-1 ``` **Paso 2. Generar los datos.** ```bash python3 genera-estaciones.py # 600 ítems en 24 ficheros (o: genera-estaciones.py 6 5 -> 30 ítems en 2 ficheros) ls batch_estaciones_*.json ``` Qué verás: 24 ficheros `batch_estaciones_00.json` ... `batch_estaciones_23.json` (cada uno con 25 ítems). **Paso 3. Cargar los lotes y comprobar.** ```bash for f in batch_estaciones_*.json; do aws dynamodb batch-write-item --request-items file://$f --region eu-north-1 done aws dynamodb scan --table-name Estaciones --select COUNT --region eu-north-1 ``` Qué verás: cada `batch-write-item` imprime un JSON de tres líneas (`{`, `"UnprocessedItems": {}` y `}`; si `UnprocessedItems` no estuviera vacío, reenvía esos ítems) y el `scan --select COUNT` da `"Count": 600`. En PowerShell sustituye el bucle por `Get-ChildItem batch_estaciones_*.json | ForEach-Object { aws dynamodb batch-write-item --request-items file://$($_.Name) --region eu-north-1 }` (o usa CloudShell: las comillas simples de los JSON dan problemas en Windows PowerShell 5.1). **Paso 4. `query` frente a `scan`.** Con los comandos de la presentación (con `--return-consumed-capacity TOTAL`). Resultados para contrastar (medidos): el `query` de E12 desde las 18:00 devuelve 18 ítems (`Count` 18, `ScannedCount` 18) y consume **0.5**; todas las lecturas de E12, 30 ítems, también **0.5**; el `scan` con `--filter-expression "estacion_id = :e"` devuelve 30 pero lee 600 (`ScannedCount` 600) y consume **6.5** (13.0 con `--consistent-read`). Con solo 30 ítems en la tabla ya se ve la diferencia (`query` 0.5 frente a `scan` 2.0). **Paso 5. Índice secundario global para «estaciones sin bicis ahora mismo».** Hace falta un índice (`estado` + `ts`): crearlo con 600 ítems tarda unos 9 minutos hasta `IndexStatus ACTIVE`, y después una `query` con `--index-name estado-ts-index` lo resuelve con 0.5. Comandos (los mismos que se midieron; en una tabla On-demand el índice no necesita capacidad aparte). Acota `ts` a una ventana reciente: sin esa condición salen todas las lecturas históricas con `SIN_BICIS` (28 en total), no las de «ahora»: ```bash aws dynamodb update-table --table-name Estaciones --region eu-north-1 --attribute-definitions AttributeName=estado,AttributeType=S AttributeName=ts,AttributeType=S --global-secondary-index-updates '[{"Create":{"IndexName":"estado-ts-index","KeySchema":[{"AttributeName":"estado","KeyType":"HASH"},{"AttributeName":"ts","KeyType":"RANGE"}],"Projection":{"ProjectionType":"ALL"}}}]' aws dynamodb describe-table --table-name Estaciones --region eu-north-1 --query "Table.GlobalSecondaryIndexes[0].IndexStatus" --output text # espera a ACTIVE (~9 min) aws dynamodb query --table-name Estaciones --index-name estado-ts-index --region eu-north-1 --key-condition-expression "estado = :s AND ts >= :d" --expression-attribute-values '{":s":{"S":"SIN_BICIS"},":d":{"S":"2026-03-14T19:25:00"}}' --return-consumed-capacity TOTAL ``` Qué verás: con `ts >= 19:25` sale 1 ítem y 0.5 RCU (el `scan` equivalente lee los 600 y cuesta 6.5). ## Errores frecuentes | Síntoma | Causa | Solución | |---|---|---| | `Member must have length less than or equal to 25` | Un `batch-write-item` con más de 25 ítems | Usa los ficheros que genera el script (25 ítems cada uno) | | `ResourceNotFoundException` al cargar o consultar | La tabla no existe todavía, o está en otra región | `aws dynamodb wait table-exists --table-name Estaciones --region eu-north-1` y comprueba `--region` | | `UnprocessedItems` no está vacío | DynamoDB no procesó algún ítem | Reenvía esos ítems | | `Permission denied` al lanzar `./genera-estaciones.py` | Sin permiso de ejecución | `chmod +x genera-estaciones.py` (o `python3 genera-estaciones.py`) | | `bad interpreter` o `$'\r': command not found` | Finales de línea de Windows | `sed -i 's/\r$//' *.py` y repite | | En PowerShell 5.1 los JSON con comillas simples dan error | Las comillas simples se interpretan distinto | Usa CloudShell | | El `scan --select COUNT` da menos de 600 | Faltan lotes por cargar | Repite el bucle (los `put` repetidos no duplican: misma clave) | | La `query` al índice no devuelve nada | El índice aún no está `ACTIVE`, o la ventana de `ts` no contiene ninguna lectura `SIN_BICIS` | Espera a `ACTIVE` (~9 min) y usa `ts >= 2026-03-14T19:25:00` | Las filas de esta tabla distintas de la primera y de la de PowerShell son deducciones, sin probar. ## Limpieza ```bash aws dynamodb delete-table --table-name Estaciones --region eu-north-1 rm batch_estaciones_*.json ``` El borrado de la tabla se lleva también el índice. Comprueba que ya no está: ```bash aws dynamodb list-tables --region eu-north-1 ``` Qué debes ver: la lista sin `Estaciones`. Esta actividad no comparte recursos con las demás (la tabla `Usuarios` es de [03.07](../03.07-dynamodbcli/README.md)); la última carpeta que borra la RDS y Adminer es la 03.09. ## Cómo sabes que has terminado - [ ] `Estaciones` tiene 600 ítems (`Count` 600). - [ ] El `query` de E12 consume 0.5 frente a 6.5 del `scan` con filtro. - [ ] (Opcional) El índice `estado-ts-index` está `ACTIVE` y la `query` por `SIN_BICIS` desde las 19:25 devuelve 1 ítem con 0.5. - [ ] Has borrado la tabla `Estaciones` y los ficheros `batch_estaciones_*.json`. Anterior: [03.09 · Ejercicios adicionales A y B](../03.09-ineaire/README.md) · Siguiente: [03.11 · Ejercicio D (CSV a Parquet)](../03.11-csvparquet/README.md)