Saltar a contenido

Datasets y destino

Ejercicio datasets-and-destination-annex.pdf · PDF

En la lección anterior uniste pokemon con generations, con tus propias manos. ¿Dónde está ahora esa consulta?

Búscala. Con suerte estará en algún lugar de SQL → Query History. Lo más probable es que haya desaparecido, y la semana que viene la volverás a escribir de memoria... y te saldrá un poco mal. Una consulta que tienes que reescribir es una consulta que te saldrá mal la próxima vez. Esta lección te da dos maneras de conservar tu trabajo. Una guarda la pregunta. La otra guarda la respuesta. No son lo mismo, y elegir mal es la forma de acabar con un informe que muestra las cifras del mes pasado.

Qué aprenderás

  • Distinguir un dataset físico de un dataset virtual en Superset
  • Guardar una consulta de SQL Lab como dataset virtual
  • Escribir el resultado de una consulta en la base de datos como una tabla nueva con CREATE TABLE ... AS
  • Predecir cuál de los dos se queda desfasado cuando cambia el origen, y elegir entre ellos
  • Llevar datos de una API a Superset: JSON → script → CSV → subida
  • Desactivar la trampa del día: una tabla creada a partir de una consulta no sigue conectada a ella

Receta o táper

Haces una lasaña buenísima. ¿Cómo te aseguras de poder volver a comerla el mes que viene?

Dos respuestas. Anotar la receta. O cocinar una tanda grande y guardarla en un táper en la nevera. Una analogía para toda esta página: la base de datos es una cocina, y las tablas de PostgreSQL son los ingredientes de la despensa.

Receta Táper
Guardas La pregunta (el SQL) La respuesta (las filas)
En Superset Un dataset virtual Una tabla nueva, creada con CREATE TABLE ... AS
Vive en Superset: solo el texto de la consulta PostgreSQL: una tabla real, junto a pokemon
Cuando la usas Se cocina de nuevo, cada vez Se sirve tal como se guardó
Por tanto es Siempre fresca, algo más lenta Rápida, pero tan vieja como el día que la cocinaste
flowchart TB
    Q["<b>Tu consulta</b><br>pokemon JOIN generations"]
    Q -- "guardar la pregunta" --> V["<b>Dataset virtual</b><br>(la receta)<br>guardado en Superset como texto"]
    Q -- "guardar la respuesta" --> T["<b>Tabla nueva</b><br>(el táper)<br>guardada en PostgreSQL como filas"]
    V -- "ejecuta la consulta de nuevo<br>cada vez" --> C["Gráficos"]
    T -- "lee las filas guardadas" --> C

En el juego de crafteo de este módulo, este es el momento en que dejas de llevar el botín en las manos y lo guardas en el cofre de tu base. Los datos que solo existen en tu pantalla todavía no están extraídos. Tienen que llegar a algún sitio: su destino.

Datasets: de qué puede dibujar Superset

Un dataset es lo que Superset llama "algo a partir de lo que puedo construir gráficos".

Superset no dibuja gráficos directamente desde una tabla ni desde una consulta. Los dibuja desde un dataset, y hay dos tipos:

Tipo Qué es En la cocina
Dataset físico Apunta a una tabla que existe en la base de datos Una etiqueta en un tarro de la despensa
Dataset virtual Una consulta guardada que Superset ejecuta por ti Una ficha de receta

Abre Datasets en el menú superior y mira la columna Type. Ya tienes dos: pokemon y generations, ambos Physical. Superset los registró cuando subiste los archivos CSV.

Cuidado: un dataset no es una copia

Un dataset físico es una etiqueta en el tarro, no un segundo tarro. Una tabla, cero copias. Si borras el dataset, la tabla sigue en PostgreSQL con todas sus filas. Y si la tabla cambia, el dataset muestra el cambio, porque nunca guardó datos propios.

Guardar la pregunta: un dataset virtual

Es hora de escribir la primera receta. Abre SQL → SQL Lab, base de datos Dades classe, esquema public, y ejecuta el join de la lección anterior con las columnas listadas una a una:

SELECT p.id, p.pokemon, p.type_1, p.type_2, p.hp, p.attack, p.defense, p.speed,
       g.generation_id, g.region, g.release_year
FROM pokemon p
JOIN generations g ON p.generation_id = g.generation_id

(JOIN a secas significa INNER JOIN, y el AS antes de un alias es opcional.) Ahora guárdala:

