# Relaties en JOIN

Zelf tabellen maken met CREATE TABLE, ze aan elkaar hangen met foreign keys, en met JOIN data uit meerdere tabellen tegelijk ophalen.

Multimedia & Creatieve Technologie · 2.1 Programming principles · Les 5 · Databases, deel 2 · versie 1.0.0 · 2026-10-10

Tags: database, sql, mysql

Slides: <https://slides.mctantwerp.be/nl/programming-principles/06-relations-joins>

Speaker notes: <https://slides.mctantwerp.be/nl/programming-principles/06-relations-joins.notes.md>

<!-- slide 2 -->

## Agenda

1. Tabellen maken
2. Tabellen aanpassen
3. Relaties
4. Foreign keys
5. JOIN
6. LEFT JOIN en meer
7. Opdracht

<!-- slide 3 -->

## Vorige week en vandaag

### Vorige week

- Eén tabel met 151 Pokémon
- SELECT, WHERE, ORDER BY, GROUP BY
- UPDATE, DELETE en INSERT

### Vandaag

- Zelf tabellen maken en aanpassen
- Trainers, hun vangsten en de gyms
- Foreign keys
- JOIN: meerdere tabellen in één query

<!-- slide 4 -->

## Hoeveel rijen geeft `SELECT * FROM pokemon WHERE type_2 = NULL` terug?

- A. Alle 151
- B. De 84 Pokémon zonder tweede type
- C. Geen enkele
- D. Een foutmelding

**Antwoord:** C. Geen enkele

Vergelijken met NULL is nooit waar, ook niet met =. Je krijgt 0 rijen en geen foutmelding. De 84 zonder tweede type vind je met IS NULL. Vandaag komt NULL terug bij LEFT JOIN.

<!-- slide 5 -->

## 1. Tabellen maken

<!-- slide 6 -->

### Data en structuur

- **Data**: `SELECT` ophalen, `INSERT` toevoegen, `UPDATE` aanpassen, `DELETE` verwijderen
- **Structuur**: `CREATE` een database of tabel maken, `ALTER` een tabel aanpassen, `DROP` weggooien

> [!NOTE]
> Een query op de data raakt rijen. Een query op de structuur raakt de tabel zelf: haar kolommen, hun types en hun regels.

<!-- slide 7 -->

### CREATE TABLE

```sql
CREATE TABLE trainers (
  id INT UNSIGNED NOT NULL AUTO_INCREMENT,
  name VARCHAR(50) NOT NULL,
  email VARCHAR(255) NOT NULL UNIQUE,
  hometown VARCHAR(50) NULL,
  badges TINYINT UNSIGNED NOT NULL DEFAULT 0,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id)
);
```

- De naam van de tabel, en dan haakjes
- Per kolom een regel: de naam, het type, en de regels voor de data
- De primary key sluit af. Bijna altijd `id`.
- Een komma na elke regel, behalve na de laatste

<!-- slide 8 -->

### UNSIGNED

| Type | Van | Tot |
| --- | --- | --- |
| `TINYINT` | -128 | 127 |
| `TINYINT UNSIGNED` | 0 | 255 |
| `INT` | -2.147.483.648 | 2.147.483.647 |
| `INT UNSIGNED` | 0 | 4.294.967.295 |

- Evenveel bytes, maar geen negatieve getallen. Het bereik naar boven verdubbelt.
- Een `id`, een `level` of een aantal `badges` is nooit negatief. Met UNSIGNED weigert MySQL een -1.
- Een foreign key krijgt exact hetzelfde type als de id waar hij naar verwijst: `trainer_id INT UNSIGNED`.

<!-- slide 9 -->

### Constraints

| Constraint | De regel | Voorbeeld |
| --- | --- | --- |
| `NOT NULL` | De kolom mag niet leeg zijn | `name VARCHAR(50) NOT NULL` |
| `UNIQUE` | Geen twee rijen met dezelfde waarde | `email VARCHAR(255) UNIQUE` |
| `DEFAULT` | De waarde als je er geen meegeeft | `badges TINYINT DEFAULT 0` |
| `PRIMARY KEY` | NOT NULL en UNIQUE samen, één per tabel | `PRIMARY KEY (id)` |
| `FOREIGN KEY` | Verwijst naar een rij in een andere tabel | `trainer_id` |

<!-- slide 10 -->

Querydex, een SQL editor op de slide met de tabel pokemon (151 Pokémon), waar de slide een tabel bij maakt. CREATE TABLE. Startquery: `CREATE TABLE trainers ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE, hometown VARCHAR(50) NULL, badges TINYINT UNSIGNED NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) );` Knoppen met andere queries: `CREATE TABLE trainers ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE, hometown VARCHAR(50) NULL, badges TINYINT UNSIGNED NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) );`, `INSERT INTO trainers (name, email) VALUES ('Ash', 'ash@kanto.test');`, `INSERT INTO trainers (email) VALUES ('misty@kanto.test');`, `INSERT INTO trainers (name, email) VALUES ('Gary', 'ash@kanto.test');`, `INSERT INTO trainers (name, email, hometown, badges) VALUES ('Misty', 'misty@kanto.test', 'Cerulean City', 2);`.

<!-- slide 11 -->

### De database zegt nee

```sql
INSERT INTO trainers (name, email, badges) VALUES ('Gary', 'gary@kanto.test', 'veel');
INSERT INTO trainers (name, email, badges) VALUES ('Gary', 'gary@kanto.test', 300);
INSERT INTO trainers (email) VALUES ('gary@kanto.test');
```

*MySQL*

```text
ERROR 1366: Incorrect integer value: 'veel' for column 'badges' at row 1
ERROR 1264: Out of range value for column 'badges' at row 1
ERROR 1364: Field 'name' doesn't have a default value
```

> [!TIP]
> Hoe strenger je tabel, hoe minder rommel erin belandt. Een fout bij de INSERT is goedkoper dan foute data die je een jaar later vindt.

<!-- slide 12 -->

## 2. Tabellen aanpassen

<!-- slide 13 -->

### ALTER TABLE

```sql
ALTER TABLE trainers
  ADD is_active BOOLEAN NOT NULL DEFAULT 1;

ALTER TABLE trainers
  MODIFY COLUMN name VARCHAR(100) NOT NULL;

ALTER TABLE trainers
  RENAME COLUMN hometown TO city;

ALTER TABLE trainers
  DROP COLUMN is_active;
```

- `ADD`: een kolom erbij. Elke bestaande rij krijgt de DEFAULT.
- `MODIFY COLUMN`: een ander type of andere regels. Je schrijft de hele definitie opnieuw.
- `RENAME COLUMN`: een andere naam, de data blijft
- `DROP COLUMN`: de kolom weg, met alles wat erin stond

<!-- slide 14 -->

Querydex, een SQL editor op de slide met de tabellen pokemon (151 Pokémon) en trainers (13 trainers). ALTER TABLE. Startquery: `ALTER TABLE trainers ADD is_active BOOLEAN NOT NULL DEFAULT 1;` Knoppen met andere queries: `ALTER TABLE trainers ADD is_active BOOLEAN NOT NULL DEFAULT 1;`, `ALTER TABLE trainers RENAME COLUMN hometown TO city;`, `ALTER TABLE trainers DROP COLUMN badges;`, `ALTER TABLE trainers MODIFY COLUMN name VARCHAR(100) NOT NULL;`.

<!-- slide 15 -->

### DROP

```sql
DELETE FROM trainers;

DROP TABLE trainers;

DROP DATABASE pokedex;
```

- De rijen weg, de tabel blijft staan
- De tabel weg, met alle rijen
- De database weg, met alle tabellen

> [!WARNING]
> **Geen prullenbak**
>
> MySQL vraagt niets en bewaart niets. Lees de naam van de database twee keer voor je op Enter drukt.

<!-- slide 16 -->

### In TablePlus

1. **Nieuwe tabel**: Rechtsklik in de lijst met tabellen, en New, Table
2. **Structure**: De knop onderaan een tabel toont haar kolommen, types en regels
3. **Aanpassen**: Dubbelklik op een naam of een type, of + Column voor een nieuwe
4. **Commit**: Pas met Windows <kbd>Ctrl</kbd>+<kbd>S</kbd>, Mac <kbd>Cmd</kbd>+<kbd>S</kbd> voert TablePlus de ALTER TABLE uit

> [!NOTE]
> **Klikken of typen**
>
> TablePlus schrijft dezelfde SQL als jij. Een query kan je bewaren, delen en opnieuw uitvoeren. Een reeks klikken niet.

<!-- slide 17 -->

### Tussenopdracht: Spelletjesclub

*20 minuten, Individueel*

Een spelletjesclub wil haar gezelschapsspellen bijhouden. Schrijf de SQL in TablePlus, op je eigen MySQL.

1. Maak een database `play_for_fun` en een tabel `games`
2. Per spel: de naam, wanneer het uitkwam (als ze dat weten), of het nog speelbaar is, het minimum en maximum aantal spelers, en de nieuwprijs
3. Voeg drie spellen toe die je zelf kent
4. Wanneer een spel uitkwam, gebruikt niemand: die kolom mag weg
5. Het maximum aantal spelers is niet langer verplicht

> [!TIP]
> **Spelletjesclub**
>
> *Database*
>
> Daarna selecteer je ze met Windows <kbd>Ctrl</kbd>+<kbd>K</kbd>, Mac <kbd>Cmd</kbd>+<kbd>K</kbd>.
>
> ```sql
> CREATE DATABASE play_for_fun;
> ```
>
> *Tabel*
>
> Een datum en geen leeftijd, DECIMAL voor geld, en alleen release_date mag leeg zijn.
>
> ```sql
> CREATE TABLE games (
>   id INT UNSIGNED NOT NULL AUTO_INCREMENT,
>   name VARCHAR(100) NOT NULL,
>   release_date DATE NULL,
>   playable BOOLEAN NOT NULL DEFAULT 1,
>   min_players TINYINT UNSIGNED NOT NULL,
>   max_players TINYINT UNSIGNED NOT NULL,
>   price DECIMAL(6,2) NOT NULL,
>   PRIMARY KEY (id)
> );
> ```
>
> *Spellen*
>
> id en playable laat je weg: AUTO_INCREMENT en DEFAULT vullen ze in.
>
> ```sql
> INSERT INTO games (name, release_date, min_players, max_players, price)
> VALUES
>   ('Monopoly', '1935-02-06', 2, 8, 34.99),
>   ('Catan', '1995-01-01', 3, 4, 44.95),
>   ('Weerwolven', NULL, 8, 18, 12.50);
> ```
>
> *Kolom weg*
>
> ```sql
> ALTER TABLE games
>   DROP COLUMN release_date;
> ```
>
> *Niet verplicht*
>
> De hele definitie opnieuw, met NULL in plaats van NOT NULL.
>
> ```sql
> ALTER TABLE games
>   MODIFY COLUMN max_players TINYINT UNSIGNED NULL;
> ```

<!-- slide 18 -->

## 3. Relaties

<!-- slide 19 -->

### Wie heeft welke Pokémon

#### trainers 🚨

| id | name | pokemon |
| --- | --- | --- |
| 1 | Ash | Pikachu, Bulbasaur, Charizard |
| 2 | Misty | Staryu, Starmie, Psyduck |
| 3 | Brock | Onix, Geodude |

#### Wat er misgaat

- Wie heeft een Pikachu? Dat wordt zoeken met LIKE in een lap tekst
- Hoeveel Pokémon heeft Ash? Tel de komma’s
- Welk level heeft die Charizard? Daar is geen plek voor
- Eén typfout, en Pikachuu bestaat ook

<!-- slide 20 -->

### Eén rij per vangst

ERD: trainers (id PK, name, hometown); catches (id PK, trainer_id FK, level, caught_on). trainers.id 1:n catches.trainer_id.

Een vangst onthoudt de `id` van haar trainer. De naam en de hometown staan één keer in `trainers`, en nergens anders.

<!-- slide 21 -->

### Foreign key

- Een kolom die naar de primary key van een andere tabel verwijst
- De naam: het enkelvoud van die tabel, en `_id`. Dus `trainer_id`, niet `trainers_id` of `trainer`.
- Hetzelfde type als de primary key waar hij naar wijst: `INT UNSIGNED`
- De tabel met de foreign key is de child, de andere de parent

<!-- slide 22 -->

### One to many

ERD: trainers (id PK, name, hometown); catches (id PK, trainer_id FK, level, caught_on). trainers.id 1:n catches.trainer_id.

Eén trainer heeft **meerdere** vangsten

Eén vangst is van **één** trainer

> [!TIP]
> De foreign key staat aan de kant van meerdere.

<!-- slide 23 -->

### Many to many

ERD: trainers (id PK, name); catches (id PK, trainer_id FK, pokemon_id FK, level); pokemon (id PK, name, type). trainers.id 1:n catches.trainer_id, pokemon.id 1:n catches.pokemon_id.

Eén trainer heeft **meerdere** Pokémon

Eén Pokémon zit bij **meerdere** trainers

> [!TIP]
> Twee keer meerdere: dan komt er een tussentabel, met een foreign key naar elke kant.

<!-- slide 24 -->

### One to one

ERD: gyms (id PK, name, badge, leader_id FK); trainers (id PK, name, hometown). trainers.id 1:0..1 gyms.leader_id.

Eén gym heeft **één** leader

Eén trainer leidt **één** gym

> [!TIP]
> Een foreign key die ook `UNIQUE` is. Komt weinig voor.

<!-- slide 25 -->

### Drie soorten

Eén trainer heeft meerdere vangsten. Eén vangst is van één trainer. One to many.

Eén trainer heeft meerdere Pokémon. Eén Pokémon zit bij meerdere trainers. Many to many.

Eén trainer leidt één gym. Eén gym heeft één trainer. One to one.

Eén klant heeft meerdere orders. Eén order is van één klant. One to many.

Eén order bevat meerdere producten. Eén product zit in meerdere orders. Many to many.

<!-- slide 26 -->

### Een blogpost heeft meerdere tags, en een tag hangt aan meerdere posts. Waar komt de foreign key?

- A. In posts, een kolom tag_id
- B. In tags, een kolom post_id
- C. In een tussentabel, met post_id en tag_id
- D. Nergens: de tags komen als tekst in posts

**Antwoord:** C. In een tussentabel, met post_id en tag_id

Twee keer meerdere, dus many to many, dus een tussentabel. Met tag_id in posts kan een post maar één tag hebben. Met post_id in tags hoort een tag maar bij één post. Tekst met komma's is de trainers tabel met het lijstje Pokémon.

<!-- slide 27 -->

### De hele database

ERD: gyms (id PK, name, city, badge, leader_id FK); trainers (id PK, name, email, hometown, badges, created_at); catches (id PK, trainer_id FK, pokemon_id FK, level, caught_on); pokemon (id PK, name, type, type_2, hp, evolves_from FK). trainers.id 1:0..1 gyms.leader_id, trainers.id 1:n catches.trainer_id, pokemon.id 1:n catches.pokemon_id, pokemon.id 1:0..n pokemon.evolves_from.

Zo'n tekening heet een ERD. De kraaienpoot staat aan de kant van meerdere, het streepje aan de kant van één.

<!-- slide 28 -->

## 4. Foreign keys

<!-- slide 29 -->

### Een kolom is nog geen relatie

```sql
CREATE TABLE catches (
  id INT UNSIGNED NOT NULL AUTO_INCREMENT,
  trainer_id INT UNSIGNED NOT NULL,
  pokemon_id INT UNSIGNED NOT NULL,
  level TINYINT UNSIGNED NOT NULL DEFAULT 5,
  caught_on DATE NULL,
  PRIMARY KEY (id),
  FOREIGN KEY (trainer_id) REFERENCES trainers (id),
  FOREIGN KEY (pokemon_id) REFERENCES pokemon (id)
);
```

- Zonder meer zijn dit twee getallen. Trainer 99 mag, ook als die niet bestaat.
- `FOREIGN KEY` zegt welke kolom, `REFERENCES` naar welke tabel en kolom
- Vanaf nu weigert MySQL een vangst met een trainer of een Pokémon die niet bestaat

<!-- slide 30 -->

Querydex, een SQL editor op de slide met de tabellen pokemon (151 Pokémon) en trainers (13 trainers). FOREIGN KEY. Startquery: `CREATE TABLE catches ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, trainer_id INT UNSIGNED NOT NULL, pokemon_id INT UNSIGNED NOT NULL, level TINYINT UNSIGNED NOT NULL DEFAULT 5, caught_on DATE NULL, PRIMARY KEY (id), FOREIGN KEY (trainer_id) REFERENCES trainers (id), FOREIGN KEY (pokemon_id) REFERENCES pokemon (id) );` Knoppen met andere queries: `CREATE TABLE catches ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, trainer_id INT UNSIGNED NOT NULL, pokemon_id INT UNSIGNED NOT NULL, level TINYINT UNSIGNED NOT NULL DEFAULT 5, caught_on DATE NULL, PRIMARY KEY (id), FOREIGN KEY (trainer_id) REFERENCES trainers (id), FOREIGN KEY (pokemon_id) REFERENCES pokemon (id) );`, `INSERT INTO catches (trainer_id, pokemon_id, level) VALUES (1, 25, 81);`, `INSERT INTO catches (trainer_id, pokemon_id) VALUES (99, 25);`, `INSERT INTO catches (trainer_id, pokemon_id) VALUES (1, 999);`.

<!-- slide 31 -->

### Wat MySQL zegt

*MySQL*

```text
ERROR 1452: Cannot add or update a child row: a foreign key constraint fails
(`pokedex`.`catches`, CONSTRAINT `catches_ibfk_1`
FOREIGN KEY (`trainer_id`) REFERENCES `trainers` (`id`))
```

- De tabel waar het misloopt: `catches`
- De kolom: `trainer_id`, en waar ze naar verwijst: `trainers.id`
- Child row: de rij die je toevoegt, verwijst naar een trainer die niet bestaat

<!-- slide 32 -->

### ON DELETE

|   | Verwijder je de parent, dan | In onze database |
| --- | --- | --- |
| `RESTRICT` | weigert MySQL, zolang er rijen naar verwijzen. De standaard. | `catches.pokemon_id` |
| `CASCADE` | gaan de rijen die ernaar verwijzen mee weg | `catches.trainer_id` |
| `SET NULL` | wordt de foreign key leeg. De kolom moet NULL mogen zijn. | `gyms.leader_id` |

```sql
FOREIGN KEY (trainer_id) REFERENCES trainers (id) ON DELETE CASCADE
```

<!-- slide 33 -->

Querydex, een SQL editor op de slide met vier tabellen: pokemon (151 Pokémon), trainers (13 trainers), catches (55 vangsten, met een trainer_id en een pokemon_id) en gyms (8 gyms, met een leader_id). ON DELETE. Startquery: `DELETE FROM trainers WHERE name = 'Ash';` Knoppen met andere queries: `DELETE FROM trainers WHERE name = 'Ash';`, `DELETE FROM pokemon WHERE name = 'Pikachu';`, `DELETE FROM pokemon WHERE name = 'Mew';`, `DELETE FROM trainers WHERE name = 'Brock';`.

<!-- slide 34 -->

### Vorige week: Shipping Company B

```sql
DELETE FROM shippers WHERE company = 'Shipping Company B';
```

*MySQL*

```text
ERROR 1451: Cannot delete or update a parent row: a foreign key constraint fails
(`northwind`.`orders`, CONSTRAINT `fk_orders_shippers`
FOREIGN KEY (`shipper_id`) REFERENCES `shippers` (`id`))
```

> [!NOTE]
> **Parent row**
>
> Er zijn nog orders die naar deze shipper verwijzen. Er staat geen `ON DELETE` bij, dus geldt `RESTRICT`.

<!-- slide 35 -->

### Achteraf toevoegen

```sql
ALTER TABLE catches
  ADD CONSTRAINT fk_catches_trainers
  FOREIGN KEY (trainer_id)
  REFERENCES trainers (id)
  ON DELETE CASCADE;

ALTER TABLE catches
  DROP FOREIGN KEY fk_catches_trainers;
```

- Een naam: `fk`, de child, de parent
- Zelfde type aan beide kanten. `INT` en `INT UNSIGNED` gaan niet samen.
- De data die er al staat, moet kloppen
- Een andere ON DELETE? Eerst DROP, dan opnieuw ADD.

<!-- slide 36 -->

### Tussenopdracht: Fabrikanten

*15 minuten, Individueel*

De spelletjesclub wil bij elk spel de fabrikant kennen. Van een fabrikant houden ze voorlopig alleen de naam bij.

1. Hoe los je dit op in `play_for_fun`? Zeg de relatie eerst in twee richtingen.
2. Maak wat nodig is, en vul het: drie fabrikanten, en elk spel krijgt er een
3. Leg de foreign key vast met `ON DELETE RESTRICT`, en verwijder een fabrikant
4. Vervang hem door `ON DELETE CASCADE`, en verwijder dezelfde fabrikant

> [!TIP]
> **Fabrikanten**
>
> *Relatie*
>
> Eén fabrikant heeft meerdere spellen, één spel heeft één fabrikant. One to many: de foreign key komt in games.
>
> ```sql
> CREATE TABLE manufacturers (
>   id INT UNSIGNED NOT NULL AUTO_INCREMENT,
>   name VARCHAR(100) NOT NULL,
>   PRIMARY KEY (id)
> );
>
> ALTER TABLE games
>   ADD manufacturer_id INT UNSIGNED NULL;
> ```
>
> *Vullen*
>
> ```sql
> INSERT INTO manufacturers (name)
> VALUES ('Hasbro'), ('Kosmos'), ('Asmodee');
>
> UPDATE games SET manufacturer_id = 1 WHERE name = 'Monopoly';
> UPDATE games SET manufacturer_id = 2 WHERE name = 'Catan';
> UPDATE games SET manufacturer_id = 3 WHERE name = 'Weerwolven';
> ```
>
> *RESTRICT*
>
> Error 1451: Monopoly verwijst nog naar Hasbro.
>
> ```sql
> ALTER TABLE games
>   ADD CONSTRAINT fk_games_manufacturers
>   FOREIGN KEY (manufacturer_id)
>   REFERENCES manufacturers (id)
>   ON DELETE RESTRICT;
>
> DELETE FROM manufacturers WHERE name = 'Hasbro';
> ```
>
> *CASCADE*
>
> Hasbro is weg, en Monopoly ook.
>
> ```sql
> ALTER TABLE games
>   DROP FOREIGN KEY fk_games_manufacturers;
>
> ALTER TABLE games
>   ADD CONSTRAINT fk_games_manufacturers
>   FOREIGN KEY (manufacturer_id)
>   REFERENCES manufacturers (id)
>   ON DELETE CASCADE;
>
> DELETE FROM manufacturers WHERE name = 'Hasbro';
> ```

<!-- slide 37 -->

## 5. JOIN

<!-- slide 38 -->

### Importeren

1. **Download pokedex-2.sql**: Trainers, hun vangsten en de gyms
2. **Selecteer pokedex**: De database van vorige week, met Windows <kbd>Ctrl</kbd>+<kbd>K</kbd>, Mac <kbd>Cmd</kbd>+<kbd>K</kbd>
3. **File, Import**: From SQL Dump, kies het bestand en Import
4. **Vernieuwen**: Windows <kbd>Ctrl</kbd>+<kbd>R</kbd>, Mac <kbd>Cmd</kbd>+<kbd>R</kbd>, en er staan vier tabellen

**[pokedex-2.sql](/decks/nl/programming-principles/06-relations-joins/pokedex-2.sql)**: Dezelfde tabellen als in de Querydex, voor MySQL en MariaDB

> [!WARNING]
> De tabel `pokemon` moet er al staan: `catches` verwijst ernaar.

<!-- slide 39 -->

Querydex, een SQL editor op de slide met vier tabellen: pokemon (151 Pokémon), trainers (13 trainers), catches (55 vangsten, met een trainer_id en een pokemon_id) en gyms (8 gyms, met een leader_id). Alleen nummers. Startquery: `SELECT * FROM catches;` Knoppen met andere queries: `SELECT * FROM catches;`, `SELECT * FROM trainers WHERE id = 1;`, `SELECT id, name, type FROM pokemon WHERE id = 25;`.

<!-- slide 40 -->

### JOIN

```sql
SELECT trainers.name, catches.level
FROM catches
INNER JOIN trainers
  ON catches.trainer_id = trainers.id;
```

- `FROM`: de tabel waar je vertrekt
- `INNER JOIN`: de tabel die erbij komt
- `ON`: welke kolommen gelijk moeten zijn. De foreign key en de primary key.
- `SELECT`: kolommen uit allebei, met de tabel ervoor

<!-- slide 41 -->

### Rij per rij

trainers JOIN catches ON t.id = c.trainer_id, 4 x 4. INNER JOIN (4): Eén rij per paar. Ash heeft twee vangsten en staat er twee keer. Sam heeft er geen en valt weg.

<!-- slide 42 -->

Querydex, een SQL editor op de slide met vier tabellen: pokemon (151 Pokémon), trainers (13 trainers), catches (55 vangsten, met een trainer_id en een pokemon_id) en gyms (8 gyms, met een leader_id). INNER JOIN. Startquery: `SELECT trainers.name, catches.level FROM catches INNER JOIN trainers ON catches.trainer_id = trainers.id;` Knoppen met andere queries: `SELECT trainers.name, catches.level FROM catches INNER JOIN trainers ON catches.trainer_id = trainers.id;`, `SELECT trainers.name, catches.level FROM catches INNER JOIN trainers ON catches.trainer_id = trainers.id WHERE trainers.hometown = 'Pallet Town' ORDER BY catches.level DESC;`, `SELECT trainers.name, catches.level FROM trainers INNER JOIN catches ON catches.trainer_id = trainers.id;`, `SELECT * FROM catches INNER JOIN trainers ON catches.trainer_id = trainers.id;`.

<!-- slide 43 -->

### Aliassen

```sql
SELECT t.name AS trainer, c.level
FROM catches c
INNER JOIN trainers t
  ON c.trainer_id = t.id;
```

*MySQL*

```text
ERROR 1052: Column 'name' in field list is ambiguous
```

- Een korte naam voor een tabel, binnen deze query: `catches c`
- Daarna overal `c.` en `t.`
- Twee tabellen met een kolom `name`? Zonder tabel ervoor weigert MySQL.

<!-- slide 44 -->

### Drie tabellen

```sql
SELECT t.name AS trainer, p.name AS pokemon, c.level
FROM catches c
INNER JOIN trainers t
  ON c.trainer_id = t.id
INNER JOIN pokemon p
  ON c.pokemon_id = p.id;
```

- Vertrek van de tussentabel
- Eerste JOIN: de trainer van de vangst
- Tweede JOIN: de Pokémon van de vangst
- Twee keer `name`, dus elk een eigen naam met `AS`

<!-- slide 45 -->

