Unit 4: SQL for Data Analysis

Master the universal lingua franca of data engineering and analytics: relational database concepts, primary and foreign key constraints, MySQL and PostgreSQL environment configurations, DDL/DML data manipulation, the 11-stage logical query execution pipeline, multi-table joins (Inner, Left, Right, Full Outer), subqueries and Common Table Expressions (CTEs), analytical window functions (ranking, lead/lag, running aggregations), views, B-Tree index optimization, and production-grade SQL data cleansing patterns.

Section 01

Relational Database Concepts & Architecture

Structured Query Language (SQL) is the foundational declarative language for interacting with relational database management systems (RDBMS). Formulated by Edgar F. Codd in 1970, the relational model mathematically abstracts data storage into formal relations, liberating analytical queries from low-level physical file-storage mechanics.

Relational Model Terminology

Primary Keys, Foreign Keys & Referential Integrity

Relational Entity-Relationship (ER) Architecture
    TABLE: students (Primary Entity)                 TABLE: enrollments (Foreign Junction Entity)
+---------------+---------------------+           +---------------+--------------------+
| student_id    | PK, INT, AUTO_INC   |<--+       | enrollment_id | PK, INT, AUTO_INC  |
| full_name     | VARCHAR(100)        |   |       | student_id    | FK, INT (Ref)      |----+
| email         | VARCHAR(150), UNIQUE|   +=======| course_id     | FK, INT (Ref)      |--+ |
| registered_at | TIMESTAMP           |           | enrolled_at   | TIMESTAMP          |  | |
+---------------+---------------------+           +---------------+--------------------+  | |
                                                                                          | |
                                                  TABLE: courses (Referenced Entity)      | |
                                              +---------------+--------------------+      | |
                                              | course_id     | PK, INT, AUTO_INC  |<=====+ |
                                              | title         | VARCHAR(100)       |        |
                                              | price_paise   | INT                |        |
                                              +---------------+--------------------+        |
                                                                                            |
                                      Referential Constraint: An enrollment record CANNOT   |
                                      exist with a student_id that does not exist in students<--+
        

SQL Environments: PostgreSQL vs. MySQL

Two database engines dominate modern technology infrastructure:

Standard SQL Data Types

Data Type Category SQL Type Keyword Storage & Precision Analytical Example
Exact Integer INT, BIGINT, SMALLINT 4 to 8 bytes, discrete signed whole numbers. User IDs, transaction counts, inventory units.
Exact Decimal DECIMAL(precision, scale) Fixed-point numerical representation without floating-point rounding errors. Monetary transactions (e.g., DECIMAL(12, 2) for currency amounts).
Approximate Float FLOAT, DOUBLE PRECISION IEEE 754 floating point (variable precision). GPS coordinates, sensor telemetry, statistical coefficients.
Character String VARCHAR(n), TEXT, CHAR(n) Variable or fixed-length UTF-8 character strings. Customer names, comments, ISO country codes.
Temporal DATE, TIME, TIMESTAMP WITH TIME ZONE Calendar dates and microsecond-level timestamps. Session login events, checkout completion timestamps.
Semi-Structured JSONB (Postgres) / JSON (MySQL) Decomposed binary JSON enabling fast indexing and sub-key querying. Dynamic event telemetry payloads, unstructured metadata.
Section 02

DDL & DML: Defining & Mutating Data

SQL operations are divided into functional sub-languages. The two most fundamental are Data Definition Language (DDL), which defines table architecture, and Data Manipulation Language (DML), which inserts, modifies, and deletes records.

SQL Production DDL: Creating Normalized Tables with Integrity Constraints
-- 1. Create Students Table
CREATE TABLE students (
    student_id SERIAL PRIMARY KEY,
    full_name VARCHAR(100) NOT NULL,
    email VARCHAR(150) UNIQUE NOT NULL,
    graduation_year INT CHECK (graduation_year >= 2020),
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- 2. Create Enrollments Table with Foreign Key Integrity
CREATE TABLE enrollments (
    enrollment_id SERIAL PRIMARY KEY,
    student_id INT NOT NULL REFERENCES students(student_id) ON DELETE CASCADE,
    course_name VARCHAR(100) NOT NULL,
    amount_paid DECIMAL(10, 2) CHECK (amount_paid >= 0),
    enrolled_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
SQL Production DML: Inserting, Updating, and Deleting Records
-- INSERT: Populate records
INSERT INTO students (full_name, email, graduation_year)
VALUES
    ('Aarav Sharma', 'aarav@example.com', 2025),
    ('Diya Patel', 'diya@example.com', 2026);

-- UPDATE: Modify records safely with a targeted WHERE clause
UPDATE students
SET is_active = FALSE
WHERE graduation_year < 2024;

-- DELETE: Remove targeted records
DELETE FROM enrollments
WHERE amount_paid = 0.00;

The Unbounded UPDATE/DELETE Danger

In SQL, executing UPDATE or DELETE without an explicit WHERE clause mutates or erases every single record in the entire table without warning. Production database clients should enforce "Safe Updates" mode, which strictly rejects any mutation query lacking a primary key condition.

Section 03

The Query Execution Pipeline & Filtering

One of the most critical concepts in SQL data analysis is understanding that the syntactic written order of a query does not match the logical execution order processed by the database query optimizer.

Lexical Written Order vs. Logical SQL Execution Order
WRITTEN SYNTAX (Lexical Order):              DATABASE EXECUTION PIPELINE (Logical Order):
1. SELECT column1, AGG(column2)              1. FROM table_name           (Locate base table)
2. FROM table_name                           2. JOIN other_table ON cond  (Join & build cartesian set)
3. JOIN other_table ON condition             3. WHERE filtering_condition (Filter raw input rows)
4. WHERE row_condition                       4. GROUP BY column_name      (Collapse into grouped rows)
5. GROUP BY column_name                      5. HAVING group_condition    (Filter grouped aggregations)
6. HAVING group_condition                    6. SELECT expressions/window (Compute selected columns)
7. ORDER BY sort_column                      7. DISTINCT                  (Deduplicate identical rows)
8. LIMIT n;                                  8. ORDER BY sort_column      (Sort resulting rows)
                                             9. LIMIT / OFFSET n;         (Slice final page)

CRITICAL TAKEAWAY: You CANNOT use column aliases defined in SELECT inside the WHERE clause
because WHERE executes at Step 3, long before SELECT evaluates at Step 6!
        

Filtering with WHERE

The WHERE clause filters rows before any grouping or aggregation takes place. It supports rich logical combinations:

Sorting with ORDER BY

Relational tables are unordered sets by definition. Unless an explicit ORDER BY clause is appended, the database returns rows in arbitrary physical storage order. You can sort by multiple columns, toggle ASC (default) or DESC, and explicitly control null placement with NULLS FIRST or NULLS LAST.

Section 04

Aggregation Mechanics: GROUP BY & HAVING

Aggregation transforms granular transaction rows into high-level business intelligence metrics by summarizing multiple values into a single scalar result.

The Five Standard Aggregate Functions

Function Mathematical Behavior Handling of NULL Values
COUNT(*) Returns total count of physical rows in the partition. Includes rows containing NULLs.
COUNT(column) Returns count of rows where the specific column is not null. Ignores NULLs completely.
SUM(column) Calculates total arithmetic sum of numerical values. Ignores NULLs. (Returns NULL if all values are null).
AVG(column) Calculates arithmetic mean: sum divided by count of non-nulls. Ignores NULLs in both numerator and denominator!
MIN / MAX Returns smallest and largest value in the column. Ignores NULLs. Valid on numbers, strings, and dates.

WHERE vs. HAVING: The Definitive Distinction

SQL Cohort Aggregation Demonstrating WHERE and HAVING
SELECT
    course_name,
    COUNT(student_id) AS total_students,
    AVG(amount_paid) AS avg_revenue_per_student,
    SUM(amount_paid) AS total_course_revenue
FROM enrollments
-- WHERE filters RAW INDIVIDUAL ROWS before aggregation
WHERE enrolled_at >= '2026-01-01'
GROUP BY course_name
-- HAVING filters AGGREGATED METRICS after grouping
HAVING COUNT(student_id) >= 10 AND SUM(amount_paid) > 50000.00
ORDER BY total_course_revenue DESC;

The "Non-Aggregated Column" Rule

Every column that appears in your SELECT clause must either be enclosed within an aggregate function (e.g., SUM(amount)) or explicitly listed in the GROUP BY clause. Failing to follow this rule creates ambiguity (which row's value should be displayed for the un-aggregated column?) and causes modern SQL engines to throw a syntax error.

Section 05

Relational Joins & Set Theory

Relational databases normalize data across separate tables to prevent data redundancy. Joins combine columns from two or more tables based on a shared relational predicate (usually primary key to foreign key matches).

Set Theory Architecture of the Four Primary SQL Joins
    TABLE A (Left Table)                 TABLE B (Right Table)

1. INNER JOIN (Intersection):
   (       [A & B]       )  ===> Returns ONLY rows where matching keys exist in BOTH tables.
                                Unmatched rows from both Table A and Table B are discarded.

2. LEFT OUTER JOIN:
   ( [All A] [A & B]     )  ===> Returns ALL rows from Table A.
                                If a match exists in Table B, appends columns; otherwise fills NULL.

3. RIGHT OUTER JOIN:
   (     [A & B] [All B] )  ===> Returns ALL rows from Table B.
                                If a match exists in Table A, appends columns; otherwise fills NULL.

4. FULL OUTER JOIN:
   ( [All A] [A & B] [All B] ) ===> Complete Union: Returns all rows from both tables.
                                Unmatched attributes on either side are populated with NULL.
        
SQL Multi-Table Join Query: Analyzing Student Cohorts
SELECT
    s.student_id,
    s.full_name,
    s.email,
    c.title AS course_title,
    e.amount_paid,
    e.enrolled_at
FROM students s
-- LEFT JOIN preserves all students even if they haven't enrolled yet
LEFT JOIN enrollments e ON s.student_id = e.student_id
-- INNER JOIN pulls course details only for valid enrollments
INNER JOIN courses c ON e.course_id = c.course_id
WHERE s.is_active = TRUE;

Anti-Joins: Finding Non-Existent Relationships

A common analytics question is: "Which registered students have NEVER enrolled in any course?" This is elegantly solved using a Left Anti-Join pattern:

SELECT s.student_id, s.full_name
FROM students s
LEFT JOIN enrollments e ON s.student_id = e.student_id
WHERE e.enrollment_id IS NULL;
Section 06

Subqueries & Common Table Expressions (CTEs)

Complex analytical transformations require multi-step decomposition. SQL enables modular query design via Subqueries and Common Table Expressions (CTEs).

1. Subqueries

A subquery is an inner SQL query nested inside a larger outer query. Subqueries can return:

2. Common Table Expressions (The WITH Clause)

A Common Table Expression (CTE) defines a temporary named result set that exists only during the execution scope of a single query. CTEs drastically improve readability, eliminate duplicate subqueries, and can be chained sequentially:

SQL Multi-Stage Transformation with Chained CTEs
-- Define stage 1: Aggregate spending per student
WITH student_spending AS (
    SELECT
        student_id,
        COUNT(enrollment_id) AS total_enrollments,
        SUM(amount_paid) AS total_spent
    FROM enrollments
    GROUP BY student_id
),
-- Define stage 2: Calculate global cohort benchmarks
benchmark AS (
    SELECT AVG(total_spent) AS avg_cohort_spend
    FROM student_spending
)
-- Final consumption query joining both CTEs
SELECT
    s.student_id,
    s.full_name,
    sp.total_enrollments,
    sp.total_spent,
    b.avg_cohort_spend,
    CASE
        WHEN sp.total_spent >= (b.avg_cohort_spend * 1.5) THEN 'High-Value Customer'
        ELSE 'Standard Customer'
    END AS customer_segment
FROM students s
INNER JOIN student_spending sp ON s.student_id = sp.student_id
CROSS JOIN benchmark b
ORDER BY sp.total_spent DESC;
Section 07

Analytical Window Functions

In traditional SQL, a GROUP BY clause collapses individual rows into a single aggregated summary row. Window Functions perform calculations across a set of table rows related to the current row, while retaining the unique identity and granularity of every individual row.

The OVER Clause Anatomy

Syntax: FUNCTION() OVER (PARTITION BY partition_col ORDER BY sort_col ROWS frame_clause)

Window Partitioning & Rolling Frame Mechanics
TABLE ROWS (Order Stream):
Date        Region    Revenue    Partitioning Window: PARTITION BY Region ORDER BY Date
-----------------------------------------------------------------------------------------
Oct 01      North     $100       Partition "North" [Row 1] ===> Running Total: $100
Oct 02      North     $250       Partition "North" [Row 2] ===> Running Total: $350
Oct 03      North     $150       Partition "North" [Row 3] ===> Running Total: $500
--------------------------------- Boundary resets for new partition!
Oct 01      South     $400       Partition "South" [Row 1] ===> Running Total: $400
Oct 02      South     $300       Partition "South" [Row 2] ===> Running Total: $700
        

Ranking Functions: ROW_NUMBER, RANK & DENSE_RANK

Function Ranking Behavior on Ties (e.g., scores 90, 90, 80) Practical Analytics Use Case
ROW_NUMBER() Assigns sequential unique integers (1, 2, 3) regardless of ties. Deduplicating records; pagination offsets.
RANK() Assigns identical ranks to ties, then skips subsequent ranks (1, 1, 3). Olympic medals; leaderboards where skipped spots matter.
DENSE_RANK() Assigns identical ranks to ties without skipping numbers (1, 1, 2). Top-N percentile brackets (e.g., finding the top 3 highest salaries).

Navigation & Running Metrics: LAG, LEAD & Rolling Averages

SQL Production Window Analytics: Month-over-Month Growth & 7-Day Moving Average
SELECT
    transaction_date,
    daily_revenue,
    -- 1. Running Total across all time
    SUM(daily_revenue) OVER (
        ORDER BY transaction_date
    ) AS cumulative_revenue,

    -- 2. 7-Day Centered Moving Average
    AVG(daily_revenue) OVER (
        ORDER BY transaction_date
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS rolling_7day_avg,

    -- 3. Previous Day Revenue via LAG for Month-over-Month analysis
    LAG(daily_revenue, 1) OVER (
        ORDER BY transaction_date
    ) AS prior_day_revenue
FROM daily_sales;
Section 08

Views, Index Optimization & Data Cleansing

Building scalable analytics infrastructure requires writing queries that are maintainable, performant, and resilient against data quality corruption.

Views vs. Materialized Views

Database Indexes: B-Tree Performance Mechanics

Without an index, finding a specific record requires a Full Table Scan: the database must read every single block on disk from start to finish, an order of n operation.

A B-Tree (Balanced Tree) Index creates a self-balancing sorted tree data structure that allows the query engine to pinpoint rows in order of log n time:

SQL Index Creation & Composite Indexing Strategy
-- Single Column Index for fast foreign key lookups
CREATE INDEX idx_enrollments_student_id ON enrollments(student_id);

-- Composite Index optimized for filtered sorting
-- Follows Leftmost Prefix Rule: Accelerates queries filtering on status OR (status AND created_at)
CREATE INDEX idx_students_status_created ON students(is_active, created_at);

Production SQL Data Cleansing Patterns

Data analysts spend substantial time remediating data entry errors and duplicate payloads directly in SQL:

SQL Production Cleansing: Normalization & Deduplication via ROW_NUMBER
-- 1. Standardizing Strings & Imputing Null Values
SELECT
    student_id,
    -- Trim whitespace and force lowercase email
    LOWER(TRIM(email)) AS clean_email,
    -- Coalesce fallback for missing phone numbers
    COALESCE(phone_number, 'Unknown') AS contact_phone,
    -- Convert string timestamps to native dates
    CAST(registration_date AS DATE) AS valid_reg_date
FROM raw_student_leads;

-- 2. Deduplication Pattern using CTE & ROW_NUMBER
WITH ranked_leads AS (
    SELECT
        lead_id,
        email,
        phone,
        created_at,
        ROW_NUMBER() OVER (
            PARTITION BY email 
            ORDER BY created_at DESC -- Keep the latest lead
        ) AS row_rank
    FROM raw_student_leads
)
SELECT lead_id, email, phone, created_at
FROM ranked_leads
WHERE row_rank = 1; -- Strips all duplicate submissions cleanly

Unit Summary & Key Takeaways

  • Relational databases enforce Entity Integrity via Primary Keys and Referential Integrity via Foreign Keys.
  • SQL queries execute logically in 9 distinct steps: FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT.
  • WHERE filters individual rows before aggregation; HAVING filters grouped aggregations after grouping.
  • Joins combine tables: Inner (intersection), Left/Right Outer (preserves one side), and Full Outer (complete union).
  • Common Table Expressions (CTEs) break complex queries into readable, maintainable, modular stages.
  • Window functions compute rankings (ROW_NUMBER, DENSE_RANK) and rolling totals without collapsing individual row identities.
  • B-Tree indexes speed up lookups from O(n) to O(log n); Materialized Views pre-compute expensive reporting caches on disk.