Skip to content

Datasets and destination

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

In the last lesson you joined pokemon with generations, with your own hands. Where is that query now?

Look for it. At best it is somewhere in SQL → Query History. More likely it is gone, and next week you will type it again from memory — and get it slightly wrong. A query you have to retype is a query you will get wrong next time. This lesson gives you two ways to keep your work. One keeps the question. The other keeps the answer. They are not the same thing, and choosing the wrong one is how a report ends up showing last month's numbers.

What you will learn

  • Tell a physical dataset from a virtual dataset in Superset
  • Save a SQL Lab query as a virtual dataset
  • Write a query result into the database as a new table with CREATE TABLE ... AS
  • Predict which of the two goes out of date when the source changes, and choose between them
  • Bring data from an API into Superset: JSON → script → CSV → upload
  • Defuse the trap of the day: a table made from a query does not stay connected to it

Recipe or tupperware

You make a great lasagne. How do you make sure you can eat it again next month?

Two answers. Write down the recipe. Or cook a big batch and put it in a tupperware in the fridge. One analogy for this whole page: the database is a kitchen, and the tables in PostgreSQL are the ingredients in the pantry.

Recipe Tupperware
You keep The question (the SQL) The answer (the rows)
In Superset A virtual dataset A new table, made with CREATE TABLE ... AS
It lives in Superset: only the text of the query PostgreSQL: a real table, next to pokemon
When you use it It is cooked again, every time It is served as it was stored
So it is Always fresh, a bit slower Fast, but as old as the day you cooked it
flowchart TB
    Q["<b>Your query</b><br>pokemon JOIN generations"]
    Q -- "save the question" --> V["<b>Virtual dataset</b><br>(the recipe)<br>stored in Superset as text"]
    Q -- "store the answer" --> T["<b>New table</b><br>(the tupperware)<br>stored in PostgreSQL as rows"]
    V -- "runs the query again<br>every time" --> C["Charts"]
    T -- "reads the stored rows" --> C

In the crafting game of this module, this is the moment you stop carrying loot in your hands and put it in the chest at your base. Data that only exists on your screen is not extracted yet. It has to arrive somewhere: its destination.

Datasets: what Superset can draw from

A dataset is what Superset calls "something I can build charts from".

Superset does not draw charts straight from a table or from a query. It draws them from a dataset, and there are two kinds:

Kind What it is In the kitchen
Physical dataset Points at a table that exists in the database A label on a jar in the pantry
Virtual dataset A saved query that Superset runs for you A recipe card

Open Datasets in the top menu and look at the Type column. You already have two: pokemon and generations, both Physical. Superset registered them when you uploaded the CSV files.

Careful: a dataset is not a copy

A physical dataset is a label on the jar, not a second jar. One table, zero copies. If you delete the dataset, the table is still in PostgreSQL with all its rows. And if the table changes, the dataset shows the change, because it never held any data of its own.

Save the question: a virtual dataset

Time to write the first recipe. Open SQL → SQL Lab, database Dades classe, schema public, and run the join from the last lesson with the columns listed one by one:

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 alone means INNER JOIN, and the AS before an alias is optional.) Then save it:

Step Do this You should see
1 Run the query and check the result 802 rows
2 Click the arrow next to Save, then Save dataset A small dialog
3 Choose Save as new, type the name pokemon_with_region, press Save & Explore The chart builder opens in a new browser tab
4 Close that tab (charts are Unit 3) and open Datasets pokemon_with_region in the list, Type Virtual

802 rows, not 949: it is an INNER JOIN, so the 147 alternate forms with no generation are left out. You already know why. Now that decision is written down in one place, instead of being retyped every time.

Careful: three ways to spoil a recipe

  • SELECT * on a join. Both tables have a generation_id, so the dataset gets two columns with almost the same name and fails later with column reference "generation_id" is ambiguous. List the columns, as above.
  • A forgotten LIMIT. Superset wraps your query inside its own. A LIMIT 10 you left in while testing stays in the recipe: that dataset has 10 rows for ever.
  • A name that is taken. Two datasets cannot share a name, so do not call the virtual dataset pokemon.

Store the answer: CREATE TABLE AS

CREATE TABLE name AS SELECT ... runs a query once and stores its result as a new table in the database.

Now the tupperware: a small summary with one row per generation. First run the SELECT on its own (from the second line) and look at the result. Then add the first line and run it all:

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;

This time there is no result grid. SQL Lab says The query returned no data. Nothing failed: the rows went into the database, not to your screen. Ask the new table:

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

Eight rows, one per generation. The LEFT JOIN starts from generations, so Galar stays, with n_pokemon = 0. Its avg_attack and max_speed are blank, not 0: there is nothing to average, and writing 0 would be a lie.

The table exists in PostgreSQL, but Superset has no dataset for it yet. Label the jar:

Step Do this You should see
1 Datasets → + Dataset The new-dataset form
2 Database Dades classe, Schema public, Table pokemon_by_generation The columns of the table
3 Click the arrow next to Create and explore dataset, then Create dataset pokemon_by_generation in the list, Type Physical

If SQL Lab answers back

  • relation "pokemon_by_generation" already exists: you ran the statement twice. Good news, not an error to fear: the table is there.
  • This database does not allow for DDL/DML: the connection only lets SQL Lab read. That is a setting of the class machine, not something you did: tell your teacher.

Which one goes stale?

Here is the idea of the day. A ninth generation of Pokémon came out on 18 November 2022: the region of Paldea, in the games Scarlet and Violet. Our generations table does not know. Imagine somebody adds that ninth row.

Before you read on, predict. How many rows will the table pokemon_by_generation show? And the same SELECT, run again as a query?

Before the new row After the new row
The tupperware: the table pokemon_by_generation 8 rows 8 rows
The recipe: the SELECT, run again 8 rows 9 rows, Paldea with 0 Pokémon

The recipe saw the new ingredient. The tupperware did not.

Careful: the table is not connected to its query

"I created the table from a query, so it stays linked to the query." No. CREATE TABLE ... AS runs the query once and stores the rows. A tupperware does not refill itself when you buy more tomatoes. If the source changes, you cook again: delete the old table with DROP TABLE pokemon_by_generation; and run the CREATE TABLE ... AS again. Only ever drop tables you created. A virtual dataset stores nothing, so nothing in it can go stale.

So when is the tupperware the right choice?

Choose When
Virtual dataset (recipe) The numbers must always be up to date, and the query is fast enough
CREATE TABLE ... AS (tupperware) The query is slow, another tool needs a real table, or you want to keep how things were on a given day

A source that is not a file: an API

Your Pokémon came in a CSV file. If a new Pokémon is released tomorrow, will the file know? No: a file is a tupperware too. So where does an app get the fresh list?

An API (Application Programming Interface) is an address you ask, which answers with data instead of a web page.

JSON (JavaScript Object Notation) is the text format most APIs answer in: names and values, where a value can hold more names and values inside.

The PokéAPI is a free one. No account, no login: the address is the whole question. Open these two in your browser (Firefox shows JSON as a foldable tree):

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" } }, ... ] }

(Both answers are trimmed; count was 1351 when this page was written and keeps growing.) Three things to read in them:

  • The first answer says 1351 but shows 20. The rest are at the next address. Splitting an answer into pages is called pagination.
  • The part after the ? holds the parameters: extra instructions such as limit=20.
  • The types are a list inside the answer. Data inside data is called nested.

Now compare with your table: SELECT pokemon, height, weight, type_1, type_2 FROM pokemon WHERE pokemon = 'charizard';

In the API In the table pokemon
Height 17 (decimetres) 1.7 (metres)
Weight 905 (hectograms) 90.5 (kilograms)
Types A nested list of two Two cells: type_1 = fire, type_2 = flying

Same Pokémon, different shape and different units. A source never arrives in the shape you need.

From JSON to a table: extract, flatten, load

Careful: Superset cannot connect to an API

Superset only queries SQL databases. It cannot ask a web address. So an API is a source you extract from and then load, exactly like a CSV file.

To flatten is to turn nested JSON into plain rows and columns.

flowchart LR
    A["<b>API</b><br>PokéAPI<br>answers in JSON"] -- "ask" --> S["<b>Script</b><br>api_to_csv.py<br>picks and flattens"]
    S -- "write" --> C["<b>CSV</b><br>pokeapi_sample.csv<br>20 rows, 7 columns"]
    C -- "upload" --> D["<b>Destination</b><br>table pokeapi_sample<br>in PostgreSQL"]
    D --> DS["<b>Physical dataset</b><br>in Superset"]

Flattening needs a small program. Download api_to_csv.py from the Resources page. It is 16 lines of 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"])

You do not have to write Python in this module. Read it as four verbs: ask for the list, ask for each Pokémon, pick the values (units converted, types split in two), write a row.

Step Do this You should see
1 In a terminal, in the folder of the script: python3 api_to_csv.py A line such as 1351 Pokemon in the API. Next page: ... and a new file pokeapi_sample.csv
2 Open the CSV in a text editor A header and 20 rows: 6,charizard,1.7,90.5,fire,flying,78
3 In Superset: + → Data → Upload CSV to database, database Dades classe, schema public, table name pokeapi_sample, Upload A confirmation message
4 In SQL Lab: SELECT type_1, COUNT(*) AS n FROM pokeapi_sample GROUP BY type_1 ORDER BY n DESC; bug 6, normal 5, then fire, grass and water with 3 each

