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 ageneration_id, so the dataset gets two columns with almost the same name and fails later withcolumn reference "generation_id" is ambiguous. List the columns, as above.- A forgotten
LIMIT. Superset wraps your query inside its own. ALIMIT 10you 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
nextaddress. Splitting an answer into pages is called pagination. - The part after the
?holds the parameters: extra instructions such aslimit=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 ... ASstores 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 ... ASis a snapshot and goes stale when the source changes. CREATE TABLE ... ASshows no result grid: the rows went into the database. Check with aSELECT.- 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 ... ASshow it - Neither shows it until Superset is restarted
- A query run again sees the new row; the table made earlier with
CREATE TABLE ... ASdoes not - The table made with
CREATE TABLE ... ASupdates 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
nextfield holds the address of the next one - In the
previousfield
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?