Files
vault/Career/Database - SQL/Using Joins, Constraints, Normalization, and Subqueries.md
Zaine 129ce1442b
Some checks failed
Build Quartz Notes / build (push) Failing after 20s
13
2026-07-13 09:16:09 +01:00

1300 lines
21 KiB
Markdown
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
note type:
- sql
- database
- theory
date: 2026-06-03
done: true
link: https://app.pluralsight.com/ilx/video-courses/sql-joins-constraints-normalization-subqueries/course-overview
---
# Common Aggregate Functions
|title|cost|duration|
|---|---|---|
|Gone with the wind|390000|220|
|Frankenstein|3000000|50|
|Creature from the black lagoon|500000|79|
|NULL|100|10|
## 1. COUNT()
## Count all rows
`SELECT COUNT(*) FROM Movies;`
**Output:**
```
4
```
- Counts all rows, including rows where columns are `NULL`.
### Count non-NULL values in a column
`SELECT COUNT(title) FROM Movies;`
**Output:**
```
3
```
- `NULL` title is **not counted**.
## 2. MIN()
`SELECT MIN(cost) FROM Movies;`
**Output:**
```
100
```
- Returns the smallest value in the column.
## 3. MAX()
`SELECT MAX(cost) FROM Movies;`
**Output:**
```
3000000
```
- Returns the largest value.
## 4. SUM()
`SELECT SUM(cost) FROM Movies;`
**Output:**
```
3890100
```
Calculation:
```
390000 + 3000000 + 500000 + 100 = 3890100
```
- Adds all values in the column.
- Ignores `NULL` values (none in `cost` here).
## 5. AVG()
`SELECT AVG(cost) FROM Movies;`
**Output:**
```
972525
```
Calculation:
```
3890100 / 4 = 972525
```
- Computes the average of all values.
---
## Using Multiple Aggregates
```
SELECT
    COUNT(*) AS total_movies,
    MIN(cost) AS cheapest,
    MAX(cost) AS most_expensive,
    SUM(cost) AS total_cost,
    AVG(cost) AS average_cost
FROM Movies;
```
**Output:**
```
total_movies | cheapest | most_expensive | total_cost | average_cost
4 | 100 | 3000000 | 3890100 | 972525
```
# Filtering Aggregates
| title | cost | duration | genre |
| ------------------------------ | ------- | -------- | ------ |
| Gone with the wind | 390000 | 220 | Drama |
| Frankenstein | 3000000 | 50 | Horror |
| Creature from the black lagoon | 500000 | 79 | Horror |
| Casablanca | 1000000 | 102 | Drama |
| Toy Story | 2000000 | 81 | Family |
| NULL | 100 | 10 | Horror |
## GROUP BY
`GROUP BY` groups rows so aggregates are calculated per group rather than across the whole table.
```
SELECT genre, SUM(cost) AS total_cost
FROM Movies
GROUP BY genre;
```
Output:
```
genre | total_cost
Drama | 1390000
Horror | 3500100
Family | 2000000
```
```
SELECT genre, AVG(duration) AS avg_duration
FROM Movies
GROUP BY genre;
```
Output:
```
genre | avg_duration
Drama | 161
Horror | 46.33
Family | 81
```
---
## WHERE (filtering rows before aggregation)
`WHERE` filters rows **before** grouping and aggregation.
```
SELECT genre, COUNT(*) AS num_movies
FROM Movies
WHERE duration > 60
GROUP BY genre;
```
Output:
```
genre | num_movies
Drama | 2
Horror | 1
Family | 1
```
Explanation:
- The row with duration = 10 is excluded before grouping.
## HAVING (filtering groups after aggregation)
`HAVING` filters results **after aggregation**.
```
SELECT genre, SUM(cost) AS total_cost
FROM Movies
GROUP BY genre
HAVING SUM(cost) >= 2000000;
```
Output:
```
genre | total_cost
Horror | 3500100
Family | 2000000 -- note: included only if >= was used
```
If strictly `> 2000000`, only:
```
Horror | 3500100
```
```
SELECT genre, COUNT(*) AS num_movies
FROM Movies
GROUP BY genre
HAVING COUNT(*) > 2;
```
Output:
```
genre | num_movies
Horror | 3
```
## Combined Example
```
SELECT genre, AVG(cost) AS avg_cost
FROM Movies
WHERE duration > 60
GROUP BY genre
HAVING AVG(cost) > 1000000;
```
Steps:
1. WHERE filters rows (duration > 60)
2. GROUP BY groups remaining data
3. HAVING filters grouped results
Output:
```
genre | avg_cost
Family | 2000000
```
# Constraints
## Example Table (Promotions)
```
CREATE TABLE Promotions (
    id INT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    category VARCHAR(50) NOT NULL,
    CONSTRAINT unique_name_category
        UNIQUE (name, category)
);
```
## Purpose of Constraints
- Prevent **invalid or unwanted data**
- Enforce **rules on table columns**
- Improve **data integrity**
## NOT NULL
- Prevents a column from storing `NULL` values
- Use when a value is required
`name VARCHAR(100) NOT NULL`
## UNIQUE
- Ensures all values in a column are **distinct**
- Prevents duplicate entries
`name VARCHAR(100) UNIQUE`
### Composite UNIQUE
- Enforces uniqueness across multiple columns
`UNIQUE (name, category)`
- Same name allowed if category differs
- Same category allowed if name differs
- Duplicate combinations are not allowed
## Multiple Constraints on a Column
- You can combine constraints:
`name VARCHAR(100) NOT NULL UNIQUE`
## Named Constraints
- Assign custom names for easier maintenance
`CONSTRAINT unique_name UNIQUE (name)`
## Column vs Table Constraints
- Column constraint: defined inline
- Table constraint: defined separately
Both work the same, except:
- `NOT NULL` must be defined at the column level
## PRIMARY KEY
- Uniquely identifies each row
- Automatically enforces:
- NOT NULL
- UNIQUE
`id INT PRIMARY KEY`
### Key Rules
- Only one primary key per table
- Can consist of one or multiple columns
## PRIMARY KEY vs UNIQUE + NOT NULL
- Both enforce uniqueness and no nulls
- Difference:
- PRIMARY KEY: only one per table
- UNIQUE + NOT NULL: can use multiple columns
# Value Constraints (Foreign Key & CHECK)
## Example Tables (Movies + Promotions)
```
CREATE TABLE Movies (
    id INT PRIMARY KEY,
    title VARCHAR(100) NOT NULL,
    duration INT CHECK (duration > 0)
);
CREATE TABLE Promotions (
    id INT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    category VARCHAR(50) NOT NULL,
    movie_id INT,
    CONSTRAINT fk_movie
        FOREIGN KEY (movie_id)
        REFERENCES Movies(id),
    CONSTRAINT unique_name_category
        UNIQUE (name, category)
);
```
## Foreign Key (FK)
### Definition
- A **foreign key** is a column in one table that references a **primary key in another table**
- Used to **link tables** and avoid duplicating data
`movie_id INT REFERENCES movies(id)`
This is the same as:
`movie_id INT REFERENCES movies`
## Naming Convention
- Format: `referencedTable_singular + _id`
Examples:
- `movie_id` → references Movies(id)
- `user_id` → references Users(id)
## Why Use Foreign Keys
- Prevents **invalid references**
- Ensures **data integrity**
- Avoids **duplicating data across tables**
## Behaviour Without FK
- You can insert invalid data:
`movie_id = 999  -- even if it doesn't exist`
- Creates **bad data**
## Behaviour With FK
- Database **blocks invalid inserts**
`INSERT INTO Promotions (id, name, category, movie_id)`
`VALUES (1, 'Half Off', 'Discount', 999);  -- fails`
- Error: violates foreign key constraint
## Table Creation Order Rule
- The referenced table **must be created first**
`CREATE TABLE Movies (...)`
`CREATE TABLE Promotions (... REFERENCES Movies)`
## Table Constraint Version
```
FOREIGN KEY (movie_id) REFERENCES movies
```
## Orphan Records
### Definition
- A row that references data that **no longer exists**
Example:
- Movie deleted
- Promotion still points to that movie_id
## How FK Prevents Orphans
- Prevents deleting parent rows if children exist
`DELETE FROM Movies WHERE id = 6;  -- fails if Promotions reference it`
- You must:
1. Delete child rows first
2. Then delete parent
## Dropping Tables
- Cannot drop a table if another table depends on it
`DROP TABLE Movies;  -- fails if Promotions references it`
- Must drop dependent tables first
## CHECK Constraint
### Definition
- Validates column values using a condition
`salary int CHECK (salary > 500)`
## Purpose of CHECK
- Prevent logically invalid data
Example:
- Duration cannot be negative
## Behaviour
```
INSERT INTO Movies (id, title, duration)
VALUES (1, 'Test Movie', -10);  -- fails
```
## Example Inserts
### Valid Movie
```
INSERT INTO Movies (id, title, duration)
VALUES (1, 'Gone With the Wind', 240);
```
### Valid Promotion
```
INSERT INTO Promotions (id, name, category, movie_id)
VALUES (1, 'Matinee', 'Discount', 1);
```
### Fails (invalid foreign key)
```
INSERT INTO Promotions (id, name, category, movie_id)
VALUES (2, 'Half Off', 'Discount', 999);
```
### Fails (negative duration)
```
INSERT INTO Movies (id, title, duration)
VALUES (2, 'Bad Movie', -10);
```
# SQL Relationships & Normalization (Level 3 Notes)
## Overview
- Focus: **Database relationships** and **normalization** to improve data integrity and flexibility.
- Example context: A **Movies application** where each movie can have **multiple genres**.
## Problem with Current Design
- Storing multiple genres in one column (e.g., `"Adventure, Fantasy"`) causes issues:
- Hard to query (e.g., finding all _Adventure_ movies).
- Difficult to update individual values.
- Violates normalization rules.
### Example Issue
`SELECT * FROM Movies WHERE genre = 'Adventure';`
- Returns movies with only `"Adventure"`, but **misses movies** like `"Adventure, Fantasy"`.
## Normalization Basics
### First Normal Form (1NF)
- Rule: **No repeating groups in a column**.
- Each field should contain a **single value (atomic)**.
#### Fix
Split rows so each genre is separate:
|title|genre|duration|
|---|---|---|
|Peter Pan|Adventure|120|
|Peter Pan|Fantasy|120|
- Eliminates multi-value columns.
- **Still problematic**: duplicate movie data.
### Second Normal Form (2NF)
- Rule: **No redundancy (no repeated unnecessary data)**.
- Each piece of information should be stored **once**.
#### Problem in 1NF Table
- Movie details (e.g., duration) are duplicated across rows.
## Solution: Table Decomposition
### Step 1: Create a Movies Table
- Store unique movies only.
|id|title|duration|
|---|---|---|
|1|Don Juan|110|
|2|Peter Pan|120|
### Step 2: Create a Genres Table
- Store each genre once.
|id|name|
|---|---|
|1|Romance|
|2|Adventure|
|3|Fantasy|
### Step 3: Create a Join Table (Many-to-Many)
- Name convention: `movies_genres`
- Purpose: Link movies to genres.
|movie_id|genre_id|
|---|---|
|1|1|
|2|2|
|2|3|
- `movie_id` → references `Movies.id`
- `genre_id` → references `Genres.id`
- Both are **foreign keys**
## Benefits of This Design
- No duplication (meets 2NF).
- Easy updates:
- Change movie duration in one place.
- Add/remove genres without affecting other data.
- Scalable for complex relationships.
## Querying the Data
### Step-by-step (manual approach)
1. Get movie ID:
`SELECT id FROM Movies WHERE title = 'Peter Pan';`
2. Get associated genre IDs:
`SELECT genre_id FROM movies_genres WHERE movie_id = 2;`
3. Get genre names:
`SELECT name FROM Genres WHERE id IN (2, 3);`
### Simplified Query Using `IN`
`SELECT name FROM Genres WHERE id IN (2, 3);`
## Example Operation
### Add a new genre to a movie
```
Add "Fantasy" to "Robin Hood":
INSERT INTO movies_genres (movie_id, genre_id)
VALUES (4, 3);
```
## Key Takeaways
- Avoid storing multiple values in a single column.
- Use **normalization** to:
- Eliminate redundancy
- Improve data consistency
- Use **join tables** for many-to-many relationships.
- Trade-off: Queries become slightly more complex, but data becomes more robust and flexible.
# Database Relationships
## Overview
There are three fundamental relationship types between tables:
1. **One-to-One (1:1)**
2. **One-to-Many (1:N)**
3. **Many-to-Many (N:N)**
## 1. One-to-Many (1:N)
### Definition
- A single row in Table A can relate to **multiple rows** in Table B.
- A row in Table B relates to **only one row** in Table A.
### Key Implementation
- Add a **foreign key** in the "many" table.
### Example
- **Movies → Promotions**
- One movie can have many promotions.
- Each promotion belongs to one movie.
`Movies (id) ← Promotions (movie_id)`
### Diagram Representation
- `1` on the "one" side
- `*` on the "many" side
## 2. Many-to-Many (N:N)
### Definition
- Multiple rows in Table A can relate to multiple rows in Table B.
### Key Implementation
- Requires a **join table (junction table)** that holds foreign keys from both tables.
### Example
- **Movies ↔ Genres**
- A movie can have many genres.
- A genre can belong to many movies.
```
Movies_Genres
- movie_id
- genre_id
```
### Diagram Representation
- `*` on both sides
- Join table is typically implied (not always shown explicitly)
## 3. One-to-One (1:1)
### Definition
- A row in Table A relates to exactly **one row** in Table B.
### Use Case
- Used to split large or complex tables into smaller ones.
### Example
- **Customers ↔ Addresses**
- Each customer has one address.
- Each address belongs to one customer.
`Customers (address_id) → Addresses (id)`
### Diagram Representation
- `1` on both sides
## Relationship Identification Examples
### Example 1: Movies & Reviews
- One movie can have many reviews.
- Each review belongs to one movie.
**Relationship:** One-to-Many
`Movies (id) ← Reviews (movie_id)`
### Example 2: Movies & Promotions
- A promotion can apply to many movies.
- A movie can have many promotions.
**Relationship:** Many-to-Many
```
Movies_Promotions
- movie_id
- promotion_id
```
## Summary
|Relationship Type|Key Idea|Implementation|
|---|---|---|
|One-to-One|1 row ↔ 1 row|Foreign key on one side|
|One-to-Many|1 row → many rows|Foreign key in "many" table|
|Many-to-Many|Many ↔ many|Join table with two FKs|
- Always identify how entities relate **from a business perspective**, not just technically.
- Use:
- **Foreign keys** for 1:1 and 1:N
- **Join tables** for N:N
- Diagrams use:
- `1` = single
- `*` = many
# SQL INNER JOIN — Explained with an Example
## Example Tables
### Movies
```
id | title
---|-------------
1 | Don Juan
2 | The Lost World
3 | Peter Pan
4 | Robin Hood
```
### Reviews
```
id | movie_id | body
---|----------|-------------------
1 | 1 | Great movie
2 | 1 | Loved it
3 | 1 | Classic
4 | 2 | Not bad
5 | NULL | Random review
```
## What INNER JOIN Does
Returns **only rows where both tables match**.
- Ignores:
- Movies with no reviews
- Reviews with no valid movie
## Basic Query
```
SELECT m.title, r.body
FROM Movies m
INNER JOIN Reviews r
ON m.id = r.movie_id;
```
## Result (conceptually)
```
Don Juan | Great movie
Don Juan | Loved it
Don Juan | Classic
The Lost World | Not bad
```
## Key Observations
- Only matching data is returned
- `Don Juan` appears 3 times (3 reviews)
- `Peter Pan` is missing (no reviews)
- `Random review` is missing (`movie_id = NULL`)
## Why Use INNER JOIN
Instead of doing this:
```
-- Query 1
SELECT movie_id, body FROM Reviews;
-- Query 2
SELECT title FROM Movies WHERE id IN (1, 2);
```
You can do it in one query:
```
SELECT m.title, r.body
FROM Movies m
INNER JOIN Reviews r
ON m.id = r.movie_id;
```
Cleaner and more efficient.
## Table Order
These are equivalent:
```
FROM Movies m
INNER JOIN Reviews r
ON m.id = r.movie_id;
FROM Reviews r
INNER JOIN Movies m
ON m.id = r.movie_id;
```
Because INNER JOIN only keeps matches.
## Selecting Specific Columns
Avoid `SELECT *`:
```
SELECT m.title, r.body
FROM Movies m
INNER JOIN Reviews r
ON m.id = r.movie_id;
```
This keeps output clean and avoids ambiguity.
## INNER JOIN with Multiple Tables
### Example: Add Genres
#### Additional Tables
```
Movies_Genres
movie_id | genre_id
---------|----------
1 | 10
1 | 11
3 | 12
Genres
id | name
---|---------
10 | Drama
11 | Romance
12 | Fantasy
```
### Query
```
SELECT m.title, g.name
FROM Movies m
INNER JOIN Movies_Genres mg
  ON m.id = mg.movie_id
INNER JOIN Genres g
  ON mg.genre_id = g.id;
```
### Result (conceptually)
```
Don Juan | Drama
Don Juan | Romance
Peter Pan | Fantasy
```
## Mental Model
Think of INNER JOIN as:
- “Give me only rows that exist in **both tables**
## Behaviour Summary
| Situation | Included? |
| --------------------- | --------- |
| Movie with reviews | Yes |
| Movie with no reviews | No |
| Review with no movie | No |
| | |
## One-Line Summary
INNER JOIN = **only matching rows between tables**
# SQL Aliases (Columns & Tables)
## Column Aliases
- Column aliases allow you to rename output column headers in your query results.
- Useful for making results more readable and user-friendly.
### Syntax
```
SELECT column_name AS alias_name
FROM table_name;
```
- The keyword `AS` is optional:
```
SELECT column_name alias_name
FROM table_name;
```
### Examples
```
SELECT Movies.title AS films, Reviews.review AS reviews
FROM Movies
INNER JOIN Reviews ON Movies.id = Reviews.movie_id;
```
- Without `AS`:
```
SELECT Movies.title films, Reviews.review reviews
FROM Movies
INNER JOIN Reviews ON Movies.id = Reviews.movie_id;
```
### Using Multiple Words in Aliases
- Use quotation marks when the alias contains spaces:
```
SELECT Movies.title AS "Weekly Movies", Reviews.review AS "Weekly Reviews"
FROM Movies
INNER JOIN Reviews ON Movies.id = Reviews.movie_id;
```
## Table Aliases
- Table aliases shorten table names in queries.
- Helpful when:
- Working with long table names
- Writing complex joins
- Improving readability
### Syntax
`FROM table_name alias`
### Example
```
SELECT m.title
FROM Movies m;
```
- Here, `m` is used instead of `Movies`.
## Using Table Aliases in Joins
- Once defined, aliases can be used throughout the query (SELECT, JOIN, WHERE, ORDER BY).
### Example
```
SELECT m.title, r.review
FROM Movies m
INNER JOIN Reviews r ON m.id = r.movie_id;
```
### Example
```
SELECT m.title, g.name
FROM Movies m
INNER JOIN Movies_Genres mg ON m.id = mg.movie_id
INNER JOIN Genres g ON mg.genre_id = g.id;
```
# SQL Outer Joins
## Example Tables
### Movies
```
id | title
---|-------------
1 | Don Juan
2 | The Lost World
3 | Peter Pan
4 | Robin Hood
```
### Reviews
```
id | movie_id | body
---|----------|-------------------
1 | 1 | Great movie
2 | 1 | Loved it
3 | 1 | Classic
4 | 2 | Not bad
5 | NULL | Random review
```
## LEFT OUTER JOIN
### What it means
“Show me **all movies**, and any reviews they have.”
### Query
```
SELECT m.title, r.body
FROM Movies m
LEFT JOIN Reviews r
ON m.id = r.movie_id;
```
### Result (conceptually)
```
Don Juan | Great movie
Don Juan | Loved it
Don Juan | Classic
The Lost World | Not bad
Peter Pan | NULL
Robin Hood | NULL
```
### Key idea
- Every movie is shown
- If a movie has no reviews → `NULL`
- If a movie has many reviews → it repeats
Example:
- `Don Juan` appears 3 times
- `Peter Pan` appears once with `NULL`
### How to remember
> LEFT JOIN = **“Keep everything on the left (Movies)”**
## RIGHT OUTER JOIN
### What it means
“Show me **all reviews**, even if they dont belong to a movie.”
### Query
```
SELECT m.title, r.body
FROM Movies m
RIGHT JOIN Reviews r
ON m.id = r.movie_id;
```
### Result (conceptually)
```
Don Juan | Great movie
Don Juan | Loved it
Don Juan | Classic
The Lost World | Not bad
NULL | Random review
```
### Key idea
- Every review is shown
- If a review has no movie → `NULL` in movie column
Example:
- The `Random review` has no movie → title is `NULL`
### How to remember
> RIGHT JOIN = **“Keep everything on the right (Reviews)”**
## Side-by-Side
| Situation | LEFT JOIN | RIGHT JOIN |
| --------------------- | ------------------- | ------------------ |
| Movie with no reviews | shown (NULL review) | not shown |
| Review with no movie | not shown | shown (NULL movie) |
## Quick Mental Model
- LEFT JOIN → “I care about **Movies**
- RIGHT JOIN → “I care about **Reviews**
- `NULL` = missing relationship
## One-Line Summary
- LEFT JOIN → keep all rows from **first table**
- RIGHT JOIN → keep all rows from **second table**
# SQL Subqueries (Level 5)
## Example Tables
**Movies**
|id|title|sales|duration|
|---|---|---|---|
|1|Batman|50000|120|
|2|Superman|30000|110|
|3|Iron Man|25000|105|
|4|Robin Hood|45000|140|
**Promotions**
|id|movie_id|category|
|---|---|---|
|1|1|non-cash|
|2|2|cash|
|3|4|non-cash|
---
## What is a Subquery?
- A **subquery** is a query nested inside another query.
- The inner query runs first, and its result is used by the outer query.
## Example 1: Using `IN` with a Subquery
### Goal
Find the **sum of sales** for movies with a **non-cash promotion**.
```
SELECT SUM(sales)
FROM Movies
WHERE id IN (
    SELECT movie_id
    FROM Promotions
    WHERE category = 'non-cash'
);
```
- Inner query returns: `1, 4`
- Outer query sums: `50000 + 45000 = 95000`
## Alternative: Using a Join
```
SELECT SUM(sales)
FROM Movies
INNER JOIN Promotions
  ON Movies.id = Promotions.movie_id
WHERE category = 'non-cash';
```
- Same result, different approach
- Join is typically more performant
## Subquery Variants
```
SELECT *
FROM Movies
WHERE id NOT IN (
    SELECT movie_id
    FROM Promotions
);
```
- Returns movies with **no promotions**
## Example 2: Aggregate Subquery
### Goal
Find movies with **above-average duration**
```
SELECT *
FROM Movies
WHERE duration > (
    SELECT AVG(duration)
    FROM Movies
);
```
- Average duration = `(120 + 110 + 105 + 140) / 4 = 118.75`
- Result: only **Robin Hood (140)**
## Key Takeaway
- Subqueries break problems into steps using intermediate results
- Use them when aggregates or filtering logic cant be expressed directly in `WHERE`