Skip to content
Pricing

Your first SQL query

Which ten orders were the biggest last week, and from which city?

Lesson 4Level 1: Beginner10 min readFree, runs offline
On this page
  1. Before you start
  2. The problem
  3. In plain words
  4. What you will build from scratch
  5. The idea
  6. Walk through
  7. Common mistakes
  8. Your turn
  9. Check yourself
  10. What to tell the stakeholder
  11. In real job postings
  12. Next

Get the lesson files

The dataset, exercise.sql, the solution and the test live in the open-source repo. You need only Python 3.9 or later.

git clone https://github.com/Ani19arya/analytics-from-scratch
cd analytics-from-scratch/phases/00-start-here/04-your-first-sql-query
python3 test_lesson.py
Open this lesson on GitHub

Before you start

The problem

It is Monday 26 October 2026. Neha, who leads business sales at Tiffinbell (a fictional food delivery app), thinks offices are ordering big group lunches through the normal app. If so, she wants to call them and offer a proper office lunch plan. She asks for the ten biggest delivered orders of last week, Monday 19 to Sunday 25 October, with the city of each, by lunchtime. The data is one table of 284 orders over two weeks. You will write your first SQL queries to answer her, and check them with a test.

In plain words

Think of a shop's order book with one line per order. To answer Neha by hand you would: open the book (FROM), cross out the lines you do not want, such as cancelled orders and other weeks (WHERE), copy only the columns she asked for (SELECT), sort what is left from biggest to smallest (ORDER BY), and keep the top ten (LIMIT). A SQL query is that list of instructions, written in a fixed order.

Some words:

  • SQL (often said "sequel"): Structured Query Language, the language for asking questions of tables in a database.
  • Database: a program that stores tables and answers SQL. This course uses SQLite, a small database that comes with Python, so there is nothing to install.
  • Query: one SQL question. It returns a table of results.
  • Clause: one part of a query, such as WHERE ....
  • Condition: a test that is true or false for each row, such as amount >= 1000.

What you will build from scratch

  • A first look at a table with SELECT *, ORDER BY and LIMIT.
  • The top ten delivered orders of one week, with a tie-breaker.
  • A filter that combines AND and OR correctly.
  • The habit of running any query against a lesson's CSV files with tools/query.py.

The data in data/orders.csv is synthetic, built by make_data.py with a fixed random seed. Amounts are in rupees.

The idea

The shape of a query

SELECT order_id, city, amount        -- which columns to show
FROM orders                          -- which table
WHERE status = 'delivered'           -- which rows to keep
ORDER BY amount DESC                 -- how to sort them
LIMIT 10;                            -- how many to show

Text after -- is a comment: a note for people that the database ignores. Text values go in single quotes, 'delivered'. Numbers do not. The ; ends the query.

The order the database works in

You write SELECT first, but the database does it third. This logical order explains many errors:

flowchart LR
    F["1 FROM<br/>open the table"] --> W["2 WHERE<br/>keep rows that pass"]
    W --> S["3 SELECT<br/>pick the columns"]
    S --> O["4 ORDER BY<br/>sort"]
    O --> L["5 LIMIT<br/>keep the first n"]

So LIMIT 10 keeps the first ten rows after sorting. Without ORDER BY, "the first ten" means whatever order the database happens to read, which is not "the biggest".

Comparisons, AND, OR

Write Means
= and <> (or !=) equal, not equal
< <= > >= less than, at most, more than, at least
a AND b both must be true
a OR b at least one must be true
city IN ('Pune', 'Nagpur') short for city = 'Pune' OR city = 'Nagpur'
amount BETWEEN 1000 AND 2000 short for amount >= 1000 AND amount <= 2000 (both ends included)

AND is worked out before OR, the way multiplication comes before addition in 2 + 3 x 4. So

city = 'Pune' OR city = 'Nagpur' AND amount >= 1000

means "every Pune order, plus Nagpur orders of 1000 or more". If you want both cities to need 1000 or more, add brackets: (city = 'Pune' OR city = 'Nagpur') AND amount >= 1000.

Dates here are text in the form YYYY-MM-DD, and that form sorts in date order, so order_date >= '2026-10-19' AND order_date <= '2026-10-25' selects the week.

Walk through

How to run any SQL on a lesson's data

The course has one helper for this, tools/query.py. From the top folder of the course (the one with README.md and tools/ in it), type:

python3 tools/query.py <lesson-folder> "<SQL>"

It loads every CSV in the lesson's data/ folder into a temporary SQLite database, one table per file (data/orders.csv becomes the table orders), runs your SQL and prints the result as a table with a row count. Empty cells become NULL (lesson 05 explains NULL). Numbers are stored as numbers, so 2400.00 in the file prints as 2400.0. Nothing is saved and the CSV files are never changed. With no SQL it lists the tables and their columns:

python3 tools/query.py phases/00-start-here/04-your-first-sql-query
orders (284 rows): order_id, order_date, city, status, amount

Put the SQL in double quotes and the text values inside it in single quotes. That works in macOS, Linux and Windows PowerShell terminals. Every query below was run this way.

Step 1. Look before you filter

SELECT * FROM orders ORDER BY order_id LIMIT 5
order_id | order_date | city   | status    | amount
---------+------------+--------+-----------+-------
   80001 | 2026-10-12 | Nashik | delivered | 1200.0
   80002 | 2026-10-12 | Nagpur | delivered | 3600.0
   80003 | 2026-10-12 | Nashik | delivered | 461.85
   80004 | 2026-10-12 | Pune   | delivered | 801.51
   80005 | 2026-10-12 | Pune   | delivered | 854.77
(5 rows)

* means every column. Now you know the columns, what a date looks like, and that status holds words such as delivered.

Step 2. Keep last week's delivered orders, biggest first

SELECT order_id, order_date, city, amount
FROM orders
WHERE status = 'delivered'
  AND order_date >= '2026-10-19'
  AND order_date <= '2026-10-25'
ORDER BY amount DESC
LIMIT 12
order_id | order_date | city   | amount
---------+------------+--------+-------
   80246 | 2026-10-24 | Nagpur | 3600.0
   80219 | 2026-10-22 | Pune   | 3000.0
   80162 | 2026-10-19 | Nashik | 2400.0
   80232 | 2026-10-23 | Pune   | 2400.0
   80215 | 2026-10-22 | Nagpur | 1800.0
   80229 | 2026-10-23 | Mumbai | 1800.0
   80264 | 2026-10-25 | Nagpur | 1800.0
   80241 | 2026-10-24 | Pune   | 1500.0
   80167 | 2026-10-20 | Nashik | 1200.0
   80174 | 2026-10-20 | Pune   | 1200.0
   80272 | 2026-10-25 | Pune   | 1200.0
   80253 | 2026-10-24 | Nagpur | 948.62
(12 rows)
  • WHERE has three conditions joined by AND, so a row must pass all three.
  • DESC means descending, biggest first. ASC (the default) is smallest first.
  • LIMIT 12 shows two more than Neha asked for, on purpose, so you can see what is just below the line.

Places 9, 10 and 11 all have 1200.0. LIMIT 10 must cut that tie somewhere, and with ORDER BY amount alone the database is free to cut it differently from one run, or one database, to the next.

Step 3. Break the tie on purpose

SELECT order_id, order_date, city, amount
FROM orders
WHERE status = 'delivered'
  AND order_date >= '2026-10-19'
  AND order_date <= '2026-10-25'
ORDER BY amount DESC, order_id
LIMIT 10
order_id | order_date | city   | amount
---------+------------+--------+-------
   80246 | 2026-10-24 | Nagpur | 3600.0
   80219 | 2026-10-22 | Pune   | 3000.0
   80162 | 2026-10-19 | Nashik | 2400.0
   80232 | 2026-10-23 | Pune   | 2400.0
   80215 | 2026-10-22 | Nagpur | 1800.0
   80229 | 2026-10-23 | Mumbai | 1800.0
   80264 | 2026-10-25 | Nagpur | 1800.0
   80241 | 2026-10-24 | Pune   | 1500.0
   80167 | 2026-10-20 | Nashik | 1200.0
   80174 | 2026-10-20 | Pune   | 1200.0
(10 rows)

ORDER BY amount DESC, order_id sorts by amount, and among equal amounts by order id (smallest first). Now the answer is the same every time. Order 80272 (also 1200.0) is the one left out, and Neha should be told about it.

Step 4. Two cities, done right

Neha asks a follow-up: which of last week's delivered orders of 1000 or more came from Pune or Nagpur?

SELECT order_id, city, amount
FROM orders
WHERE status = 'delivered'
  AND order_date >= '2026-10-19'
  AND order_date <= '2026-10-25'
  AND amount >= 1000
  AND city IN ('Pune', 'Nagpur')
ORDER BY order_id
order_id | city   | amount
---------+--------+-------
   80174 | Pune   | 1200.0
   80215 | Nagpur | 1800.0
   80219 | Pune   | 3000.0
   80232 | Pune   | 2400.0
   80241 | Pune   | 1500.0
   80246 | Nagpur | 3600.0
   80264 | Nagpur | 1800.0
   80272 | Pune   | 1200.0
(8 rows)

Common mistakes

OR without brackets. Write the two cities with OR and no brackets, and count the rows with COUNT(*) (it counts the rows that pass the WHERE; lesson 05 says more):

