Chuyển tới nội dung chính

SQL Aggregation & Filtering

What You'll Learn

By the end of this page, you will be able to:

  • Sử dụng các hàm tổng hợp (COUNT, SUM, AVG, MIN, MAX) để tóm tắt dữ liệu
  • Viết câu lệnh GROUP BY để tạo các nhóm phụ (subtotals) theo từng danh mục
  • Giải thích sự khác biệt quan trọng giữa WHEREHAVING
  • Sử dụng DISTINCT và hiểu khi nào nó khác với GROUP BY
  • Viết các truy vấn có nhiều mức phân nhómtổng hợp có điều kiện lọc
  • Tránh các lỗi thường gặp với NULL, nhóm rỗng, và việc lọc sai cách

Vấn đề: Tóm tắt dữ liệu (Summarizing Data)

Dữ liệu thô tồn tại ở dạng hàng — mỗi hàng là một sự kiện, một đơn hàng, một nhân viên. Nhưng các câu hỏi kinh doanh lại hỏi về nhóm, không phải từng hàng riêng lẻ:

  • "Mức lương trung bình của mỗi phòng ban là bao nhiêu?"
  • "Mỗi khách hàng đã đặt bao nhiêu đơn hàng trong tháng trước?"
  • "Phòng ban nào có nhiều hơn 5 nhân viên?"

Bạn cần một cách để gộp nhiều hàng thành các hàng tóm tắt (summary rows). Đó chính là chức năng của Aggregation (Tổng hợp dữ liệu).

Sample Tables

Tất cả ví dụ sử dụng các bảng sau:

CREATE TABLE departments (
dept_id INT PRIMARY KEY,
dept_name VARCHAR(50)
);

INSERT INTO departments VALUES
(10, 'Engineering'),
(20, 'Marketing'),
(30, 'Sales'),
(40, 'HR');

CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(50),
dept_id INT REFERENCES departments(dept_id),
salary DECIMAL(10,2),
hire_date DATE
);

INSERT INTO employees VALUES
(1, 'Alice', 10, 90000, '2020-01-15'),
(2, 'Bob', 20, 75000, '2021-03-20'),
(3, 'Carol', 10, 85000, '2022-06-10'),
(4, 'Dave', 30, 70000, '2023-09-01'),
(5, 'Eve', 10, 95000, '2019-11-05'),
(6, 'Frank', 20, 78000, '2024-02-14'),
(7, 'Grace', 30, 72000, '2023-07-22'),
(8, 'Hank', NULL, 65000, '2024-01-10');

Điểm cần lưu ý:

  • Nhân viên 8 (Hank) không có phòng ban (dept_id là NULL)
  • Phòng ban 40 (HR) không có nhân viên
  • Engineering có 3 nhân viên, Marketing và Sales mỗi phòng có 2 nhân viên

Các hàm tổng hợp (Aggregate Functions)

Các hàm tổng hợp (Aggregate Functions) tính toán một giá trị duy nhất từ tập hợp các hàng đầu vào:

Hàm (Function)Trả về (Returns)Xử lý NULL
COUNT(*)Tổng số hàng (Total number of rows)Đếm tất cả hàng, kể cả NULL
COUNT(col)Số giá trị không NULL trong cột col (Number of non-NULL values)Bỏ qua NULL
SUM(col)Tổng các giá trị không NULL (Sum of non-NULL values)Bỏ qua NULL, trả về NULL nếu không có hàng
AVG(col)Trung bình các giá trị không NULL (Average of non-NULL values)Bỏ qua NULL
MIN(col)Giá trị nhỏ nhất không NULL (Minimum non-NULL value)Bỏ qua NULL
MAX(col)Giá trị lớn nhất không NULL (Maximum non-NULL value)Bỏ qua NULL
-- Tổng hợp trên toàn bộ bảng
SELECT
COUNT(*) AS total_employees,
COUNT(dept_id) AS with_department,
AVG(salary) AS avg_salary,
MIN(salary) AS min_salary,
MAX(salary) AS max_salary,
SUM(salary) AS total_payroll
FROM employees;

