--- 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 don’t 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 can’t be expressed directly in `WHERE`