SELECT COUNT(*) AS orders
FROM orders
WHERE city = 'Pune' OR city = 'Nagpur'
  AND amount >= 1000
  AND status = 'delivered'
  AND order_date >= '2026-10-19'
  AND order_date <= '2026-10-25'
orders
------
    76
(1 row)

76 orders instead of 8. Because AND binds first, this reads "every Pune order of any size, any status, any week, plus big delivered Nagpur orders of last week". The query runs without any error. Only the number tells you.

Forgetting a filter. Drop status = 'delivered' and a cancelled order slips into the top five:

SELECT order_id, city, status, amount
FROM orders
WHERE order_date >= '2026-10-19' AND order_date <= '2026-10-25'
ORDER BY amount DESC, order_id
LIMIT 5
order_id | city   | status    | amount
---------+--------+-----------+-------
   80246 | Nagpur | delivered | 3600.0
   80219 | Pune   | delivered | 3000.0
   80162 | Nashik | delivered | 2400.0
   80232 | Pune   | delivered | 2400.0
   80189 | Nagpur | cancelled | 1800.0
(5 rows)

Neha would call an office about an order it cancelled.

Numbers stored as text. Some tools load every CSV column as text. Text sorts letter by letter, so "9" beats "3" and 948.62 ranks above 3600. You can see the effect by turning the amount into text on purpose with CAST(amount AS TEXT):

SELECT order_id, amount
FROM orders
WHERE status = 'delivered' AND order_date >= '2026-10-19' AND order_date <= '2026-10-25'
ORDER BY CAST(amount AS TEXT) DESC
LIMIT 3
order_id | amount
---------+-------
   80203 |  96.27
   80253 | 948.62
   80169 | 941.28
(3 rows)

A Rs 96.27 order comes first, because the text "96.27" starts with a 9. None of these is in the real top ten. If a "top" list looks odd, check the column's type. tools/query.py and the tests store numbers as numbers, so this does not happen in the course.

LIMIT without ORDER BY. LIMIT 10 on its own returns ten rows in whatever order the table is stored, not the ten biggest. Always sort before you limit.

SQLite and other databases. The SQL in this lesson works almost everywhere. Two differences to know: SQL Server writes SELECT TOP 10 ... instead of LIMIT 10, and Oracle writes FETCH FIRST 10 ROWS ONLY. Most other databases have a real date type: PostgreSQL, MySQL and Oracle accept a date written DATE '2026-10-19', while SQL Server takes the plain text '2026-10-19'. SQLite has no date type and keeps dates as text, which works because of the YYYY-MM-DD form.

Your turn

Open exercise.sql. Write the three queries under their -- query: markers: first_look, top_ten_last_week and big_orders_pune_nagpur. Try each with tools/query.py first, then run, from this folder:

python3 test_lesson.py

The test runs your SQL on the lesson data, on a second dataset with different orders, and on small hand-made tables: an empty table, the first and last day of the week, a cancelled order, and eleven orders with the same amount.

Stretch: write a query for the five smallest delivered orders of last week. Do any have an amount of 0?

Check yourself

  1. In which order does the database run SELECT, FROM, WHERE, ORDER BY and LIMIT?
  2. What does WHERE a OR b AND c mean, and how do you write "(a or b) and c"?
  3. Why add order_id to ORDER BY amount DESC when you only care about amount?
  4. Your "top 10 by revenue" puts 950 above 3,600. What is the likely cause?
Answers
  1. FROM, WHERE, SELECT, ORDER BY, LIMIT.
  2. It means "a, or (b and c)", because AND is done first. Write (a OR b) AND c.
  3. To break ties. Without it, rows with the same amount can come back in any order, so the tenth place can change between runs.
  4. The amount column is stored as text, so it sorts letter by letter. Convert it to a number (in SQL, CAST(amount AS REAL)) or load the data with the right types.

What to tell the stakeholder

"Last week's ten biggest delivered orders range from Rs 1,200 to Rs 3,600: four from Pune, three from Nagpur, two from Nashik and one from Mumbai. One more Pune order, 80272, also came to Rs 1,200 and ties for tenth place. One Nagpur order of Rs 1,800 was cancelled and is not in the list."

In real job postings

SQL is the tool analyst postings name most often. In the Real Analyst Jobs data drop of 25 October 2026, 1,477 of the 2,056 US postings that list a tool ask for SQL (71.8%), and 634 of 900 in India (70.4%). Browse them at SQL data analyst jobs.

Next

05 NULLs, CASE and COALESCE

Practise this skill on real postings

Analyst roles read daily from company career pages. Browsing the board is free.

Browse all analyst jobs

All lessons in Analytics from Scratch

Raj