Now open Datasets and look at the Type of pokeapi_sample: Physical. Once the data is in the database, Superset cannot tell it came from an API. And it is a tupperware: a snapshot, a copy of the API as it was the minute the script ran. To refresh it, extract and load again.

The API checklist

Six questions to answer before you build anything on an API — for example in your project.

Ask What to find out PokéAPI
Address The URL, and its parameters after the ? .../pokemon?limit=20&offset=0
Key Does it need an account or an API key? A key is a password: never paste it in a chat, a shared document or a screenshot None: no authentication is required
Limits How many requests may you make? Ask only for what you need, and save what you get No fixed limit, but its fair use policy asks you to keep requests low
Pagination Does one answer hold everything? Which field points to the next page? 20 per page by default; next and count
Licence May you reuse and publish the data? Who must you credit? Read its fair use policy and cite the source
Snapshot Your table is a copy from one moment. Write down the date Run the script again to refresh

Key takeaways

  • Keep the question or keep the answer: a virtual dataset is a saved query (the recipe); CREATE TABLE ... AS stores the rows in a new table (the tupperware).
  • A dataset is what Superset builds charts from. A physical dataset is a label on a table, not a copy of it.
  • A virtual dataset is always fresh: it runs the query every time. A table made with CREATE TABLE ... AS is a snapshot and goes stale when the source changes.
  • CREATE TABLE ... AS shows no result grid: the rows went into the database. Check with a SELECT.
  • An API answers in JSON, often nested and paginated. Superset cannot query it: you extract, flatten to a CSV and load.
  • Before using an API, run the checklist: address, key, limits, pagination, licence, snapshot.

Quick check

1. What is a virtual dataset in Superset?
  • A copy of a table, stored inside Superset
  • A saved query that Superset runs every time the dataset is used
  • A table created with CREATE TABLE ... AS
  • A CSV file waiting to be uploaded

A virtual dataset is the recipe: only the text of the query is stored, and it is cooked again each time. A table made with CREATE TABLE ... AS is a real table, which you then register as a physical dataset.

2. You delete the physical dataset generations from Superset's Datasets list. What happens to the table in PostgreSQL?
  • It is deleted too
  • It loses its rows but keeps its columns
  • Nothing: the table is still there with all its rows
  • It becomes a virtual dataset

A physical dataset is a label on the jar, not the jar. One table, zero copies: removing the label does not touch what is in the pantry.

3. You run a CREATE TABLE ... AS SELECT ... in SQL Lab and no result grid appears. What does that mean?
  • The statement failed
  • The table was created empty
  • The rows were written into the database instead of being shown on screen
  • You need to press Run a second time

CREATE TABLE ... AS sends the result into a new table, so there is nothing to display. Check it with SELECT * FROM the new table. Running it a second time would only answer already exists.

4. A new row is added to generations. Which statement is true?
  • Both the virtual dataset and the table made with CREATE TABLE ... AS show it
  • Neither shows it until Superset is restarted
  • A query run again sees the new row; the table made earlier with CREATE TABLE ... AS does not
  • The table made with CREATE TABLE ... AS updates itself, because it came from a query

CREATE TABLE ... AS runs the query once: the tupperware does not refill itself. The recipe is cooked again every time, so it sees the new ingredient. To refresh the table you must drop it and create it again.

5. Why can Superset not show PokéAPI data directly?
  • Because the PokéAPI needs a password
  • Because Superset only queries SQL databases, so API data must be extracted and loaded first
  • Because JSON files are too big for Superset
  • Because the PokéAPI only works in Firefox

Superset asks databases, not web addresses. The path is extract → flatten → load: a script asks the API, writes a CSV, and you upload the CSV as a table. The PokéAPI needs no key at all.

6. The PokéAPI answers with a count of 1351, but the list only holds 20 Pokémon. Where are the others?
  • They were deleted from the API
  • They need an API key
  • On the following pages: the next field holds the address of the next one
  • In the previous field

That is pagination: a long answer is split into pages, here of 20. count says how many there are in total and next points to the next page.

7. Your boss wants (a) a chart of sales per shop that is always up to date and (b) a table with last Friday's totals that must never change. Pick the two right choices.
  • (a) uses a virtual dataset, because the query runs again every time
  • (b) uses CREATE TABLE ... AS, because the rows are stored once and stay as they were
  • (a) uses CREATE TABLE ... AS, because a real table is always fresher
  • (b) uses a virtual dataset, because a saved query never changes its result

Recipe for fresh, tupperware for frozen. A virtual dataset re-runs the query, so new sales appear. A table made with CREATE TABLE ... AS is a snapshot: exactly what you want for "how things were on Friday".

You can now bring data home and store it — with Superset. The same job exists in every data tool, with other buttons. Next: Getting data with Power BI, where you can ask the same question: does this tool keep the recipe or the tupperware?