Understanding how SQL processes your queries step by step
SQL doesn't execute in the order you write it! Here's the actual sequence:
Let's see how SQL processes this query:
SQL starts with the entire employees table:
| id | name | department | salary |
|---|---|---|---|
| 1 | Alice | Sales | 5000 |
| 2 | Bob | Engineering | 6000 |
| 3 | Charlie | Sales | 5500 |
| 4 | Diana | Marketing | 4800 |
| 5 | Eve | Sales | 4500 |
SQL filters to keep only Sales department rows:
| id | name | department | salary |
|---|---|---|---|
| 1 | Alice | Sales | 5000 |
| 2 | Bob | Engineering | 6000 |
| 3 | Charlie | Sales | 5500 |
| 4 | Diana | Marketing | 4800 |
| 5 | Eve | Sales | 4500 |
✓ Highlighted rows pass the filter | ✗ Strikethrough rows are excluded
SQL selects only the requested columns and calculates annual_salary:
| name | annual_salary |
|---|---|
| Alice | 60000 |
| Charlie | 66000 |
| Eve | 54000 |
SQL sorts the results in descending order by annual_salary:
| name | annual_salary |
|---|---|
| Charlie | 66000 |
| Alice | 60000 |
| Eve | 54000 |
🎉 Final Result: Sorted from highest to lowest salary!
Let's explore how GROUP BY and HAVING fit into the execution order:
SQL starts with the entire employees table:
| id | name | department | salary |
|---|---|---|---|
| 1 | Alice | Sales | 5000 |
| 2 | Bob | Engineering | 6000 |
| 3 | Charlie | Sales | 5500 |
| 4 | Diana | Marketing | 3500 |
| 5 | Eve | Sales | 4500 |
| 6 | Frank | Engineering | 6500 |
| 7 | Grace | Engineering | 5800 |
SQL filters rows based on individual row conditions (before grouping):
| id | name | department | salary |
|---|---|---|---|
| 1 | Alice | Sales | 5000 |
| 2 | Bob | Engineering | 6000 |
| 3 | Charlie | Sales | 5500 |
| 4 | Diana | Marketing | 3500 |
| 5 | Eve | Sales | 4500 |
| 6 | Frank | Engineering | 6500 |
| 7 | Grace | Engineering | 5800 |
✓ Diana is excluded (salary 3500 ≤ 4000)
SQL groups rows by department. Each group becomes one row in the result:
| name | salary |
|---|---|
| Alice | 5000 |
| Charlie | 5500 |
| Eve | 4500 |
Count: 3 | Avg: 5000
| name | salary |
|---|---|
| Bob | 6000 |
| Frank | 6500 |
| Grace | 5800 |
Count: 3 | Avg: 6100
💡 Marketing group was excluded earlier by WHERE clause, so it doesn't appear here.
SQL filters the groups based on aggregate conditions:
| department | emp_count | avg_salary |
|---|---|---|
| Sales | 3 | 5000 |
| Engineering | 3 | 6100 |
✓ Both groups have 3 employees (>= 2), so both pass the HAVING filter
SQL selects the specified columns with aggregations and aliases:
| department | emp_count | avg_salary |
|---|---|---|
| Sales | 3 | 5000 |
| Engineering | 3 | 6100 |
SQL sorts the results by average salary in descending order:
| department | emp_count | avg_salary |
|---|---|---|
| Engineering | 3 | 6100 |
| Sales | 3 | 5000 |
🎉 Final Result: Engineering department has the highest average salary!
Timing: Executes before GROUP BY
Purpose: Filters individual rows
Works with: Column values
Cannot use: Aggregate functions
Timing: Executes after GROUP BY
Purpose: Filters grouped results
Works with: Aggregate functions
Can use: COUNT, SUM, AVG, etc.
The order you write SQL clauses: SELECT → FROM → WHERE → ORDER BY
The order SQL actually processes: FROM → WHERE → SELECT → ORDER BY
Understanding execution order helps you debug queries and use aliases correctly.
You can use SELECT aliases in ORDER BY because SELECT runs first!
This fails because WHERE executes before SELECT, so the alias doesn't exist yet!
In WHERE, you must use the original expression, not the alias.
ORDER BY executes after SELECT, so you can use the alias here!
When your query includes GROUP BY, HAVING, and LIMIT, here's the full execution sequence: