After learning to query and filter a single table, this chapter adds two tools: GROUP BY, which summarises rows in groups using aggregate functions (with HAVING to filter the groups), and joins, which combine data from two or more tables, such as the Equi-Join and the Natural Join.
This chapter has two main sections โ GROUP BY (summarising groups of rows) and Joins (combining data from multiple tables). Read the Board Exam Tips with each section, and pay particular attention to Section 12.3.2 (HAVING clause) and Section 12.4.3 (Equi-Join) during revision.
12.1 ๐ Introduction
In Chapter 11 we learnt to query a single table. But real-world databases almost always need answers like:
- "How many employees are in each department?"
- "What is the average salary per city?"
- "Show me each employee's name along with their department name from a separate table."
These questions need two new powerful tools:
graph LR
G["๐ GROUP BY\nSummarise rows\ninto groups"]
J["๐ Joins\nCombine data from\nmultiple tables"]
POWER["๐ช Real-World\nSQL Power!"]
G --> POWER
J --> POWER
style G fill:#2196F3,color:#fff
style J fill:#9C27B0,color:#fff
style POWER fill:#4CAF50,color:#fff
Think of it this way: Chapter 11 was like reading individual student report cards. Chapter 13 lets you summarise the whole class (GROUP BY) and combine the report card with the attendance register (Joins)!
12.2 โ๏ธ Types of SQL Functions
Before diving into GROUP BY, it helps to understand the two broad categories of SQL functions:
Single-Row Functions
Operate on ONE row at a time and return one result per row
UPPER()ROUND()YEAR()LENGTH() โ we covered these in Ch 11Multi-Row (Aggregate) Functions
Operate on a GROUP of rows and return ONE summary result
COUNT()SUM()AVG()MAX()MIN() โ the stars of this chapter!graph TD
F["SQL FUNCTIONS"]
F1["๐ข Single-Row Functions\nOne input row โ One output row\nOutput has same number of rows as input"]
F2["๐ Aggregate Functions\nMany input rows โ One output value\nCollapses many rows into a summary"]
F --> F1
F --> F2
F1 --> EX1["UPPER(Name)\nUPPER('priya') โ 'PRIYA'\nfor EVERY row"]
F2 --> EX2["AVG(Salary)\nAll salaries โ one average\nfor the WHOLE group"]
style F fill:#FF9800,color:#fff
style F1 fill:#2196F3,color:#fff
style F2 fill:#9C27B0,color:#fff
Aggregate Functions โ Quick Reference:
| Function | Purpose | Ignores NULL? |
|---|---|---|
COUNT(*) |
Count all rows | No (counts everything) |
COUNT(col) |
Count non-NULL values | Yes |
SUM(col) |
Total of all values | Yes |
AVG(col) |
Average of all values | Yes |
MAX(col) |
Largest value | Yes |
MIN(col) |
Smallest value | Yes |
"What is the difference between single-row and aggregate functions?" โ 2-mark question. Answer: Single-row functions process one row at a time and return one result per row (same number of output rows as input rows). Aggregate functions process a group of rows and return a single summary value for the entire group (e.g., SUM, AVG, COUNT).
12.3 ๐ Grouping Result โ GROUP BY
Without GROUP BY, aggregate functions summarise the entire table into one row. With GROUP BY, they summarise each group separately โ giving you one result row per group!
The Problem Without GROUP BY:
-- without_group_by.sql
SELECT COUNT(*) FROM Employee;
-- Output: 6 (total employees โ just ONE number, not very useful!)
The Solution With GROUP BY:
-- with_group_by.sql
SELECT Dept, COUNT(*) AS TotalEmployees
FROM Employee
GROUP BY Dept;
Output:
| Dept | TotalEmployees |
|---|---|
| Sales | 2 |
| IT | 2 |
| HR | 2 |
graph TD
ALL["๐ All 6 Employee Rows"]
GRP["GROUP BY Dept"]
G1["๐ข Sales Group\nPriya, Anjali\nCOUNT = 2"]
G2["๐ป IT Group\nArjun, Rohan\nCOUNT = 2"]
G3["๐ฅ HR Group\nSita, Vikram\nCOUNT = 2"]
ALL --> GRP
GRP --> G1
GRP --> G2
GRP --> G3
style GRP fill:#FF9800,color:#fff
style G1 fill:#4CAF50,color:#fff
style G2 fill:#2196F3,color:#fff
style G3 fill:#9C27B0,color:#fff
Our Sample Employee Table (used throughout this chapter):
| EmpId | Name | Dept | Salary | DOJ | City |
|---|---|---|---|---|---|
| 101 | Priya Sharma | Sales | 45000 | 2022-03-15 | Delhi |
| 102 | Arjun Kumar | IT | 62000 | 2021-07-01 | Mumbai |
| 103 | Sita Roy | HR | 38000 | 2023-01-20 | Delhi |
| 104 | Rohan Das | IT | 71000 | 2020-11-05 | Bangalore |
| 105 | Anjali Gupta | Sales | NULL | 2023-06-10 | Mumbai |
| 106 | Vikram Singh | HR | 41000 | 2022-09-25 | Delhi |
More GROUP BY Examples:
-- group_by_examples.sql
-- Average salary per department:
SELECT Dept, AVG(Salary) AS AvgSalary
FROM Employee
GROUP BY Dept;
| Dept | AvgSalary |
|---|---|
| Sales | 45000.00 |
| IT | 66500.00 |
| HR | 39500.00 |
Notice: AVG(Salary) for Sales is 45000, not NULL-affected โ because AVG ignores NULL. Only Priya's 45000 counts for Sales (Anjali is NULL).
-- Count employees per city:
SELECT City, COUNT(*) AS EmpCount
FROM Employee
GROUP BY City;
| City | EmpCount |
|---|---|
| Delhi | 3 |
| Mumbai | 2 |
| Bangalore | 1 |
General GROUP BY Syntax:
SELECT column, aggregate_function(column)
FROM table_name
[WHERE condition]
GROUP BY column
[HAVING condition]
[ORDER BY column];
Any column in the SELECT list that is NOT inside an aggregate function MUST appear in the GROUP BY clause. Writing SELECT Dept, Name, COUNT(*) FROM Employee GROUP BY Dept; is an error โ Name is not aggregated and not in GROUP BY. Either add Name to GROUP BY or remove it from SELECT!
"Write a query to display the number of employees in each department." โ 2-mark question.
Answer: SELECT Dept, COUNT(*) AS TotalEmployees FROM Employee GROUP BY Dept;
12.3.1 Nested Groups โ Grouping on Multiple Columns ๐ช
You can GROUP BY more than one column to create finer sub-groups โ like grouping first by Department, then by City within each department.
Real-World Need: "How many employees are in each city, within each department?"
-- nested_groups.sql
SELECT Dept, City, COUNT(*) AS EmpCount
FROM Employee
GROUP BY Dept, City;
Output:
| Dept | City | EmpCount |
|---|---|---|
| Sales | Delhi | 1 |
| Sales | Mumbai | 1 |
| IT | Mumbai | 1 |
| IT | Bangalore | 1 |
| HR | Delhi | 2 |
graph TD
ALL["๐ All 6 Employees"]
D1["๐ผ Sales Dept"]
D2["๐ป IT Dept"]
D3["๐ฅ HR Dept"]
S1["Delhi โ 1 emp\n(Priya)"]
S2["Mumbai โ 1 emp\n(Anjali)"]
I1["Mumbai โ 1 emp\n(Arjun)"]
I2["Bangalore โ 1 emp\n(Rohan)"]
H1["Delhi โ 2 emps\n(Sita, Vikram)"]
ALL --> D1
ALL --> D2
ALL --> D3
D1 --> S1
D1 --> S2
D2 --> I1
D2 --> I2
D3 --> H1
style D1 fill:#4CAF50,color:#fff
style D2 fill:#2196F3,color:#fff
style D3 fill:#9C27B0,color:#fff
Another Example โ Total salary per department per city:
-- nested_group_salary.sql
SELECT Dept, City, SUM(Salary) AS TotalSalary
FROM Employee
GROUP BY Dept, City;
GROUP BY Dept, City creates groups of (Dept, City) combinations. The order in GROUP BY determines how rows are sub-divided โ first by Dept, then within each Dept by City. This is exactly like an Excel pivot table!
"Write a query to display total salary for each department in each city." โ 3-mark question!
Answer: SELECT Dept, City, SUM(Salary) AS TotalSalary FROM Employee GROUP BY Dept, City;
12.3.2 Placing Conditions on Groups โ HAVING Clause ๐ฏ
WHERE filters individual rows before grouping. HAVING filters groups after grouping. You use HAVING when you want to show only groups that meet a certain condition.
Real-World Need: "Show only departments where the average salary is more than โน50,000."
-- having_clause.sql
SELECT Dept, AVG(Salary) AS AvgSalary
FROM Employee
GROUP BY Dept
HAVING AVG(Salary) > 50000;
Output (only IT qualifies):
| Dept | AvgSalary |
|---|---|
| IT | 66500.00 |
graph LR
DATA["All Rows"]
WHERE_STEP["WHERE\n(filters individual rows\nbefore grouping)"]
GROUP_STEP["GROUP BY\n(creates groups)"]
AGG_STEP["Aggregate\nFunctions\n(SUM, AVG etc.)"]
HAVING_STEP["HAVING\n(filters groups\nafter grouping)"]
RESULT["Final Result"]
DATA --> WHERE_STEP
WHERE_STEP --> GROUP_STEP
GROUP_STEP --> AGG_STEP
AGG_STEP --> HAVING_STEP
HAVING_STEP --> RESULT
style WHERE_STEP fill:#FF9800,color:#fff
style HAVING_STEP fill:#F44336,color:#fff
style GROUP_STEP fill:#2196F3,color:#fff
WHERE vs HAVING โ The Critical Difference:
| Feature | WHERE | HAVING |
|---|---|---|
| What it filters | Individual rows | Groups (after GROUP BY) |
| When it runs | BEFORE grouping | AFTER grouping |
| Can use aggregates? | โ No (WHERE AVG(Salary) > 5000 is illegal) |
โ
Yes (HAVING AVG(Salary) > 5000 is correct) |
| Used with | Any SELECT statement | Only with GROUP BY |
Using WHERE and HAVING Together:
-- where_and_having.sql
-- Show departments where average salary > 40000,
-- but only considering employees who joined before 2023
SELECT Dept, AVG(Salary) AS AvgSalary
FROM Employee
WHERE YEAR(DOJ) < 2023
GROUP BY Dept
HAVING AVG(Salary) > 40000;
Step-by-step:
- WHERE first removes Sita Roy (2023) and Anjali Gupta (2023) โ 4 rows remain
- GROUP BY groups the remaining 4 rows by Dept
- HAVING keeps only groups with AVG Salary > 40000
"Differentiate between WHERE and HAVING clause." โ 2-mark question. Answer: WHERE filters individual rows before grouping and cannot use aggregate functions. HAVING filters groups after GROUP BY is applied and CAN use aggregate functions. You cannot replace one with the other โ they serve different purposes.
More HAVING Examples:
-- more_having_examples.sql
-- Departments with more than 1 employee:
SELECT Dept, COUNT(*) AS Total
FROM Employee
GROUP BY Dept
HAVING COUNT(*) > 1;
-- Cities where maximum salary exceeds 60000:
SELECT City, MAX(Salary) AS MaxSal
FROM Employee
GROUP BY City
HAVING MAX(Salary) > 60000;
12.3.3 Non-Group Expressions with GROUP BY ๐
When using GROUP BY, the SELECT list must follow strict rules โ any column that is not in an aggregate function must be in the GROUP BY clause. This avoids ambiguity about which value to show.
Legal โ column in GROUP BY:
-- legal_group_by.sql
SELECT Dept, COUNT(*) AS Total
FROM Employee
GROUP BY Dept;
-- โ
Legal: Dept is in GROUP BY
Illegal โ column NOT in GROUP BY:
-- illegal_group_by.sql
SELECT Dept, Name, COUNT(*) AS Total
FROM Employee
GROUP BY Dept;
-- โ ILLEGAL: Name is not in GROUP BY and not aggregated!
-- MySQL error: 'Name' is not in GROUP BY clause
Why is it illegal? If Sales has Priya and Anjali, which Name should MySQL show in the output row for Sales? It's ambiguous โ MySQL has no way to know!
The fix โ add Name to GROUP BY (creates finer groups):
-- fix_non_group.sql
SELECT Dept, Name, COUNT(*) AS Total
FROM Employee
GROUP BY Dept, Name;
Older versions of MySQL would sometimes allow non-group expressions and return an arbitrary value (any row from the group). Modern MySQL (from version 5.7+) runs in ONLY_FULL_GROUP_BY mode by default, which correctly rejects these ambiguous queries with an error. Always follow the rule: every non-aggregated SELECT column must be in GROUP BY!
12.4 ๐ Joins
So far, all queries used a single table. But real databases split data across multiple tables โ a Join combines rows from two (or more) tables based on a related column between them.
Real-World Analogy: Imagine your school keeps student names in one register and their marks in another. To see "Priya's marks", someone needs to join the two registers using the Roll Number as the common link โ that's exactly what SQL Joins do!
Our Two Sample Tables for Join Examples:
Table: Employee
| EmpId | Name | DeptId | Salary | City |
|---|---|---|---|---|
| 101 | Priya Sharma | D01 | 45000 | Delhi |
| 102 | Arjun Kumar | D02 | 62000 | Mumbai |
| 103 | Sita Roy | D03 | 38000 | Delhi |
| 104 | Rohan Das | D02 | 71000 | Bangalore |
| 105 | Anjali Gupta | D01 | NULL | Mumbai |
| 106 | Vikram Singh | D03 | 41000 | Delhi |
Table: Department
| DeptId | DeptName | Location |
|---|---|---|
| D01 | Sales | New Delhi |
| D02 | IT | Mumbai |
| D03 | HR | Kolkata |
| D04 | Finance | Chennai |
12.4.1 Cartesian Product ๐
A Cartesian Product (also called a Cross Join) combines every row of the first table with every row of the second table โ producing all possible combinations.
-- cartesian_product.sql
SELECT * FROM Employee, Department;
-- OR equivalently:
SELECT * FROM Employee CROSS JOIN Department;
Result: 6 rows ร 4 rows = 24 rows โ every employee paired with every department!
graph LR
E["Employee\n6 rows"]
D["Department\n4 rows"]
CP["Cartesian Product\n6 ร 4 = 24 rows\n(Every combination!)"]
E --> CP
D --> CP
style CP fill:#F44336,color:#fff
Sample Output (showing just a few rows):
| EmpId | Name | DeptId | DeptId | DeptName |
|---|---|---|---|---|
| 101 | Priya | D01 | D01 | Sales |
| 101 | Priya | D01 | D02 | IT |
| 101 | Priya | D01 | D03 | HR |
| 101 | Priya | D01 | D04 | Finance |
| 102 | Arjun | D02 | D01 | Sales |
| ... | ... | ... | ... | ... |
In most real queries, you don't want every possible combination โ you want matching rows. A Cartesian Product of two large tables produces a massive, meaningless result. It's mainly shown to explain WHY you need a proper Join condition. Always add a WHERE or JOIN condition to link tables meaningfully!
"What is a Cartesian Product in SQL?" โ 2-mark question. Answer: A Cartesian Product results when two tables are joined without a joining condition. It produces every possible combination of rows from both tables. If Table A has m rows and Table B has n rows, the Cartesian Product has m ร n rows.
12.4.2 Table Aliases ๐ท๏ธ
When joining tables, column names from different tables may conflict (both tables might have a DeptId column). Table Aliases give tables short, temporary names to:
- Distinguish columns from different tables
- Make long queries shorter and more readable
-- table_aliases.sql
SELECT E.Name, E.Salary, D.DeptName
FROM Employee E, Department D
WHERE E.DeptId = D.DeptId;
-- 'E' is an alias for Employee, 'D' is an alias for Department
Syntax Rules:
FROM Employee E -- 'E' is the alias (no AS needed for table aliases)
FROM Employee AS E -- Also valid with AS
| Without Alias | With Alias |
|---|---|
Employee.Name |
E.Name |
Department.DeptName |
D.DeptName |
Employee.DeptId = Department.DeptId |
E.DeptId = D.DeptId |
When joining 3+ tables, writing full table names every time makes queries unreadable. Short aliases like E, D, S instantly tell you which table a column comes from. It's a best practice even when not strictly necessary!
12.4.3 Equi-Join and Natural Join ๐ค
Equi-Join
An Equi-Join links two tables using an equality condition (=) on a common column โ typically a Foreign Key in one table matching the Primary Key in another.
-- equi_join.sql
SELECT E.EmpId, E.Name, E.Salary, D.DeptName
FROM Employee E, Department D
WHERE E.DeptId = D.DeptId;
Output (only rows where DeptId matches in both tables):
| EmpId | Name | Salary | DeptName |
|---|---|---|---|
| 101 | Priya Sharma | 45000 | Sales |
| 102 | Arjun Kumar | 62000 | IT |
| 103 | Sita Roy | 38000 | HR |
| 104 | Rohan Das | 71000 | IT |
| 105 | Anjali Gupta | NULL | Sales |
| 106 | Vikram Singh | 41000 | HR |
graph LR
E["Employee Table\n(has DeptId FK)"]
D["Department Table\n(has DeptId PK)"]
MATCH["WHERE E.DeptId = D.DeptId\n๐ Matching rows joined!"]
RESULT["๐ Combined Result\nName + Salary + DeptName\nin one query!"]
E --> MATCH
D --> MATCH
MATCH --> RESULT
style MATCH fill:#4CAF50,color:#fff
style RESULT fill:#2196F3,color:#fff
What happened to Department 'Finance' (D04)? Notice it's not in the output! Because no employee has DeptId = D04, there's no matching row โ so Finance doesn't appear. This is a key property of Equi-Join (which is an Inner Join).
Alternative Modern Syntax (JOIN ... ON):
-- modern_join_syntax.sql
SELECT E.EmpId, E.Name, E.Salary, D.DeptName
FROM Employee E
JOIN Department D ON E.DeptId = D.DeptId;
-- Produces exactly the same result โ cleaner, modern syntax
Both the old comma-separated syntax (FROM Employee E, Department D WHERE E.DeptId = D.DeptId) and the modern JOIN ON syntax are correct. The textbook mostly uses the comma-separated form. Use whichever you're most comfortable with โ both are accepted in answers!
Natural Join
A Natural Join automatically joins tables on all columns with the same name and data type โ no explicit join condition needed.
-- natural_join.sql
SELECT * FROM Employee NATURAL JOIN Department;
-- MySQL automatically joins on DeptId (the common column name)
Output: Same as Equi-Join above โ MySQL finds DeptId as the common column name and joins on it automatically.
Equi-Join vs Natural Join โ Quick Comparison:
| Feature | Equi-Join | Natural Join |
|---|---|---|
| Join condition | Explicitly written by programmer | Automatically found by MySQL |
| Basis | Equality on specified columns | ALL columns with same name |
| Duplicate column in output? | Yes โ DeptId appears twice | No โ common column appears once |
| Control | High (you choose which column) | Low (MySQL decides) |
| Risk | Safer โ explicit | Can cause surprises if unexpected columns share names |
"Differentiate between Equi-Join and Natural Join." โ 2-mark question asked frequently! Answer: Equi-Join links tables using an explicit equality condition written by the programmer (WHERE E.DeptId = D.DeptId). Natural Join automatically links tables on all columns that share the same name and data type โ no condition needs to be written. Natural Join also eliminates duplicate columns from the result.
12.4.4 Additional Search Conditions in Joins ๐ฏ
Real queries often need both a Join condition AND a filter condition (using WHERE) at the same time. You simply add more conditions using AND.
Real-World Need: "Show employees who work in IT department, with their department location."
-- additional_search_conditions.sql
SELECT E.Name, E.Salary, D.DeptName, D.Location
FROM Employee E, Department D
WHERE E.DeptId = D.DeptId
AND D.DeptName = 'IT';
Output:
| Name | Salary | DeptName | Location |
|---|---|---|---|
| Arjun Kumar | 62000 | IT | Mumbai |
| Rohan Das | 71000 | IT | Mumbai |
The Logic:
WHERE E.DeptId = D.DeptId โ Join condition (links the tables)
AND D.DeptName = 'IT' โ Search condition (filters the result)
More Examples of Combined Conditions:
-- combined_join_conditions.sql
-- Show all employees earning above 50000 with their department name:
SELECT E.Name, E.Salary, D.DeptName
FROM Employee E, Department D
WHERE E.DeptId = D.DeptId
AND E.Salary > 50000;
-- Show Delhi employees with their department location:
SELECT E.Name, E.City, D.DeptName, D.Location
FROM Employee E, Department D
WHERE E.DeptId = D.DeptId
AND E.City = 'Delhi';
Output of last query:
| Name | City | DeptName | Location |
|---|---|---|---|
| Priya Sharma | Delhi | Sales | New Delhi |
| Sita Roy | Delhi | HR | Kolkata |
| Vikram Singh | Delhi | HR | Kolkata |
In real applications, you almost always combine a Join condition with one or more filter conditions. The Join links the tables; the filter narrows the result. Think of them as: Join = which rows fit together, Filter = which of those combined rows I actually want!
"Write a query to display the name, salary, and department name of all employees working in Delhi." โ 3-mark Join query. Answer:
SELECT E.Name, E.Salary, D.DeptName
FROM Employee E, Department D
WHERE E.DeptId = D.DeptId
AND E.City = 'Delhi';๐ Full SELECT Statement Order โ The Complete Picture
Now that we have all clauses, here is the complete ORDER in which they must appear in a SELECT statement:
SELECT column/aggregate/expression -- WHAT to show
FROM table(s) -- WHERE data comes from
WHERE row_condition -- WHICH rows to include (before grouping)
GROUP BY column(s) -- HOW to group the rows
HAVING group_condition -- WHICH groups to include (after grouping)
ORDER BY column [ASC|DESC] -- HOW to sort the final result
A typical 1-mark question: "In what order do SQL clauses appear?" Remember this as: SF WG HO โ SELECT โ FROM โ WHERE โ GROUP BY โ HAVING โ ORDER BY
The Processing Order (different from writing order!):
- FROM โ identify the table(s)
- WHERE โ filter individual rows
- GROUP BY โ group the remaining rows
- HAVING โ filter the groups
- SELECT โ compute and display the output columns
- ORDER BY โ sort the final result
โ ๏ธ Common Errors and Misconceptions
| Misconception | Correct Fact |
|---|---|
| โ WHERE and HAVING do the same thing | โ WHERE filters rows BEFORE grouping; HAVING filters groups AFTER grouping. You cannot use aggregate functions in WHERE! |
| โ You can SELECT any column with GROUP BY | โ Every non-aggregated column in SELECT must appear in GROUP BY. Otherwise MySQL throws an error |
| โ Cartesian Product is a useful join | โ Cartesian Product (joining without condition) produces meaningless results; always use a proper Join condition |
| โ Natural Join always gives the same result as Equi-Join | โ Natural Join joins on ALL shared column names (which could be more than intended). Equi-Join gives you explicit control over which columns to join on |
| โ Table aliases permanently rename the table | โ Table aliases are temporary โ they only exist for the duration of that query. The actual table name in the database is unchanged |
| โ Joins always show rows from both tables | โ An Equi-Join (inner join) only shows rows where the join condition is satisfied in BOTH tables โ unmatched rows (like Finance with no employees) are excluded |
| โ HAVING can be used without GROUP BY | โ HAVING is meaningful only when used with GROUP BY. Without GROUP BY, the entire table is treated as one group |
GROUP BY โ One-Line Summary:
- GROUP BY col โ Groups rows with the same value into one summary row
- Multiple columns in GROUP BY โ Creates finer sub-groups (nested grouping)
- HAVING โ Filters groups (like WHERE, but for groups); can use aggregate functions
- WHERE vs HAVING โ WHERE = before grouping, rows; HAVING = after grouping, groups
Joins โ One-Line Summary:
- Cartesian Product โ Every row ร every row; no condition; m ร n rows โ almost always wrong!
- Table Alias โ Short temporary name for a table (E for Employee, D for Department)
- Equi-Join โ Links tables using equality on a common column (FK = PK); most common join
- Natural Join โ Automatically joins on all columns with the same name; no condition needed
Complete SELECT Clause Order:
SELECT โ FROM โ WHERE โ GROUP BY โ HAVING โ ORDER BY
Key Formulas:
- Cartesian Product rows = rows in Table A ร rows in Table B
- GROUP BY always used with aggregate functions
- HAVING always used with GROUP BY
- Equi-Join condition: child.FK = parent.PK
๐ฏ Sample Board Exam Questions
Q1: Very Short Answer [1 mark each]
a) Which clause is used to filter groups in SQL? โ HAVING
b) What is a Cartesian Product of a 5-row table and a 3-row table? โ 5 ร 3 = 15 rows
c) Which type of Join automatically joins on all columns with the same name? โ Natural Join
d) What does GROUP BY Dept do in a SELECT statement?
โ It groups all rows with the same department into a single group so aggregate functions can summarise each group separately.
e) Can you use an aggregate function like AVG() in a WHERE clause? โ No โ aggregate functions cannot be used in WHERE. Use HAVING instead.
Q2: Short Answer [2 marks]
Q: Differentiate between WHERE and HAVING clause with an example.
WHERE filters individual rows before grouping. It cannot use aggregate functions.
SELECT Dept, AVG(Salary) FROM Employee WHERE City = 'Delhi' GROUP BY Dept; โ WHERE here removes non-Delhi rows before grouping.
HAVING filters groups after GROUP BY. It can use aggregate functions.
SELECT Dept, AVG(Salary) FROM Employee GROUP BY Dept HAVING AVG(Salary) > 50000; โ HAVING here removes groups whose average salary is โค 50000.
Q3: Query Writing [3 marks]
Q: Write SQL queries based on the Employee and Department tables:
a) Display each department name and the number of employees in it.
SELECT D.DeptName, COUNT(*) AS TotalEmployees
FROM Employee E, Department D
WHERE E.DeptId = D.DeptId
GROUP BY D.DeptName;
b) Display departments where total salary paid exceeds โน80,000.
SELECT Dept, SUM(Salary) AS TotalSalary
FROM Employee
GROUP BY Dept
HAVING SUM(Salary) > 80000;
c) Display the name, salary, and department name of employees earning more than โน50,000.
SELECT E.Name, E.Salary, D.DeptName
FROM Employee E, Department D
WHERE E.DeptId = D.DeptId
AND E.Salary > 50000;
Q4: Output Based Question [3 marks]
Q: Based on the Employee table, what is the output of the following query?
SELECT City, COUNT(*) AS Total, MAX(Salary) AS MaxSal
FROM Employee
GROUP BY City
HAVING COUNT(*) > 1;
Output:
| City | Total | MaxSal |
|---|---|---|
| Delhi | 3 | 45000 |
| Mumbai | 2 | 62000 |
*(Bangalore has only 1 employee so it is filtered out by HAVING COUNT(*) > 1)*
Q5: Short Answer [2 marks]
Q: What is a Cartesian Product? Why should it be avoided in practice?
A Cartesian Product is produced when two tables are combined in a FROM clause without any Join condition โ every row of the first table is paired with every row of the second table. If Employee has 6 rows and Department has 4 rows, the Cartesian Product has 24 rows. It should be avoided because it produces meaningless data (an employee paired with every department, even irrelevant ones) and can generate enormously large result sets that slow down the database.
โ๏ธ Practice Problems
-
Write a query to display the average salary for each city, but only for cities where more than one employee works.
-
Write a query to find the total salary spent on each department. Display results in descending order of total salary.
-
Write a query to display the name and department name of all employees whose city is 'Mumbai', using an Equi-Join.
-
A table Student(RollNo, Name, Class, Marks) and Section(Class, SectionName, Teacher) are given. Write a query to display student names, their marks, and the teacher's name for all students scoring above 80.
-
Explain why
SELECT Dept, Name, COUNT(*) FROM Employee GROUP BY Dept;produces an error. How would you fix it? -
Write a query to display each department name along with the highest and lowest salary in that department.
-
Without running the query, predict how many rows
SELECT * FROM Employee, Department;will return if Employee has 6 rows and Department has 4 rows. -
Rewrite this Equi-Join using a Natural Join:
SELECT E.Name, D.DeptName FROM Employee E, Department D WHERE E.DeptId = D.DeptId; -
Write a query to display department names that have more than one employee earning above โน40,000.
-
What is the difference between
GROUP BY DeptandGROUP BY Dept, City? Explain with a real-world example using the Employee table.