The RIGHT JOIN is the exact mirror of the LEFT JOIN: it keeps every row of the right table, whether or not it matches the left one. Conceptually it adds nothing new —in fact, in this lesson you'll prove that any RIGHT JOIN can be rewritten as a LEFT JOIN— and yet it deserves a lesson of its own for three very practical reasons: you're going to run into it in other people's code, there are specific situations where it appears naturally, and mixing it with a LEFT JOIN in a three-table chain produces queries almost nobody can read.
Along the way, GreenStore's five employees who have never handled an order finally show up.
Contents
- The
RIGHT JOINrule and the set diagram - Every employee with the orders they handled
- The formal equivalence:
A RIGHT JOIN B≡B LEFT JOIN A - Why almost everybody prefers
LEFT - When a
RIGHT JOINdoes feel natural - The anti-join in its
RIGHTversion - The real danger: mixing
LEFTandRIGHTin a chain - Support by engine
- Common Mistakes and Tips
- Exercises
- Conclusion
- The
RIGHT JOIN rule and the set diagram
RIGHT JOIN rule and the set diagramflowchart LR
subgraph R[" "]
direction LR
A(("only on the left<br/>❌ out"))
I(("they match<br/>✅ result"))
B(("only on the right<br/>✅ kept<br/>with NULL on the left"))
end
style A fill:#f8f8f8,stroke:#bbb,stroke-dasharray: 4 4
style I fill:#d9f0d9,stroke:#2b7a2b,stroke-width:3px
style B fill:#d9f0d9,stroke:#2b7a2b,stroke-width:3px
Compare the module's three diagrams so far:
| Type | Orphan on the left | They match | Orphan on the right |
|---|---|---|---|
INNER JOIN |
❌ | ✅ | ❌ |
LEFT JOIN |
✅ | ✅ | ❌ |
RIGHT JOIN |
❌ | ✅ | ✅ |
Everything learned in 03-03 applies just the same, with the sides swapped:
RIGHT JOIN=RIGHT OUTER JOIN; the wordOUTERis optional.- The
NULLs are manufactured by the engine, now in the left table's columns. - Step
1dof the logical order reintroduces the orphan right-hand rows inside theFROM, before theWHERE. - And therefore a condition in the
WHEREover a column of the left table degrades theRIGHT JOINto anINNER JOIN. It's exactly 03-03's trap, reflected.
- Every employee with the orders they handled
The question: "give me the eight employees, with the orders each one has handled". Since we want every employee to appear, the table that must be kept in full is employees. If we write it on the right, the operator is a RIGHT JOIN:
SELECT e.id AS employee_id,
e.name || ' ' || e.last_name AS employee,
e.job_title,
o.id AS order_id,
o.order_date,
o.status
FROM orders AS o
RIGHT JOIN employees AS e ON o.employee_id = e.id
ORDER BY e.id, o.id;| employee_id | employee | job_title | order_id | order_date | status |
|---|---|---|---|---|---|
| 1 | Rosa Alcázar Vives | General manager | (null) | (null) | (null) |
| 2 | Andrés Company Talens | Sales manager | (null) | (null) | (null) |
| 3 | Beatriz Nadal Ripoll | Logistics manager | (null) | (null) | (null) |
| 4 | Óscar Peris Blasco | Sales rep | 2 | 2025-03-12 | delivered |
| 4 | Óscar Peris Blasco | Sales rep | 6 | 2025-05-23 | cancelled |
| 4 | Óscar Peris Blasco | Sales rep | 10 | 2025-08-03 | delivered |
| 4 | Óscar Peris Blasco | Sales rep | 16 | 2025-12-19 | shipped |
| 5 | Laia Puig Sanchis | Sales rep | 4 | 2025-04-19 | delivered |
| 5 | Laia Puig Sanchis | Sales rep | 8 | 2025-06-28 | delivered |
| 5 | Laia Puig Sanchis | Sales rep | 12 | 2025-10-01 | delivered |
| 5 | Laia Puig Sanchis | Sales rep | 18 | 2026-01-27 | paid |
| 6 | Marc Estévez Roig | Customer support | 14 | 2025-11-14 | delivered |
| 6 | Marc Estévez Roig | Customer support | 20 | 2026-02-21 | pending |
| 7 | Irene Salvador Mira | Warehouse operator | (null) | (null) | (null) |
| 8 | Daniel Vercher Lluch | Data analyst | (null) | (null) | (null) |
15 rows: the 10 orders with a sales rep plus 5 employees with no order assigned. The arithmetic is 03-03's, reflected:
And the result reads like a portrait of the org chart:
- Óscar and Laia are the two sales reps: four orders each.
- Marc, from customer support, handled two.
- Irene Salvador Mira (warehouse operator) and Daniel Vercher Lluch (data analyst) have handled none, and that's what you'd expect: their roles don't sell.
- Rosa, Andrés and Beatriz haven't either, for the same reason: management and area managers don't handle orders directly.
An INNER JOIN would have returned 10 rows and three employees. The other five wouldn't have existed for the report, and an "activity by employee" query that leaves out five people out of eight isn't a report: it's a problem.
- The formal equivalence:
A RIGHT JOIN B ≡ B LEFT JOIN A
A RIGHT JOIN B ≡ B LEFT JOIN ALet's write the same question the other way: putting employees on the left and using a LEFT JOIN.
SELECT e.id AS employee_id,
e.name || ' ' || e.last_name AS employee,
e.job_title,
o.id AS order_id,
o.order_date,
o.status
FROM employees AS e
LEFT JOIN orders AS o ON o.employee_id = e.id
ORDER BY e.id, o.id;| employee_id | employee | job_title | order_id | order_date | status |
|---|---|---|---|---|---|
| 1 | Rosa Alcázar Vives | General manager | (null) | (null) | (null) |
| 2 | Andrés Company Talens | Sales manager | (null) | (null) | (null) |
| 3 | Beatriz Nadal Ripoll | Logistics manager | (null) | (null) | (null) |
| 4 | Óscar Peris Blasco | Sales rep | 2 | 2025-03-12 | delivered |
| 4 | Óscar Peris Blasco | Sales rep | 6 | 2025-05-23 | cancelled |
| 4 | Óscar Peris Blasco | Sales rep | 10 | 2025-08-03 | delivered |
| 4 | Óscar Peris Blasco | Sales rep | 16 | 2025-12-19 | shipped |
| 5 | Laia Puig Sanchis | Sales rep | 4 | 2025-04-19 | delivered |
| 5 | Laia Puig Sanchis | Sales rep | 8 | 2025-06-28 | delivered |
| 5 | Laia Puig Sanchis | Sales rep | 12 | 2025-10-01 | delivered |
| 5 | Laia Puig Sanchis | Sales rep | 18 | 2026-01-27 | paid |
| 6 | Marc Estévez Roig | Customer support | 14 | 2025-11-14 | delivered |
| 6 | Marc Estévez Roig | Customer support | 20 | 2026-02-21 | pending |
| 7 | Irene Salvador Mira | Warehouse operator | (null) | (null) | (null) |
| 8 | Daniel Vercher Lluch | Data analyst | (null) | (null) | (null) |
An identical result, row by row. It doesn't resemble it: it is it.
The general rule, which you can apply mechanically:
A RIGHT JOIN B ON <cond>≡B LEFT JOIN A ON <cond>To turn a
RIGHT JOINinto aLEFT JOIN: swap the order of the two tables and change the word. TheONcondition stays exactly as it is.
flowchart LR
A["FROM orders<br/>RIGHT JOIN employees<br/>ON o.employee_id = e.id"] -->|"swap the tables<br/>+ change the word"| B["FROM employees<br/>LEFT JOIN orders<br/>ON o.employee_id = e.id"]
The only things that can change between the two versions are the column order if you were using SELECT *, and the row order if there were no ORDER BY. The data set is the same.
An honest conclusion comes out of this: the RIGHT JOIN is dispensable. There's no query that can only be written with a RIGHT JOIN. And yet you have to know it, because it exists in the code you'll inherit.
- Why almost everybody prefers
LEFT
LEFTIf they're equivalent, why do practically all professional style guides recommend LEFT?
| Reason | Explanation |
|---|---|
| It reads in the order you think | "Every employee, with their orders" starts with employees. In the RIGHT version, the protagonist table is buried in the last line |
| The main table comes first | The reader identifies the result's granularity at a glance: one row per... whatever's in the FROM |
| Consistency in long chains | With five tables, if every outer join is a LEFT, the FROM reads top to bottom like a path. If you alternate, you have to go back and forth |
| Less mental load when debugging | When something breaks, finding "which table am I keeping in full" is immediate if it's always the first one |
| It's what the team expects | The LEFT JOIN is overwhelmingly dominant in practice. A RIGHT JOIN makes whoever reviews the code stop to check whether it's intentional |
Course convention: we'll write
LEFT JOINwhenever we can choose, and we'll put as theFROM's first table the one whose rows must all appear. TheRIGHT JOINappears in this course so that you can read it and rewrite it, not as recommended style.
- When a
RIGHT JOIN does feel natural
RIGHT JOIN does feel naturalThat said, there are two situations where writing a RIGHT JOIN is reasonable.
5.1. Adding a table to the end of an already written query
Imagine you have this query running in production, with its twelve-column SELECT and its filters:
SELECT ol.id, ol.quantity, ol.unit_price, ...
FROM order_lines AS ol
JOIN orders AS o ON ol.order_id = o.id
WHERE o.order_date >= DATE '2025-01-01'
AND o.order_date < DATE '2026-01-01';And you're asked to make the report include the products that weren't sold too. The "correct" rewrite forces you to put products as the first table and reorder the whole FROM. Adding a line at the end is far less invasive:
It's a legitimate use, with one condition: document it. A one-line comment (-- RIGHT to keep the products with no sales) stops the next reader thinking it's a typo.
5.2. Translating a requirement mechanically
When the requirement arrives worded as "the order lines, and on top of that every product even if it has no lines", writing it in that same order is the most faithful thing to do:
SELECT p.id AS product_id,
p.name AS product,
ol.id AS line_id,
ol.quantity
FROM order_lines AS ol
RIGHT JOIN products AS p ON ol.product_id = p.id
ORDER BY p.id, ol.id;It returns 50 rows: the 47 lines plus the 3 never-sold products. It's exactly the same result as 03-03's products LEFT JOIN order_lines.
Even so, the usual practice is to translate the requirement first and rewrite it afterwards as a LEFT before pushing it to the repository.
- The anti-join in its
RIGHT version
RIGHT versionThe pattern from 03-03 works the same, changing which side you check. To answer "which employees have never handled an order?":
SELECT e.id AS employee_id,
e.name || ' ' || e.last_name AS employee,
e.job_title,
e.hire_date
FROM orders AS o
RIGHT JOIN employees AS e ON o.employee_id = e.id
WHERE o.id IS NULL
ORDER BY e.id;| employee_id | employee | job_title | hire_date |
|---|---|---|---|
| 1 | Rosa Alcázar Vives | General manager | 2024-09-01 |
| 2 | Andrés Company Talens | Sales manager | 2024-10-15 |
| 3 | Beatriz Nadal Ripoll | Logistics manager | 2024-11-02 |
| 7 | Irene Salvador Mira | Warehouse operator | 2025-04-07 |
| 8 | Daniel Vercher Lluch | Data analyst | 2025-06-16 |
5 rows. And the same query written as a LEFT, which is how we'd write it in the course:
-- ✅ Preferred
SELECT e.id AS employee_id,
e.name || ' ' || e.last_name AS employee,
e.job_title,
e.hire_date
FROM employees AS e
LEFT JOIN orders AS o ON o.employee_id = e.id
WHERE o.id IS NULL
ORDER BY e.id;An identical result. Notice the detail that stays the same across both versions: the column checked with IS NULL is always the primary key of the optional table (o.id), not that of the side being kept. The word LEFT or RIGHT changes; the anti-join's logic doesn't.
Reading the result: five of the eight employees having no orders isn't a data problem. Only two sales reps and one customer support person handle sales; management, logistics and data analysis don't. It's a query that confirms the business model rather than contradicting it.
- The real danger: mixing
LEFT and RIGHT in a chain
LEFT and RIGHT in a chainHere's the serious reason to have style discipline. Consider this query, written by someone who wanted "every customer, with their orders and the employee of each order":
-- ⚠️ INCORRECT: it doesn't do what its author thinks
SELECT c.name AS customer,
o.id AS order_id,
e.name AS employee
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
RIGHT JOIN employees AS e ON o.employee_id = e.id
ORDER BY e.id, o.id;| customer | order_id | employee |
|---|---|---|
| (null) | (null) | Rosa |
| (null) | (null) | Andrés |
| (null) | (null) | Beatriz |
| Carlos | 2 | Óscar |
| Ana | 6 | Óscar |
| Camille | 10 | Óscar |
| Javier | 16 | Óscar |
| Javier | 4 | Laia |
| Sofia | 8 | Laia |
| Julien | 12 | Laia |
| Ana | 18 | Laia |
| Diego | 14 | Marc |
| Camille | 20 | Marc |
| (null) | (null) | Irene |
| (null) | (null) | Daniel |
15 rows, and not a trace of Núria, Hugo or Inés. The LEFT JOIN written to keep them has been of no use whatsoever.
Why
JOINs are resolved left to right:
flowchart TD
A["customers (15)"] --> B["customers LEFT JOIN orders<br/>= 23 rows<br/>(includes Núria, Hugo, Inés)"]
B --> C["that 23-row result<br/>RIGHT JOIN employees"]
C --> D["RIGHT keeps EVERY employee<br/>but discards the left rows<br/>with no matching employee"]
D --> E["15 rows:<br/>10 pairings + 5 lone employees<br/>❌ Núria, Hugo and Inés disappear"]
The RIGHT JOIN keeps the right table (employees) in full and discards the left rows that don't match. And Núria's, Hugo's and Inés's rows have o.employee_id = NULL, so they match no employee and off they go. Just like the 10 web-order rows.
It's the same logic as 03-03's "LEFT followed by INNER", but in disguise: here the RIGHT acts on the accumulation of everything before it, not on the table sitting next to it.
The rewrite
The query above actually answers "every employee with the orders they handled and their customers". Written with LEFT alone it becomes transparent:
-- ✅ CORRECT: the same thing, readable
SELECT c.name AS customer,
o.id AS order_id,
e.name AS employee
FROM employees AS e
LEFT JOIN orders AS o ON o.employee_id = e.id
LEFT JOIN customers AS c ON o.customer_id = c.id
ORDER BY e.id, o.id;The same 15 rows. Now the FROM reads top to bottom: employees → their orders → the customer of each order, and the first table announces unambiguously that the report has at least one row per employee.
And if what the author really wanted was to keep the three customers with no orders, the query is a different one:
-- ✅ CORRECT for "every customer"
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
LEFT JOIN employees AS e ON o.employee_id = e.idWhich returns 23 rows, as you saw in 03-03.
Golden rule: in a chain of three or more tables, don't mix
LEFTandRIGHT. Choose the side you want to keep, put it as theFROM's first table and use onlyLEFT JOINfrom there on. A query withLEFTandRIGHTalternating is correct for the engine and unreadable for people, which is the worst possible combination.
A comparison of the three versions over the same three tables:
| Query | Rows | What it keeps |
|---|---|---|
customers LEFT orders RIGHT employees |
15 | Every employee. The customers with no order are lost |
employees LEFT orders LEFT customers |
15 | Every employee (identical to the previous one, readable) |
customers LEFT orders LEFT employees |
23 | Every customer, including the three with no orders |
- Support by engine
| Engine | RIGHT JOIN |
Note |
|---|---|---|
| PostgreSQL | ✅ Always | No restrictions |
| MySQL / MariaDB | ✅ | No restrictions |
| SQL Server | ✅ | The old *= syntax has been removed since 2012 |
| Oracle | ✅ | The old (+) syntax still exists but is discouraged |
| SQLite | ✅ only since 3.39 (June 2022) | Before that you had to rewrite it as a LEFT JOIN |
That last case is an extra argument in favour of the LEFT JOIN: a LEFT JOIN works on any engine and any version, including the SQLite versions embedded in old mobile apps. Always writing LEFT is a portability decision too.
Dialect note: in old Oracle,
A RIGHT JOIN Bwas written by putting the(+)on the opposite side to the one being kept:WHERE a.id(+) = b.id. That notation is an inexhaustible source of mistakes and shouldn't be used in new code, although you'll find it in legacy systems.
Common Mistakes and Tips
- Confusing which side is kept. In
A RIGHT JOIN B, what's kept in full is B, the table that comes after the word. It's the opposite of what left-to-right reading suggests. - Mixing
LEFTandRIGHTin the same chain. TheRIGHTacts on everything accumulated up to that point, not on the adjacent table, and it can silently cancel a previousLEFT. - Putting a condition on the left table in a
RIGHT JOIN'sWHERE. It degrades it to anINNER JOIN, exactly as in 03-03 but with the sides swapped. - Doing the anti-join against the wrong column. In a
RIGHT JOIN, the column checked withIS NULLis the primary key of the left table (the optional one), not that of the one being kept. - Using a
RIGHT JOINwithout commenting on why. Whoever reviews the code will assume it's an oversight. A one-line comment sorts it out. - Writing
RIGHT JOINin code that has to run on old SQLite. It doesn't exist before version 3.39. - Tip: learn the mechanical conversion. Swap the two tables, change
RIGHTforLEFT, leave theONuntouched. It always works and it's the fastest way to understand somebody else'sRIGHT JOIN. - Tip: when reviewing code, mentally rewrite every
RIGHTas aLEFTbefore reasoning about it. It's quicker than trying to follow the inverted logic. - Tip: decide the protagonist side before writing a single line. "Which noun has to come out complete in the report?" That table goes first, and everything else is a
LEFT JOIN.
Exercises
Exercise 1
Write with a RIGHT JOIN the query that returns every product with the sales lines it may have had: the product's id and name, the line's id and quantity. Then rewrite it with a LEFT JOIN and check that the result is identical.
How many rows does it return and where does that number come from?
Exercise 2
HR wants the listing of employees who have never handled an order, with their job title, their city and the first name of their manager.
- Write it with a
RIGHT JOINfor the orders part. - Rewrite it entirely with
LEFT JOINs. - Explain why the
JOINwith the managers table must be aLEFTand not anINNER.
(Hint: joining employees to itself to get the manager is a SELF JOIN; it's explained fully in 03-06, but here it's enough to use two different aliases for the same table.)
Exercise 3
Analyse this query without running it:
SELECT p.name AS product, ol.id AS line_id, cat.name AS category
FROM order_lines AS ol
RIGHT JOIN products AS p ON ol.product_id = p.id
LEFT JOIN categories AS cat ON p.category_id = cat.id
WHERE ol.quantity > 2;- What do you think the author's intention was?
- What does it actually do? Do the never-sold products appear?
- Fix it so that it returns every product, showing only the lines of more than 2 units and
NULLfor the rest.
Solutions
Solution 1
With a RIGHT JOIN:
SELECT p.id AS product_id,
p.name AS product,
ol.id AS line_id,
ol.quantity
FROM order_lines AS ol
RIGHT JOIN products AS p ON ol.product_id = p.id
ORDER BY p.id, ol.id;With a LEFT JOIN (the preferred form):
SELECT p.id AS product_id,
p.name AS product,
ol.id AS line_id,
ol.quantity
FROM products AS p
LEFT JOIN order_lines AS ol ON ol.product_id = p.id
ORDER BY p.id, ol.id;Both return 50 rows:
The three orphan rows are:
| product_id | product | line_id | quantity |
|---|---|---|---|
| 13 | Soy wax candles (pack of 2) | (null) | (null) |
| 19 | Natural stick deodorant 50 g | (null) | (null) |
| 20 | Spirulina capsules 120 units | (null) | (null) |
The transformation applied is section 3's mechanical one: order_lines and products are swapped, RIGHT becomes LEFT, and the condition ON ol.product_id = p.id isn't touched.
Solution 2
1. With a RIGHT JOIN on the orders part:
SELECT e.id AS employee_id,
e.name || ' ' || e.last_name AS employee,
e.job_title,
e.city,
manager.name AS manager
FROM orders AS o
RIGHT JOIN employees AS e ON o.employee_id = e.id
LEFT JOIN employees AS manager ON e.manager_id = manager.id
WHERE o.id IS NULL
ORDER BY e.id;2. Rewritten with LEFT only:
-- ✅ Preferred
SELECT e.id AS employee_id,
e.name || ' ' || e.last_name AS employee,
e.job_title,
e.city,
manager.name AS manager
FROM employees AS e
LEFT JOIN orders AS o ON o.employee_id = e.id
LEFT JOIN employees AS manager ON e.manager_id = manager.id
WHERE o.id IS NULL
ORDER BY e.id;| employee_id | employee | job_title | city | manager |
|---|---|---|---|---|
| 1 | Rosa Alcázar Vives | General manager | Valencia | (null) |
| 2 | Andrés Company Talens | Sales manager | Valencia | Rosa |
| 3 | Beatriz Nadal Ripoll | Logistics manager | Valencia | Rosa |
| 7 | Irene Salvador Mira | Warehouse operator | Valencia | Beatriz |
| 8 | Daniel Vercher Lluch | Data analyst | Valencia | Rosa |
5 rows.
3. Why the JOIN with the managers must be a LEFT: because Rosa Alcázar Vives has no manager (manager_id IS NULL, she's the general manager). With an INNER JOIN against employees AS manager, her row would find no partner and would disappear from the result, which would drop from 5 to 4 rows. It's exactly 03-03's "LEFT followed by INNER" case: a single INNER in the chain removes the very row that matters most. The complete employees hierarchy is studied in 03-06.
Solution 3
1. The intention was, in all likelihood, "every product with its lines of more than 2 units", keeping the whole catalogue thanks to the RIGHT JOIN.
2. What it actually does. The condition ol.quantity > 2 is in the WHERE and affects a column of the left table, which is precisely the optional one in a RIGHT JOIN. The three never-sold products reach the WHERE with ol.quantity = NULL, and NULL > 2 isn't TRUE. They disappear. The RIGHT JOIN degrades to an INNER JOIN and the query returns only the lines of more than 2 units with their product: no never-sold product appears at all.
It's 03-03's trap seen in the mirror: there the dangerous condition was on the right table of a LEFT; here, on the left of a RIGHT.
3. The fix. You have to move the condition into the ON and, while you're at it, rewrite everything with LEFT putting products as the protagonist table:
-- ✅ CORRECT
SELECT p.name AS product,
cat.name AS category,
ol.id AS line_id,
ol.quantity
FROM products AS p
LEFT JOIN categories AS cat ON p.category_id = cat.id
LEFT JOIN order_lines AS ol
ON ol.product_id = p.id
AND ol.quantity > 2
ORDER BY p.id, ol.id;Now all 20 products always appear; the ones with no line of more than 2 units show *(null)* in line_id and quantity. Notice as well that the JOIN with categories has been written as a LEFT out of caution and stylistic consistency: it changes nothing here because every product has a category, but it upholds the rule "once you've opened a branch with LEFT, carry on with LEFT".
Conclusion
The RIGHT JOIN won't surprise you in anybody's code any more:
- It keeps every row of the right table, the one that comes after the word
JOIN, padding the left-hand columns withNULL.RIGHT JOIN=RIGHT OUTER JOIN. - You've finally met the five employees with no orders: Rosa, Andrés, Beatriz, Irene and Daniel. 15 rows against the
INNER JOIN's 10. - You know the formal equivalence:
A RIGHT JOIN B≡B LEFT JOIN A. Swap the tables, change the word, leave theONuntouched. No query needs aRIGHT JOIN. - You know why
LEFTis preferred: the protagonist table reads first, long chains read top to bottom, and it works on every engine and version (SQLite doesn't supportRIGHTuntil 3.39). - You know when a
RIGHTis defensible: when adding a table to the end of an already written query or when translating a requirement literally. Always with a comment justifying it. - The anti-join in its
RIGHTversion works the same, checking the left table's primary key withIS NULL. - And above all: don't mix
LEFTandRIGHTin a chain. TheRIGHTacts on everything accumulated and can silently cancel a previousLEFT, as in the example where Núria, Hugo and Inés disappeared despite theLEFT JOINwritten to keep them.
In the next lesson, FULL OUTER JOIN, we'll close the outer-join family with the one that keeps the orphans on both sides at once. You'll see why in a schema with referential integrity —like GreenStore's— there are almost never orphans on both sides, and why its real playing field is the reconciliation of two independent data sources: a catalogue against an external sales file, an inventory against an accounting system.
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