Result:

total_employeeswith_departmentavg_salarymin_salarymax_salarytotal_payroll
8778750.0065000.0095000.00630000.00
mẹo

COUNT(*) đếm tất cả hàng (all rows). COUNT(column) chỉ đếm các hàng mà cột đó không NULL. Sự khác biệt này rất quan trọng khi cột của bạn chứa giá trị NULL — Hank không có phòng ban, nên COUNT(dept_id) trả về 7 chứ không phải 8.

GROUP BY: Nhóm các hàng (Grouping Rows)

GROUP BY chia các hàng thành các nhóm dựa trên giá trị cột, sau đó áp dụng hàm tổng hợp cho từng nhóm độc lập.

SELECT
dept_id,
COUNT(*) AS employee_count,
AVG(salary) AS avg_salary,
SUM(salary) AS total_salary
FROM employees
GROUP BY dept_id;

Result:

dept_idemployee_countavg_salarytotal_salary
10390000.00270000.00
20276500.00153000.00
30271000.00142000.00
NULL165000.0065000.00

Quy tắc của GROUP BY (Rules of GROUP BY)

  1. Mọi cột trong SELECT (không được tổng hợp) đều phải xuất hiện trong GROUP BY:
-- Hợp lệ: dept_id có trong GROUP BY
SELECT dept_id, AVG(salary)
FROM employees
GROUP BY dept_id;

-- KHÔNG HỢP LỆ trong hầu hết CSDL: name không có trong GROUP BY
-- (PostgreSQL/SQL Server sẽ báo lỗi; MySQL có thể cho phép nhưng kết quả không đoán trước được)
SELECT dept_id, name, AVG(salary)
FROM employees
GROUP BY dept_id;
  1. NULL tạo thành một nhóm riêng — tất cả hàng có NULL trong cột phân nhóm sẽ được gộp lại với nhau.

  2. Có thể phân nhóm theo nhiều cột:

SELECT dept_id, hire_date
FROM employees
GROUP BY dept_id, hire_date;

GROUP BY kết hợp với JOIN

Kết hợp phân nhóm (grouping) với JOIN để tạo báo cáo qua các bảng liên quan:

SELECT
d.dept_name,
COUNT(*) AS employee_count,
AVG(e.salary) AS avg_salary
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
GROUP BY d.dept_name;

Result:

dept_nameemployee_countavg_salary
Engineering390000.00
Marketing276500.00
Sales271000.00
mẹo

Chú ý rằng Hank (không có phòng ban) và HR (không có nhân viên) đều vắng mặt. INNER JOIN loại bỏ cả hai. Sử dụng LEFT JOIN nếu bạn muốn bao gồm tất cả nhân viên hoặc tất cả phòng ban.

WHERE vs HAVING

Đây là câu hỏi phỏng vấn phổ biến nhất về tổng hợp dữ liệu (aggregation). Điểm mấu chốt là thứ tự thực thi (order of execution):

Mệnh đề (Clause)Bộ lọc (Filters)Khi nào chạy (When)Dùng hàm tổng hợp được không?
WHERETừng hàng riêng lẻ (Individual rows)Trước khi phân nhómKhông
HAVINGNhóm (sau khi tổng hợp)Sau khi phân nhóm

WHERE: Lọc trước khi phân nhóm (Filter Before Grouping)

-- "Mức lương trung bình theo phòng ban cho nhân viên được thuê sau năm 2022"
SELECT
dept_id,
COUNT(*) AS employee_count,
AVG(salary) AS avg_salary
FROM employees
WHERE hire_date >= '2023-01-01'
GROUP BY dept_id;

Result:

dept_idemployee_countavg_salary
30271000.00
NULL165000.00