Querydex, een SQL editor op de slide met vier tabellen: pokemon (151 Pokémon), trainers (13 trainers), catches (55 vangsten, met een trainer_id en een pokemon_id) en gyms (8 gyms, met een leader_id). Drie tabellen. Startquery: `SELECT t.name AS trainer, p.name AS pokemon, c.level FROM catches c INNER JOIN trainers t ON c.trainer_id = t.id INNER JOIN pokemon p ON c.pokemon_id = p.id;` Knoppen met andere queries: `SELECT t.name AS trainer, p.name AS pokemon, c.level FROM catches c INNER JOIN trainers t ON c.trainer_id = t.id INNER JOIN pokemon p ON c.pokemon_id = p.id;`, `SELECT t.name AS trainer, p.name AS pokemon, p.type FROM catches c INNER JOIN trainers t ON c.trainer_id = t.id INNER JOIN pokemon p ON c.pokemon_id = p.id WHERE p.type = 'Fire';`, `SELECT t.name AS trainer, c.level FROM catches c INNER JOIN trainers t ON c.trainer_id = t.id INNER JOIN pokemon p ON c.pokemon_id = p.id WHERE p.name = 'Pikachu';`, `SELECT p.name AS pokemon, p.type, c.level FROM catches c INNER JOIN trainers t ON c.trainer_id = t.id INNER JOIN pokemon p ON c.pokemon_id = p.id WHERE t.name = 'Misty' ORDER BY c.level DESC;`, `SELECT name, level FROM catches c INNER JOIN trainers t ON c.trainer_id = t.id INNER JOIN pokemon p ON c.pokemon_id = p.id;`.

<!-- slide 46 -->

Querydex, een SQL editor op de slide met vier tabellen: pokemon (151 Pokémon), trainers (13 trainers), catches (55 vangsten, met een trainer_id en een pokemon_id) en gyms (8 gyms, met een leader_id). Met komma's. Startquery: `SELECT t.name AS trainer, p.name AS pokemon FROM trainers t, catches c, pokemon p WHERE c.trainer_id = t.id AND c.pokemon_id = p.id;` Knoppen met andere queries: `SELECT t.name AS trainer, p.name AS pokemon FROM trainers t, catches c, pokemon p WHERE c.trainer_id = t.id AND c.pokemon_id = p.id;`, `SELECT t.name AS trainer, p.name AS pokemon FROM trainers t, pokemon p;`.

<!-- slide 47 -->

Querydex, een SQL editor op de slide met vier tabellen: pokemon (151 Pokémon), trainers (13 trainers), catches (55 vangsten, met een trainer_id en een pokemon_id) en gyms (8 gyms, met een leader_id). Vang ze. Startquery: `SELECT t.name AS trainer, c.level FROM catches c INNER JOIN trainers t ON c.trainer_id = t.id;` Opdracht: De trainer, de Pokémon en het level van elke vangst van level 50 of hoger van een trainer uit Pallet Town. Een oplossing: `SELECT t.name, p.name, c.level FROM catches c INNER JOIN trainers t ON c.trainer_id = t.id INNER JOIN pokemon p ON c.pokemon_id = p.id WHERE t.hometown = 'Pallet Town' AND c.level >= 50;`

<!-- slide 48 -->

### trainers heeft 13 rijen, catches 55. Sam heeft geen enkele vangst. Hoeveel rijen geeft een INNER JOIN van de twee op trainer_id?

- A. 13
- B. 55
- C. 56
- D. 715

**Antwoord:** B. 55

Eén rij per paar dat matcht. Elke vangst heeft precies één trainer, dus 55. Sam matcht met niets en valt weg. 56 krijg je met een LEFT JOIN vanuit trainers, 715 als je de ON vergeet.

<!-- slide 49 -->

## 6. LEFT JOIN en meer

<!-- slide 50 -->

### Wie valt weg

trainers JOIN catches ON t.id = c.trainer_id, 4 x 4. INNER JOIN (4): Alleen de paren. Sam heeft geen vangst en valt weg. LEFT JOIN (5): Elke rij van de linkse tabel blijft. Sam staat erbij, met NULL waar zijn vangst had moeten staan.

<!-- slide 51 -->

Querydex, een SQL editor op de slide met vier tabellen: pokemon (151 Pokémon), trainers (13 trainers), catches (55 vangsten, met een trainer_id en een pokemon_id) en gyms (8 gyms, met een leader_id). LEFT JOIN. Startquery: `SELECT t.name, c.level FROM trainers t LEFT JOIN catches c ON c.trainer_id = t.id ORDER BY c.level;` Knoppen met andere queries: `SELECT t.name, c.level FROM trainers t INNER JOIN catches c ON c.trainer_id = t.id ORDER BY c.level;`, `SELECT t.name, c.level FROM trainers t LEFT JOIN catches c ON c.trainer_id = t.id ORDER BY c.level;`, `SELECT t.name FROM trainers t LEFT JOIN catches c ON c.trainer_id = t.id WHERE c.id IS NULL;`, `SELECT p.name, p.type FROM pokemon p LEFT JOIN catches c ON c.pokemon_id = p.id WHERE c.id IS NULL;`.

<!-- slide 52 -->

### Links en rechts

gyms JOIN trainers ON g.leader_id = t.id, 3 x 3. INNER JOIN (2): Alleen de gyms met een leader. LEFT JOIN (3): Alle gyms. Viridian Gym heeft geen leader en krijgt NULL. RIGHT JOIN (3): Alle trainers. Ash leidt geen gym en krijgt NULL.

<!-- slide 53 -->

### Welke JOIN

|   | Geeft terug | Wanneer |
| --- | --- | --- |
| `INNER JOIN` | Alleen de rijen met een match aan beide kanten | Bijna altijd |
| `LEFT JOIN` | Alle rijen van links, met NULL waar rechts niets is | Ook wie niets heeft, of net alleen die |
| `RIGHT JOIN` | Alle rijen van rechts | Nooit. Wissel de tabellen om en schrijf LEFT. |
| `FULL OUTER JOIN` | Alle rijen van beide kanten | Bestaat niet in MySQL |

<!-- slide 54 -->

Querydex, een SQL editor op de slide met vier tabellen: pokemon (151 Pokémon), trainers (13 trainers), catches (55 vangsten, met een trainer_id en een pokemon_id) en gyms (8 gyms, met een leader_id). JOIN en GROUP BY. Startquery: `SELECT t.name, COUNT(*) AS total FROM trainers t INNER JOIN catches c ON c.trainer_id = t.id GROUP BY t.id, t.name ORDER BY total DESC;` Knoppen met andere queries: `SELECT t.name, COUNT(*) AS total FROM trainers t INNER JOIN catches c ON c.trainer_id = t.id GROUP BY t.id, t.name ORDER BY total DESC;`, `SELECT t.name, COUNT(c.id) AS total FROM trainers t LEFT JOIN catches c ON c.trainer_id = t.id GROUP BY t.id, t.name ORDER BY total;`, `SELECT t.name, COUNT(*) AS total FROM trainers t LEFT JOIN catches c ON c.trainer_id = t.id GROUP BY t.id, t.name ORDER BY total;`, `SELECT t.name, ROUND(AVG(c.level), 1) AS avg_level, MAX(c.level) AS top FROM trainers t INNER JOIN catches c ON c.trainer_id = t.id GROUP BY t.id, t.name ORDER BY avg_level DESC;`, `SELECT p.type, COUNT(*) AS total FROM catches c INNER JOIN pokemon p ON c.pokemon_id = p.id GROUP BY p.type ORDER BY total DESC;`.

