"Bought Everything"
Find customers who bought all products in the Điện tử (Electronics) category.
Objective: Understand the nature of division in SQL, recognize "all" style problems, write queries using GROUP BY + HAVING COUNT and double NOT EXISTS, use aggregate functions, combine TOP/ORDER BY/WITH TIES/DISTINCT, and use LEFT/RIGHT/FULL OUTER JOIN so no data is missed.
01 · DIVISION IN SQL
R that relate to all objects in a table S.When a problem contains phrases like all, every, the entirety of, "bought all products", "participated in all projects", "registered for all courses" - think of division right away.
Suppose we have 3 tables:
| KhachHang | |
|---|---|
| MaKH | TenKH |
| KH01 | Nguyễn An |
| KH02 | Trần Bình |
| KH03 | Lê Cường |
| SanPham | |
|---|---|
| MaSP | TenSP |
| SP01 | Laptop |
| SP02 | Chuột |
| SP03 | Bàn phím |
| MuaHang | |
|---|---|
| MaKH | MaSP |
| KH01 | SP01 |
| KH01 | SP02 |
| KH01 | SP03 |
| KH02 | SP01 |
| KH02 | SP02 |
| KH03 | SP01 |
| KH03 | SP03 |
We need to find: customers who bought all products.
Result: KH01. This is exactly the way of thinking behind division.
The problem "Find A that has done all of B" can be understood as:
In SQL, this idea is typically implemented with GROUP BY + HAVING COUNT(DISTINCT ...).
02 · BASIC DIVISION
Model: R(A, B, C, D, E) and S(D, E). Division R / S finds the values A, B, C of R that relate to all values D, E in S.
SELECT B1.<Column 1>, ...
FROM <Table 1> B1
JOIN <Table 3> B3
ON B1.<Column 1> = B3.<Column 1>
GROUP BY B1.<Column 1>
HAVING COUNT(DISTINCT B3.<Column 2>) =
(
SELECT COUNT(DISTINCT B2.<Column 2>)
FROM <Table 2> B2
);Tables: KhachHang(MaKH, TenKH), SanPham(MaSP, TenSP), MuaHang(MaKH, MaSP).
SELECT KH.MaKH
FROM KhachHang KH
JOIN MuaHang MH
ON KH.MaKH = MH.MaKH
GROUP BY KH.MaKH
HAVING COUNT(DISTINCT MH.MaSP) =
(
SELECT COUNT(*)
FROM SanPham
);Step 1 - Join customers to their purchase history: FROM KhachHang KH JOIN MuaHang MH ON KH.MaKH = MH.MaKH
Step 2 - Group by each customer with GROUP BY KH.MaKH: KH01 ➜ SP01,SP02,SP03, KH02 ➜ SP01,SP02, KH03 ➜ SP01,SP03.
Step 3 - Count distinct products purchased with COUNT(DISTINCT MH.MaSP): KH01 ➜ 3, KH02 ➜ 2, KH03 ➜ 2.
Step 4 - Count the total number of products: SELECT COUNT(*) FROM SanPham ➜ 3.
Step 5 - Compare: number of products the customer bought = total number of products that exist ➜ the customer bought all of them.
Tables: SinhVien(MaSV, TenSV), MonHoc(MaMH, TenMH), DangKy(MaSV, MaMH).
SELECT SV.MaSV
FROM SinhVien SV
JOIN DangKy DK
ON SV.MaSV = DK.MaSV
GROUP BY SV.MaSV
HAVING COUNT(DISTINCT DK.MaMH) =
(
SELECT COUNT(*)
FROM MonHoc
);Student A registered for 5 courses, total required courses = 5 ➜ passes. Student B registered for 4 courses, total required courses = 5 ➜ fails.
Tables: NhanVien(MaNV, TenNV), DuAn(MaDA, TenDA), ThamGia(MaNV, MaDA).
SELECT NV.MaNV
FROM NhanVien NV
JOIN ThamGia TG
ON NV.MaNV = TG.MaNV
GROUP BY NV.MaNV
HAVING COUNT(DISTINCT TG.MaDA) =
(
SELECT COUNT(*)
FROM DuAn
);03 · CONDITIONAL DIVISION
Example: "Find customers who bought all products in the Electronics category" - this is no longer "all products," but "all products WHERE DanhMuc = 'Điện tử'".
| SanPham | ||
|---|---|---|
| Code | Name | Category |
| SP01 | Laptop | Điện tử |
| SP02 | Điện thoại | Điện tử |
| SP03 | Bàn phím | Phụ kiện |
| SP04 | Áo | Thời trang |
The set to check is only SP01, SP02, not all 4 products.
SELECT KH.MaKH
FROM KhachHang KH
JOIN MuaHang MH
ON KH.MaKH = MH.MaKH
JOIN SanPham SP
ON MH.MaSP = SP.MaSP
WHERE SP.DanhMuc = N'Điện tử'
GROUP BY KH.MaKH
HAVING COUNT(DISTINCT MH.MaSP) =
(
SELECT COUNT(*)
FROM SanPham
WHERE DanhMuc = N'Điện tử'
);Problem: "Find students who registered for all required courses worth more than 3 credits." First determine the set of courses to check using WHERE SoTinChi > 3, then check whether each student registered for the entire set.
SELECT SV.MaSV
FROM SinhVien SV
JOIN DangKy DK
ON SV.MaSV = DK.MaSV
JOIN MonHoc MH
ON MH.MaMH = DK.MaMH
WHERE MH.SoTinChi > 3
GROUP BY SV.MaSV
HAVING COUNT(DISTINCT DK.MaMH) =
(
SELECT COUNT(*)
FROM MonHoc
WHERE SoTinChi > 3
);04 · DIVISION WITH NOT EXISTS
This is the second way to solve "all" problems - using negation to check whether any value fails to satisfy the condition.
"The customer bought all electronics products" can be restated as "There does not exist an electronics product that the customer has not bought."
SELECT ...
FROM A
WHERE NOT EXISTS
(
SELECT *
FROM B
WHERE NOT EXISTS
(
SELECT *
FROM C
WHERE ...
)
);Problem: "Find customers who bought all products in the Electronics category."
SELECT KH.MaKH
FROM KhachHang KH
WHERE NOT EXISTS
(
SELECT *
FROM SanPham SP
WHERE SP.DanhMuc = N'Điện tử'
AND NOT EXISTS
(
SELECT *
FROM MuaHang MH
WHERE MH.MaKH = KH.MaKH
AND MH.MaSP = SP.MaSP
)
);Don't read SQL line by line. Instead, read: "Get customers for whom there does not exist any electronics product that the customer hasn't bought." If no such product exists ➜ the customer has bought all of them.
Problem: "Find students who registered for all required courses worth more than 3 credits."
SELECT SV.MaSV
FROM SinhVien SV
WHERE NOT EXISTS
(
SELECT *
FROM MonHoc MH
WHERE MH.SoTinChi > 3
AND NOT EXISTS
(
SELECT *
FROM DangKy DK
WHERE DK.MaMH = MH.MaMH
AND DK.MaSV = SV.MaSV
)
);| Form | Signal |
|---|---|
| Basic division | GROUP BY + HAVING COUNT |
| Conditional division | WHERE + GROUP BY + HAVING COUNT |
| NOT EXISTS division | Two nested NOT EXISTS layers |
NOT EXISTS without the inner layer produces a completely wrong result - always double-check against the pattern "no A exists that lacks B," made of exactly two nested NOT EXISTS as shown above.05 · AGGREGATE FUNCTIONS & GROUPING
Aggregate functions let you perform calculations over a set of multiple rows.
| Function | Meaning |
|---|---|
COUNT(*) | Counts rows |
COUNT(column) | Counts non-NULL values |
COUNT(DISTINCT column) | Counts distinct, non-NULL values |
SUM(x) | Sum |
AVG(x) | Average |
MAX(x) | Maximum |
MIN(x) | Minimum |
| MaNV | Luong | ChucVu |
|---|---|---|
| NV01 | 10M | Dev |
| NV02 | 12M | Dev |
| NV03 | NULL | Tester |
| NV04 | 15M | Dev |
| NV05 | 15M | NULL |
SELECT COUNT(*) FROM NhanVien; ➜ 5 (counts all rows)
SELECT COUNT(Luong) FROM NhanVien; ➜ 4 (NV03 has Luong = NULL)
SELECT COUNT(DISTINCT ChucVu) FROM NhanVien; ➜ 2 (Dev, Tester - NULL doesn't count)
SELECT SUM(Luong) AS TongLuong
FROM NhanVien;SELECT AVG(Luong) AS LuongTrungBinh
FROM NhanVien;Note: AVG ignores NULL values.
SELECT
MAX(Luong) AS LuongCaoNhat,
MIN(Luong) AS LuongThapNhat
FROM NhanVien;Example data:
| MaNV | Phong | Luong |
|---|---|---|
| NV01 | P01 | 10M |
| NV02 | P01 | 12M |
| NV03 | P02 | 15M |
| NV04 | P02 | 18M |
GROUP BY Phong splits the data into groups P01: NV01, NV02 and P02: NV03, NV04, and only then runs MAX/MIN/AVG/SUM/COUNT on each group.
SELECT
Phong,
MAX(Luong) AS LuongCaoNhat,
MIN(Luong) AS LuongThapNhat,
AVG(Luong) AS LuongTrungBinh
FROM NhanVien
GROUP BY Phong;SELECT but forgetting to add it to GROUP BY. The mandatory rule: every column in SELECT that is not inside an aggregate function (SUM, COUNT, AVG...) must also appear in GROUP BY, or SQL Server will raise a syntax error.LEFT JOIN keeps a department visible even when it has no employees yet:
SELECT
PB.MaPH,
PB.TenPH,
COUNT(NV.MaNV) AS SLNV
FROM PHONGBAN PB
LEFT JOIN NHANVIEN NV
ON NV.Phong = PB.MaPH
GROUP BY
PB.MaPH,
PB.TenPH;Problem: "Find departments with more than 10 employees, showing department code, name, and employee count, sorted in descending order."
SELECT
PB.MaPH,
PB.TenPH,
COUNT(NV.MaNV) AS SLNV
FROM PHONGBAN PB
LEFT JOIN NHANVIEN NV
ON NV.Phong = PB.MaPH
GROUP BY
PB.MaPH,
PB.TenPH
HAVING COUNT(NV.MaNV) > 10
ORDER BY
COUNT(NV.MaNV) DESC;WHERE filters rows before grouping (WHERE Luong > 10000000). HAVING filters groups after grouping (HAVING COUNT(*) > 10).
QUICK CHECK
To keep only departments with more than 10 employees (after GROUP BY), which clause should you use?
WHERE can only filter individual rows before GROUP BY.06 · SELECT TOP
TOP is used to limit the number of rows returned.
SELECT TOP 4
HoTen,
Luong
FROM NhanVien;To get the 4 highest-paid employees:
SELECT TOP 4
HoTen,
Luong
FROM NhanVien
ORDER BY Luong DESC;SELECT TOP 4 * FROM NhanVien; if the problem asks for "the 4 highest-paid employees" - you must include ORDER BY Luong DESC.If the 4th-highest salary is 15 million and someone else also earns 15 million, WITH TIES will include them too:
SELECT TOP 4 WITH TIES
HoTen,
Luong
FROM NhanVien
ORDER BY Luong DESC;Get the first 4 distinct job titles:
SELECT DISTINCT TOP 4
ChucVu
FROM NhanVien;Problem: "Get the 4 departments with the largest total payroll, considering only departments with a total salary above 5 million."
SELECT TOP 4
MaPB,
SUM(Luong) AS TongLuong
FROM NhanVien
GROUP BY MaPB
HAVING SUM(Luong) > 5000000
ORDER BY SUM(Luong) DESC;07 · OUTER JOINS
INNER JOIN only returns rows with matching data. But sometimes you need to know about objects with no related data - for example, "show employees who aren't assigned to any project." With INNER JOIN, employees with no project would be excluded since there's no corresponding record. That's why LEFT JOIN is needed.
SELECT
NV.MaNV,
NV.HoTen
FROM NHANVIEN NV
LEFT OUTER JOIN PHANCONG PC
ON NV.MaNV = PC.MaNV
WHERE PC.MaDA IS NULL;LEFT JOIN + WHERE right_table.key IS NULL ➜ find records with no matching record in the right-hand table.WHERE condition on a different column (not a NULL check) of the right table after LEFT JOIN - the NULL rows produced by the LEFT JOIN then get filtered out by that condition, making the statement accidentally behave like an INNER JOIN. If you need to filter the right table further while keeping LEFT JOIN behavior, put that condition in the ON clause instead of WHERE.| Type | Keeps |
|---|---|
LEFT JOIN | All of the left table |
RIGHT JOIN | All of the right table |
FULL OUTER JOIN | All of both sides |
08 · QUICK REFERENCE TABLE
| Looking for... | Think of... |
|---|---|
| All | Division |
| All + a condition | Conditional division |
| All, via negation | NOT EXISTS × 2 |
| Total | SUM() |
| Average | AVG() |
| Max / min | MAX() / MIN() |
| Row count | COUNT(*) |
| Count values / distinct values | COUNT(column) / COUNT(DISTINCT column) |
| Top N | TOP |
| Top N including ties | TOP ... WITH TIES |
| Top N distinct values | DISTINCT TOP |
| Stats per group / filter groups | GROUP BY / HAVING |
| Records with no link | LEFT JOIN ... IS NULL |
Week 1 was about designing & manipulating data, Week 2 was about querying & combining data, and Week 3 is advanced querying: division ("all" ➜ GROUP BY+HAVING COUNT or NOT EXISTS×2), grouping (Aggregates, TOP/HAVING), and outer joins ("none" ➜ LEFT JOIN+IS NULL).
09 · PRACTICE EXERCISES
See the full assignment, attached files, and detailed answers (access code required) for BTTH3.
Beyond the official questions 26-53, here are extra practice problems to deepen your understanding of Week 3's way of thinking - not part of the BTTH3 submission. Each has a hidden hint - click to reveal.
Find customers who bought all products in the Điện tử (Electronics) category.
Find departments that don't have any employees yet.
Find the 3 highest salary levels and return every employee earning one of those salaries.
Find students who registered for all courses worth 3 or more credits.
Find employees who participated in every project. Write 2 different SQL queries: one using GROUP BY + HAVING COUNT, the other using double NOT EXISTS.
This is a great exercise for understanding that a single SQL problem can have multiple valid solutions.