Relational Databases (SQL) - Fundamentals

Learning Objectives: By the end of this lesson, you will be able to:

  • Understand the theoretical foundations of the relational model
  • Design normalized database schemas following best practices
  • Write complex SQL queries including joins, subqueries, and aggregations
  • Implement proper constraints and relationships between tables
  • Compare popular RDBMS systems and their unique characteristics

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Imagine you’re tasked with organizing all the data for a medium-sized company: employees, departments, projects, clients, invoices, and products. You could throw everything into one massive spreadsheet, but what happens when an employee changes departments? You’d have to update that information in dozens or hundreds of rows. What if you make a typo in one place but not another?

This organizational nightmare is exactly what relational databases were designed to solve—and they’ve been doing it successfully for over 50 years.

Lesson Overview

  • Duration: 120 minutes
  • Prerequisites: Basic understanding of database concepts, familiarity with data types
  • Materials Needed: PostgreSQL or MySQL installed (Docker recommended), SQL client tool

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Relational Model Theory - Understanding Codd’s relational model
  2. SQL Fundamentals - DDL, DML, and query construction
  3. Database Normalization - First through third normal forms
  4. Constraints and Relationships - Ensuring data integrity
  5. Advanced Queries - Joins, subqueries, and window functions
  6. Popular RDBMS Systems - PostgreSQL, MySQL, SQL Server comparison

Success Criteria:

By the end of this session, you should be able to design a normalized database schema, write complex SQL queries, and understand the trade-offs between different relational database systems.

3. Foundation Knowledge (15-20 minutes)

Core Concepts

The Relational Model

The relational model, introduced by Edgar F. Codd in 1970, is based on mathematical set theory and predicate logic. It provides a way to represent data as relations (tables) and manipulate them through relational algebra.

Key Principles:

  1. Data is represented as relations (tables)
  2. Each relation has a schema defining its attributes
  3. Rows are unique tuples of attribute values
  4. Operations produce new relations from existing ones

Real-World Analogy: Think of a relational database like a set of interconnected spreadsheets where each sheet has a specific purpose, and you can look up information across sheets using common reference values. Unlike regular spreadsheets, the database enforces rules to keep everything consistent.

Why This Matters: The relational model provides mathematical rigor to data management, enabling optimization, consistency guarantees, and predictable behavior that other data storage methods struggle to match.

SQL: The Language of Relational Databases

SQL (Structured Query Language) is the standard language for interacting with relational databases. It encompasses several sub-languages:

DDL (Data Definition Language):

  • CREATE - Create databases, tables, indexes
  • ALTER - Modify existing structures
  • DROP - Remove structures
  • TRUNCATE - Remove all data from a table

DML (Data Manipulation Language):

  • SELECT - Query data
  • INSERT - Add new data
  • UPDATE - Modify existing data
  • DELETE - Remove data

DCL (Data Control Language):

  • GRANT - Give permissions
  • REVOKE - Remove permissions

TCL (Transaction Control Language):

  • BEGIN - Start transaction
  • COMMIT - Save transaction
  • ROLLBACK - Undo transaction
  • SAVEPOINT - Create transaction checkpoint

Key Terminology

  • Relation (Table): A set of tuples sharing the same attributes
  • Tuple (Row): A single record in a relation
  • Attribute (Column): A named property of a relation
  • Domain: The set of allowable values for an attribute
  • Primary Key: Unique identifier for tuples in a relation
  • Foreign Key: Reference to a primary key in another relation
  • Candidate Key: An attribute or set that could serve as primary key
  • Composite Key: A primary key made up of multiple attributes
  • Cardinality: Number of rows in a table
  • Degree: Number of columns in a table

4. Detailed Explanation and Examples (25-30 minutes)

Database Normalization

Normalization is the process of organizing data to reduce redundancy and improve integrity. There are several normal forms, each building on the previous:

First Normal Form (1NF)

Rule: Each column contains atomic (indivisible) values, and each row is unique.

Violation Example:

| StudentID | Name  | Courses               |
|-----------|-------|----------------------|
| 1         | Alice | Math, Physics, CS    |  -- Multiple values!
| 2         | Bob   | Chemistry, Biology   |

1NF Compliant:

| StudentID | Name  | Course    |
|-----------|-------|-----------|
| 1         | Alice | Math      |
| 1         | Alice | Physics   |
| 1         | Alice | CS        |
| 2         | Bob   | Chemistry |
| 2         | Bob   | Biology   |

Second Normal Form (2NF)

Rule: Must be in 1NF and all non-key attributes must depend on the entire primary key (no partial dependencies).

Violation Example (with composite key StudentID + CourseID):

| StudentID | CourseID | CourseName | Grade |
|-----------|----------|------------|-------|
| 1         | CS101    | Intro CS   | A     |  -- CourseName depends only on CourseID
| 2         | CS101    | Intro CS   | B     |

2NF Compliant:

-- Courses table
| CourseID | CourseName |
|----------|------------|
| CS101    | Intro CS   |

-- Enrollments table  
| StudentID | CourseID | Grade |
|-----------|----------|-------|
| 1         | CS101    | A     |
| 2         | CS101    | B     |

Third Normal Form (3NF)

Rule: Must be in 2NF and no transitive dependencies (non-key attributes depending on other non-key attributes).

Violation Example:

| EmployeeID | DepartmentID | DepartmentName | DepartmentHead |
|------------|--------------|----------------|----------------|
| 1          | D001         | Engineering    | Jane Smith     |  -- DeptName, DeptHead depend on DeptID
| 2          | D001         | Engineering    | Jane Smith     |

3NF Compliant:

-- Employees table
| EmployeeID | DepartmentID |
|------------|--------------|
| 1          | D001         |
| 2          | D001         |

-- Departments table
| DepartmentID | DepartmentName | DepartmentHead |
|--------------|----------------|----------------|
| D001         | Engineering    | Jane Smith     |

Types of Relationships

One-to-One (1:1)

Each record in Table A relates to exactly one record in Table B.

-- Example: User and UserProfile
CREATE TABLE users (
    user_id SERIAL PRIMARY KEY,
    email VARCHAR(100) UNIQUE NOT NULL
);

CREATE TABLE user_profiles (
    profile_id SERIAL PRIMARY KEY,
    user_id INTEGER UNIQUE REFERENCES users(user_id),
    bio TEXT,
    avatar_url VARCHAR(255)
);

One-to-Many (1:N)

Each record in Table A can relate to many records in Table B.