<!-- slide 55 -->

Querydex, een SQL editor op de slide met vier tabellen: pokemon (151 Pokémon), trainers (13 trainers), catches (55 vangsten, met een trainer_id en een pokemon_id) en gyms (8 gyms, met een leader_id). Self join. Startquery: `SELECT p.name, e.name AS evolves_from FROM pokemon p INNER JOIN pokemon e ON p.evolves_from = e.id;` Knoppen met andere queries: `SELECT p.name, e.name AS evolves_from FROM pokemon p INNER JOIN pokemon e ON p.evolves_from = e.id;`, `SELECT p.name, e.name AS evolves_from FROM pokemon p LEFT JOIN pokemon e ON p.evolves_from = e.id;`, `SELECT e.name, COUNT(*) AS evolutions FROM pokemon p INNER JOIN pokemon e ON p.evolves_from = e.id GROUP BY e.id, e.name ORDER BY evolutions DESC;`.

<!-- slide 56 -->

Querydex, een SQL editor op de slide met vier tabellen: pokemon (151 Pokémon), trainers (13 trainers), catches (55 vangsten, met een trainer_id en een pokemon_id) en gyms (8 gyms, met een leader_id). Vang ze. Startquery: `SELECT g.badge, t.name AS leader FROM gyms g INNER JOIN trainers t ON g.leader_id = t.id;` Opdracht: Per gym de badge, de naam van de leader en hoeveel Pokémon die heeft. Ook de gym zonder leader. Een oplossing: `SELECT g.badge, t.name, COUNT(c.id) FROM gyms g LEFT JOIN trainers t ON g.leader_id = t.id LEFT JOIN catches c ON c.trainer_id = t.id GROUP BY g.id, g.badge, t.name;`

<!-- slide 57 -->

### Je wil alle producten van een webshop die nog nooit besteld zijn. Wat schrijf je?

- A. products INNER JOIN order_details, en WHERE order_details.id IS NULL
- B. products LEFT JOIN order_details, en WHERE order_details.id IS NULL
- C. order_details LEFT JOIN products, en WHERE products.id IS NULL
- D. products LEFT JOIN order_details, en WHERE order_details.id = NULL

**Antwoord:** B. products LEFT JOIN order_details, en WHERE order_details.id IS NULL

Vertrek van wat je wil zien, de producten, en hou met LEFT JOIN ook die zonder match. IS NULL op de rechtse kant laat net die over. Een INNER JOIN heeft nooit NULL aan de rechtse kant. Vanuit order_details vind je bestellingen zonder product, en = NULL is nooit waar.

<!-- slide 58 -->

## 7. Opdracht

<!-- slide 59 -->

### Opdracht: Databases, deel 2

*Individueel*

Joins op Northwind, de groothandel van vorige week, en daarna een database die je zelf ontwerpt.

1. Begin met een verse Northwind: `DROP DATABASE northwind;`, een nieuwe maken en de twee bestanden opnieuw importeren
2. Maak de 15 queries met JOIN
3. Los de foutmelding van Shipping Company B op
4. Bouw de database van een to-do app, met foreign keys

**[northwind.sql](/decks/nl/programming-principles/05-databases/northwind.sql)**: De structuur, 20 tabellen

**[northwind-data.sql](/decks/nl/programming-principles/05-databases/northwind-data.sql)**: De data

<!-- slide 60 -->

### Opdracht: JOIN: twee tabellen

1. Elk order met zijn datum en de `company` van de customer
2. Elk order met de voornaam en de achternaam van de employee die het aannam
3. Elk order met de `company` van de shipper. Je krijgt er geen 48: welke ontbreken?
4. Alle orders van customers uit de state `NY`
5. Elke invoice met de `order_date` van haar order

> [!TIP]
> **JOIN: twee tabellen**
>
> *Customer*
>
> 48 rijen, één per order.
>
> ```sql
> SELECT o.id, o.order_date, c.company
> FROM orders o
> INNER JOIN customers c ON o.customer_id = c.id;
> ```
>
> *Employee*
>
> 48 rijen.
>
> ```sql
> SELECT o.id, e.first_name, e.last_name
> FROM orders o
> INNER JOIN employees e ON o.employee_id = e.id;
> ```
>
> *Shipper*
>
> 43 rijen. Vijf orders hebben nog geen shipper: hun shipper_id is NULL, en NULL matcht met niets.
>
> ```sql
> SELECT o.id, s.company
> FROM orders o
> INNER JOIN shippers s ON o.shipper_id = s.id;
> ```
>
> *NY*
>
> 5 rijen. De WHERE staat op een kolom van de andere tabel.
>
> ```sql
> SELECT o.id, o.order_date, c.company
> FROM orders o
> INNER JOIN customers c ON o.customer_id = c.id
> WHERE c.state_province = 'NY';
> ```
>
> *Invoice*
>
> 35 rijen.
>
> ```sql
> SELECT i.id, i.invoice_date, o.order_date
> FROM invoices i
> INNER JOIN orders o ON i.order_id = o.id;
> ```

<!-- slide 61 -->

### Opdracht: LEFT JOIN

1. Alle customers met de `id` van hun orders, ook de customers zonder order
2. Welke customers hebben nog nooit iets besteld?
3. Welke producten zijn nog nooit besteld?
4. Welke employee nam nog geen enkel order aan?
5. Elk order met de company van de shipper, ook de orders zonder shipper

> [!TIP]
> **LEFT JOIN**
>
> *Customers*
>
> 62 rijen: 48 orders, en 14 customers zonder order met NULL.
>
> ```sql
> SELECT c.company, o.id
> FROM customers c
> LEFT JOIN orders o ON o.customer_id = c.id;
> ```
>
> *Nooit besteld*
>
> 14 rijen.
>
> ```sql
> SELECT c.company
> FROM customers c
> LEFT JOIN orders o ON o.customer_id = c.id
> WHERE o.id IS NULL;
> ```
>
> *Producten*
>
> 21 rijen.
>
> ```sql
> SELECT p.product_name
> FROM products p
> LEFT JOIN order_details od ON od.product_id = p.id
> WHERE od.id IS NULL;
> ```
>
> *Employee*
>
> 1 rij: Steven Thorpe.
>
> ```sql
> SELECT e.first_name, e.last_name
> FROM employees e
> LEFT JOIN orders o ON o.employee_id = e.id
> WHERE o.id IS NULL;
> ```
>
> *Shipper*
>
> 48 rijen, vijf met NULL als shipper.
>
> ```sql
> SELECT o.id, s.company
> FROM orders o
> LEFT JOIN shippers s ON o.shipper_id = s.id;
> ```