Chỉ những nhân viên được thuê sau năm 2022 mới được bao gồm trước khi phân nhóm xảy ra. Engineering (Alice, Carol, Eve đều được thuê trước 2023) biến mất hoàn toàn.

HAVING: Lọc sau khi phân nhóm (Filter After Grouping)

-- "Các phòng ban có nhiều hơn 1 nhân viên"
SELECT
dept_id,
COUNT(*) AS employee_count,
AVG(salary) AS avg_salary
FROM employees
GROUP BY dept_id
HAVING COUNT(*) > 1;

Result:

dept_idemployee_countavg_salary
10390000.00
20276500.00
30271000.00

Tất cả hàng được phân nhóm trước, sau đó các nhóm có COUNT(*) <= 1 (nhóm NULL của Hank) sẽ bị loại bỏ.

Kết hợp WHERE và HAVING (Combining WHERE and HAVING)

-- "Các phòng ban mà nhân viên được thuê sau 2020 có mức lương trung bình trên 72000"
SELECT
dept_id,
COUNT(*) AS employee_count,
AVG(salary) AS avg_salary
FROM employees
WHERE hire_date >= '2020-01-01' -- lọc hàng trước
GROUP BY dept_id -- rồi phân nhóm
HAVING AVG(salary) > 72000; -- rồi lọc nhóm

Result:

dept_idemployee_countavg_salary
10390000.00
20276500.00

Quá trình thực thi (Execution breakdown):

  1. FROM — tất cả 8 nhân viên
  2. WHERE — loại Eve (được thuê 2019-11-05) → còn 7 hàng
  3. GROUP BY — phân nhóm 7 hàng theo dept_id
  4. HAVING — giữ lại các nhóm có AVG(salary) > 72000
  5. SELECT — xuất các cột kết quả

DISTINCT vs GROUP BY

Cả hai đều có thể tạo ra các giá trị duy nhất (unique values), nhưng phục vụ mục đích khác nhau:

-- Lấy các dept_id duy nhất (cả hai tạo ra cùng kết quả)
SELECT DISTINCT dept_id FROM employees;
SELECT dept_id FROM employees GROUP BY dept_id;

Result (cả hai truy vấn):

dept_id
10
20
30
NULL

Khi nào chúng khác nhau

Khía cạnh (Aspect)DISTINCTGROUP BY
Mục đíchLoại bỏ các hàng trùng lặp (Remove duplicate rows)Tổng hợp dữ liệu theo nhóm (Aggregate data per group)
Hàm tổng hợp (Aggregate functions)Không thể sử dụngCó thể sử dụng (COUNT, SUM, AVG, ...)
Nhiều cột (Multiple columns)Loại trùng lặp trên tất cả cột được liệt kêPhân nhóm theo tất cả cột được liệt kê
Hiệu năng (Performance)Thường tương đươngThường tương đương (cùng kế hoạch thực thi)
Lọc (Filtering)Không có lọc theo nhómHỗ trợ HAVING

Sử dụng DISTINCT khi:

  • Bạn chỉ cần giá trị duy nhất mà không cần tổng hợp (aggregation)
-- "Phòng ban nào có nhân viên?"
SELECT DISTINCT dept_id FROM employees;

Sử dụng GROUP BY khi:

  • Bạn cần tổng hợp dữ liệu theo từng nhóm
-- "Mỗi phòng ban có bao nhiêu nhân viên?"
SELECT dept_id, COUNT(*) FROM employees GROUP BY dept_id;
  • Bạn cần lọc nhóm bằng HAVING
-- "Các phòng ban có nhiều hơn 2 nhân viên"
SELECT dept_id FROM employees GROUP BY dept_id HAVING COUNT(*) > 2;
mẹo

Bên dưới, hầu hết các CSDL tạo ra cùng một kế hoạch thực thi (execution plan) cho SELECT DISTINCT colSELECT col GROUP BY col. Hãy sử dụng cách nào thể hiện rõ ý định của bạn hơn.