Paso Haz esto Deberías ver
1 Ejecuta la consulta y comprueba el resultado 802 filas
2 Haz clic en la flecha junto a Save, y luego en Save dataset Un pequeño cuadro de diálogo
3 Elige Save as new, escribe el nombre pokemon_with_region y pulsa Save & Explore El constructor de gráficos se abre en una pestaña nueva del navegador
4 Cierra esa pestaña (los gráficos son la Unidad 3) y abre Datasets pokemon_with_region en la lista, Type Virtual

802 filas, no 949: es un INNER JOIN, así que las 147 formas alternativas sin generación quedan fuera. Ya sabes por qué. Ahora esa decisión queda escrita en un solo lugar, en vez de reescribirla cada vez.

Cuidado: tres maneras de estropear una receta

  • SELECT * en un join. Las dos tablas tienen un generation_id, así que el dataset recibe dos columnas con casi el mismo nombre y falla más adelante con column reference "generation_id" is ambiguous. Lista las columnas, como arriba.
  • Un LIMIT olvidado. Superset envuelve tu consulta dentro de la suya. Un LIMIT 10 que dejaste mientras probabas se queda en la receta: ese dataset tendrá 10 filas para siempre.
  • Un nombre ocupado. Dos datasets no pueden compartir nombre, así que no llames pokemon al dataset virtual.

Guardar la respuesta: CREATE TABLE AS

CREATE TABLE nombre AS SELECT ... ejecuta una consulta una sola vez y guarda su resultado como una tabla nueva en la base de datos.

Ahora el táper: un pequeño resumen con una fila por generación. Primero ejecuta solo el SELECT (desde la segunda línea) y mira el resultado. Luego añade la primera línea y ejecuta todo:

CREATE TABLE pokemon_by_generation AS
SELECT g.generation_id, g.region, g.release_year,
       COUNT(p.id) AS n_pokemon,
       ROUND(AVG(p.attack)) AS avg_attack,
       MAX(p.speed) AS max_speed
FROM generations g
LEFT JOIN pokemon p ON p.generation_id = g.generation_id
GROUP BY g.generation_id, g.region, g.release_year;

Esta vez no hay cuadrícula de resultados. SQL Lab dice The query returned no data. No ha fallado nada: las filas han ido a la base de datos, no a tu pantalla. Pregúntale a la tabla nueva:

SELECT * FROM pokemon_by_generation ORDER BY generation_id;
generation_id region release_year n_pokemon avg_attack max_speed
1 Kanto 1996 151 73 150
2 Johto 1999 100 68 130
3 Hoenn 2002 135 73 160
4 Sinnoh 2006 107 80 125
5 Unova 2010 156 81 145
6 Kalos 2013 72 73 126
7 Alola 2016 81 83 151
8 Galar 2019 0

Ocho filas, una por generación. El LEFT JOIN parte de generations, así que Galar se queda, con n_pokemon = 0. Su avg_attack y su max_speed están en blanco, no a 0: no hay nada que promediar, y escribir 0 sería mentir.

La tabla existe en PostgreSQL, pero Superset todavía no tiene un dataset para ella. Etiqueta el tarro:

Paso Haz esto Deberías ver
1 Datasets → + Dataset El formulario de dataset nuevo
2 Base de datos Dades classe, Schema public, Table pokemon_by_generation Las columnas de la tabla
3 Haz clic en la flecha junto a Create and explore dataset, y luego en Create dataset pokemon_by_generation en la lista, Type Physical

Si SQL Lab te contesta con un error

  • relation "pokemon_by_generation" already exists: has ejecutado la sentencia dos veces. Buena noticia, no un error que temer: la tabla ya está ahí.
  • This database does not allow for DDL/DML: la conexión solo deja a SQL Lab leer. Es un ajuste de la máquina de clase, no algo que hayas hecho tú: díselo a tu profesor.

¿Cuál se queda desfasado?

Aquí va la idea del día. El 18 de noviembre de 2022 salió una novena generación de Pokémon: la región de Paldea, en los juegos Escarlata y Púrpura. Nuestra tabla generations no lo sabe. Imagina que alguien añade esa novena fila.

Antes de seguir leyendo, haz una predicción. ¿Cuántas filas mostrará la tabla pokemon_by_generation? ¿Y el mismo SELECT, ejecutado de nuevo como consulta?

Antes de la fila nueva Después de la fila nueva
El táper: la tabla pokemon_by_generation 8 filas 8 filas
La receta: el SELECT, ejecutado de nuevo 8 filas 9 filas, Paldea con 0 Pokémon

La receta vio el ingrediente nuevo. El táper no.

Cuidado: la tabla no está conectada a su consulta

"He creado la tabla a partir de una consulta, así que sigue enlazada a la consulta." No. CREATE TABLE ... AS ejecuta la consulta una vez y guarda las filas. Un táper no se rellena solo cuando compras más tomates. Si el origen cambia, vuelves a cocinar: borra la tabla antigua con DROP TABLE pokemon_by_generation; y ejecuta de nuevo el CREATE TABLE ... AS. Borra únicamente tablas que hayas creado tú. Un dataset virtual no guarda nada, así que nada en él puede quedarse desfasado.

Entonces, ¿cuándo es el táper la elección correcta?

Elige Cuándo
Dataset virtual (receta) Las cifras deben estar siempre al día y la consulta es lo bastante rápida
CREATE TABLE ... AS (táper) La consulta es lenta, otra herramienta necesita una tabla real, o quieres conservar cómo estaban las cosas un día concreto

Una fuente que no es un archivo: una API

Tus Pokémon llegaron en un archivo CSV. Si mañana sale un Pokémon nuevo, ¿lo sabrá el archivo? No: un archivo también es un táper. Entonces, ¿de dónde saca una app la lista actualizada?

Una API (Application Programming Interface) es una dirección a la que preguntas y que responde con datos en lugar de una página web.

JSON (JavaScript Object Notation) es el formato de texto en el que responden la mayoría de las API: nombres y valores, donde un valor puede contener más nombres y valores dentro.

La PokéAPI es una API gratuita. Sin cuenta, sin inicio de sesión: la dirección es toda la pregunta. Abre estas dos en tu navegador (Firefox muestra el JSON como un árbol plegable):

https://pokeapi.co/api/v2/pokemon?limit=20
{ "count": 1351,
  "next": "https://pokeapi.co/api/v2/pokemon?offset=20&limit=20",
  "previous": null,
  "results": [ { "name": "bulbasaur", "url": "https://pokeapi.co/api/v2/pokemon/1/" }, ... ] }
https://pokeapi.co/api/v2/pokemon/charizard
{ "id": 6, "name": "charizard", "height": 17, "weight": 905,
  "types": [ { "slot": 1, "type": { "name": "fire" } },
             { "slot": 2, "type": { "name": "flying" } } ],
  "stats": [ { "base_stat": 78, "stat": { "name": "hp" } }, ... ] }

(Las dos respuestas están recortadas; count valía 1351 cuando se escribió esta página y sigue creciendo.) Tres cosas que leer en ellas:

  • La primera respuesta dice 1351 pero muestra 20. El resto está en la dirección next. Dividir una respuesta en páginas se llama paginación.
  • La parte que hay después de la ? contiene los parámetros: instrucciones adicionales como limit=20.
  • Los tipos son una lista dentro de la respuesta. Datos dentro de datos se llaman anidados.

Ahora compara con tu tabla: SELECT pokemon, height, weight, type_1, type_2 FROM pokemon WHERE pokemon = 'charizard';

En la API En la tabla pokemon
Altura 17 (decímetros) 1.7 (metros)
Peso 905 (hectogramos) 90.5 (kilogramos)
Tipos Una lista anidada de dos Dos celdas: type_1 = fire, type_2 = flying

El mismo Pokémon, otra forma y otras unidades. Una fuente nunca llega con la forma que necesitas.

De JSON a una tabla: extraer, aplanar, cargar

Cuidado: Superset no puede conectarse a una API

Superset solo consulta bases de datos SQL. No puede preguntar a una dirección web. Así que una API es una fuente de la que extraes y luego cargas, exactamente igual que un archivo CSV.

Aplanar es convertir un JSON anidado en filas y columnas planas.

flowchart LR
    A["<b>API</b><br>PokéAPI<br>responde en JSON"] -- "preguntar" --> S["<b>Script</b><br>api_to_csv.py<br>elige y aplana"]
    S -- "escribir" --> C["<b>CSV</b><br>pokeapi_sample.csv<br>20 filas, 7 columnas"]
    C -- "subir" --> D["<b>Destino</b><br>tabla pokeapi_sample<br>en PostgreSQL"]
    D --> DS["<b>Dataset físico</b><br>en Superset"]