-- Example: Department and Employees
CREATE TABLE departments (
    department_id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE employees (
    employee_id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    department_id INTEGER REFERENCES departments(department_id)
);

Many-to-Many (M:N)

Records in Table A can relate to many in Table B and vice versa. Requires a junction table.

-- Example: Students and Courses
CREATE TABLE students (
    student_id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE courses (
    course_id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

-- Junction table
CREATE TABLE enrollments (
    student_id INTEGER REFERENCES students(student_id),
    course_id INTEGER REFERENCES courses(course_id),
    enrollment_date DATE DEFAULT CURRENT_DATE,
    grade VARCHAR(2),
    PRIMARY KEY (student_id, course_id)
);

Common Misconceptions

  1. Misconception: “Higher normalization is always better” Reality: Over-normalization can lead to excessive joins and poor performance. Denormalization is sometimes necessary for read-heavy workloads.

  2. Misconception: “SQL databases can’t scale horizontally” Reality: Modern solutions like PostgreSQL with Citus, MySQL with Vitess, or cloud services provide horizontal scaling capabilities.

  3. Misconception: “ORMs eliminate the need to understand SQL” Reality: Understanding SQL is essential for debugging, optimization, and complex queries that ORMs handle poorly.

5. Live Coding Demonstration (20-25 minutes)

Example 1: Complete Database Schema

-- Create a complete e-commerce database schema

-- Categories table
CREATE TABLE categories (
    category_id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    parent_category_id INTEGER REFERENCES categories(category_id),
    description TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Products table
CREATE TABLE products (
    product_id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    price DECIMAL(10, 2) NOT NULL CHECK (price > 0),
    category_id INTEGER REFERENCES categories(category_id),
    stock_quantity INTEGER DEFAULT 0 CHECK (stock_quantity >= 0),
    sku VARCHAR(50) UNIQUE,
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Customers table
CREATE TABLE customers (
    customer_id SERIAL PRIMARY KEY,
    email VARCHAR(100) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    phone VARCHAR(20),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Addresses table (one customer can have multiple addresses)
CREATE TABLE addresses (
    address_id SERIAL PRIMARY KEY,
    customer_id INTEGER REFERENCES customers(customer_id) ON DELETE CASCADE,
    address_type VARCHAR(20) CHECK (address_type IN ('billing', 'shipping')),
    street_address VARCHAR(255) NOT NULL,
    city VARCHAR(100) NOT NULL,
    state VARCHAR(100),
    postal_code VARCHAR(20) NOT NULL,
    country VARCHAR(100) NOT NULL,
    is_default BOOLEAN DEFAULT false
);

-- Orders table
CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    customer_id INTEGER REFERENCES customers(customer_id),
    shipping_address_id INTEGER REFERENCES addresses(address_id),
    billing_address_id INTEGER REFERENCES addresses(address_id),
    order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    status VARCHAR(20) DEFAULT 'pending' 
        CHECK (status IN ('pending', 'confirmed', 'shipped', 'delivered', 'cancelled')),
    subtotal DECIMAL(10, 2),
    tax DECIMAL(10, 2),
    shipping_cost DECIMAL(10, 2),
    total DECIMAL(10, 2),
    notes TEXT
);

-- Order items (junction table for orders and products)
CREATE TABLE order_items (
    order_item_id SERIAL PRIMARY KEY,
    order_id INTEGER REFERENCES orders(order_id) ON DELETE CASCADE,
    product_id INTEGER REFERENCES products(product_id),
    quantity INTEGER NOT NULL CHECK (quantity > 0),
    unit_price DECIMAL(10, 2) NOT NULL,
    total_price DECIMAL(10, 2) GENERATED ALWAYS AS (quantity * unit_price) STORED
);

-- Create indexes for common queries
CREATE INDEX idx_products_category ON products(category_id);
CREATE INDEX idx_products_active ON products(is_active) WHERE is_active = true;
CREATE INDEX idx_orders_customer ON orders(customer_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_date ON orders(order_date);

Explanation:

  • Self-referencing foreign key in categories for hierarchical structure
  • CHECK constraints ensure data validity
  • ON DELETE CASCADE automatically removes related records
  • GENERATED ALWAYS computed column for automatic calculations
  • Partial index (WHERE is_active = true) for optimization

Example 2: Complex Queries with Joins

-- Get all orders with customer and product details
SELECT 
    o.order_id,
    c.first_name || ' ' || c.last_name AS customer_name,
    c.email,
    o.order_date,
    o.status,
    o.total,
    json_agg(
        json_build_object(
            'product', p.name,
            'quantity', oi.quantity,
            'price', oi.unit_price
        )
    ) AS items
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
WHERE o.order_date >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY o.order_id, c.first_name, c.last_name, c.email
ORDER BY o.order_date DESC;

-- Find top customers by total spending
SELECT 
    c.customer_id,
    c.first_name || ' ' || c.last_name AS customer_name,
    COUNT(DISTINCT o.order_id) AS total_orders,
    SUM(o.total) AS total_spent,
    AVG(o.total) AS average_order_value
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE o.status != 'cancelled'
GROUP BY c.customer_id, c.first_name, c.last_name
HAVING SUM(o.total) > 100
ORDER BY total_spent DESC
LIMIT 10;

-- Products with low stock that have been ordered recently
SELECT 
    p.product_id,
    p.name,
    p.stock_quantity,
    COALESCE(SUM(oi.quantity), 0) AS units_sold_last_30_days
FROM products p
LEFT JOIN order_items oi ON p.product_id = oi.product_id
LEFT JOIN orders o ON oi.order_id = o.order_id 
    AND o.order_date >= CURRENT_DATE - INTERVAL '30 days'
    AND o.status != 'cancelled'
WHERE p.stock_quantity < 10
    AND p.is_active = true
GROUP BY p.product_id, p.name, p.stock_quantity
ORDER BY p.stock_quantity ASC;

Example 3: Window Functions

-- Running total and ranking
SELECT 
    order_id,
    customer_id,
    order_date,
    total,
    SUM(total) OVER (
        PARTITION BY customer_id 
        ORDER BY order_date
    ) AS running_total,
    ROW_NUMBER() OVER (
        PARTITION BY customer_id 
        ORDER BY order_date
    ) AS order_number,
    RANK() OVER (
        ORDER BY total DESC
    ) AS spending_rank
FROM orders
WHERE status != 'cancelled';

-- Month-over-month sales comparison
WITH monthly_sales AS (
    SELECT 
        DATE_TRUNC('month', order_date) AS month,
        SUM(total) AS total_sales
    FROM orders
    WHERE status != 'cancelled'
    GROUP BY DATE_TRUNC('month', order_date)
)
SELECT 
    month,
    total_sales,
    LAG(total_sales) OVER (ORDER BY month) AS previous_month,
    total_sales - LAG(total_sales) OVER (ORDER BY month) AS change,
    ROUND(
        (total_sales - LAG(total_sales) OVER (ORDER BY month)) * 100.0 / 
        NULLIF(LAG(total_sales) OVER (ORDER BY month), 0), 
        2
    ) AS percent_change
FROM monthly_sales
ORDER BY month;

Example 4: Common Table Expressions (CTEs)

-- Complex reporting query with CTEs
WITH customer_stats AS (
    SELECT 
        customer_id,
        COUNT(*) AS order_count,
        SUM(total) AS total_spent,
        MIN(order_date) AS first_order,
        MAX(order_date) AS last_order
    FROM orders
    WHERE status != 'cancelled'
    GROUP BY customer_id
),
customer_segments AS (
    SELECT 
        customer_id,
        order_count,
        total_spent,
        first_order,
        last_order,
        CASE 
            WHEN total_spent >= 1000 AND order_count >= 5 THEN 'VIP'
            WHEN total_spent >= 500 OR order_count >= 3 THEN 'Regular'
            ELSE 'New'
        END AS segment
    FROM customer_stats
)
SELECT 
    c.customer_id,
    c.first_name || ' ' || c.last_name AS name,
    cs.segment,
    cs.order_count,
    cs.total_spent,
    cs.first_order,
    cs.last_order,
    CURRENT_DATE - cs.last_order::date AS days_since_last_order
FROM customers c
JOIN customer_segments cs ON c.customer_id = cs.customer_id
ORDER BY cs.total_spent DESC;

6. Hands-On Practice (30-40 minutes)

Exercise 1: Design a Blog Database (15 minutes)

Objective: Create a normalized schema for a blogging platform

Requirements:

  • Users can create posts and comments
  • Posts can have multiple tags
  • Users can follow other users
  • Posts can be liked by users

Starter Code:

-- Create your tables here
-- Consider:
-- 1. What entities do you need?
-- 2. What are the relationships?
-- 3. What constraints should you add?

Solution:

-- Users table
CREATE TABLE users (
    user_id SERIAL PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    display_name VARCHAR(100),
    bio TEXT,
    avatar_url VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Posts table
CREATE TABLE posts (
    post_id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
    title VARCHAR(255) NOT NULL,
    slug VARCHAR(255) UNIQUE NOT NULL,
    content TEXT NOT NULL,
    excerpt TEXT,
    status VARCHAR(20) DEFAULT 'draft' CHECK (status IN ('draft', 'published', 'archived')),
    published_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Tags table
CREATE TABLE tags (
    tag_id SERIAL PRIMARY KEY,
    name VARCHAR(50) UNIQUE NOT NULL,
    slug VARCHAR(50) UNIQUE NOT NULL
);

-- Post-Tags junction table (many-to-many)
CREATE TABLE post_tags (
    post_id INTEGER REFERENCES posts(post_id) ON DELETE CASCADE,
    tag_id INTEGER REFERENCES tags(tag_id) ON DELETE CASCADE,
    PRIMARY KEY (post_id, tag_id)
);

-- Comments table (self-referencing for replies)
CREATE TABLE comments (
    comment_id SERIAL PRIMARY KEY,
    post_id INTEGER NOT NULL REFERENCES posts(post_id) ON DELETE CASCADE,
    user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
    parent_comment_id INTEGER REFERENCES comments(comment_id) ON DELETE CASCADE,
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Follows table (self-referencing many-to-many)
CREATE TABLE follows (
    follower_id INTEGER REFERENCES users(user_id) ON DELETE CASCADE,
    following_id INTEGER REFERENCES users(user_id) ON DELETE CASCADE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (follower_id, following_id),
    CHECK (follower_id != following_id)  -- Can't follow yourself
);

-- Likes table
CREATE TABLE post_likes (
    user_id INTEGER REFERENCES users(user_id) ON DELETE CASCADE,
    post_id INTEGER REFERENCES posts(post_id) ON DELETE CASCADE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (user_id, post_id)
);

-- Indexes
CREATE INDEX idx_posts_user ON posts(user_id);
CREATE INDEX idx_posts_status ON posts(status);
CREATE INDEX idx_posts_published ON posts(published_at) WHERE status = 'published';
CREATE INDEX idx_comments_post ON comments(post_id);
CREATE INDEX idx_comments_user ON comments(user_id);

Exercise 2: Write Complex Queries (15 minutes)

Objective: Practice writing advanced SQL queries

Using the blog schema above, write queries to:

  1. Get the top 5 most followed users
  2. Find all posts with their like counts and comment counts
  3. Get a user’s feed (posts from users they follow)

Solution:

-- 1. Top 5 most followed users
SELECT 
    u.user_id,
    u.username,
    u.display_name,
    COUNT(f.follower_id) AS follower_count
FROM users u
LEFT JOIN follows f ON u.user_id = f.following_id
GROUP BY u.user_id, u.username, u.display_name
ORDER BY follower_count DESC
LIMIT 5;

-- 2. Posts with engagement metrics
SELECT 
    p.post_id,
    p.title,
    u.username AS author,
    p.published_at,
    COUNT(DISTINCT pl.user_id) AS like_count,
    COUNT(DISTINCT c.comment_id) AS comment_count
FROM posts p
JOIN users u ON p.user_id = u.user_id
LEFT JOIN post_likes pl ON p.post_id = pl.post_id
LEFT JOIN comments c ON p.post_id = c.post_id
WHERE p.status = 'published'
GROUP BY p.post_id, p.title, u.username, p.published_at
ORDER BY like_count DESC, comment_count DESC;

-- 3. User's feed (posts from followed users)
SELECT 
    p.post_id,
    p.title,
    p.excerpt,
    p.published_at,
    u.username AS author,
    u.avatar_url
FROM posts p
JOIN users u ON p.user_id = u.user_id
JOIN follows f ON p.user_id = f.following_id
WHERE f.follower_id = 1  -- Current user's ID
    AND p.status = 'published'
ORDER BY p.published_at DESC
LIMIT 20;

Exercise 3: Performance Optimization (10 minutes)

Objective: Identify and fix performance issues

Challenge: The following query is slow. Analyze and optimize it:

-- Slow query - find reasons and optimize
SELECT * FROM orders 
WHERE YEAR(order_date) = 2024 
    AND customer_id IN (
        SELECT customer_id FROM customers WHERE city = 'New York'
    );

Solution:

-- Problems:
-- 1. SELECT * fetches unnecessary columns
-- 2. YEAR(order_date) prevents index usage
-- 3. Subquery might be inefficient

-- Optimized version
SELECT 
    o.order_id,
    o.customer_id,
    o.order_date,
    o.total,
    o.status
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.order_date >= '2024-01-01' 
    AND o.order_date < '2025-01-01'
    AND c.city = 'New York';

-- Also ensure these indexes exist:
-- CREATE INDEX idx_orders_date ON orders(order_date);
-- CREATE INDEX idx_customers_city ON customers(city);

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Using SELECT * in production code Cause: Convenience over efficiency Solution: Always specify needed columns; reduces network transfer and allows index-only scans

  2. Problem: Missing indexes on foreign keys Cause: Assuming the database creates them automatically Solution: Always create indexes on foreign key columns for join performance

  3. Problem: N+1 query problem in application code Cause: Fetching related data in loops instead of joins Solution: Use JOIN queries or batch fetching to retrieve related data

  4. Problem: Not using parameterized queries Cause: Concatenating user input into SQL strings Solution: Always use prepared statements/parameterized queries to prevent SQL injection

Debugging Tips

  • Use EXPLAIN ANALYZE: See actual execution plans and timing
  • Check for table scans: Full table scans often indicate missing indexes
  • Monitor slow query logs: Identify problematic queries in production
  • Test with realistic data volumes: Small test datasets hide performance issues

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Use meaningful naming: customer_id not cid, order_items not oi
  • Consistent naming conventions: snake_case or camelCase, but be consistent
  • Version control schemas: Track changes with migration scripts
  • Document constraints: Comment why specific constraints exist
  • Regular maintenance: VACUUM, ANALYZE, index rebuilding
Feature PostgreSQL MySQL SQL Server Oracle
License Open Source Open Source/Commercial Commercial Commercial
JSON Support Excellent Good Good Good
Full-Text Search Built-in Built-in Built-in Built-in
Replication Streaming, Logical Master-Slave, Group Always On Data Guard
Extensions Rich ecosystem Limited Limited Limited
Best For General purpose Web applications Enterprise/.NET Enterprise

Next Steps in Your Learning Journey

  • Advanced SQL: Window functions, recursive CTEs, lateral joins
  • Performance Tuning: Query optimization, indexing strategies
  • Database Administration: Backup, recovery, security
  • Specific RDBMS: Deep dive into PostgreSQL or MySQL internals

9. Summary and Review (5 minutes)

Key Takeaways

  1. Relational databases use tables with relationships enforced by keys and constraints
  2. Normalization reduces redundancy but may require denormalization for performance
  3. SQL provides powerful query capabilities including joins, aggregations, and window functions
  4. Proper indexing is crucial for query performance

Quick Review Questions

  1. What are the three main normal forms and what do they address? Answer: 1NF (atomic values), 2NF (no partial dependencies), 3NF (no transitive dependencies)

  2. What’s the difference between INNER JOIN and LEFT JOIN? Answer: INNER JOIN returns only matching rows; LEFT JOIN returns all rows from left table plus matches

  3. When would you denormalize a database? Answer: For read-heavy workloads, reporting queries, or when join performance becomes a bottleneck


For Instructors

Teaching Tips

  • Use visual ERD diagrams when explaining relationships
  • Live-code queries showing results in real-time
  • Demonstrate query plan analysis with EXPLAIN
  • Show performance differences with and without indexes on larger datasets

Assessment Ideas

  • Database design from business requirements
  • Query writing challenges with increasing complexity
  • Performance optimization exercises
  • Normalization/denormalization decision scenarios

Adaptation Notes

  • For beginners: Spend more time on basic CRUD operations before joins
  • For advanced students: Include advanced topics like recursive CTEs, materialized views
  • Online delivery: Use shared database instances for collaborative exercises
  • Time constraints: Core concepts can be covered in 60 minutes; practice requires additional time