Lesson 1
The shop's tables and the shape of a query
What you'll learn: how an online shop's data is laid out in tables, and how to read it with SELECT, ORDER BY and a row limit while knowing the order in which the database really runs your query.
Meet Leila and Fernleaf
Leila has just joined Fernleaf, a small online shop that sells houseplants, pots and plant care supplies, as its first analyst. She is quick with spreadsheets and has written the odd SELECT before, but she has never had a whole database to herself. Her manager, Tom, runs marketing. His questions will drive this course: who buys, what sells, which customers come back, and where shoppers give up.
Fernleaf's data lives in a relational database: a set of tables, each like a spreadsheet tab with fixed, named columns, linked to each other by shared id columns. Four tables matter to Leila.
| Table | One row per | Main columns |
|---|---|---|
customers | customer account | customer_id, name, email, country, signup_date |
orders | order placed | order_id, customer_id, order_date, status, total_amount |
order_items | product line within an order | order_id, product_id, quantity, unit_price |
products | product in the catalogue | product_id, name, category, price |
The middle column is the most important thing on that table. Analysts call it the grain: what one row represents. An order with three different plants is one row in orders and three rows in order_items. Know the grain of every table you touch and you avoid half the classic SQL mistakes. It will matter a great deal when we reach joins.
Fernleaf's history starts in January 2025, when the current website launched. Right now the tables hold 4,812 customers, 12,640 orders, 29,350 order lines and 186 products.
Choosing columns with SELECT
The smallest useful query names some columns and a table:
SELECT name, country, signup_date FROM customers;
SELECT * returns every column. It is handy for a first look, but in queries you keep, list the columns you need. The result is easier to read, it will not change shape when someone adds a column, and on cloud warehouses that charge by data scanned (BigQuery's on-demand pricing works this way) it can cost noticeably less.
You can calculate new columns and name them with AS. Tom is wondering what a 10% price rise would look like:
SELECT name, price, price * 1.1 AS new_price FROM products;
DISTINCT removes duplicate rows from the result, which is a quick way to see which values a column holds:
SELECT DISTINCT country FROM customers;
Keywords are not case-sensitive, so select works as well as SELECT; writing them in capitals is just a common habit that makes queries easier to scan. A double hyphen starts a comment that runs to the end of the line, such as -- top orders for Tom.
Sorting and limiting
ORDER BY sorts the result, ascending by default; add DESC for largest first. Without ORDER BY, the database promises no order at all, even if the rows happen to come back sorted today.
Tom's first request is simple: "What were our ten biggest orders?" How you ask for only the first rows depends on the database:
| Database | How to return the top 10 |
|---|---|
| PostgreSQL, MySQL, BigQuery, Snowflake | LIMIT 10 at the very end |
| SQL Server | TOP 10 straight after SELECT |
| Oracle, and standard SQL (PostgreSQL and Snowflake accept it too) | FETCH FIRST 10 ROWS ONLY at the end |
Fernleaf runs on PostgreSQL, so Leila writes the query over several lines, one clause per line:
SELECT order_id, customer_id, order_date, total_amount
FROM orders
ORDER BY total_amount DESC
LIMIT 10;
The top result is a 412.50 order from an office that bought twenty large plants. Leila notices that two orders in the top ten have the status cancelled. That is a question for the next lesson.
Written order versus running order
Here is the idea that makes the rest of SQL easier. You write the clauses of a query in one fixed order, but the database works through them, logically, in a different one. Think of the address on an envelope: you write the person's name first and the country last, but the postal service reads it the other way round, country first, then town, then street, then name. SQL is similar. You write SELECT first, but it is one of the last things to happen.
The logical running order is:
FROM(and any joins): which table or tables to read.WHERE: which rows to keep.GROUP BY: how to bunch the remaining rows together.HAVING: which groups to keep.SELECT: which columns to compute and return.ORDER BY: how to sort the result.LIMIT(orTOP, orFETCH): how many rows to hand back.
Real databases are free to optimise how they do the work, but the result always behaves as if these steps ran in this order. That explains several puzzling errors. For example, this fails in PostgreSQL, MySQL, SQL Server and BigQuery:
SELECT name, price * 1.1 AS new_price FROM products WHERE new_price > 20;
WHERE runs before SELECT, so the alias new_price does not exist yet. Repeat the expression instead:
SELECT name, price * 1.1 AS new_price FROM products WHERE price * 1.1 > 20;
Snowflake is unusually lenient and accepts the alias in WHERE, but writing the expression out works everywhere. ORDER BY runs after SELECT, so ORDER BY new_price is fine in every database.
Recap
- A relational database stores data in tables linked by id columns; Fernleaf's four are
customers,orders,order_itemsandproducts. - Every table has a grain, what one row represents. Learn it before you query.
- List the columns you need instead of
SELECT *in queries you keep, and useASto name calculated columns. - Without
ORDER BYthere is no guaranteed order. Row limits areLIMIT,TOPorFETCH FIRST, depending on the database. - Queries run logically as FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT, which is why a
SELECTalias cannot be used inWHEREin most databases.