Aplanar necesita un programa pequeño. Descarga api_to_csv.py de la página de Recursos. Son 16 líneas de Python:

import csv, json, urllib.request

def get(url):
    req = urllib.request.Request(url, headers={"User-Agent": "erd-class"})
    return json.load(urllib.request.urlopen(req))

page = get("https://pokeapi.co/api/v2/pokemon?limit=20")   # page 1 of the list
with open("pokeapi_sample.csv", "w", newline="") as f:
    out = csv.writer(f)
    out.writerow(["id", "name", "height_m", "weight_kg", "type_1", "type_2", "hp"])
    for item in page["results"]:                  # each item: a name and a url
        p = get(item["url"])                      # one request per Pokemon
        types = [t["type"]["name"] for t in p["types"]] + [""]
        out.writerow([p["id"], p["name"], p["height"] / 10, p["weight"] / 10,
                      types[0], types[1], p["stats"][0]["base_stat"]])
print(page["count"], "Pokemon in the API. Next page:", page["next"])

En este módulo no tienes que escribir Python. Léelo como cuatro verbos: pedir la lista, pedir cada Pokémon, elegir los valores (unidades convertidas, tipos separados en dos), escribir una fila.

Paso Haz esto Deberías ver
1 En una terminal, en la carpeta del script: python3 api_to_csv.py Una línea como 1351 Pokemon in the API. Next page: ... y un archivo nuevo pokeapi_sample.csv
2 Abre el CSV en un editor de texto Una cabecera y 20 filas: 6,charizard,1.7,90.5,fire,flying,78
3 En Superset: + → Data → Upload CSV to database, base de datos Dades classe, esquema public, nombre de tabla pokeapi_sample, Upload Un mensaje de confirmación
4 En SQL Lab: SELECT type_1, COUNT(*) AS n FROM pokeapi_sample GROUP BY type_1 ORDER BY n DESC; bug 6, normal 5, y luego fire, grass y water con 3 cada uno

Ahora abre Datasets y mira el Type de pokeapi_sample: Physical. Una vez que los datos están en la base de datos, Superset no puede saber que vinieron de una API. Y es un táper: una instantánea, una copia de la API tal como estaba el minuto en que se ejecutó el script. Para refrescarla, vuelve a extraer y a cargar.

La lista de comprobación de una API

Seis preguntas que contestar antes de construir nada sobre una API, por ejemplo en tu proyecto.

Pregunta Qué averiguar PokéAPI
Dirección La URL y sus parámetros después de la ? .../pokemon?limit=20&offset=0
Clave ¿Necesita una cuenta o una clave de API? Una clave es una contraseña: nunca la pegues en un chat, en un documento compartido ni en una captura de pantalla Ninguna: no requiere autenticación
Límites ¿Cuántas peticiones puedes hacer? Pide solo lo que necesites y guarda lo que obtengas Sin límite fijo, pero su política de uso razonable pide que hagas pocas peticiones
Paginación ¿Una respuesta contiene todo? ¿Qué campo apunta a la página siguiente? 20 por página por defecto; next y count
Licencia ¿Puedes reutilizar y publicar los datos? ¿A quién debes dar crédito? Lee su política de uso razonable y cita la fuente
Instantánea Tu tabla es una copia de un momento. Anota la fecha Ejecuta el script de nuevo para refrescarla

Ideas clave

  • Conserva la pregunta o conserva la respuesta: un dataset virtual es una consulta guardada (la receta); CREATE TABLE ... AS guarda las filas en una tabla nueva (el táper).
  • Un dataset es lo que Superset usa para construir gráficos. Un dataset físico es una etiqueta sobre una tabla, no una copia de ella.
  • Un dataset virtual está siempre fresco: ejecuta la consulta cada vez. Una tabla creada con CREATE TABLE ... AS es una instantánea y se queda desfasada cuando cambia el origen.
  • CREATE TABLE ... AS no muestra cuadrícula de resultados: las filas han ido a la base de datos. Compruébalo con un SELECT.
  • Una API responde en JSON, a menudo anidado y paginado. Superset no puede consultarla: tú extraes, aplanas a un CSV y cargas.
  • Antes de usar una API, repasa la lista: dirección, clave, límites, paginación, licencia, instantánea.

Comprobación rápida

