You now have every piece of a query but one: deciding how many rows you want to receive. With the 20 rows of products it looks irrelevant, but the day you connect to a production table with fifty million records, a bare SELECT * FROM orders; can bring your terminal down, saturate the network and get a systems administrator looking for you with a face like thunder. LIMIT is the first line of defence. And it's also the basis of pagination, one of those problems that look solved in two minutes and that on large tables become a classic headache. In this lesson you'll learn both: the everyday use and the serious one.
Contents
LIMIT: capping the result- Why
LIMITwithoutORDER BYisn't deterministic OFFSETand classic pagination- The two problems of
OFFSETon large tables - Cursor or keyset pagination
FETCH FIRST n ROWS ONLY: the standard form- Typical use cases
LIMITin the logical execution order- Common Mistakes and Tips
- Exercises
- Conclusion
LIMIT: capping the result
LIMIT: capping the resultLIMIT n states how many rows, at most, you want to receive. It's written at the end of the query, after ORDER BY:
| id | name | price |
|---|---|---|
| 15 | Ceremonial matcha green tea 30 g | 22.00 |
| 6 | Aloe vera face cream 50 ml | 18.90 |
| 20 | Spirulina capsules 120 units | 16.40 |
| 8 | Almond body oil 200 ml | 14.25 |
| 13 | Soy wax candles (pack of 2) | 13.75 |
The five most expensive products in the catalogue. This pattern —sort and trim— is the canonical way of answering any "top N" question.
LIMIT's behaviour in the edge cases:
| How it's written | What it returns |
|---|---|
LIMIT 5 |
At most 5 rows |
LIMIT 100 over products |
The 20 there are. LIMIT is a maximum, not a demand |
LIMIT 0 |
No rows, but the headers yes. Useful for inspecting a query's column types without fully running it |
LIMIT ALL |
All of them. Equivalent to no LIMIT; handy for building queries programmatically |
LIMIT NULL |
All of them, the same as LIMIT ALL |
LIMIT -1 |
ERROR: LIMIT must not be negative |
The most frequent day-to-day use doesn't even carry an ORDER BY: it's taking a look at a table you don't know.
| id | order_id | product_id | quantity | unit_price | discount |
|---|---|---|---|---|---|
| 1 | 1 | 1 | 2 | 11.95 | 0.00 |
| 2 | 1 | 2 | 3 | 3.90 | 0.00 |
| 3 | 1 | 14 | 2 | 3.25 | 0.00 |
| 4 | 2 | 6 | 1 | 17.50 | 0.00 |
| 5 | 2 | 9 | 2 | 4.60 | 0.00 |
Here SELECT * and the absence of ORDER BY are perfectly legitimate: you don't want particular rows, you want to see what the data looks like. It's exploration, not a report.
Professional habit: when you connect to a database you don't know, write
LIMIT 10before writing the rest of the query. It costs eight characters and saves you the embarrassment of locking up a production session.
- Why
LIMIT without ORDER BY isn't deterministic
LIMIT without ORDER BY isn't deterministicWe pick up the warning from 02-05, because with LIMIT it becomes far more dangerous.
| id | name | price |
|---|---|---|
| 1 | Extra virgin olive oil 500 ml | 12.50 |
| 2 | Organic brown rice 1 kg | 3.90 |
| 3 | Raw orange blossom honey 500 g | 9.75 |
What exactly did you ask for? Any three rows. Not the first three by id, not the three cheapest: three arbitrary rows, whichever the engine had closest to hand. That today they turn out to be 1, 2 and 3 is a coincidence of the physical order.
The difference from a query without LIMIT is subtle but crucial:
| Query | Without ORDER BY |
|---|---|
SELECT ... FROM products |
Returns all 20 rows, in an unpredictable order. The set is correct; only the order is uncertain |
SELECT ... FROM products LIMIT 3 |
Returns three unpredictable rows. The set itself is uncertain |
In the first case, sorting the result in your application fixes the problem. In the second there's no possible fix: you've lost 17 rows and you don't know which.
Rule:
LIMITwithoutORDER BYis only acceptable for exploring. As soon as the result is used for anything, theORDER BYis mandatory, and it has to be deterministic (ending in a unique column, as you saw in 02-05).
OFFSET and classic pagination
OFFSET and classic paginationOFFSET m discards the first m rows of the result before applying LIMIT. Combined, they let you walk a large result in chunks:
Let's see it with GreenStore's catalogue sorted by price descending and pages of five products.
Page 1 — LIMIT 5 OFFSET 0:
| id | name | price |
|---|---|---|
| 15 | Ceremonial matcha green tea 30 g | 22.00 |
| 6 | Aloe vera face cream 50 ml | 18.90 |
| 20 | Spirulina capsules 120 units | 16.40 |
| 8 | Almond body oil 200 ml | 14.25 |
| 13 | Soy wax candles (pack of 2) | 13.75 |
Page 2 — LIMIT 5 OFFSET 5:
| id | name | price |
|---|---|---|
| 1 | Extra virgin olive oil 500 ml | 12.50 |
| 10 | Concentrated eco laundry detergent 1 L | 11.20 |
| 12 | Reusable cotton bags (pack of 5) | 9.90 |
| 3 | Raw orange blossom honey 500 g | 9.75 |
| 7 | Rosemary solid shampoo 80 g | 8.40 |
Page 3 — LIMIT 5 OFFSET 10:
| id | name | price |
|---|---|---|
| 19 | Natural stick deodorant 50 g | 7.80 |
| 11 | Loofah scrubber (pack of 3) | 5.50 |
| 17 | Cold-pressed orange juice 1 L | 5.40 |
| 16 | Ginger kombucha 750 ml | 4.95 |
| 9 | Calendula lip balm 15 ml | 4.60 |
And the fourth and last would close with products 2, 18, 14, 4 and 5.
A summary of the arithmetic:
| Page | OFFSET |
LIMIT |
Products returned |
|---|---|---|---|
| 1 | 0 | 5 | 15, 6, 20, 8, 13 |
| 2 | 5 | 5 | 1, 10, 12, 3, 7 |
| 3 | 10 | 5 | 19, 11, 17, 16, 9 |
| 4 | 15 | 5 | 2, 18, 14, 4, 5 |
Important details:
- The
ORDER BYis mandatory. Without it, each page is computed over a different order and you could see the same product on two pages and another on none. - The
ORDER BYmust be deterministic. That's why we've writtenORDER BY price DESC, id: if two products cost the same, the tie-break byidguarantees they always fall on the same page. OFFSETwithoutLIMITis valid:OFFSET 15returns the rows from the 16th onwards.OFFSETlarger than the total returns zero rows, with no error.OFFSET 100overproductsgives nothing.
- The two problems of
OFFSET on large tables
OFFSET on large tablesThis pagination works, it's the one everybody learns, and it has two serious flaws that only show up when the data grows.
4.1. The cost grows with the page number
OFFSET m doesn't skip the first m rows: it computes them and throws them away. The engine has no way of guessing which row is number 500,001 without having produced the previous 500,000.
| Page | Rows the engine produces | Rows it hands you |
|---|---|---|
| 1 | 20 | 20 |
| 50 | 1,000 | 20 |
| 500 | 10,000 | 20 |
| 50,000 | 1,000,000 | 20 |
The last page of a large listing can take hundreds of times longer than the first. It's a known pattern: users complain that "the site is slow at the end of the catalogue", and the cause is right here. With EXPLAIN ANALYZE (module 8) you can see it perfectly clearly in the Limit node, which reports how many rows it discarded.
4.2. The data moves between one page and the next
This one is worse, because it isn't slow: it's wrong.
Imagine a user is looking at page 1 of the catalogue sorted by price descending and, at that very moment, somebody adds a €25 product (more expensive than the matcha). When the user hits "next":
| Moment | What happens |
|---|---|
| Page 1 (before) | 15, 6, 20, 8, 13 |
| A new €25.00 product is inserted | The whole listing shifts one position |
Page 2 (OFFSET 5) |
Position 6 is now held by product 13, which was already on page 1 |
The user sees product 13 twice. And with a deletion the opposite happens: a row disappears without ever having been shown.
| Change in the data | Symptom for the user |
|---|---|
| A row is inserted before the current page | They see a repeated row |
| A row before the current page is deleted | A row gets skipped and they never see it |
Each page is an independent query, run at a different moment and over a different state of the table. OFFSET has no memory of what it already showed you.
When can you live with OFFSET?
| Scenario | Is OFFSET acceptable? |
|---|---|
| An internal dashboard with a few thousand rows | Yes, no problem |
| Data that doesn't change during the session (a historical report) | Yes |
| You need to "go to page 47" directly | Yes: it's the only thing that allows it |
| A public catalogue with millions of rows and constant writes | No |
| Infinite scroll in a mobile app | No |
| Exporting a whole table in chunks | No: use keyset |
- Cursor or keyset pagination
The alternative consists of no longer saying "skip 500,000 rows" and starting to say "give me what comes after this". Instead of a position, you remember the last value seen.
Let's go back to our catalogue. Page 1 ended with product 13, at €13.75. Page 2 is requested like this:
| id | name | price |
|---|---|---|
| 1 | Extra virgin olive oil 500 ml | 12.50 |
| 10 | Concentrated eco laundry detergent 1 L | 11.20 |
| 12 | Reusable cotton bags (pack of 5) | 9.90 |
| 3 | Raw orange blossom honey 500 g | 9.75 |
| 7 | Rosemary solid shampoo 80 g | 8.40 |
Identical to page 2 from section 3, but obtained without discarding anything: the engine goes straight to the rows that satisfy price < 13.75.
What happens if there are ties. If two products cost exactly €13.75, WHERE price < 13.75 would skip the second. The solution is to compare the complete tuple of the sort columns, something SQL allows:
SELECT id, name, price
FROM products
WHERE (price, id) < (13.75, 13)
ORDER BY price DESC, id DESC
LIMIT 5;The comparison (price, id) < (13.75, 13) is lexicographic: it holds if price < 13.75, or if price = 13.75 and additionally id < 13. It's exactly the condition "strictly after the last item seen" for that order. Careful with one detail: for the tuple comparison to line up, every ORDER BY column has to run in the same direction, which is why id DESC appears here.
A comparison of the two approaches:
| Aspect | OFFSET |
Keyset |
|---|---|---|
| Cost of page N | Grows linearly with N | Constant |
| Does it use an index? | Only for sorting | Yes, to position itself directly |
| Rows repeated or skipped if the data changes | Yes | No |
| Can it jump to page 47? | Yes | No: only "next" and "previous" |
| Can it show "page 3 of 120"? | Yes | Not without an extra query |
| Implementation complexity | Trivial | Medium: you have to carry the cursor around |
In practice: if your interface is an infinite scroll or a "load more" button, keyset is always the right answer. If you need clickable page numbers, OFFSET is the only viable option and you have to accept its limits (or cap the number of navigable pages, which is what nearly every search engine does).
For keyset to perform you need an index over the sort columns, in this case (price, id). Without it, PostgreSQL has to sort the whole table anyway and you gain nothing. Indexes are module 8.
FETCH FIRST n ROWS ONLY: the standard form
FETCH FIRST n ROWS ONLY: the standard formLIMIT is convenient, universally known... and it isn't standard SQL. MySQL introduced it, PostgreSQL adopted it and today nearly every engine understands it, but the official SQL:2008 syntax is another one:
| id | name | price |
|---|---|---|
| 15 | Ceremonial matcha green tea 30 g | 22.00 |
| 6 | Aloe vera face cream 50 ml | 18.90 |
| 20 | Spirulina capsules 120 units | 16.40 |
| 8 | Almond body oil 200 ml | 14.25 |
| 13 | Soy wax candles (pack of 2) | 13.75 |
Exactly the same result. PostgreSQL accepts both forms and treats them identically.
PostgreSQL also adds WITH TIES, which widens the result to include every row tied with the last one:
On this data it returns the same 5 rows, because no two products share a price. But if there were three products at €13.75, WITH TIES would return 7 rows instead of 5: it's the correct way to do an honest "top 5" in a competition. WITH TIES requires ORDER BY and doesn't work with LIMIT, only with FETCH.
A comparison table across engines:
| Engine | Main syntax | Alternatives |
|---|---|---|
| SQL:2008 standard | OFFSET m ROWS FETCH FIRST n ROWS ONLY |
— |
| PostgreSQL | LIMIT n OFFSET m |
FETCH FIRST n ROWS ONLY, WITH TIES |
| MySQL / MariaDB | LIMIT n OFFSET m |
LIMIT m, n (the arguments the other way round!) |
| SQLite | LIMIT n OFFSET m |
LIMIT m, n |
| SQL Server 2012+ | OFFSET m ROWS FETCH NEXT n ROWS ONLY |
SELECT TOP (n) ... (no offset) |
| Oracle 12c+ | OFFSET m ROWS FETCH FIRST n ROWS ONLY |
— |
| Oracle 11g and earlier | WHERE ROWNUM <= n over a sorted subquery |
— |
Two traps in that table worth underlining:
- MySQL's
LIMIT m, nreverses the meaning: the first number is the offset and the second the count.LIMIT 5, 10in MySQL isLIMIT 10 OFFSET 5in PostgreSQL. A query copied without reading it returns the wrong data. - Oracle's
ROWNUMis assigned before theORDER BY.WHERE ROWNUM <= 5 ORDER BY price DESCreturns five arbitrary rows and then sorts them: it isn't the top 5. You have to sort in a subquery and applyROWNUMoutside.
Which one should you use? In this course, LIMIT: it's what you'll see in 99 % of PostgreSQL code. If you write SQL that has to run on several engines, OFFSET ... FETCH FIRST ... ROWS ONLY is the safe bet.
- Typical use cases
7.1. Top N
You've already seen it with the most expensive products. Another variant, the five with the highest margin:
SELECT name,
price,
cost,
price - cost AS margin
FROM products
WHERE active
ORDER BY margin DESC, id
LIMIT 5;| name | price | cost | margin |
|---|---|---|---|
| Ceremonial matcha green tea 30 g | 22.00 | 12.50 | 9.50 |
| Aloe vera face cream 50 ml | 18.90 | 9.50 | 9.40 |
| Almond body oil 200 ml | 14.25 | 7.10 | 7.15 |
| Soy wax candles (pack of 2) | 13.75 | 6.90 | 6.85 |
| Reusable cotton bags (pack of 5) | 9.90 | 4.30 | 5.60 |
Notice that the Spirulina capsules (margin €7.70, third in the catalogue) don't appear: WHERE active discards them before sorting. The logical order rules: first you filter, then you sort, and only at the end do you trim.
7.2. The latest records
SELECT id, customer_id, order_date, status, payment_method
FROM orders
ORDER BY order_date DESC, id DESC
LIMIT 3;| id | customer_id | order_date | status | payment_method |
|---|---|---|---|---|
| 20 | 9 | 2026-02-21 | pending | cash_on_delivery |
| 19 | 6 | 2026-02-09 | paid | card |
| 18 | 5 | 2026-01-27 | paid | transfer |
The last three orders received: the natural content of a "recent activity" panel.
7.3. The extreme record
| id | name | last_name | signup_date |
|---|---|---|---|
| 1 | Lucía | Martínez Soler | 2025-01-10 |
GreenStore's first customer. This pattern —ORDER BY ... LIMIT 1— is equivalent to using MIN/MAX, but with an advantage: it gives you the whole row, not just the minimum value. With MIN(signup_date) you'd know the date, but not who it was. Aggregate functions arrive in lesson 04-04.
7.4. A quick sample for exploration
| id | product_id | customer_id | rating | comment | date |
|---|---|---|---|---|---|
| 1 | 1 | 1 | 5 | Excellent oil, intense flavour and beautiful packaging. | 2025-03-15 |
| 2 | 2 | 1 | 4 | Good rice, though it takes a little longer than usual. | 2025-03-16 |
| 3 | 6 | 2 | 5 | The cream leaves the skin very soft. I will buy it again. | 2025-03-25 |
LIMIT in the logical execution order
LIMIT in the logical execution orderWith LIMIT the cycle we started in 02-01 is complete. This is the module's definitive diagram:
flowchart LR
A["1 · FROM<br/>source of the rows"] --> B["2 · WHERE<br/>filters rows"]
B --> C["3 · SELECT<br/>projects and computes<br/>aliases are born here"]
C --> D["3b · DISTINCT<br/>removes duplicates"]
D --> E["4 · ORDER BY<br/>sorts the result"]
E --> F["5 · LIMIT / OFFSET<br/>trims"]
LIMIT is always last. Consequences worth being clear about follow from that:
| Question | Answer |
|---|---|
Does LIMIT 5 make WHERE examine only 5 rows? |
No. WHERE is applied to every row of the table |
Does LIMIT 5 with ORDER BY sort only 5 rows? |
No. Logically everything is sorted and then trimmed |
So LIMIT doesn't save any work? |
It does save work, but in the physical plan, not the logical one |
That last point deserves a nuance. Even though logically everything is sorted, PostgreSQL is smarter than that: when it sees an ORDER BY ... LIMIT n with a small n, it uses an algorithm called a top-N heapsort, which keeps only the best n rows seen so far in memory instead of sorting the entire set. And if an index exists that already returns the rows in the requested order, it can read the first n and stop, without touching the rest of the table.
It's the perfect example of the distinction from 02-01: the logical order defines the meaning of the query, and the physical plan the optimizer picks can be radically more efficient as long as it produces the same result. You'll see it with your own eyes in module 8 with EXPLAIN.
Common Mistakes and Tips
LIMITwithoutORDER BYin any query that isn't exploratory. It returns arbitrary rows and the problem can't be fixed afterwards.- A non-deterministic
ORDER BYwhen paginating. If there are ties and you don't break them with a unique column, the same row can appear on two pages and another on none. - Copying
LIMIT 10, 20from a MySQL example. In MySQL it means "20 rows starting at the 11th"; in PostgreSQL it isn't even valid syntax. - Believing
OFFSETskips rows for free. It produces them and throws them away. On page 5,000 you'll notice. - Paginating with
OFFSETover data that changes. Repeated rows and skipped rows, with no error to give it away. - Applying keyset with an
ORDER BYthat doesn't match theWHEREcondition. The columns must be the same, in the same order and in the same direction. - Using
LIMITto "fix" a slow query. If the query is slow for lack of an index,LIMITcan still be slow: the engine has to find the rows before trimming them. - Expecting
LIMIT 1to replace a well-writtenWHERE. Returning any old row among those that meet the condition is rarely what you want. - Tip:
LIMITfirst, query afterwards. When exploring an unfamiliar database, write it before anything else. - Tip:
LIMIT 0is useful. It gives you the columns and their types without fetching data, ideal for checking the shape of a complex query. - Tip: for an honest "top N", consider
FETCH FIRST n ROWS WITH TIES. Cutting at exactly number 5 when 5 and 6 are tied is misleading.
Exercises
Exercise 1
Customer support needs a panel with the five oldest orders that are neither delivered nor cancelled, showing id, customer_id, order_date, status and shipping_cost. Write the query and explain how many rows it actually returns and why.
Exercise 2
Build the second page of a customer listing sorted by country and, within each country, by surname, with pages of four customers. Write it first with OFFSET and then with keyset pagination. Explain what information the application needs in each case to request the next page.
Exercise 3
The purchasing team wants the three order lines with the highest amount. Write the query using the standard FETCH FIRST syntax, and state what would change if you used WITH TIES. Then answer: why can't this query tell you which product is involved?
Solutions
Solution 1
SELECT id,
customer_id,
order_date,
status,
shipping_cost
FROM orders
WHERE status <> 'delivered'
AND status <> 'cancelled'
ORDER BY order_date, id
LIMIT 5;| id | customer_id | order_date | status | shipping_cost |
|---|---|---|---|---|
| 16 | 4 | 2025-12-19 | shipped | 4.95 |
| 17 | 7 | 2026-01-13 | shipped | 9.90 |
| 18 | 5 | 2026-01-27 | paid | 4.95 |
| 19 | 6 | 2026-02-09 | paid | 4.95 |
| 20 | 9 | 2026-02-21 | pending | 12.50 |
5 rows, which turn out to be all the ones meeting the condition: of the 20 orders, 14 are delivered and 1 cancelled, so only 5 remain in progress. The LIMIT 5 hasn't trimmed anything, and that's normal behaviour: LIMIT is a maximum, not a promise.
The reasoning by the logical order: FROM brings 20 rows → WHERE leaves 5 → SELECT projects → ORDER BY order_date, id sorts them from oldest to most recent → LIMIT 5 discards none. If ten new orders come in tomorrow, the query will still return the five oldest without changing a comma.
In module 4 you'll write that condition more cleanly as WHERE status NOT IN ('delivered', 'cancelled').
Solution 2
With OFFSET, page 2 (customers 5 to 8 of the listing):
SELECT id, name, last_name, city, country
FROM customers
ORDER BY country, last_name, id
LIMIT 4 OFFSET 4;The complete listing sorted by country, last_name starts like this: Dubois (9), Moreau (10), Almeida Nunes (8), Moreira Costa (7) —page 1—, and continues:
| id | name | last_name | city | country |
|---|---|---|---|---|
| 5 | Ana | Belmonte Roca | Barcelona | Spain |
| 13 | Núria | Bosch Ferrer | Barcelona | Spain |
| 15 | Inés | Carrasco Vega | Valencia | Spain |
| 2 | Carlos | Ferrer Ibáñez | Valencia | Spain |
With keyset pagination, starting from the last item of page 1 —Sofia Moreira Costa, country Portugal, surname Moreira Costa, id 7—:
SELECT id, name, last_name, city, country
FROM customers
WHERE (country, last_name, id) > ('Portugal', 'Moreira Costa', 7)
ORDER BY country, last_name, id
LIMIT 4;It returns exactly the same four rows.
What the application needs in each case:
| Approach | What it carries between requests |
|---|---|
OFFSET |
Just a number: the current page. Easy to put in a URL (?page=2) and it allows jumping to any page |
| Keyset | The sort values of the last row shown (here country, last_name and id). They're stored in a "cursor" that the application sends back when requesting the next page |
Notice two details of the keyset: the WHERE's tuple contains exactly the same columns as the ORDER BY and in the same order, and id is included as the last column precisely to break ties between two customers who shared country and surname. Without that id, a repeated surname would cause a customer to be lost between one page and the next.
Solution 3
SELECT id,
order_id,
quantity,
unit_price,
discount,
ROUND(quantity * unit_price * (1 - discount), 2) AS amount
FROM order_lines
ORDER BY quantity * unit_price * (1 - discount) DESC, id
FETCH FIRST 3 ROWS ONLY;| id | order_id | quantity | unit_price | discount | amount |
|---|---|---|---|---|---|
| 28 | 12 | 2 | 22.00 | 0.00 | 44.00 |
| 18 | 8 | 3 | 12.50 | 0.05 | 35.63 |
| 24 | 10 | 2 | 18.90 | 0.10 | 34.02 |
What would change with WITH TIES. Nothing, in this case: the fourth line is worth €26.73, a long way from the third's €34.02, so there's no tie to drag in. But it's worth noting that WITH TIES can't carry an artificial tie-break in the ORDER BY: if you leave the trailing , id, every row has a unique sort value and there will never be ties to include. For WITH TIES to be of any use you have to sort only by the real criterion:
On this data it returns the same 3 rows. If there were two lines tied at €34.02, it would return 4.
Why you can't tell which product is involved. Because order_lines stores product_id, a number, and the product's name lives in the products table. With this module's tools you can only query one table at a time: the most you can say is that line 28 corresponds to product 15. To write "Ceremonial matcha green tea 30 g" in the report you need to combine both tables, and that's exactly what starts in the next lesson.
Conclusion
You've closed the full cycle of a query:
LIMIT ncaps the result at a maximum ofnrows and is the first line of defence when exploring large or unfamiliar tables.LIMITwithoutORDER BYisn't deterministic: it returns arbitrary rows, and that damage can't be repaired afterwards.OFFSET mdiscards the firstmrows and enables the classicLIMIT size OFFSET (N-1)*sizepagination, which demands a deterministicORDER BY.- That pagination has two flaws on large tables: the cost grows with the page number, and insertions or deletions cause rows to repeat or be skipped.
- Cursor or keyset pagination —
WHERE (columns) < (last values seen)— has constant cost and never drifts out of sync, at the price of not being able to jump to an arbitrary page. FETCH FIRST n ROWS ONLYis the standard form, andWITH TIESwidens the result with the tied rows. Each engine has its dialect, and MySQL'sLIMIT m, nreverses the arguments.LIMITis step 5 and the last of the logical order; the optimizer, however, does take advantage of its presence with strategies such as the top-N heapsort.
And with this you close the whole of module 2. You know how to build a query from start to finish: choose its source with FROM, filter rows with WHERE, project and compute columns with SELECT and its aliases, remove repetitions with DISTINCT, impose an order with ORDER BY and trim the result with LIMIT. And you know in what logical order all of that happens, which is what has let you understand why an alias works in one place and not in another.
But notice the ceiling you've hit again and again: orders tells you customer_id = 9 and not "Camille Dubois"; order_lines tells you product_id = 15 and not "Ceremonial matcha green tea"; products tells you category_id = 2 and not "Natural cosmetics". Every one of your queries has looked at a single table, and the questions GreenStore really cares about —what each customer bought, which category bills the most, which sales rep closes the most orders, which products nobody has bought— live precisely in the relationships between tables that you drew in the diagram in 01-06. In module 3, Queries with Multiple Tables, you'll learn to walk those relationships with JOIN: the INNER JOIN for what matches on both sides, the LEFT and RIGHT for keeping what doesn't match —there the three customers with no orders and the three never-sold products will finally show up—, the FULL OUTER for both at once, the SELF JOIN for the reflexive relationships of employees and customers, and UNION, INTERSECT and EXCEPT for combining whole results. GreenStore's nine tables stop being nine islands.
SQL Course
Module 1: Introduction to SQL
- What is SQL?
- Setting up your SQL environment
- Basic SQL syntax
- Understanding databases and tables
- The relational model: primary and foreign keys
- The course database: GreenStore
Module 2: Basic SQL queries
- The SELECT statement
- Aliases, expressions and calculated columns
- Filtering data with WHERE
- DISTINCT and removing duplicates
- Sorting data with ORDER BY
- Limiting results with LIMIT
Module 3: Working with multiple tables
- JOIN operations
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN and CROSS JOIN
- Set operations: UNION, INTERSECT and EXCEPT
Module 4: Advanced data filtering
- Using LIKE for pattern matching
- The IN and BETWEEN operators
- NULL values and IS NULL
- Aggregate functions: COUNT, SUM, AVG, MIN and MAX
- Aggregating data with GROUP BY
- The HAVING clause
Module 5: Data manipulation
- Creating tables and constraints with CREATE TABLE
- The INSERT statement
- The UPDATE statement
- The DELETE statement
- The UPSERT (MERGE) statement
- Changing the schema: ALTER TABLE and safe migrations
Module 6: Advanced SQL functions
- String functions
- Numeric functions
- Date and time functions
- Type conversion and handling NULL: CAST and COALESCE
- Conditional expressions
Module 7: Subqueries and nested queries
- Introduction to subqueries
- Correlated subqueries
- EXISTS and NOT EXISTS
- Using subqueries in SELECT, FROM and WHERE
- Subquery or JOIN: which one to choose
Module 8: Indexes and performance tuning
- Understanding indexes
- Creating and managing indexes
- Index types and when not to index
- Query optimization techniques
- Analyzing query performance
Module 9: Transactions and concurrency
- Introduction to transactions
- ACID properties
- Transaction control statements
- Isolation levels and concurrency anomalies
- Handling concurrency: locks and deadlocks
Module 10: Advanced topics
- Views
- Common table expressions (CTEs)
- Window functions
- Stored procedures
- Triggers
- JSON and semi-structured data
Module 11: SQL in practice
- Real-world use cases
- Best practices
- Security: SQL injection, permissions and roles
- SQL for data analysis
- SQL in web development