Thứ tự thực thi các mệnh đề SQL (SQL Clause Execution Order)

Hiểu thứ tự thực thi logic (logical execution order) của các mệnh đề SQL giúp bạn viết truy vấn đúng:

Thứ tựMệnh đề (Clause)Mục đích (Purpose)Dùng alias được không?Dùng hàm tổng hợp được không?
1FROMXác định bảng nguồn (Identify source tables)KhôngKhông
2JOINKết hợp các bảng (Combine tables)KhôngKhông
3WHERELọc từng hàng riêng lẻ (Filter individual rows)KhôngKhông
4GROUP BYPhân nhóm hàng (Group rows)Không (trong hầu hết CSDL)Không
5HAVINGLọc nhóm (Filter groups)Không
6SELECTTính toán cột kết quả (Compute output columns)N/A
7ORDER BYSắp xếp kết quả (Sort results)
8LIMITGiới hạn số hàng (Restrict row count)KhôngKhông
cảnh báo

Alias được định nghĩa trong SELECT không khả dụng trong WHERE hay HAVING — vì SELECT chạy sau WHERE và HAVING. Chúng chỉ khả dụng trong ORDER BY (chạy cuối cùng).

Các mẫu nâng cao (Advanced Patterns)

COUNT có điều kiện (COUNT with conditionals)

Đếm hàng thỏa mãn một điều kiện bên trong một nhóm:

SELECT
dept_id,
COUNT(*) AS total,
COUNT(CASE WHEN salary > 80000 THEN 1 END) AS high_earners,
COUNT(CASE WHEN salary <= 80000 THEN 1 END) AS standard_earners
FROM employees
WHERE dept_id IS NOT NULL
GROUP BY dept_id;

Result:

dept_idtotalhigh_earnersstandard_earners
10330
20211
30202

Cách này thường nhanh hơn việc viết nhiều truy vấn con (subqueries).

GROUP BY với nhiều cột (GROUP BY with multiple columns)

SELECT
d.dept_name,
EXTRACT(YEAR FROM e.hire_date) AS hire_year,
COUNT(*) AS hires_that_year
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
GROUP BY d.dept_name, EXTRACT(YEAR FROM e.hire_date)
ORDER BY d.dept_name, hire_year;

Result:

dept_namehire_yearhires_that_year
Engineering20191
Engineering20201
Engineering20221
Marketing20211
Marketing20241
Sales20232

Các hàm tổng hợp với NULLs (Aggregates with NULLs)

SELECT
COUNT(*) AS count_all,
COUNT(dept_id) AS count_dept,
SUM(salary) AS sum_salary,
AVG(salary) AS avg_salary
FROM employees;

Result:

count_allcount_deptsum_salaryavg_salary
87630000.0078750.00
  • COUNT(*) = 8 (đếm tất cả hàng)
  • COUNT(dept_id) = 7 (bỏ qua NULL của Hank)
  • AVG(salary) = 78750.00 (trung bình của tất cả 8 giá trị salary không NULL)
cảnh báo

AVG bỏ qua giá trị NULL, chứ không coi chúng là số 0. Nếu bạn muốn NULL được tính là 0, hãy sử dụng COALESCE:

-- AVG coi NULL là "không tồn tại"
-- COALESCE ép NULL thành 0, thay đổi giá trị trung bình
SELECT AVG(COALESCE(bonus, 0)) FROM employees;

Các lỗi thường gặp (Common Pitfalls)

  • Sử dụng WHERE với hàm tổng hợpWHERE COUNT(*) > 1 là không hợp lệ. Hãy sử dụng HAVING COUNT(*) > 1.
-- SAI
SELECT dept_id, COUNT(*)
FROM employees
WHERE COUNT(*) > 1
GROUP BY dept_id;

