Your customers table has 40,000 rows. Your manager wants the customers who signed up in Pune during June, newest first, and she wants it before the 10am call. Scrolling a spreadsheet will not find them, and a pivot table cannot really answer it either. This is the exact job SQL was built for, and three keywords do almost all of it: SELECT to choose the columns, WHERE to keep only the rows you want, and ORDER BY to sort what is left.
Part 5 gave you the mental model, that a database keeps data in typed tables, one record per row and one field per column. Now you talk to those tables. A query is a plain request you hand the database: from this table, give me these columns, for the rows that match this rule, in this order. Learn the shape of that one sentence and you can already answer most of the everyday questions a junior analyst gets. Joins and grouping, coming in Parts 7 and 8, both hang off this same frame.
Key takeaways
SELECT names the columns you want, FROM names the table. Listing columns beats SELECT star for anything you will run more than once.
WHERE keeps only the rows that make its condition true. You combine conditions with AND and OR, and you match text, ranges and lists with LIKE, BETWEEN and IN.
ORDER BY sorts the result, ascending by default, and LIMIT caps how many rows come back. A LIMIT without an ORDER BY returns an unpredictable slice.
What a SELECT actually asks for
Every query in this part reads from two small tables. A customers table holds one row per customer, with a customer_id, a name, a city, a signup_date and an is_active flag. An orders table holds one row per order. Here is the customers data the examples run against, so you can check each result by eye.
| customer_id | name | city | signup_date | is_active |
|---|---|---|---|---|
| 1001 | Ana Ruiz | Pune | 2026-06-14 | true |
| 1002 | Sam Lee | Lucknow | 2026-02-03 | true |
| 1003 | Priya Nair | Pune | 2026-06-28 | false |
| 1004 | Diego Alonso | Delhi | 2026-05-19 | true |
| 1005 | Mei Chen | Mumbai | 2026-06-02 | true |
The sample customers table. Five rows is enough to see exactly what each query keeps and drops.
The smallest useful query names the columns you want and the table they live in. SELECT lists the columns, separated by commas, and FROM names the table. Read it as a sentence: select name and city from customers.
Result: two columns, five rows, one row per customer. name and city for Ana, Sam, Priya, Diego and Mei.
Common failure: forgetting the semicolon. Most tools need it to know the statement has ended, and a query that just hangs is often a missing semicolon, not a broken database.
Name your columns, skip the star
You will see SELECT star everywhere, written with an asterisk. It means give me every column. It is fine for a quick look when you are exploring a table you do not know yet, and it is a poor habit for anything you save or schedule. Naming the columns you actually need makes the query say what it means, moves less data across the network, and does not silently break when someone adds a wide new column to the table next quarter. Type the extra words. Future-you, reading the query in six months, will know exactly what it returns.
You can also rename a column in the output with AS, which is called an alias. It does not change the table, only the label on the result, which is handy when a column name is cryptic or when you want a cleaner heading for a report.
Result: the same rows, but the headings now read customer_name and joined_on. The data is untouched, only the labels change.
Remove duplicates with SELECT DISTINCT
Sometimes you do not want every row, you want every distinct value. A quick question like which cities do we have customers in should return each city once, not once per customer. Put DISTINCT right after SELECT and the database collapses repeats for you. It looks at the whole selected row, so DISTINCT city returns each city a single time, while DISTINCT city, is_active returns each unique pairing of the two.
This is the fastest way to audit a column before you trust it. Run DISTINCT on a city column and you often find Pune, pune and Pune with a trailing space all sitting there as separate values, which is exactly the messy-data problem Part 9 will teach you to clean. Seeing the distinct list is how you catch it early.
Result: four rows, Delhi, Lucknow, Mumbai, Pune. Pune appears once even though two customers live there.
Common failure: expecting DISTINCT to dedupe just one column while showing others. It cannot; it dedupes the entire selected row, so adding more columns usually brings the duplicates back.
Keep only the rows you want with WHERE
WHERE is the workhorse. It sits after FROM and holds a condition, a test that is either true or false for each row. The database keeps the rows where the test is true and drops the rest. The condition uses the comparison operators you already know from arithmetic, plus a few words that read like English.
Text values go in single quotes, numbers do not. That single rule trips up more beginners than anything else. A city is text, so it is ‘Pune’ with quotes; an amount is a number, so it is 500 with none. Getting this backwards is the most common error you will hit in your first week.
Result: two rows, Ana Ruiz and Priya Nair, the only customers whose city is Pune.
Common failure: writing WHERE city = Pune without the quotes. The database reads Pune as a column name, cannot find one, and errors. Quote your text.
Real questions need more than one test. AND keeps a row only when both conditions are true; OR keeps it when either is true. Answering the manager from the opening now takes shape: customers in Pune, who signed up in June. June is a date range, so you test the signup_date against the first and last of the month.
Result: Ana Ruiz (2026-06-14) and Priya Nair (2026-06-28). Diego signed up in May and drops out, even though the rest of his row would qualify.
Worked example
Mixing AND and OR without parentheses is a classic trap. WHERE city = ‘Pune’ OR city = ‘Delhi’ AND is_active = true does not mean what it looks like, because AND binds tighter than OR, so the database reads it as Pune, OR (Delhi that is active).
Say what you mean with brackets: WHERE (city = ‘Pune’ OR city = ‘Delhi’) AND is_active = true. Now both cities must also be active. When AND and OR appear together, parenthesise, every time.
Matching text, ranges and lists
Equals is fine when you know the exact value, but often you do not. Four extra operators cover most of what is left, and each reads close to plain English. LIKE matches a text pattern, where the percent sign stands for any run of characters and the underscore stands for exactly one. BETWEEN tests a range and includes both ends. IN checks membership in a list, a tidy replacement for a pile of OR tests. IS NULL checks for a missing value, which is its own thing and not the same as an empty string or a zero.
| Operator | Tests for | Example condition | Matches |
|---|---|---|---|
| = | Exact match | city = ‘Pune’ | Pune only |
| <> | Not equal | city <> ‘Pune’ | every city except Pune |
| LIKE | Text pattern, case sensitive | name LIKE ‘A%’ | names starting with A |
| ILIKE | Pattern, case insensitive | name ILIKE ‘a%’ | A or a to start |
| BETWEEN | Range, ends included | amount BETWEEN 500 AND 2000 | 500 up to 2000 |
| IN | Membership in a list | city IN (‘Pune’,’Delhi’) | either city |
| IS NULL | Missing value | signup_date IS NULL | rows with no date |
The WHERE operators you reach for daily. ILIKE is a PostgreSQL convenience; on other databases you lower-case both sides instead.
IN deserves a moment because it saves so much typing. These two conditions return the same rows, and the second is far easier to read and to extend.
Result: rows for Ana, Priya, Diego and Mei. Only Sam, in Lucknow, drops out.
Here is a detail that clears up a lot of confusion later. You write SELECT first, but the database does not run it first. It starts at FROM to find the table, applies WHERE to throw out rows, then works out SELECT, then ORDER BY, and finally LIMIT. That is why you cannot filter on a column alias you invented in SELECT: at the moment WHERE runs, that alias does not exist yet.
Sort the result with ORDER BY
A result comes back in whatever order the database finds convenient, which is to say no reliable order at all. ORDER BY fixes that. Name a column and the rows sort by it, ascending by default, meaning smallest to largest for numbers, earliest to latest for dates, and A to Z for text. Add DESC for the reverse. The manager wanted newest first, which is signup_date descending.
Result: Priya Nair (2026-06-28) first, then Ana Ruiz (2026-06-14). Same two rows as before, now newest at the top.
You can sort by more than one column. List them in priority order and the second column only breaks ties inside the first. Sorting by city ascending, then signup_date descending, groups the cities alphabetically and, within each city, puts the newest signup on top. Each column can have its own ASC or DESC.
Result order: Delhi (Diego), Lucknow (Sam), Mumbai (Mei), then Pune with Priya above Ana because her signup is later.
Cap the rows with LIMIT
LIMIT caps how many rows come back. It goes last and takes a number. Asking for the ten newest signups is an ORDER BY to get newest first, then a LIMIT to keep the top ten. On a 40,000 row table this is also how you peek at data without dragging every row into your tool.
Result: the three newest signups, Priya (06-28), Ana (06-14), Mei (06-02), in that order.
Common failure: LIMIT with no ORDER BY. The database is then free to hand you any three rows, and it may hand you different ones next time. If LIMIT is meant to pick the top of something, it needs an ORDER BY to define the top.
My take
When a query gets long, I stop thinking about SELECT and start with WHERE. The columns are easy; the rows are where the mistakes and the money are. Write the filter first, run it, eyeball the row count against what you expect, and only then decide which columns to show. Nine times out of ten a wrong number in a report traces back to a WHERE that kept too much or too little, not to the SELECT.
Build the query one clause at a time
The habit that will serve you longest is to build a query in layers rather than all at once. Start with SELECT and FROM and run it to see the raw rows. Add the WHERE and run again to confirm the filter drops what it should. Add ORDER BY, then LIMIT, checking the result at each step. When something looks wrong, you know it was the clause you just added, so you are never staring at a five-line query wondering which part lied to you. This is slower for the first day and much faster for every day after.
You can now answer the opening question in full: SELECT the name and signup_date, FROM customers, WHERE the city is Pune and the signup_date falls in June, ORDER BY signup_date descending. That is a real analyst task, done in four lines, and you understand every one of them. Part 7 takes the next step, pulling columns from two tables at once by joining them on a key, which is how you attach each order to the customer who placed it. If any operator here felt slippery, run it against the five-row sample until the result stops surprising you, and keep the Data Analyst guide open as your map. Reach back to Part 5 if the table and key vocabulary needs a refresher.
This week, open a free SQL sandbox in your browser, type out the five queries above by hand rather than copying them, and change one value in each to see what moves. Typing SQL yourself, wrong then right, is how the syntax stops being something you look up and starts being something you know.
References
- SELECT, PostgreSQL Documentation
- LIMIT and OFFSET, PostgreSQL Documentation
- Pattern Matching, LIKE and ILIKE, PostgreSQL Documentation
- Comparison Functions and Operators, PostgreSQL Documentation


DrJha