<!-- slide 62 -->

### Opdracht: Meer tabellen en GROUP BY

1. Elke rij van `order_details` met de naam van het product en de datum van het order
2. Welke producten bestelde `Company A`? Elk product één keer.
3. Het aantal orders per customer, de meeste eerst
4. Het totaalbedrag per order: de som van `quantity * unit_price`
5. De omzet per `category` van product, de hoogste eerst
6. Het aantal orders per shipper, ook voor een shipper zonder orders

> [!TIP]
> **Meer tabellen en GROUP BY**
>
> *Orderlijnen*
>
> 58 rijen. order_details is de tussentabel.
>
> ```sql
> SELECT od.id, p.product_name, o.order_date
> FROM order_details od
> INNER JOIN products p ON od.product_id = p.id
> INNER JOIN orders o ON od.order_id = o.id;
> ```
>
> *Company A*
>
> 4 rijen. Vier tabellen, drie joins.
>
> ```sql
> SELECT DISTINCT p.product_name
> FROM customers c
> INNER JOIN orders o ON o.customer_id = c.id
> INNER JOIN order_details od ON od.order_id = o.id
> INNER JOIN products p ON od.product_id = p.id
> WHERE c.company = 'Company A';
> ```
>
> *Per customer*
>
> 15 rijen. Company F en Company H hebben er elk 6.
>
> ```sql
> SELECT c.company, COUNT(*) AS total
> FROM customers c
> INNER JOIN orders o ON o.customer_id = c.id
> GROUP BY c.id, c.company
> ORDER BY total DESC;
> ```
>
> *Per order*
>
> 40 rijen. De orders 38 en 41 zijn elk 13800 waard.
>
> ```sql
> SELECT o.id, SUM(od.quantity * od.unit_price) AS total
> FROM orders o
> INNER JOIN order_details od ON od.order_id = o.id
> GROUP BY o.id
> ORDER BY total DESC;
> ```
>
> *Per category*
>
> 14 rijen. Beverages staat bovenaan met 38260.25.
>
> ```sql
> SELECT p.category, ROUND(SUM(od.quantity * od.unit_price), 2) AS revenue
> FROM order_details od
> INNER JOIN products p ON od.product_id = p.id
> GROUP BY p.category
> ORDER BY revenue DESC;
> ```
>
> *Per shipper*
>
> 3 rijen: 8, 18 en 17 orders. COUNT(o.id) en geen sterretje, anders krijgt een shipper zonder orders 1.
>
> ```sql
> SELECT s.company, COUNT(o.id) AS total
> FROM shippers s
> LEFT JOIN orders o ON o.shipper_id = s.id
> GROUP BY s.id, s.company;
> ```

<!-- slide 63 -->

### Opdracht: Shipping Company B

De DELETE van vorige week, die MySQL weigerde.

1. Voer hem opnieuw uit: `DELETE FROM shippers WHERE company = 'Shipping Company B';`
2. Lees de foutmelding: welke tabel en welke kolom houden je tegen?
3. Hoeveel orders verwijzen naar deze shipper?
4. Zorg dat ze er niet meer naar verwijzen, zonder een order te verwijderen
5. Verwijder de shipper

> [!TIP]
> **Shipping Company B**
>
> *Hoeveel*
>
> 18 orders. De foutmelding noemt de tabel orders en de kolom shipper_id.
>
> ```sql
> SELECT COUNT(*)
> FROM orders o
> INNER JOIN shippers s ON o.shipper_id = s.id
> WHERE s.company = 'Shipping Company B';
> ```
>
> *Losmaken*
>
> 18 rijen. Shipping Company B heeft id 2. Een andere shipper invullen mag ook.
>
> ```sql
> UPDATE orders
> SET shipper_id = NULL
> WHERE shipper_id = 2;
> ```
>
> *Verwijderen*
>
> 1 rij. Niets verwijst er nog naar, dus MySQL laat het toe.
>
> ```sql
> DELETE FROM shippers
> WHERE company = 'Shipping Company B';
> ```

<!-- slide 64 -->

### Opdracht: Een to-do app

Meerdere mensen houden er hun lijstjes in bij.

1. Een database `todo_app`, met `users`: een naam en een uniek e-mailadres
2. `lists`: een titel, en van welke user ze is. Verdwijnt de user, dan ook zijn lijsten.
3. `todos`: een tekst, klaar of niet, een deadline die leeg mag zijn, en in welke lijst ze staat
4. `tags`: een to-do heeft meerdere tags, en een tag hangt aan meerdere to-do's
5. Vul elke tabel met een paar rijen, waaronder één user zonder lijsten
6. Twee queries: elke to-do met haar lijst en haar user, en het aantal open to-do’s per user

> [!TIP]
> **Een to-do app**
>
> *users en lists*
>
> Eén user heeft meerdere lijsten, één lijst is van één user.
>
> ```sql
> CREATE TABLE users (
>   id INT UNSIGNED NOT NULL AUTO_INCREMENT,
>   name VARCHAR(100) NOT NULL,
>   email VARCHAR(255) NOT NULL UNIQUE,
>   PRIMARY KEY (id)
> );
>
> CREATE TABLE lists (
>   id INT UNSIGNED NOT NULL AUTO_INCREMENT,
>   user_id INT UNSIGNED NOT NULL,
>   title VARCHAR(100) NOT NULL,
>   PRIMARY KEY (id),
>   FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
> );
> ```
>
> *todos*
>
> Een to-do kent haar lijst, en via de lijst haar user. Geen user_id in todos.
>
> ```sql
> CREATE TABLE todos (
>   id INT UNSIGNED NOT NULL AUTO_INCREMENT,
>   list_id INT UNSIGNED NOT NULL,
>   text VARCHAR(255) NOT NULL,
>   done BOOLEAN NOT NULL DEFAULT 0,
>   due_on DATE NULL,
>   PRIMARY KEY (id),
>   FOREIGN KEY (list_id) REFERENCES lists (id) ON DELETE CASCADE
> );
> ```
>
> *tags*
>
> Many to many, dus een tussentabel met twee foreign keys.
>
> ```sql
> CREATE TABLE tags (
>   id INT UNSIGNED NOT NULL AUTO_INCREMENT,
>   name VARCHAR(50) NOT NULL UNIQUE,
>   PRIMARY KEY (id)
> );
>
> CREATE TABLE tag_todo (
>   id INT UNSIGNED NOT NULL AUTO_INCREMENT,
>   tag_id INT UNSIGNED NOT NULL,
>   todo_id INT UNSIGNED NOT NULL,
>   PRIMARY KEY (id),
>   FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE,
>   FOREIGN KEY (todo_id) REFERENCES todos (id) ON DELETE CASCADE
> );
> ```
>
> *Elke to-do*
>
> ```sql
> SELECT t.text, l.title AS list, u.name AS user
> FROM todos t
> INNER JOIN lists l ON t.list_id = l.id
> INNER JOIN users u ON l.user_id = u.id;
> ```
>
> *Open per user*
>
> De voorwaarde op done staat in de ON. In een WHERE zou ze de users zonder open to-do weer weggooien.
>
> ```sql
> SELECT u.name, COUNT(t.id) AS open_todos
> FROM users u
> LEFT JOIN lists l ON l.user_id = u.id
> LEFT JOIN todos t ON t.list_id = l.id AND t.done = 0
> GROUP BY u.id, u.name;
> ```

