SQL Lab: filters¶
Exercise sql-lab-filters-annex.pdf · PDF
Somebody hands you a shoebox with 949 Pokémon trading cards and asks: which is the fastest electric one? You do not read 949 cards aloud. You take out the electric ones, sort them by speed, look at the top card and read its name.
Four moves with your hands. In a database those four moves have names, and together they make a sentence the database understands. Last lesson you connected Superset to the class database and uploaded the table pokemon. Now you learn to ask it for less: fewer columns, fewer rows, in a useful order. In the crafting game of this module you are still in the farm and loot stage — but a good player does not carry the whole map home, only what the recipe needs.
What you will learn
- Write a query that returns only the columns you need, instead of
SELECT * - Keep only the rows you need with
WHERE, using comparison operators andAND/OR - Sort a result with
ORDER BYand cut it withLIMITto answer "top N" questions - List the different values of a column with
DISTINCT - Choose the right special filter:
LIKE,IN,BETWEENorIS NULL - Defuse the trap of the day: why
= NULLfinds nothing, and why a wrong query rarely gives an error
A shoebox with 949 cards¶
A query is a question written in a language the database understands. That language is SQL (Structured Query Language).
One analogy for this whole page: the table pokemon is a shoebox of trading cards.
| In the database | In the shoebox |
|---|---|
The table pokemon |
The box |
| A row | One card: one Pokémon |
| A column | One line printed on every card: name, type, attack... |
| A query | What you do with your hands to answer a question |
The box holds 949 cards, and every card has 22 lines printed on it. In this lesson we use eight of them:
| Column | What it holds | Example (charizard) |
|---|---|---|
pokemon |
The name, in lower case | charizard |
type_1 |
First type | fire |
type_2 |
Second type, blank if there is none | flying |
hp, attack, speed |
Base stats, whole numbers | 78, 84, 100 |
weight |
Weight in kilograms | 90.5 |
generation_id |
Generation number, 1 to 7, blank for some | 1 |
Open SQL → SQL Lab in Superset, choose the database Dades classe and the schema public, and you are ready to type. You run a query with the Run button or with Ctrl+Enter.
Why SQL looks like English
In 1974, Donald Chamberlin and Raymond Boyce, two researchers at IBM in San Jose (California), published a language for asking questions to databases. They wanted people who were not programmers to be able to read it, so they called it SEQUEL: Structured English Query Language. The name was already a registered trademark of an aircraft company, so it lost its vowels and became SQL. More than fifty years later you still write SELECT, FROM and WHERE exactly as they did.
The shape of a query¶
A query has up to five lines, always in this order. Each one is a move with your hands.
| Line | With the shoebox | Rule |
|---|---|---|
SELECT |
Which lines of the card you read | Column names, separated by commas. * means all of them. |
FROM |
Which box | The table. |
WHERE |
Which cards you take out | A condition that is true or false for each row. |
ORDER BY |
How you sort the pile | ASC (small first, the default) or DESC (big first). |
LIMIT |
How many cards you keep from the top | A number. Always last. |
flowchart LR
A["<b>FROM</b><br>open the box:<br>949 cards"] --> B["<b>WHERE</b><br>take out the cards<br>you want"]
B --> C["<b>ORDER BY</b><br>sort the pile"]
C --> D["<b>LIMIT</b><br>keep the top ones"]
D --> E["<b>SELECT</b><br>read only the lines<br>you asked for"]
Here is the question from the top of the page, written as a query:
SELECT pokemon, speed
FROM pokemon
WHERE type_1 = 'electric'
ORDER BY speed DESC
LIMIT 1;
| pokemon | speed |
|---|---|
| electrode | 150 |
One row, two columns. The answer, and nothing else. The next sections build that sentence one line at a time.
SELECT: read only the lines you need¶
SELECTchooses the columns of the result.FROMnames the table they come from.
SELECT * FROM pokemon hands you all 22 columns of all 949 rows. It is useful for a first look, and useless as an answer. Name the columns instead:
SELECT pokemon, type_1, attack FROM pokemon;
It still returns 949 rows, but only three columns. The first four:
| pokemon | type_1 | attack |
|---|---|---|
| bulbasaur | grass | 49 |
| ivysaur | grass | 62 |
| venusaur | grass | 82 |
| charmander | fire | 52 |
Careful: a missing comma gives no error
Write SELECT pokemon type_1 FROM pokemon (no comma) and the query runs. But you get one column, called type_1, full of names. Without the comma, SQL reads the second word as a new name for the first column. If a column you asked for is missing, count your commas.
WHERE: take cards out of the box¶
WHEREkeeps only the rows for which a condition is true. A condition is a yes-or-no question asked to every row, one by one.
SELECT pokemon, type_1, attack FROM pokemon WHERE type_1 = 'fire';
This returns 59 rows: charmander, charmeleon, charizard, vulpix... The condition type_1 = 'fire' was asked 949 times, and 59 cards said yes.
The = is a comparison operator. There are six:
| Operator | Meaning | Example | Rows |
|---|---|---|---|
= |
equal to | type_1 = 'fire' |
59 |
<> |
different from | type_1 <> 'fire' |
890 |
> |
greater than | attack > 150 |
19 |
>= |
greater than or equal to | attack >= 150 |
30 |
< |
less than | speed < 20 |
17 |
<= |
less than or equal to | speed <= 20 |
33 |
Two rules cause most of the errors in a first week of SQL:
| Rule | Right | Wrong | What happens |
|---|---|---|---|
| Text goes in single quotes, numbers do not | type_1 = 'fire', attack > 150 |
type_1 = fire |
An error: the database thinks fire is a column. SQL Lab says We can't seem to resolve the column "fire". |
| Text is case-sensitive | 'fire' |
'Fire' |
No error, and 0 rows. |
Careful: zero rows is not always 'there are none'
WHERE type_1 = 'Fire' returns nothing because no card says Fire with a capital F. The database did not complain: it answered the question you asked. Before you believe an empty result, check your spelling. And double quotes are not a way out: in PostgreSQL "fire" means a column name, 'fire' means text.
Two conditions: AND, OR¶
A logical operator joins two conditions.
AND: both must be true.OR: one is enough.
SELECT pokemon, type_1, type_2 FROM pokemon
WHERE type_1 = 'dragon' AND type_2 = 'flying';
Six rows: dragonite, altaria, salamence, rayquaza and the Mega forms of the last two. Change AND to OR and ask something wider:
SELECT pokemon, type_1, type_2 FROM pokemon
WHERE type_1 = 'dragon' OR type_2 = 'dragon';
61 rows: every Pokémon that is a dragon in either slot. AND makes a result smaller, OR makes it bigger.
ORDER BY and LIMIT: sort the pile, keep the top¶
ORDER BYsorts the result by a column.LIMITkeeps only the first rows.
SELECT pokemon, type_1, attack FROM pokemon
WHERE attack > 150
ORDER BY attack DESC;
19 rows, strongest first. Together, the two lines answer every "top N" question:
SELECT pokemon, attack FROM pokemon ORDER BY attack DESC LIMIT 5;
| pokemon | attack |
|---|---|
| mewtwo-mega-x | 190 |
| heracross-mega | 185 |
| kartana | 181 |
| deoxys-attack | 180 |
| groudon-primal | 180 |
Look at the last two rows. Three Pokémon have an attack of 180: deoxys-attack, groudon-primal and rayquaza-mega. Only two fit in a top 5, and the database picked them however it liked. Your screen may show a different pair, and you would both be right.
Careful: LIMIT cuts a tie arbitrarily
When several rows share the value you sort by, their order is not defined. To decide it yourself, add a second sort column: ORDER BY attack DESC, pokemon sorts ties by name. And remember that LIMIT 5 alone, with no ORDER BY, gives you any five rows — not the five best.
DISTINCT: getting to know a column¶
DISTINCTremoves repeated rows from the result, so each different value appears once.
Before you filter by a column, find out which values it can take:
SELECT DISTINCT type_1 FROM pokemon ORDER BY type_1;
18 rows, from bug to water: the 18 types, each one once. Now you know that the type is written fire, not Fire and not FIRE. DISTINCT is the cure for the zero-rows trap above.
Four special filters¶
Comparison operators cover numbers and exact text. Four more filters cover the rest.
| Filter | With the shoebox | Example | Rows |
|---|---|---|---|
LIKE |
Cards whose text looks like a pattern | pokemon LIKE 'pika%' |
13 |
IN |
Cards whose value is on my list | type_1 IN ('fire', 'water', 'grass') |
269 |
BETWEEN |
Cards inside a range, both ends included | speed BETWEEN 100 AND 120 |
126 |
IS NULL |
Cards where that line is blank | type_2 IS NULL |
439 |
LIKE uses two wildcards, symbols that stand for text you do not know: % is any text (also none), _ is exactly one character. 'pika%' means "starts with pika": pikachu and its twelve costumes (pikachu-rock-star, pikachu-phd...).
IN is short for several OR: type_1 = 'fire' OR type_1 = 'water' OR type_1 = 'grass'. Put NOT in front for the opposite: type_1 NOT IN ('fire', 'water', 'grass') returns 680 rows, and 269 + 680 = 949. Nobody got lost.
BETWEEN includes both ends: speed BETWEEN 100 AND 120 is the same as speed >= 100 AND speed <= 120. Both return 126. With > and < you would get 83, because the Pokémon at exactly 100 and exactly 120 fall out.
IS NULL finds blanks: the 439 Pokémon with only one type. IS NOT NULL finds the other 510. This one deserves its own section.
NULL: the blank line on the card¶
NULLmeans unknown: nothing is written here. It is not zero, and it is not an empty text.
Run this, and count:
SELECT pokemon, type_1, type_2 FROM pokemon WHERE type_2 = NULL;
0 rows. No error. Did the 439 single-type Pokémon disappear?
No. Think of two cards with a blank line. Are the two blanks equal? You cannot tell: something might belong there, and you do not know what. So when you compare anything with NULL, the database answers "unknown" — and WHERE only keeps the rows where the answer is a clear yes.
| You write | The database answers | Rows |
|---|---|---|
type_2 = NULL |
"unknown" for every row | 0 |
type_2 = '' |
"no": a blank is not an empty text either | 0 |
type_2 IS NULL |
"yes" for the blank ones | 439 |
type_2 IS NOT NULL |
"yes" for the filled ones | 510 |
That is why SQL has a separate question for blanks. Remember it: IS NULL, never = NULL.
Wrong answers that look right¶
Every trap on this page has one thing in common: none of them gives an error. The database does exactly what you wrote, and returns a believable number.
| Trap | What you get | Lesson |
|---|---|---|
LIKE '%mega%' to count Mega evolutions |
50 instead of 48: meganium and yanmega sneak in |
A pattern matches text, not meaning. Use '%-mega%', and look at the rows, not only the count. |
= NULL |
0 rows | Blanks need IS NULL. |
Mixing AND and OR without brackets |
79 instead of 34 | AND binds first, like × before +. Use brackets. |
LIMIT on a tie |
A different top 5 from your classmate | Add a second sort column. |
The third one, in detail. You want fire or water Pokémon with an attack above 100:
SELECT COUNT(*) FROM pokemon
WHERE type_1 = 'fire' OR type_1 = 'water' AND attack > 100;
79. The database read it as "all fire Pokémon (59), plus the water ones with attack above 100 (20)". Add brackets around the OR part:
SELECT COUNT(*) FROM pokemon
WHERE (type_1 = 'fire' OR type_1 = 'water') AND attack > 100;
34. Same words, different question. (COUNT(*) counts rows; it opens the next lesson.)
Try it: the Pokédex hunt¶
Open SQL Lab and answer each question with one query. Write the query first, then open the solution and compare your number.
| N. | Question |
|---|---|
| 1 | Name and weight of the three heaviest Pokémon. |
| 2 | How many Mega evolutions are there? Their name contains -mega. |
| 3 | How many Pokémon have no generation (generation_id)? |
| 4 | Electric, single-type, speed from 100 to 120: names, fastest first. |
| 5 | Name and attack of the single-type Pokémon with an attack above 150, strongest first. |
Solutions
1 — SELECT pokemon, weight FROM pokemon ORDER BY weight DESC LIMIT 3; → celesteela and cosmoem (999.9), then groudon-primal (999.7).
2 — SELECT COUNT(*) FROM pokemon WHERE pokemon LIKE '%-mega%'; → 48.
3 — SELECT COUNT(*) FROM pokemon WHERE generation_id IS NULL; → 147. Keep that number in mind: it comes back in the next two lessons.
4 — SELECT pokemon, speed FROM pokemon WHERE type_1 = 'electric' AND type_2 IS NULL AND speed BETWEEN 100 AND 120 ORDER BY speed DESC; → 6 rows, from zebstrika (116) down to voltorb (100).
5 — SELECT pokemon, attack FROM pokemon WHERE type_2 IS NULL AND attack > 150 ORDER BY attack DESC; → 5 rows, first deoxys-attack (180).
If SQL Lab answers with a red box, read the message: it usually names the word it tripped on. Look just before that word for a missing quote or a misspelt keyword. And run one query at a time: with several in the editor, you only see the result of the last one.
Key takeaways¶
- A query has a fixed shape:
SELECT(columns) ·FROM(table) ·WHERE(rows) ·ORDER BY(sort) ·LIMIT(cut). WHEREasks a condition to every row. Text goes in single quotes and is case-sensitive; numbers have no quotes.ANDnarrows,ORwidens.ANDbinds first: when you mix them, use brackets.ORDER BY ... DESC LIMIT nanswers "top N", butLIMITcuts ties arbitrarily.DISTINCTshows the different values of a column: use it before you filter.LIKEmatches a pattern,INa list,BETWEENa range with both ends,IS NULLa blank.= NULLis never true.- The dangerous mistakes do not give errors. They return a number that looks right.
Quick check¶
1. Which query returns only the name and the attack of every Pokémon?
-
SELECT * FROM pokemon; -
SELECT pokemon, attack FROM pokemon; -
SELECT pokemon attack FROM pokemon; -
FROM pokemon SELECT pokemon, attack;
SELECT lists the columns, separated by commas, and comes before FROM. * returns all 22 columns. Without the comma, the query runs but renames the first column: one column instead of two.
2. WHERE type_1 = 'Fire' returns 0 rows and no error. What is the most likely reason?
- The table has no fire Pokémon
- Text must go in double quotes
- Text is case-sensitive, and the table stores
firein lower case -
WHEREcannot compare text
The database compared every card with Fire and none matched, because text is case-sensitive. Zero rows is not proof that "there are none": run SELECT DISTINCT type_1 to see how the values are really written.
3. What does SELECT pokemon, speed FROM pokemon ORDER BY speed DESC LIMIT 3; return?
- Any three Pokémon
- The three slowest Pokémon
- The three fastest Pokémon
- Every Pokémon with a speed above 3
ORDER BY speed DESC sorts the pile with the biggest speed on top, and LIMIT 3 keeps the first three cards. Without ORDER BY, LIMIT 3 would give any three rows.
4. Why does WHERE type_2 = NULL return 0 rows?
- Because every Pokémon has a second type
- Because
NULLmust be written in quotes - Because NULL means unknown, so a comparison with it is never a clear yes
- Because
=only works with numbers
NULL is not a value: it means "nothing written here". Comparing with it gives "unknown", and WHERE keeps only the rows that answer yes. The right question is type_2 IS NULL, which finds 439 rows.
5. You count Mega evolutions with LIKE '%mega%' and get 50. The real number is 48. What happened?
-
LIKEignores the first and last letter of a name - The pattern also matches
meganiumandyanmega, which are not Mega evolutions -
%only matches one character - Two Mega evolutions are stored twice
A pattern matches text, not meaning. %mega% means "contains mega anywhere". The Mega evolutions are written with a hyphen, so '%-mega%' returns the right 48. Always look at the rows, not only at the count.
6. WHERE type_1 = 'fire' OR type_1 = 'water' AND attack > 100 returns 79 rows. With brackets around the OR part it returns 34. Why?
- Brackets make the query faster, so fewer rows arrive
-
ORbinds first, so the first version ignores the attack -
ANDbinds first: without brackets the attack condition only applies to water - The first version counts some Pokémon twice
AND binds before OR, like × before +. Without brackets you get all 59 fire Pokémon plus the 20 strong water ones. With brackets, the attack condition applies to both types: 34.
7. A table songs has the columns title, artist, year and streams. Pick the two queries that correctly return the five most-streamed songs from 2020 to 2023.
-
SELECT title, streams FROM songs WHERE year BETWEEN 2020 AND 2023 ORDER BY streams DESC LIMIT 5; -
SELECT title, streams FROM songs WHERE year >= 2020 AND year <= 2023 ORDER BY streams DESC LIMIT 5; -
SELECT title, streams FROM songs WHERE year BETWEEN 2020 AND 2023 LIMIT 5; -
SELECT title, streams FROM songs WHERE year > 2020 AND year < 2023 ORDER BY streams DESC LIMIT 5;
BETWEEN includes both ends, so it equals >= and <=. The third query has no ORDER BY: it returns any five songs. The fourth uses > and <, which silently drops 2020 and 2023.
So far every answer was a list of cards. What if the question is "how many fire Pokémon?" — and then the same for all 18 types at once? Next: SQL Lab: groups.