Ask where the world's data actually lives, and the honest answer is in relational databases, organized into tables and queried with SQL. The language is over fifty years old, it is remarkably stable, and it runs everything from a phone app's local storage to a bank's core systems to the cloud warehouses behind modern analytics. Learn it once and it pays off for a career, because every tool in this part, Python, R, and the commercial packages, ultimately pulls its data with SQL.
SQL is a doing skill, so this chapter is a five-notebook mini-course: foundations (SELECT/WHERE/ORDER BY), aggregation (GROUP BY), joins, window functions, and subqueries with the handoff to pandas. Every query runs for real against a small shop database loaded into SQLite, which is built into Python, so there is nothing to install. Read a section here, then run its notebook and change the SQL.
The Relational Model
A relational database stores data in tables, each row an record and each column a field. The insight that makes it powerful is to split data across tables to avoid repeating it, and to link the tables with keys. A primary key uniquely identifies each row of a table; a foreign key in another table points back to it. Store each customer once, each product once, and let the orders reference them by key.
Many database systems speak SQL, and the core language is portable across them. SQLite is a tiny file-based engine built into your phone and into Python (this chapter uses it). PostgreSQL and MySQL are the popular open-source servers. SQL Server and Oracle run large enterprises. And cloud warehouses like BigQuery, Snowflake, and Redshift answer SQL over enormous datasets. The dialects differ at the edges; the SELECT you learn here works in all of them.
The Anatomy of a Query
Almost every SQL query is one sentence with up to six clauses, and they are always written in the same order. You will recognize the logic immediately, because it is the same describe-filter-group-sort you have done throughout this book.
SELECT columns, and aggregates FROM a table (or joined tables) WHERE row conditions GROUP BY columns to summarize by HAVING conditions on the groups ORDER BY a sort key LIMIT how many rows to return
There is one surprise worth learning early, and it prevents a lot of confusion: SQL does not run in the order
you write it. It reads FROM first to get the rows, filters with WHERE, forms groups,
filters those with HAVING, and only then evaluates the SELECT list, sorts, and limits. That is
why you can filter groups in HAVING but not yet refer to a SELECT alias there.
WHERE acts before grouping so it cannot see aggregates, while
HAVING acts after and is made for them. It is the same split-apply-combine you have used all along.
Aggregation is the workhorse. COUNT, SUM, AVG, MIN, and
MAX collapse many rows to one number, and GROUP BY computes that number for each group. If
this feels familiar, it should: SELECT category, SUM(revenue) FROM ... GROUP BY category is the exact SQL
twin of df.groupby("category").revenue.sum() from the Python chapter.
Joins: The Relational Payoff
Splitting data across tables only pays off because you can put it back together. A JOIN matches rows from two tables on a shared key. The revenue-by-region question is impossible from any single table, but a join from line items to orders to customers makes it a few lines of SQL.
| Join type | What it keeps |
|---|---|
| INNER JOIN | Only rows that match on both sides. The default and most common; drops non-matches. |
| LEFT JOIN | Every row from the left table, plus matches from the right, with NULL where there is none. How you find records with no match, like customers who never ordered. |
| RIGHT / FULL JOIN | The mirror image, and both sides kept. Less common, and some engines (including SQLite historically) lean on LEFT instead. |
SELECT c.region, ROUND(SUM(oi.revenue), 2) AS revenue FROM order_items oi JOIN orders o ON oi.order_id = o.order_id JOIN customers c ON o.customer_id = c.customer_id GROUP BY c.region ORDER BY revenue DESC;
A join is the SQL counterpart of a pandas merge and an R dplyr join: same idea, same shared
key, different words. Join to reassemble, then GROUP BY to summarize, and you can answer almost any
cross-table business question. The notebooks run this exact query and several more.
Window Functions: Summarize Without Collapsing
A GROUP BY reduces each group to a single row, which is exactly what you want for a summary and exactly
wrong when you need to keep every row and add a group-wise number beside it. That is what a
window function does: it computes an aggregate over a window of rows defined by an
OVER clause, but returns a value for every row.
OVER (...). With it, SUM becomes a running total, RANK
numbers rows, PARTITION BY restarts the window per group, and LAG reaches to the previous
row for period-over-period change.
Window functions are what let SQL answer questions a spreadsheet user reaches for constantly: rank the products, show each region's top customer, compute a running total, find month-over-month growth. In the notebook these are one clause each. They are also, not coincidentally, the SQL that analysts are most often asked about in interviews.
The Five-Notebook Mini-Course
Work through these in order. Each loads the shop database into SQLite and runs real SQL, building from a single SELECT up to window functions and the handoff to pandas.
SQL Foundations
The core sentence: SELECT and FROM, WHERE with comparisons, BETWEEN, IN and LIKE, ORDER BY and LIMIT for top-N, DISTINCT, and computed columns.
Aggregation & GROUP BY
COUNT, SUM, AVG, MIN, MAX; grouping by one column and several; the difference between WHERE and HAVING; counting distinct values.
Joins
INNER JOIN on a key, join-then-aggregate for revenue by region and category, LEFT JOIN to find non-matches, and a full four-table join for top customers.
Window Functions
SUM OVER for running totals and shares, RANK and ROW_NUMBER, PARTITION BY to rank within groups, and LAG for month-over-month growth.
Subqueries & pandas
Subqueries and CTEs (the WITH clause) for readable complex logic, then the professional handoff: SQL prepares the data, pandas analyzes and plots it.
A four-table shop database. customers (40) and
products (12) are lookup tables; orders (600) records who bought when through which channel;
and order_items (1,494 lines) holds the quantities, prices, and revenue, linked by
order_id and product_id. The notebooks load these four sheets into SQLite and query them.
Running the whole course reproduces the shop's year: 114,592 dollars of revenue, an average order of
191 dollars, Equipment the top category at 57,006, North the top
region at 43,069, a July revenue peak, and 4 customers who never placed an order.
Every notebook runs on pandas, the built-in
sqlite3, and (notebook 5) matplotlib, all available on Colab with nothing to install.
SQL in Data Science, Machine Learning & AI
SQL is not a rival to Python and R; it is the stage before them. The professional pattern is to push the heavy joining, filtering, and aggregating into the database, where the data already lives and the engine is optimized for it, then pull a compact, tidy result into pandas or R for modeling and charts. Because you know the pandas verbs, the SQL ones need no new concepts.
| In SQL | The pandas you already know |
|---|---|
SELECT col1, col2 | df[["col1", "col2"]] |
WHERE x > 100 | df[df.x > 100] or df.query("x > 100") |
GROUP BY region | df.groupby("region") |
JOIN ... ON key | df.merge(other, on="key") |
ORDER BY x DESC | df.sort_values("x", ascending=False) |
SUM() OVER (...) | df.groupby(...).cumsum() and friends |
For machine learning, this matters more than it first appears. Real models are trained on features that are almost always engineered in SQL: a customer's total spend, their order count in the last 90 days, their days since last purchase. Computing those at the source and reading the result into a training frame is the daily reality of applied ML. And the modern data stack has only deepened SQL's role: cloud warehouses run SQL over petabytes, and tools like dbt let analysts build tested, version-controlled transformation pipelines in pure SQL, a discipline now called analytics engineering.
SQL rests on a genuine piece of theory: Edgar F. Codd's relational model (1970), which recast data as mathematical relations and gave querying a rigorous, set-based foundation. That is why the language is declarative, you state what you want and the database's query planner decides how to compute it. Half a century of hardware has changed underneath, yet the same SELECT still runs, because it was built on an abstraction, not an implementation. Few skills in computing have aged so well, which is a strong argument for learning it deeply.
π Key Takeaways
- βMost data lives in relational databases, stored as tables linked by primary and foreign keys, and queried with SQL, a fifty-year-old, remarkably durable skill.
- βAlmost every query is one sentence: SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, and it runs FROM-first, not top-down.
- βAggregation is split-apply-combine: GROUP BY with COUNT/SUM/AVG is the SQL twin of a pandas groupby; WHERE filters rows, HAVING filters groups.
- βJoins reassemble split tables on a shared key: INNER keeps matches, LEFT keeps every left row and marks misses with NULL. A join is a pandas merge in different words.
- βWindow functions summarize without collapsing: OVER turns SUM into a running total, adds rankings with RANK, and compares rows with LAG, all while keeping every row.
- βThe pattern is SQL then Python: push joins, filters, and aggregation into the database, then read a tidy result into pandas or R to model and plot.
- βSQL is central to modern ML and analytics: features are engineered in SQL, warehouses run it at scale, and dbt made it a version-controlled engineering discipline.
Practice Challenges
Five exercises, one per notebook area, on the shop database. Full solutions are in the companion solutions notebook.
Filter and sort
List the Accessories products priced under 15 dollars, cheapest first.
WHERE category = 'Accessories' AND unit_price < 15, then ORDER BY.Aggregate with HAVING
Total revenue per product id, keeping only products that brought in more than 8,000 dollars.
GROUP BY product_id then HAVING SUM(revenue) > 8000.Three-table join
Report revenue by customer segment (Consumer vs Business), joining line items to orders to customers.
JOIN ... ON clauses, then GROUP BY c.segment.Window ranking
Rank the channels (web, mobile, store) by total revenue using a window function, keeping the revenue beside the rank.
RANK() OVER (ORDER BY revenue DESC).CTE to pandas
Use a CTE to get revenue per region, read it into pandas, and report each region's share of the total.
WITH r AS (...), then read_sql_query and a pandas share calculation.Solutions notebook
All five challenges worked as real SQL against the shop database: a filtered SELECT, an aggregation with HAVING, a three-table join by segment, a window ranking of channels, and a CTE read into pandas for a share calculation.
Quiz: Test Yourself
Eight questions on the relational model, SELECT, joins, aggregation, and window functions. Answer them, hit Check Answers, and keep refining until you score 100%. Your progress is saved.
Databases are where analysts get data; spreadsheets are where many people first meet it. Excel & BI Tools covers the spreadsheet as an analysis tool and the business-intelligence dashboards, Power BI, Tableau, and Looker, that turn queries into shareable visuals. Browse the full Contents for what is published and what is on the way.