-- ĐÚNG
SELECT dept_id, COUNT(*)
FROM employees
GROUP BY dept_id
HAVING COUNT(*) > 1;
  • SELECT cột không có trong GROUP BY — Mọi cột không được tổng hợp trong SELECT đều phải có trong GROUP BY. Hầu hết CSDL sẽ báo lỗi.
-- SAI: name không được tổng hợp và không có trong GROUP BY
SELECT dept_id, name, AVG(salary)
FROM employees
GROUP BY dept_id;

-- ĐÚNG: hoặc tổng hợp name, hoặc thêm vào GROUP BY
SELECT dept_id, MAX(name) AS representative_name, AVG(salary)
FROM employees
GROUP BY dept_id;
  • HAVING không có GROUP BY — Sử dụng HAVING mà không có GROUP BY coi toàn bộ bảng như một nhóm. Điều này hợp lệ nhưng dễ gây nhầm lẫn — nên ưu tiên dùng WHERE.

  • Lọc sai mệnh đề — Sử dụng HAVING trong khi WHERE đã đủ. HAVING được đánh giá sau khi phân nhóm, nên nó xử lý nhiều dữ liệu hơn cần thiết.

-- KHÔNG TỐI ƯU: HAVING lọc sau khi phân nhóm tất cả hàng
SELECT dept_id, COUNT(*)
FROM employees
HAVING dept_id IS NOT NULL
GROUP BY dept_id;

-- TỐT HƠN: WHERE lọc trước khi phân nhóm (ít hàng cần gộp hơn)
SELECT dept_id, COUNT(*)
FROM employees
WHERE dept_id IS NOT NULL
GROUP BY dept_id;
  • Nhầm lẫn giữa COUNT(*) và COUNT(col)COUNT(*) đếm tất cả hàng; COUNT(col) bỏ qua NULL. Hãy hiểu rõ cái nào bạn cần.

Best Practices

  1. Lọc sớm bằng WHERE — giảm số hàng trước khi phân nhóm để hiệu năng tốt hơn
  2. Chỉ dùng HAVING cho điều kiện tổng hợp — nếu điều kiện không dùng hàm tổng hợp, nó thuộc về WHERE
  3. Luôn GROUP BY tất cả cột không được tổng hợp — đừng dựa vào hành vi lỏng lẻo của MySQL
  4. Sử dụng alias có ý nghĩaCOUNT(*) AS employee_count rõ ràng hơn COUNT(*) đơn thuần
  5. Đánh index các cột dùng trong GROUP BY — phân nhóm thường liên quan đến sắp xếp hoặc băm (hashing)
  6. Kiểm tra kế hoạch thực thi — dùng EXPLAIN để xác nhận CSDL đang sử dụng index thay vì quét toàn bảng
  7. Cân nhắc sử dụng materialized views — cho các truy vấn tổng hợp tốn kém chạy thường xuyên

Interview Questions

1. Sự khác biệt giữa WHERE và HAVING là gì?

WHEREHAVING
Lọc (Filters)Từng hàng riêng lẻ (Individual rows)Nhóm (sau khi tổng hợp)
Chạy khi nào (Runs)Trước GROUP BYSau GROUP BY
Dùng hàm tổng hợp được không?Không
Trường hợp sử dụng (Use case)"Lọc nhân viên có lương > 80000""Lọc phòng ban có lương TB > 80000"
-- WHERE: lọc hàng trước khi phân nhóm
SELECT dept_id, AVG(salary)
FROM employees
WHERE salary > 70000
GROUP BY dept_id;

-- HAVING: lọc nhóm sau khi phân nhóm
SELECT dept_id, AVG(salary)
FROM employees
GROUP BY dept_id
HAVING AVG(salary) > 70000;

Truy vấn đầu tiên loại bỏ từng nhân viên có mức lương 70000 trở xuống, sau đó tính trung bình phần còn lại. Truy vấn thứ hai tính trung bình tất cả nhân viên trong mỗi phòng ban, rồi giữ lại các phòng ban có trung bình vượt quá 70000.