<!-- slide 65 -->

**Volgende week: ERD.**

Entiteiten, attributen en relaties tekenen voor je één tabel maakt. En wat normaliseren is.

## Begrippen

- **CREATE TABLE**: Maakt een tabel: de naam, en per kolom een naam, een type en de regels. De primary key sluit af. (slide 7)
- **UNSIGNED**: Een geheel getal zonder minteken: INT UNSIGNED loopt van 0 tot 4.294.967.295. Voor een id, een aantal of een level, die nooit negatief zijn. Een foreign key krijgt hetzelfde type als de id waar hij naar verwijst. (slide 8)
- **Constraint**: Een regel op een kolom die de database zelf bewaakt: NOT NULL, UNIQUE, DEFAULT, PRIMARY KEY, FOREIGN KEY. Een rij die de regel breekt, wordt geweigerd. (slide 9)
- **UNIQUE**: Een constraint: geen twee rijen met dezelfde waarde in deze kolom. Voor een e-mailadres of een gebruikersnaam. (slide 9)
- **DEFAULT**: De waarde die een kolom krijgt als je er bij een INSERT geen meegeeft: badges DEFAULT 0, created_at DEFAULT CURRENT_TIMESTAMP. (slide 9)
- **ALTER TABLE**: Past een bestaande tabel aan: een kolom toevoegen (ADD), van type veranderen (MODIFY COLUMN), hernoemen (RENAME COLUMN) of weggooien (DROP COLUMN). (slide 13)
- **DROP TABLE**: Gooit een tabel weg, met alle rijen erin. DROP DATABASE doet hetzelfde met een hele database. Geen prullenbak. (slide 15)
- **Relatie**: Het verband tussen twee tabellen: een trainer heeft vangsten, een order heeft producten. Er zijn er drie soorten: one to one, one to many en many to many. (slide 25)
- **Foreign key**: Een kolom die naar de primary key van een andere tabel verwijst, zoals trainer_id in catches. De naam is het enkelvoud van die tabel met \_id erachter. (slide 21)
- **One to many**: Eén rij in de ene tabel hoort bij meerdere rijen in de andere: één trainer, meerdere vangsten. De foreign key staat aan de kant van meerdere. (slide 22)
- **Many to many**: Rijen aan beide kanten horen bij meerdere rijen van de andere tabel: een trainer heeft meerdere Pokémon, een Pokémon zit bij meerdere trainers. Je hebt een tussentabel nodig. (slide 23)
- **Tussentabel**: De tabel tussen twee tabellen met een many to many relatie, met een foreign key naar elk van de twee. Ook pivot table of junction table genoemd. (slide 23)
- **One to one**: Eén rij hoort bij hoogstens één rij van de andere tabel: een gym en zijn leader. Een foreign key die ook UNIQUE is. Komt weinig voor. (slide 24)
- **Parent en child**: De tabel met de foreign key is de child, de tabel waar hij naar verwijst de parent. catches is een child van trainers. (slide 21)
- **ON DELETE**: Wat er met de child rijen gebeurt als de parent verwijderd wordt: RESTRICT weigert, CASCADE verwijdert ze mee, SET NULL maakt de foreign key leeg. (slide 32)
- **CASCADE**: ON DELETE CASCADE: verwijder je de parent, dan gaan de rijen die ernaar verwijzen mee weg. Een trainer weg, al zijn vangsten weg. (slide 32)
- **RESTRICT**: ON DELETE RESTRICT, de standaard: MySQL weigert een rij te verwijderen zolang er andere rijen naar verwijzen. Error 1451. (slide 32)
- **JOIN**: Voegt rijen van twee tabellen samen op een kolom die ze delen, bijna altijd een foreign key en de primary key waar hij naar verwijst. (slide 40)
- **INNER JOIN**: Alleen de rijen die aan beide kanten een match hebben. Een trainer zonder vangsten komt er niet in voor. (slide 40)
- **LEFT JOIN**: Alle rijen van de linkse tabel, met de rijen van de rechtse erbij als ze matchen. Zonder match staat er NULL. (slide 50)
- **RIGHT JOIN**: Het spiegelbeeld van LEFT JOIN: alle rijen van de rechtse tabel. Bijna iedereen draait de tabellen om en schrijft een LEFT JOIN. (slide 53)
- **Tabelalias**: Een korte naam voor een tabel binnen één query: FROM catches c. Daarna schrijf je c.level in plaats van catches.level. (slide 43)
- **Self join**: Een tabel die je met zichzelf joint, met twee aliassen. Zo zet je naast elke Pokémon de naam van de Pokémon waaruit hij evolueert. (slide 55)

## Links en bronnen

### Bronnen

- [MySQL 8.4 Reference Manual, CREATE TABLE Statement](https://dev.mysql.com/doc/refman/8.4/en/create-table.html)
- [MySQL 8.4 Reference Manual, ALTER TABLE Statement](https://dev.mysql.com/doc/refman/8.4/en/alter-table.html)
- [MySQL 8.4 Reference Manual, FOREIGN KEY Constraints](https://dev.mysql.com/doc/refman/8.4/en/create-table-foreign-keys.html): RESTRICT, CASCADE en SET NULL
- [MySQL 8.4 Reference Manual, JOIN Clause](https://dev.mysql.com/doc/refman/8.4/en/join.html): MySQL kent geen FULL OUTER JOIN
- [MySQL 8.4 Reference Manual, Integer Types](https://dev.mysql.com/doc/refman/8.4/en/integer-types.html): TINYINT UNSIGNED loopt van 0 tot 255
- [SQLite Foreign Key Support](https://www.sqlite.org/foreignkeys.html): In SQLite staan foreign keys standaard uit
- [PokeAPI](https://pokeapi.co/): De data in de tabel pokemon
- [mywind, Northwind voor MySQL](https://github.com/dalers/mywind): De database van de opdracht, BSD 2-Clause