1. ¿Qué es un dataset virtual en Superset?
  • Una copia de una tabla, guardada dentro de Superset
  • Una consulta guardada que Superset ejecuta cada vez que se usa el dataset
  • Una tabla creada con CREATE TABLE ... AS
  • Un archivo CSV a la espera de ser subido

Un dataset virtual es la receta: solo se guarda el texto de la consulta, y se cocina de nuevo cada vez. Una tabla creada con CREATE TABLE ... AS es una tabla real, que después registras como dataset físico.

2. Borras el dataset físico generations de la lista Datasets de Superset. ¿Qué pasa con la tabla de PostgreSQL?
  • También se borra
  • Pierde sus filas pero conserva sus columnas
  • Nada: la tabla sigue ahí con todas sus filas
  • Se convierte en un dataset virtual

Un dataset físico es una etiqueta en el tarro, no el tarro. Una tabla, cero copias: quitar la etiqueta no toca lo que hay en la despensa.

3. Ejecutas un CREATE TABLE ... AS SELECT ... en SQL Lab y no aparece ninguna cuadrícula de resultados. ¿Qué significa?
  • La sentencia ha fallado
  • La tabla se ha creado vacía
  • Las filas se han escrito en la base de datos en lugar de mostrarse en pantalla
  • Tienes que pulsar Run una segunda vez

CREATE TABLE ... AS envía el resultado a una tabla nueva, así que no hay nada que mostrar. Compruébalo con SELECT * FROM la tabla nueva. Ejecutarlo una segunda vez solo respondería already exists.

4. Se añade una fila nueva a generations. ¿Qué afirmación es cierta?
  • Tanto el dataset virtual como la tabla creada con CREATE TABLE ... AS la muestran
  • Ninguno la muestra hasta que se reinicie Superset
  • Una consulta ejecutada de nuevo ve la fila nueva; la tabla creada antes con CREATE TABLE ... AS no
  • La tabla creada con CREATE TABLE ... AS se actualiza sola, porque salió de una consulta

CREATE TABLE ... AS ejecuta la consulta una vez: el táper no se rellena solo. La receta se cocina de nuevo cada vez, así que ve el ingrediente nuevo. Para refrescar la tabla hay que borrarla y crearla otra vez.

5. ¿Por qué no puede Superset mostrar directamente los datos de la PokéAPI?
  • Porque la PokéAPI necesita una contraseña
  • Porque Superset solo consulta bases de datos SQL, así que los datos de una API hay que extraerlos y cargarlos primero
  • Porque los archivos JSON son demasiado grandes para Superset
  • Porque la PokéAPI solo funciona en Firefox

Superset pregunta a bases de datos, no a direcciones web. El camino es extraer → aplanar → cargar: un script pregunta a la API, escribe un CSV, y tú subes el CSV como una tabla. La PokéAPI no necesita ninguna clave.

6. La PokéAPI responde con un count de 1351, pero la lista solo contiene 20 Pokémon. ¿Dónde están los demás?
  • Se borraron de la API
  • Necesitan una clave de API
  • En las páginas siguientes: el campo next contiene la dirección de la siguiente
  • En el campo previous

Eso es la paginación: una respuesta larga se divide en páginas, aquí de 20. count dice cuántos hay en total y next apunta a la página siguiente.

7. Tu jefe quiere (a) un gráfico de ventas por tienda que esté siempre al día y (b) una tabla con los totales del viernes pasado que no cambie nunca. Elige las dos opciones correctas.
  • (a) usa un dataset virtual, porque la consulta se ejecuta de nuevo cada vez
  • (b) usa CREATE TABLE ... AS, porque las filas se guardan una vez y se quedan como estaban
  • (a) usa CREATE TABLE ... AS, porque una tabla real siempre es más fresca
  • (b) usa un dataset virtual, porque una consulta guardada nunca cambia su resultado

Receta para lo fresco, táper para lo congelado. Un dataset virtual vuelve a ejecutar la consulta, así que las ventas nuevas aparecen. Una tabla creada con CREATE TABLE ... AS es una instantánea: justo lo que quieres para "cómo estaban las cosas el viernes".

Ya sabes traer datos a casa y guardarlos, con Superset. El mismo trabajo existe en todas las herramientas de datos, con otros botones. Siguiente: Obtener datos con Power BI, donde puedes hacerte la misma pregunta: ¿esta herramienta guarda la receta o el táper?