2. Sự khác biệt giữa COUNT(*) và COUNT(column) là gì?

  • COUNT(*) đếm tất cả hàng, kể cả hàng có giá trị NULL
  • COUNT(column) chỉ đếm các hàng mà cột đó không NULL
SELECT
COUNT(*) AS total_rows, -- 8
COUNT(dept_id) AS with_dept -- 7 (Hank có dept_id NULL)
FROM employees;

3. Sự khác biệt giữa DISTINCT và GROUP BY là gì?

Cả hai đều có thể trả về các giá trị duy nhất:

-- Hai truy vấn này tạo ra kết quả giống hệt nhau
SELECT DISTINCT dept_id FROM employees;
SELECT dept_id FROM employees GROUP BY dept_id;

Nhưng GROUP BY làm được nhiều hơn — nó hỗ trợ tổng hợp (aggregation) và HAVING:

-- GROUP BY với hàm tổng hợp (DISTINCT không thể làm điều này)
SELECT dept_id, COUNT(*), AVG(salary)
FROM employees
GROUP BY dept_id;

Sử dụng DISTINCT cho việc loại trùng lặp đơn giản. Sử dụng GROUP BY khi bạn cần tổng hợp hoặc lọc theo nhóm.

4. Viết truy vấn tìm các phòng ban có nhiều hơn 2 nhân viên có lương trên 75000.

Câu hỏi này yêu cầu cả WHERE và HAVING:

SELECT
d.dept_name,
COUNT(*) AS high_earner_count,
AVG(e.salary) AS avg_high_salary
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
WHERE e.salary > 75000 -- lọc từng hàng trước
GROUP BY d.dept_name -- rồi phân nhóm theo phòng ban
HAVING COUNT(*) > 2; -- rồi lọc nhóm

5. Thứ tự thực thi của các mệnh đề SQL là gì?

FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT

Điều này giải thích tại sao:

  • Bạn không thể dùng alias từ SELECT trong WHERE (WHERE chạy trước)
  • Bạn không thể dùng hàm tổng hợp trong WHERE (aggregation chưa xảy ra)
  • Bạn có thể dùng alias từ SELECT trong ORDER BY (ORDER BY chạy cuối cùng)

6. Điều gì xảy ra khi GROUP BY một cột chứa NULL?

Tất cả hàng có NULL trong cột phân nhóm sẽ được đưa vào một nhóm duy nhất:

SELECT dept_id, COUNT(*)
FROM employees
GROUP BY dept_id;

Truy vấn này tạo ra một hàng cho dept_id = NULL với số lượng 1 (Hank). NULL được coi là một giá trị phân nhóm riêng biệt.

7. Làm thế nào để đếm hàng thỏa mãn điều kiện trong một nhóm?

Sử dụng COUNT(CASE WHEN ... THEN 1 END) hoặc mệnh đề FILTER (PostgreSQL):

-- SQL chuẩn (hoạt động ở mọi nơi)
SELECT
dept_id,
COUNT(*) AS total,
COUNT(CASE WHEN salary > 80000 THEN 1 END) AS high_earners
FROM employees
GROUP BY dept_id;

-- Đặc thù PostgreSQL (cú pháp gọn hơn)
SELECT
dept_id,
COUNT(*) AS total,
COUNT(*) FILTER (WHERE salary > 80000) AS high_earners
FROM employees
GROUP BY dept_id;

Learn More

  • SQL Joins — kết hợp dữ liệu từ nhiều bảng trước khi tổng hợp
  • Database Indexing — cách index tăng tốc GROUP BY và WHERE
  • ACID Properties — đảm bảo giao dịch (transaction guarantees) cho toàn vẹn dữ liệu
  • Materialized Views — kết quả tổng hợp được tính trước (pre-computed) để tối ưu hiệu năng