Before You Start
SQL is not a general-purpose language, and it has no single official implementation. These two lessons explain what that means before you write a single query.
What is SQL?
A 1970 IBM paper on relational theory, and the language built to make it usable
SQL traces back to a 1970 paper by Edgar Codd proposing the relational model, and to IBM's System R project that turned it into a working query language soon after. Unlike every other language in this course, SQL is declarative: you describe the result you want, not the steps to compute it, and the database engine decides how.
SQL's origin in relational theory, the declarative-vs-imperative distinction, and where SQL is used across the industry.
Setting Up SQL
SQL has no interpreter of its own — pick an engine, then a client
There is no single “SQL runtime” to install the way there is a Python or Go compiler. You pick a database engine (this course uses PostgreSQL for examples) and a client to talk to it. This lesson gets a real database running locally so every later lesson can be typed and run, not just read.
Installing PostgreSQL (or using a hosted/Docker instance), connecting with psql or a GUI client, and creating your first database.
Querying Fundamentals
The core of daily SQL: asking for rows, narrowing them down, and ordering what comes back. Six lessons — everything here compounds into every query you will ever write.
SELECT & FROM
The two words every SQL query starts with
Every query starts by naming which columns you want and which table they come from. This sounds trivial, but the exact order SQL evaluates a query in (FROM before SELECT, conceptually) explains several beginner surprises you will hit later, like why you cannot reference a computed column alias everywhere you might expect to.
SELECT and FROM syntax, column aliases, SELECT *, and a first look at the logical query-evaluation order.
Filtering with WHERE
Comparisons, ranges, and pattern matching narrow down which rows come back
WHERE is the workhorse of nearly every query: it decides which rows survive before anything else happens to them. This lesson covers comparison operators, combining conditions with AND/OR, ranges with BETWEEN, and pattern matching with LIKE.
Comparison operators, AND/OR/NOT, BETWEEN, IN, LIKE pattern matching, and WHERE clause precedence.
Sorting & Limiting
ORDER BY sets the sequence; LIMIT caps how many rows come back
Without ORDER BY, a database makes no promise about what order rows come back in at all — an easy beginner assumption to get wrong. LIMIT (and its engine-specific cousins) then caps the result to a manageable size, essential for pagination.
ORDER BY on one or more columns, ASC/DESC, LIMIT and OFFSET, and the equivalent syntax in other engines like SQL Server's TOP.
NULL Handling
NULL is not equal to anything — not zero, not empty string, not even another NULL
NULL represents the absence of a value, and SQL's three-valued logic (true, false, unknown) trips up nearly everyone once: `WHERE column = NULL` never matches anything, even a row where the column really is NULL. You need `IS NULL` instead.
Three-valued logic, IS NULL vs IS NOT NULL, NULL in comparisons and aggregates, and COALESCE.
Data Types in SQL
Types vary more across engines than most languages' do
Unlike a programming language with one type system, SQL's types genuinely differ between PostgreSQL, MySQL, and SQLite — SQLite in particular uses “type affinity” rather than strict typing, which is the strangest case among the three. Knowing your engine's actual behavior matters here more than almost anywhere else in SQL.
Numeric, text, date/time and boolean types, engine-specific differences, and casting between types.
Querying, Tied Together
SELECT, WHERE, ORDER BY, subqueries, and NULL handling in real, realistic queries
This lesson is the synthesis for this stage: seeing SELECT, WHERE, ORDER BY, CASE expressions, a first look at subqueries, and NULL-aware logic combined into the kind of query you would actually write against a real table, rather than each clause in isolation.
SELECT/WHERE/ORDER BY/LIMIT together, CASE expressions, a first look at subqueries, and string/date functions with PostgreSQL examples.
Schema Design — SQL's Defining Discipline
This is the stage that makes SQL different from every other language in this course: before you can query good data, someone has to design a schema that keeps the data trustworthy. Five lessons on tables, keys, and the theory that keeps a schema from rotting.
Creating Tables
CREATE TABLE defines your schema up front — ALTER TABLE changes it later, carefully
CREATE TABLE is where a schema becomes real: every column's name, type, and rules are declared before a single row exists. ALTER TABLE lets you change it afterward, but on a table with real data and real traffic, that has to be done carefully, not casually.
CREATE TABLE syntax, column definitions, ALTER TABLE for adding/changing/dropping columns, and DROP TABLE.
Constraints
NOT NULL, UNIQUE, CHECK, and DEFAULT — rules the database enforces on every row, always
A constraint is a rule the database itself enforces, rejecting any row that would break it, rather than trusting application code to always get validation right. This is one of SQL's biggest structural advantages over storing data as plain files or documents.
NOT NULL, UNIQUE, CHECK, and DEFAULT constraints, and what happens when an INSERT violates one.
Primary & Foreign Keys
A primary key identifies each row uniquely; a foreign key enforces a relationship actually exists
A primary key is a promise: this column (or combination of columns) is unique for every row, forever. A foreign key is a different promise: this value must exist as a primary key somewhere else, which is exactly what stops you from ever having an order that points at a customer who does not exist.
Primary keys (single and composite), foreign keys, referential integrity, and ON DELETE/ON UPDATE behavior.
Normalization
Normal forms are a checklist for eliminating redundant, inconsistency-prone data
Normalization is a series of “normal forms,” each one eliminating a specific kind of redundancy that would otherwise let the same fact get stored in two places and quietly disagree with itself. You do not need to memorize the formal definitions to internalize the actual habit: each fact lives in exactly one place.
First, second, and third normal forms, what each one actually eliminates, and when denormalizing on purpose is the right call.
Database Design
Putting normalization, keys, and indexing together into an actual schema
This lesson is the synthesis for this stage: taking normalization, primary/foreign keys, and a first look at indexing and actually designing a schema for a realistic small system, before any application code exists to use it.
Entity-relationship thinking, translating requirements into tables and relationships, and where indexing decisions belong in the design process.
Joins, Aggregation & Advanced Queries
Real questions usually span more than one table, and often need a number computed across many rows rather than the rows themselves. Seven lessons on combining, grouping, and layering queries inside each other.
Inner & Outer Joins
INNER JOIN keeps only matches; LEFT/RIGHT/FULL OUTER JOIN keep the unmatched rows too, as NULLs
An INNER JOIN only returns rows that match on both sides — which quietly drops data if that is not what you meant. An OUTER JOIN is how you deliberately keep the customers with zero orders, or the products that have never sold, showing NULLs for the side with no match.
INNER JOIN, LEFT/RIGHT/FULL OUTER JOIN, CROSS JOIN, self-joins, and how NULL shows up on the unmatched side.
GROUP BY & HAVING
WHERE filters rows before grouping; HAVING filters groups after aggregation
GROUP BY collapses many rows into one per group, and aggregate functions like COUNT and SUM summarize each group. The classic confusion is WHERE vs HAVING: WHERE runs before grouping and cannot reference an aggregate, HAVING runs after and can.
GROUP BY on one or more columns, aggregate functions (COUNT, SUM, AVG, MIN, MAX), and the WHERE-vs-HAVING distinction.
Subqueries
A query nested inside another — in WHERE, in FROM, or correlated to the outer row
A subquery is a complete SELECT living inside a larger query, and it comes in three flavors that behave quite differently: a scalar subquery in WHERE, a derived table in FROM, and a correlated subquery that re-runs once per outer row, which is easy to write by accident and expensive to run.
Scalar subqueries, subqueries in FROM (derived tables), correlated subqueries, and EXISTS vs IN.
Common Table Expressions
WITH names a temporary result set for readability — and can even reference itself, recursively
A CTE, written with `WITH`, names a subquery so you can reference it by a readable name instead of nesting parentheses several layers deep. A recursive CTE goes further, referencing itself to walk a tree or graph structure, like an employee-to-manager hierarchy.
WITH syntax, chaining multiple CTEs, and recursive CTEs for hierarchical data.
Window Functions
Computing across related rows without collapsing them into one, unlike GROUP BY
A window function computes something across a set of related rows — a running total, a rank, the previous row's value — without collapsing those rows into one, the way GROUP BY does. This is the feature that finally makes “rank each product within its category” a single clause instead of a self-join.
OVER() and PARTITION BY, ROW_NUMBER/RANK/DENSE_RANK, running totals, and LAG/LEAD for comparing adjacent rows.
Set Operations
UNION, INTERSECT, and EXCEPT combine the results of two queries like set operations
These three operators treat query results the way you would treat mathematical sets: UNION combines them, INTERSECT keeps only what is in both, and EXCEPT keeps what is in the first but not the second. Each requires both queries to return the same number of compatible columns.
UNION and UNION ALL, INTERSECT, EXCEPT, and the column-compatibility rule they all share.
Joins & Aggregates, Tied Together
Joins, GROUP BY, subqueries, and window functions in one realistic multi-table query
This lesson is the synthesis for this stage: combining joins across several tables with aggregation, a subquery, and a window function in the kind of query a real report or dashboard would actually need — seeing how the pieces you just learned individually compose in practice.
JOINs (inner, left, right, full, cross, self-join) combined with GROUP BY, HAVING, and window functions in realistic queries.
Transactions, Indexing & Performance
A database's real job is protecting data under concurrent, failure-prone conditions, and answering queries fast as a table grows to millions of rows. Five lessons most beginners skip and every production incident eventually forces you to learn anyway.
Transactions
Several statements succeed or fail together, as one indivisible unit
A transaction groups multiple statements so that either all of them take effect or none do — transferring money between two accounts is the classic example, since a crash between the debit and the credit must not leave the money in neither account or both.
BEGIN, COMMIT, ROLLBACK, savepoints, and what a transaction actually guarantees.
ACID Properties
Atomicity, Consistency, Isolation, Durability — the four guarantees a real transaction makes
ACID names the four properties a transactional database promises: Atomicity (all-or-nothing), Consistency (never leaving the data in an invalid state), Isolation (concurrent transactions do not corrupt each other), and Durability (once committed, it survives a crash).
Atomicity, Consistency, Isolation, Durability in detail, and an introduction to isolation levels and the anomalies they prevent.
Indexing
An index speeds up reads and slows down writes — there is no free lunch, only a tradeoff
An index lets the database find matching rows without scanning the whole table, the same way a book's index beats reading every page. But every index also has to be updated on every INSERT/UPDATE/DELETE, so adding indexes freely is a real tradeoff, not a pure win.
B-tree indexes, when a column is worth indexing, composite indexes, and the write-cost tradeoff.
Execution Plans
EXPLAIN shows exactly how the engine intends to run your query
EXPLAIN (and EXPLAIN ANALYZE) shows you the actual plan the database chose: which indexes it used, in what order it joined tables, and how many rows it expects at each step. Reading one is how you stop guessing why a query is slow and start knowing.
EXPLAIN vs EXPLAIN ANALYZE, sequential scans vs index scans, join strategies, and reading estimated vs actual row counts.
Query Optimization
A handful of habits separate a query that scales from one that quietly gets slower every month
Most real-world slow queries share a small set of causes: a missing index, a function applied to a column that blocks index use, a correlated subquery that should have been a join, or a SELECT * pulling far more than is needed. This lesson is a checklist built from those recurring causes.
Common performance pitfalls, rewriting a correlated subquery as a join, avoiding functions on indexed columns, and SELECT * in production code.
Beyond Basic Queries
Views, stored logic, security, and the DDL/transaction/EXPLAIN material advanced-sql ties together. Five lessons that round out what a working database professional actually uses day to day.
Views
A saved query that looks like a table — no data is duplicated, unless you materialize it
A view wraps a query behind a name you can SELECT from like a table, which is useful for hiding complexity or restricting which columns a user can see. A regular view re-runs its query every time; a materialized view caches the result and has to be refreshed on purpose.
CREATE VIEW, querying through a view, updatable views, and materialized views.
Stored Procedures
Reusable logic stored inside the database itself — syntax varies wildly between engines
A stored procedure keeps logic next to the data it operates on, callable from any client without duplicating the logic in every application. This is also the topic where engines diverge the most — PostgreSQL's PL/pgSQL, MySQL's syntax, and SQL Server's T-SQL are not interchangeable.
CREATE PROCEDURE/FUNCTION basics, parameters, and the major engine-specific dialects.
Triggers
Runs automatically on insert/update/delete — powerful, and easy to hide bugs inside
A trigger fires automatically in response to a data change, useful for enforcing rules an ordinary constraint cannot express or for keeping an audit log. The risk is real too: logic hidden inside a trigger is easy to forget exists, and can surprise anyone debugging unexpected side effects later.
CREATE TRIGGER, BEFORE/AFTER and row/statement-level triggers, and when a trigger is the wrong tool.
SQL Injection & Security
Parameterized queries are the fix — not sanitizing input, not escaping quotes
SQL injection happens when untrusted input gets concatenated directly into a query string, letting an attacker change what the query actually does. The fix is not more careful string escaping — it is never building a query by concatenation in the first place: parameterized queries separate code from data entirely.
A concrete SQL injection example, why escaping quotes is not a real fix, and parameterized queries / prepared statements as the actual solution.
Advanced SQL, Tied Together
CTEs, DDL, transactions, ACID, EXPLAIN ANALYZE, and JSONB in combination
This lesson is the course's final synthesis: recursive CTEs, schema changes with ALTER, a transaction wrapping several statements, reading its EXPLAIN ANALYZE output, and a look at PostgreSQL's JSONB type for when a column needs to hold semi-structured data — the advanced end of everything this course has covered, working together.
CTEs and recursive queries, DDL (CREATE TABLE, ALTER, indexes, views) in combination, transactions and ACID in practice, EXPLAIN ANALYZE, and PostgreSQL's JSONB type.
Concepts That Cross Languages
SQL is declarative, not imperative, so it shares fewer of these cross-language ideas than a general-purpose language does — there is no SQL equivalent of a loop or a user-defined function in ordinary querying. The handful below genuinely apply and are worth reading once.
Build Real Things
Every other language on this site pairs its course with the same 10-project set (a number-guessing game, a REST API, and so on). SQL genuinely does not fit that set — there is no idiomatic “chat server” or “compiler” written in pure SQL. SQL’s own project set needs to be SQL-shaped instead: schema-design exercises, query challenges against a real dataset, and the kind of performance-tuning problem EXPLAIN was built for. That set has not been designed yet, so nothing is linked here that does not exist. The most useful thing to do with what you have learned so far is practice directly in the playground below against real tables, and this section will be filled in with SQL-appropriate projects in a future update.
Practice & Experimentation
Ongoing, not a final step. Run every query you read against a real table rather than just reading it — SQL rewards typing things out far more than most languages do.
Run real SQL queries against sample data without installing anything.
Copy-ready idiomatic queries to keep beside you while you write your own.
All 30 topic pages in one index, for looking things up later.