Buổi 2: Sắp xếp dữ liệu và các hàm thống kê cơ bản
Buổi 1 giúp chúng ta lấy ra từng dòng thỏa điều kiện. Buổi này chuyển sang câu hỏi mà lãnh đạo và bộ phận phân tích quan tâm hơn: "tổng cộng bao nhiêu?", "trung bình bao nhiêu?", "top 10 là ai?", "mỗi thành phố/mỗi mã có bao nhiêu?". Đó là lúc chúng ta dùng các hàm thống kê (aggregate functions), GROUP BY, HAVING và cách giới hạn/phân trang kết quả – nền tảng của mọi báo cáo số liệu trong công ty chứng khoán.
Lưu ý: cơ sở dữ liệu mẫu (9 bảng
Customers,Accounts,Securities,MarketPrices,Orders,Trades,Portfolio,CustomerStats,AuditLog) đã được mô tả đầy đủ, kèm sơ đồ quan hệ, ở mục "Bộ dữ liệu mẫu của khóa học" trong bài giảng Buổi 1. Hôm nay chủ yếu dùngCustomers,Accounts,Orders,Securities,MarketPrices. "Hôm nay" của bộ dữ liệu là 2026-09-25. Vì chưa họcJOIN(Buổi 8), các bài "tài sản khách hàng" được tính từ bảngAccountsbằngGROUP BY customer_id.
Mục tiêu buổi học
Sau buổi này bạn có thể:
- Sắp xếp kết quả tất định với
ORDER BYnhiều cột, khóa phụ và biểu thức. - Dùng
COUNT,SUM,AVG,MIN,MAX; phân biệtCOUNT(*)vàCOUNT(cột); hiểu cách xử lý NULL và chia số nguyên ở hai hệ. - Giới hạn và phân trang kết quả:
TOP,OFFSET ... FETCH(SQL Server) vàLIMIT ... OFFSET(MySQL); làm bài "Top 10 khách hàng giàu nhất". - Thống kê theo nhóm với
GROUP BY(một hoặc nhiều cột) và lọc nhóm bằngHAVING; phân biệtWHEREvớiHAVING. - Dùng
COUNT(DISTINCT),STRING_AGG/GROUP_CONCAT,ROUND,CASTđể làm báo cáo thống kê chính xác và giống nhau trên hai hệ quản trị.
Bản đồ buổi học
| Phần | Nội dung | Hình thức | Thời lượng |
|---|---|---|---|
| 1 | Ôn tập và mở rộng ORDER BY + bài 2.1 |
5 phút lý thuyết + 18 phút thực hành | 23 phút |
| 2 | Hàm thống kê COUNT/SUM/AVG/MIN/MAX + bài 2.2 |
11 phút lý thuyết + 24 phút thực hành | 35 phút |
| 3–4 | TOP/LIMIT/OFFSET, GROUP BY, ứng dụng Top 10 + bài 2.3 |
18 phút lý thuyết + 22 phút thực hành | 40 phút |
| 5 | HAVING, WHERE vs HAVING + bài 2.4 |
9 phút lý thuyết + 32 phút thực hành | 41 phút |
| 6 | COUNT(DISTINCT), STRING_AGG/GROUP_CONCAT, ROUND/CAST + bài 2.5 |
9 phút lý thuyết + 22 phút thực hành | 31 phút |
| 7–9 | Áp dụng nghiệp vụ, sai lầm thường gặp, tóm tắt, chữa bài, Q&A | Thực hành + thảo luận (làm lại bài khó, đổi chéo chữa bài) | 10 phút |
| Tổng | ~52 phút lý thuyết + demo, ~128 phút thực hành (~71%) | 180 phút |
1. Ôn tập và mở rộng ORDER BY
Ba yêu cầu sắp xếp thường gặp trên danh sách khách hàng, tài khoản. Bấm Chạy thử để xem từng câu:
-- Theo tên A → Z; cùng tên thì theo mã để kết quả ổn định
SELECT customer_id, full_name, city FROM Customers ORDER BY full_name, customer_id;8 Bui Thi Lan Ha Noi
29 Bui Van Manh TP.HCM
19 Cao Van Yen Ha Noi
35 Cong ty CP Chung khoan An Binh Ha Noi
... (40 dòng)-- Theo ngày tham gia, mới nhất trước
SELECT customer_id, full_name, created_date
FROM Customers
ORDER BY created_date DESC, customer_id;5 Hoang Van Em 2026-09-23
26 Hoang Thi Huong 2026-09-23
12 Ly Thi Quynh 2026-09-19
19 Cao Van Yen 2026-09-14
... (40 dòng)-- Theo số dư, cao nhất trước
SELECT account_id, account_no, balance FROM Accounts ORDER BY balance DESC, account_id;4 ACC00004 8600000000.00
35 ACC00035 8600000000.00
93 ACC00093 8600000000.00
11 ACC00011 3400000000.00
33 ACC00033 3400000000.00
... (93 dòng)Nhắc lại ba điều quan trọng:
ORDER BYluôn nằm cuối câu lệnh (trước phần giới hạn số dòng) và là mệnh đề duy nhất được dùng bí danh cột trên cả hai hệ.- Có thể sắp xếp theo biểu thức (ví dụ ngay bên dưới).
- Luôn thêm khóa phụ duy nhất (thường là khóa chính) khi có khả năng đồng hạng.
Ví dụ sắp xếp theo biểu thức – lệnh có giá trị (khối lượng × giá) lớn nhất trước:
SELECT order_id, symbol, quantity, price, quantity * price AS gia_tri
FROM Orders
ORDER BY quantity * price DESC, order_id;7 VCB 5000 93000.00 465000000.00
104 VCB 5000 93000.00 465000000.00
15 VCS 5000 61000.00 305000000.00
107 VCS 5000 61000.00 305000000.00
... (120 dòng)Cảnh báo: trong dữ liệu mẫu, ba tài khoản
ACC00004,ACC00035,ACC00093đều có số dư đúng 8,6 tỷ. Nếu chỉORDER BY balance DESC, thứ tự giữa ba tài khoản này không được đảm bảo và có thể khác nhau giữa SQL Server và MySQL – bài chấm tự động sẽ báo sai dù logic của bạn "đúng".
Thực hành: vừa ôn xong ORDER BY với khóa phụ, hãy làm bài 2.1 để sắp xếp danh sách khách hàng theo tên, theo ngày tham gia và tài khoản theo số dư.
2. Các hàm thống kê tổng hợp cơ bản
Hàm thống kê nhận nhiều dòng và trả về một giá trị.
| Hàm | Ý nghĩa | Ghi chú |
|---|---|---|
COUNT(*) |
Đếm số dòng | Đếm cả dòng có NULL |
COUNT(cột) |
Đếm dòng mà cột khác NULL |
Bỏ qua NULL |
SUM(cột) |
Tổng | Bỏ qua NULL; không có dòng nào → NULL |
AVG(cột) |
Trung bình | Bỏ qua NULL (không tính vào mẫu số) |
MIN(cột), MAX(cột) |
Nhỏ nhất, lớn nhất | Dùng được cho số, chuỗi, ngày |
2.1. Ví dụ tổng hợp trên bảng Accounts
SELECT COUNT(*) AS so_tai_khoan,
SUM(balance) AS tong_so_du,
MIN(balance) AS nho_nhat,
MAX(balance) AS lon_nhat
FROM Accounts;93 66987000000.00 3000000.00 8600000000.00Một câu SELECT chỉ dùng hàm thống kê (không có GROUP BY) luôn trả về đúng một dòng. Kết hợp WHERE để thống kê trên một tập con:
-- Chỉ tính tài khoản đang hoạt động
SELECT COUNT(*) AS so_tai_khoan, SUM(balance) AS tong_so_du
FROM Accounts
WHERE status = 'ACTIVE';88 63932000000.00WHERE được áp dụng trước khi thống kê: chỉ những dòng còn lại mới được đếm/cộng.
2.2. COUNT(*) khác COUNT(cột)
Cột email có 5 giá trị NULL trong 40 khách hàng:
SELECT COUNT(*) AS tong_khach_hang, COUNT(email) AS co_email
FROM Customers;40 35Đếm số khách hàng đang hoạt động:
SELECT COUNT(*) AS so_khach_hang_active FROM Customers WHERE status = 'ACTIVE';31SQL Server vs MySQL: khi hàm thống kê bỏ qua NULL, SQL Server in thêm thông báo
Warning: Null value is eliminated by an aggregate or other SET operation.– đó chỉ là cảnh báo, kết quả vẫn đúng. Muốn tắt, thêmSET ANSI_WARNINGS OFF;ở đầu script. MySQL không có cảnh báo này.
2.3. Ba bẫy của SUM/AVG
Bẫy 1 – Tập rỗng. SUM trên tập rỗng trả về NULL chứ không phải 0, trong khi COUNT(*) trả về 0:
SELECT COUNT(*) AS so_dong, SUM(balance) AS tong_so_du, COALESCE(SUM(balance), 0) AS tong_hoac_0
FROM Accounts
WHERE status = 'KHONG_TON_TAI';0 NULL 0.00Bẫy 2 – Trung bình bỏ qua NULL. AVG(cột) = tổng ÷ số dòng có giá trị, không phải tổng ÷ COUNT(*).
Bẫy 3 – Chia số nguyên (khác biệt lớn giữa hai hệ). T-SQL chia hai số nguyên cho ra số nguyên:
| Biểu thức | SQL Server | MySQL |
|---|---|---|
7 / 2 |
3 (chia nguyên) |
3.5000 |
AVG(quantity) với quantity là INT |
1280 (mất phần lẻ) |
1280.8333 |
AVG(1.0 * quantity) |
1280.833333 |
1280.8333 |
SQL Server:
SELECT 7 / 2 AS chia_nguyen, 7 / 2.0 AS chia_thap_phan, 7.0 / 2 AS chia_thap_phan_2;3 3.500000 3.500000MySQL:
SELECT 7 / 2 AS chia_thuong, 7 DIV 2 AS chia_nguyen;3.5000 3Cùng biểu thức 7 / 2 nhưng SQL Server cho 3, MySQL cho 3.5000 – đây chính là bẫy khi chuyển câu lệnh giữa hai hệ. MySQL muốn chia nguyên phải dùng toán tử DIV.
Mẹo: quy tắc vàng – nếu cần kết quả thập phân, nhân với
1.0(1.0 * a / b,100.0 * a / b) và làm tròn/ép kiểu rõ ràng (mục 6.3). Cách này chạy giống nhau trên cả hai hệ.
Ví dụ nghiệp vụ: "tài sản trung bình mỗi khách hàng" không phải là
AVG(balance)– đó là số dư trung bình mỗi tài khoản. Vì một khách hàng có nhiều tài khoản, phải lấy tổng số dư chia cho số khách hàng khác nhau:
SELECT COUNT(DISTINCT customer_id) AS so_khach_hang,
SUM(balance) AS tong_so_du,
CAST(ROUND(SUM(balance) / COUNT(DISTINCT customer_id), 2) AS DECIMAL(18,2)) AS tb_moi_khach,
CAST(ROUND(AVG(balance), 2) AS DECIMAL(18,2)) AS tb_moi_tai_khoan
FROM Accounts
WHERE status = 'ACTIVE';35 63932000000.00 1826628571.43 726500000.00Nhìn kết quả: trung bình mỗi khách hàng nắm ~1,83 tỷ, mỗi tài khoản trung bình ~0,73 tỷ – hai con số trả lời hai câu hỏi khác nhau.
MIN/MAX dùng được cho ngày và chuỗi – ví dụ ngày mở hồ sơ sớm nhất và muộn nhất:
SELECT MIN(created_date) AS som_nhat, MAX(created_date) AS muon_nhat FROM Customers;2018-06-18 2026-09-23Thực hành: vừa học xong COUNT, SUM, AVG, MIN, MAX, hãy làm bài 2.2 để thống kê tài sản trên tài khoản đang hoạt động, tính tài sản trung bình mỗi khách hàng và đếm khách hàng đang hoạt động.
3. Mệnh đề giới hạn số dòng: TOP, LIMIT và OFFSET
Khi chỉ cần "N dòng đầu" (top 10, 5 lệnh lớn nhất...) hoặc lấy dữ liệu theo từng trang, chúng ta phải giới hạn kết quả sau khi đã sắp xếp. Đây là chỗ hai hệ khác cú pháp.
3.1. Lấy N hàng đầu tiên
SQL Server – TOP ngay sau SELECT:
SELECT TOP 5 customer_id, total_trade_value, trade_count
FROM CustomerStats
ORDER BY total_trade_value DESC, customer_id;MySQL – LIMIT ở cuối câu lệnh:
SELECT customer_id, total_trade_value, trade_count
FROM CustomerStats
ORDER BY total_trade_value DESC, customer_id
LIMIT 5;Cả hai cùng cho năm khách hàng có tổng giá trị giao dịch lớn nhất:
16 703300000.00 6
36 422600000.00 4
22 420525000.00 7
35 256600000.00 4
5 220400000.00 5Chuẩn ANSI – dùng được ở SQL Server 2012+ (không có ở MySQL): OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY.
| SQL Server | MySQL | |
|---|---|---|
| Lấy n dòng đầu | SELECT TOP n ... (ngay sau SELECT) |
... LIMIT n (cuối câu lệnh) |
| Bỏ qua k dòng | OFFSET k ROWS |
OFFSET k |
| Đồng hạng | TOP n WITH TIES |
không có |
Lưu ý quan trọng:
- Luôn có
ORDER BYkhi dùngTOP/LIMIT; không có nó, "5 dòng đầu" là 5 dòng bất kỳ. - Đồng hạng: ba tài khoản 8,6 tỷ đang cùng đứng đầu.
SELECT TOP 2 WITH TIES ... ORDER BY balance DESC(chỉ SQL Server) trả về 3 dòng (lấy thêm mọi dòng bằng dòng thứ 2), cònTOP 2/LIMIT 2cắt cứng đúng 2 dòng và nếu thiếu khóa phụ thì việc 2 trong 3 tài khoản nào được chọn là không xác định.
Cảnh báo: bài tập của khóa không dùng
WITH TIESmà luôn thêm khóa phụ để top-N tất định trên cả hai hệ.TOPkhông kèmORDER BYlà lỗi logic phổ biến nhất khi làm báo cáo "top N".
3.2. Phân trang bằng OFFSET
Một màn hình quản trị không hiện 40, hay 40.000 khách hàng cùng lúc mà chia thành từng trang. Công thức: bỏ qua (số_trang − 1) × kích_thước_trang dòng rồi lấy kích_thước_trang dòng.
SQL Server – OFFSET ... FETCH (bắt buộc đi cùng ORDER BY, đặt sau ORDER BY):
-- Trang 2, mỗi trang 10 khách hàng
SELECT customer_id, full_name
FROM Customers
ORDER BY customer_id
OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;MySQL:
-- Trang 2, mỗi trang 10 khách hàng
SELECT customer_id, full_name
FROM Customers
ORDER BY customer_id
LIMIT 10 OFFSET 10; -- hoặc: LIMIT 10, 10 (cú pháp LIMIT offset, số_dòng)Cùng cho mười khách hàng có customer_id 11–20:
11 Duong Van Phong
12 Ly Thi Quynh
13 Trinh Van Son
14 Phan Thi Thao
... (10 dòng)| Trang (10 dòng/trang) | Bỏ qua | SQL Server | MySQL |
|---|---|---|---|
| 1 | 0 | OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY |
LIMIT 10 OFFSET 0 |
| 2 | 10 | OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY |
LIMIT 10 OFFSET 10 |
| 3 | 20 | OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY |
LIMIT 10 OFFSET 20 |
| n, kích thước k | (n−1)·k | OFFSET (n-1)*k ROWS FETCH NEXT k ROWS ONLY |
LIMIT k OFFSET (n-1)*k |
Sai lầm thường gặp khi phân trang:
| Sai lầm | Hậu quả | Cách đúng |
|---|---|---|
ORDER BY created_date DESC nhưng nhiều khách hàng cùng ngày |
Một dòng có thể xuất hiện ở cả hai trang hoặc biến mất | Thêm khóa duy nhất: ORDER BY created_date DESC, customer_id |
| Không xử lý trang cuối chưa đầy | Với 40 khách hàng và 15 dòng/trang, trang 3 chỉ có 10 dòng, trang 4 rỗng | Ứng dụng phải xử lý kết quả ít dòng hoặc rỗng |
OFFSET rất lớn trên bảng hàng triệu dòng |
Chậm (vẫn phải duyệt rồi bỏ qua các dòng đầu) | Kỹ thuật "keyset": WHERE id > id_cuối_trang_trước |
Lưu ý: bài 2.3 gồm cả phân trang (mục này) lẫn Top 10 khách hàng giàu nhất (cần
GROUP BY, mục 4.5). Chúng ta sẽ làm bài 2.3 ngay sau khi học xongGROUP BYđể làm trọn cả hai.
4. Mệnh đề GROUP BY
Hàm thống kê ở mục 2 gom cả bảng thành một dòng. Thực tế ta thường muốn thống kê theo từng nhóm: mỗi thành phố, mỗi loại tài khoản, mỗi mã cổ phiếu. GROUP BY chia các dòng thành các nhóm có cùng giá trị ở cột nhóm, rồi hàm thống kê được tính riêng trong từng nhóm.
Hình dung với 6 tài khoản của hai khách hàng 2 và 3, khi nhóm theo customer_id và tính SUM(balance):
| Bước 1 – dòng gốc | Bước 2 – chia nhóm theo customer_id |
Bước 3 – SUM(balance) |
||
|---|---|---|---|---|
| ACC00002 | customer 2 | 8.000.000 | Nhóm customer 2 (3 dòng) | 8.640.000.000 |
| ACC00003 | customer 2 | 32.000.000 | ↑ | ↑ |
| ACC00004 | customer 2 | 8.600.000.000 | ↑ | ↑ |
| ACC00005 | customer 3 | 3.000.000 | Nhóm customer 3 (3 dòng) | 168.000.000 |
| ACC00006 | customer 3 | 67.000.000 | ↑ | ↑ |
| ACC00007 | customer 3 | 98.000.000 | ↑ | ↑ |
Kết quả cuối cùng chỉ còn một dòng cho mỗi nhóm.
Hình dưới cho thấy GROUP BY làm gì với dữ liệu: các dòng cùng giá trị cột nhóm được gom lại, hàm tổng hợp tính trên từng nhóm, cuối cùng HAVING giữ hoặc loại cả nhóm.
Accounts (các dòng gốc)
| account_type | balance |
|---|---|
| CASH | 50 |
| MARGIN | 30 |
| CASH | 70 |
| MARGIN | 10 |
| CASH | 20 |
GROUP BY account_type
SUM(balance) → HAVING SUM > 50
| account_type | tong |
|---|---|
| CASH | 140 |
| MARGIN (bị loại: 40) | — |
Thứ tự: WHERE lọc dòng → GROUP BY chia nhóm → hàm tổng hợp (SUM, COUNT...) tính mỗi nhóm → HAVING giữ lại các nhóm thỏa điều kiện.
4.1. Nhóm theo một cột
SELECT city, COUNT(*) AS so_khach_hang
FROM Customers
GROUP BY city
ORDER BY so_khach_hang DESC, city;Ha Noi 22
TP.HCM 7
Da Nang 5
Can Tho 3
Hai Phong 3Đọc câu lệnh: "chia khách hàng theo city, với mỗi thành phố đếm số dòng, rồi sắp xếp".
4.2. Nhóm theo nhiều cột
Mỗi tổ hợp giá trị của các cột nhóm tạo thành một nhóm:
SELECT city, customer_type, COUNT(*) AS so_khach_hang
FROM Customers
GROUP BY city, customer_type
ORDER BY city, customer_type;Can Tho INDIVIDUAL 3
Da Nang INDIVIDUAL 5
Ha Noi INDIVIDUAL 16
Ha Noi INSTITUTION 6
Hai Phong INDIVIDUAL 3
TP.HCM INDIVIDUAL 3
TP.HCM INSTITUTION 44.3. Kết hợp GROUP BY với nhiều hàm thống kê
SELECT account_type,
COUNT(*) AS so_tai_khoan,
SUM(balance) AS tong_so_du,
MAX(balance) AS lon_nhat
FROM Accounts
WHERE status = 'ACTIVE'
GROUP BY account_type
ORDER BY account_type;CASH 53 45560000000.00 8600000000.00
MARGIN 35 18372000000.00 3400000000.004.4. Lưu ý khi dùng GROUP BY
Buổi 1 đã giới thiệu thứ tự xử lý logic của câu SELECT (hình ở mục 4.3 Buổi 1). Giờ hai bước còn thiếu trong hình đã có vai trò:
| Bước | Mệnh đề | Việc làm |
|---|---|---|
| 1 | FROM |
Lấy bảng nguồn |
| 2 | WHERE |
Lọc dòng |
| 3 | GROUP BY |
Chia dòng còn lại thành nhóm |
| 4 | HAVING |
Lọc nhóm (học ở mục 5) |
| 5 | SELECT |
Tính hàm thống kê, chọn cột, đặt bí danh |
| 6 | ORDER BY |
Sắp xếp kết quả |
| 7 | TOP / LIMIT |
Cắt bớt dòng |
Từ thứ tự này suy ra các quy tắc sau:
- Quy tắc cột trong SELECT: mọi cột trong
SELECTphải là (a) cột có trongGROUP BYhoặc (b) nằm trong một hàm thống kê. CâuSELECT customer_id, city FROM Customers GROUP BY citybáo lỗi trên cả SQL Server lẫn MySQL 8 (ONLY_FULL_GROUP_BY): mỗi thành phố có nhiều khách hàng, nêncustomer_idnào? - NULL cũng là một nhóm:
GROUP BY referrer_idcho một nhómNULLgồm 32 khách hàng chưa có người giới thiệu (ví dụ ngay bên dưới). WHERElọc dòng trước khi nhóm; điều kiện trên kết quả thống kê phải để sau (mục 5).- Bí danh:
ORDER BY so_khach_hangđược cả hai hệ; nhưngGROUP BYbằng bí danh chỉ MySQL chấp nhận – hãy viết lại biểu thức/cột gốc trongGROUP BYđể chạy được ở cả hai. GROUP BYkhông tự sắp xếp kết quả – luôn thêmORDER BYnếu cần thứ tự.
Ví dụ nhóm NULL:
SELECT referrer_id, COUNT(*) AS so_khach_hang
FROM Customers
GROUP BY referrer_id
ORDER BY referrer_id;NULL 32
1 2
2 1
3 2
8 2
31 1Cảnh báo: đừng "chữa cháy" lỗi
GROUP BYbằng cách thêm mọi cột vàoGROUP BY– bạn sẽ chia nhóm mịn đến mức mỗi nhóm chỉ có 1 dòng và số liệu mất ý nghĩa.
4.5. Ứng dụng: Top 10 khách hàng có tài sản lớn nhất
"Tài sản" của một khách hàng = tổng số dư các tài khoản đang hoạt động. Chưa có JOIN, ta nhóm theo customer_id trên bảng Accounts, sắp xếp giảm dần rồi lấy 10 dòng đầu.
SQL Server:
SELECT TOP 10 customer_id, SUM(balance) AS tong_tai_san
FROM Accounts
WHERE status = 'ACTIVE'
GROUP BY customer_id
ORDER BY tong_tai_san DESC, customer_id;MySQL:
SELECT customer_id, SUM(balance) AS tong_tai_san
FROM Accounts
WHERE status = 'ACTIVE'
GROUP BY customer_id
ORDER BY tong_tai_san DESC, customer_id
LIMIT 10;Cùng cho kết quả:
17 12067000000.00
40 9500000000.00
2 8640000000.00
34 4690000000.00
38 4661000000.00
32 4345000000.00
5 3432000000.00
31 1735000000.00
6 1600000000.00
30 1402000000.00Khóa phụ customer_id bảo đảm nếu hai khách hàng cùng tổng tài sản thì luôn xếp theo mã. (Ở Buổi 8 ta sẽ ghép thêm bảng Customers để hiện tên khách hàng.)
Thực hành: vừa học xong TOP/LIMIT, phân trang và GROUP BY, hãy làm bài 2.3 – lấy Top 10 khách hàng có tài sản lớn nhất và phân trang danh sách khách hàng.
5. Mệnh đề HAVING
Sau khi nhóm, ta thường muốn lọc theo kết quả thống kê của nhóm: "chỉ giữ thành phố có ít nhất 5 khách hàng", "khách hàng có tổng tài sản trên 4 tỷ". WHERE không làm được việc này vì WHERE chạy trước khi nhóm, chưa có COUNT/SUM.
WHERE |
HAVING |
|
|---|---|---|
| Lọc cái gì | từng dòng | từng nhóm |
| Chạy khi nào | trước GROUP BY |
sau GROUP BY |
| Dùng được hàm thống kê | Không | Có |
| Dùng được cột thường | Có | Chỉ cột trong GROUP BY hoặc hàm thống kê |
-- Thành phố có ít nhất 5 khách hàng
SELECT city, COUNT(*) AS so_khach_hang
FROM Customers
GROUP BY city
HAVING COUNT(*) >= 5
ORDER BY so_khach_hang DESC;Ha Noi 22
TP.HCM 7
Da Nang 5Ứng dụng thực tế: WHERE + HAVING cùng lúc
"Khách hàng có tổng tài sản (tài khoản ACTIVE) trên 4 tỷ":
SELECT customer_id, COUNT(*) AS so_tai_khoan, SUM(balance) AS tong_so_du
FROM Accounts
WHERE status = 'ACTIVE' -- lọc dòng trước: bỏ tài khoản đã đóng
GROUP BY customer_id
HAVING SUM(balance) > 4000000000 -- lọc nhóm sau: giữ khách hàng lớn
ORDER BY tong_so_du DESC, customer_id;17 3 12067000000.00
40 3 9500000000.00
2 3 8640000000.00
34 4 4690000000.00
38 4 4661000000.00
32 4 4345000000.00Mẹo: điều kiện chỉ dùng cột của từng dòng (trạng thái, loại, ngày...) → đặt ở
WHERE(nhanh hơn vì loại bỏ dòng sớm); điều kiện dùng kết quảCOUNT/SUM/AVG/MIN/MAX→HAVING.
Sai lầm thường gặp:
| Sai lầm | Hậu quả | Cách đúng |
|---|---|---|
HAVING status = 'ACTIVE' khi status không có trong GROUP BY |
Lỗi | Đưa điều kiện dòng lên WHERE |
Dùng bí danh trong HAVING (HAVING tong > 4000000000) |
MySQL chấp nhận, SQL Server báo Invalid column name |
Lặp lại biểu thức: HAVING SUM(balance) > 4000000000 |
Dùng hàm thống kê trong WHERE (WHERE COUNT(*) > 3) |
Lỗi trên cả hai hệ | Chuyển sang HAVING |
Thực hành: vừa học xong GROUP BY nhiều cột và HAVING, hãy làm bài 2.4 – thống kê khách hàng theo loại/trạng thái, lọc khách hàng nhiều tài khoản tài sản lớn và làm báo cáo giá trị khớp lệnh theo mã. Bài 2.4c (hard) còn dùng COUNT(DISTINCT) và ROUND/CAST – bạn đã gặp ở mục 2.3, và sẽ học kỹ hơn ở mục 6.
6. Các hàm tổng hợp nâng cao
6.1. COUNT(DISTINCT ...) – đếm giá trị duy nhất
COUNT(cột) đếm số dòng; COUNT(DISTINCT cột) đếm số giá trị khác nhau.
SELECT COUNT(*) AS so_lenh,
COUNT(DISTINCT account_id) AS so_tai_khoan_da_dat_lenh,
COUNT(DISTINCT symbol) AS so_ma_duoc_giao_dich
FROM Orders;120 59 12120 lệnh do 59 tài khoản khác nhau đặt, trên đủ 12 mã. Thường dùng theo nhóm, ví dụ trên từng trạng thái lệnh:
SELECT status, COUNT(*) AS so_lenh,
COUNT(DISTINCT account_id) AS so_tai_khoan,
COUNT(DISTINCT symbol) AS so_ma
FROM Orders
GROUP BY status
ORDER BY status;CANCELLED 17 14 9
MATCHED 69 42 12
PARTIAL 17 16 10
PENDING 17 14 9Lưu ý:
COUNT(DISTINCT ...)bỏ qua NULL và là phép tính "đắt" trên bảng lớn (phải loại trùng).
6.2. Nối chuỗi trong nhóm: STRING_AGG / GROUP_CONCAT
Đôi khi ta muốn gộp các giá trị của một nhóm thành một chuỗi (ví dụ mỗi sàn có những mã nào). Hai hệ dùng hàm khác tên:
SQL Server (2017+) – STRING_AGG(cột, dấu_phân_cách), sắp xếp phần tử bằng WITHIN GROUP (ORDER BY ...):
SELECT exchange, COUNT(*) AS so_ma,
STRING_AGG(symbol, ',') WITHIN GROUP (ORDER BY symbol) AS danh_sach_ma
FROM Securities
GROUP BY exchange
ORDER BY exchange;MySQL – GROUP_CONCAT(cột ORDER BY ... SEPARATOR 'dấu'):
SELECT exchange, COUNT(*) AS so_ma,
GROUP_CONCAT(symbol ORDER BY symbol SEPARATOR ',') AS danh_sach_ma
FROM Securities
GROUP BY exchange
ORDER BY exchange;Cùng kết quả:
HNX 8 CEO,IDC,MBS,NVB,PVI,PVS,SHS,VCS
HOSE 4 FPT,HPG,VCB,VNMMột ví dụ khác – liệt kê số tài khoản của từng khách hàng:
SQL Server:
SELECT customer_id, STRING_AGG(account_no, ', ') WITHIN GROUP (ORDER BY account_no) AS cac_tai_khoan
FROM Accounts
WHERE customer_id IN (2, 3, 5)
GROUP BY customer_id
ORDER BY customer_id;MySQL:
SELECT customer_id, GROUP_CONCAT(account_no ORDER BY account_no SEPARATOR ', ') AS cac_tai_khoan
FROM Accounts
WHERE customer_id IN (2, 3, 5)
GROUP BY customer_id
ORDER BY customer_id;2 ACC00002, ACC00003, ACC00004
3 ACC00005, ACC00006, ACC00007
5 ACC00010, ACC00011Cảnh báo: nếu không chỉ định thứ tự (
WITHIN GROUP/ORDER BYtrong hàm), thứ tự phần tử trong chuỗi không được đảm bảo và có thể khác nhau giữa hai hệ → luôn chỉ định để kết quả tất định.NULLbị bỏ qua khi nối. MySQL giới hạn độ dài mặc định củaGROUP_CONCAT(1024 ký tự) – đủ cho các bài của khóa.
Thực hành: vừa học xong COUNT(DISTINCT) và STRING_AGG/GROUP_CONCAT, hãy làm bài 2.5 – thống kê lệnh theo trạng thái và gộp danh sách mã chứng khoán theo sàn.
6.3. Định dạng số trong thống kê: ROUND và CAST
ROUND(x, n)làm trònxđếnnchữ số thập phân;nâm làm tròn về hàng chục/trăm/nghìn (ROUND(1250000, -3)→ làm tròn đến nghìn).CAST(biểu_thức AS kiểu)ép kiểu, ví dụCAST(x AS DECIMAL(18,2))– tổng 18 chữ số, trong đó 2 chữ số thập phân.
Vì sao cần cả hai? Hai hệ in kết quả ROUND khác nhau: với ROUND(1280.8333..., 2), SQL Server in 1280.830000 (giữ số chữ số thập phân của kiểu gốc) còn MySQL in 1280.83. Bọc thêm CAST(... AS DECIMAL(18,2)) để cả hai in đúng hai chữ số:
SELECT CAST(ROUND(AVG(1.0 * quantity), 2) AS DECIMAL(18,2)) AS khoi_luong_tb
FROM Orders;1280.83Ví dụ báo cáo thị trường – thống kê giá của từng mã trong 10 phiên gần nhất:
SELECT symbol,
MAX(high_price) AS cao_nhat,
MIN(low_price) AS thap_nhat,
CAST(ROUND(AVG(close_price), 2) AS DECIMAL(18,2)) AS dong_cua_tb,
SUM(volume) AS tong_khoi_luong
FROM MarketPrices
GROUP BY symbol
ORDER BY symbol;CEO 22110.00 19030.00 20306.00 43500000
FPT 126600.00 115800.00 121440.00 53700000
HPG 27675.00 26055.00 27027.00 41800000
IDC 54750.00 49750.00 51950.00 54700000
... (12 dòng)Đổi đơn vị sang tỷ đồng: CAST(ROUND(SUM(balance) / 1000000000.0, 2) AS DECIMAL(18,2)) (chia cho 1000000000.0 – có .0 để không bị chia nguyên):
SELECT CAST(ROUND(SUM(balance) / 1000000000.0, 2) AS DECIMAL(18,2)) AS tong_so_du_ty
FROM Accounts
WHERE status = 'ACTIVE';63.93SQL Server vs MySQL:
CAST(3.5 AS INT)cho3(cắt phần lẻ) trong SQL Server, còn MySQL không có kiểuINTtrongCASTmà dùngCAST(3.5 AS SIGNED)và cho4(làm tròn). Để tránh sai khác, hãy dùngROUNDrõ ràng rồi mới ép kiểu, hoặc ép sangDECIMALnhư trên.
SQL Server:
SELECT CAST(3.5 AS INT) AS ep_kieu_int;3MySQL:
SELECT CAST(3.5 AS SIGNED) AS ep_kieu_signed;47. Áp dụng: từ câu hỏi nghiệp vụ đến báo cáo
Yêu cầu: "Với các lệnh đã khớp hết, cho biết mỗi mã có bao nhiêu lệnh, tổng giá trị (khối lượng × giá), và chỉ giữ những mã có từ 5 lệnh trở lên; mã giá trị lớn nhất lên đầu."
Bóc tách: bảng Orders, WHERE status = 'MATCHED'; nhóm theo symbol; thống kê COUNT(*), SUM(quantity * price); lọc nhóm bằng HAVING COUNT(*) >= 5; sắp ORDER BY giá trị giảm dần, thêm symbol làm khóa phụ.
SELECT symbol, COUNT(*) AS so_lenh, SUM(quantity * price) AS gia_tri
FROM Orders
WHERE status = 'MATCHED'
GROUP BY symbol
HAVING COUNT(*) >= 5
ORDER BY gia_tri DESC, symbol;VCS 6 803950000.00
VCB 8 563650000.00
VNM 9 514050000.00
PVS 8 383450000.00
MBS 6 202850000.00
... (9 dòng)Trước khi nộp bài, hãy tự kiểm tra ba điều: (1) cột trong SELECT đã nằm trong GROUP BY hoặc hàm thống kê chưa? (2) điều kiện đặt đúng chỗ WHERE/HAVING chưa? (3) ORDER BY đã đủ khóa phụ để kết quả không phụ thuộc thứ tự vật lý chưa?
Sai lầm thường gặp
| Sai lầm | Hậu quả | Cách đúng |
|---|---|---|
Nhầm AVG(balance) với "trung bình mỗi khách hàng" |
Số trung bình mỗi tài khoản – sai đơn vị | SUM(balance) / COUNT(DISTINCT customer_id) |
Chia số nguyên trên SQL Server (AVG của INT, a / b) |
Mất phần thập phân, hai hệ cho kết quả khác nhau | Nhân 1.0, bọc CAST(ROUND(..., 2) AS DECIMAL(18,2)) |
Cột không nằm trong GROUP BY cũng không trong hàm thống kê |
Lỗi (ONLY_FULL_GROUP_BY) |
Thêm vào GROUP BY nếu cần chia nhóm mịn hơn, hoặc bọc bằng hàm thống kê |
Lọc theo hàm thống kê trong WHERE, hoặc lọc điều kiện dòng trong HAVING |
Lỗi hoặc chậm | Điều kiện dòng → WHERE; điều kiện nhóm → HAVING |
TOP/LIMIT/phân trang không có ORDER BY hoặc thiếu khóa phụ |
Dòng lặp/thiếu giữa các trang, chấm sai | ORDER BY luôn có khóa duy nhất ở cuối |
STRING_AGG/GROUP_CONCAT không chỉ định thứ tự |
Chuỗi khác nhau giữa hai hệ | WITHIN GROUP (ORDER BY ...) / ORDER BY ... SEPARATOR |
Quên SUM trên tập rỗng cho NULL |
Báo cáo hiện NULL thay vì 0 |
COALESCE(SUM(...), 0) |
Tóm tắt
| Nội dung | Ghi nhớ |
|---|---|
| Hàm thống kê | COUNT, SUM, AVG, MIN, MAX; bỏ qua NULL (trừ COUNT(*)); SUM trên tập rỗng là NULL |
COUNT(*) vs COUNT(cột) |
Đếm mọi dòng vs đếm dòng khác NULL |
| Chia số nguyên | SQL Server chia nguyên (AVG của INT, 7/2); nhân 1.0 để có phần thập phân |
| Top-N | SQL Server SELECT TOP n ...; MySQL ... LIMIT n; luôn có ORDER BY + khóa phụ |
| Phân trang | SQL Server OFFSET (n-1)*k ROWS FETCH NEXT k ROWS ONLY; MySQL LIMIT k OFFSET (n-1)*k |
GROUP BY |
Chia nhóm; cột trong SELECT phải thuộc GROUP BY hoặc hàm thống kê |
WHERE vs HAVING |
Lọc dòng (trước nhóm) vs lọc nhóm (sau nhóm, dùng được hàm thống kê) |
COUNT(DISTINCT x) |
Đếm giá trị khác nhau |
| Nối chuỗi trong nhóm | SQL Server STRING_AGG(x, ',') WITHIN GROUP (ORDER BY x); MySQL GROUP_CONCAT(x ORDER BY x SEPARATOR ',') |
| Định dạng số | CAST(ROUND(x, 2) AS DECIMAL(18,2)) để hai hệ in giống nhau |
Gợi ý mở rộng
- Thử tìm mã có khối lượng khớp trung bình mỗi lệnh cao nhất:
GROUP BY symboltrênOrders(chú ý chia số nguyên) rồiTOP 1/LIMIT 1. - Thống kê theo tháng:
GROUP BY YEAR(order_date), MONTH(order_date)–YEAR/MONTHgiống nhau ở hai hệ (Buổi 4 sẽ học kỹ các hàm ngày). - Trong bảng
CustomerStats, tính giá trị giao dịch trung bình mỗi lần giao dịch (total_trade_value / trade_count) cho các khách hàng cótrade_count > 0– chú ý tránh chia cho 0. - Đọc thêm:
GROUP BY ... WITH ROLLUP(MySQL) vàGROUP BY ROLLUP(...)(SQL Server) để có dòng tổng cộng trong báo cáo; hàm cửa sổ (Buổi 3) sẽ giải quyết những bài "top N trong mỗi nhóm".
Bài tập thực hành của buổi
Danh sách đầy đủ các bài của buổi (bạn đã gặp từng nhóm ngay trong bài giảng), nộp trên Codelab bằng SQL Server hoặc MySQL: