Mệnh đề PARTITION BY trong MySQL
Xem thửTình huống mở đầu
Mai làm ở nhóm chăm sóc khách hàng. Bảng orders có 6 đơn của 3 khách, và sáng nay cô cần gọi điện cho mỗi khách đúng một lần, kèm số tiền của đơn mới nhất để hỏi thăm. Cô viết GROUP BY customer với MAX(placed_at) và lấy được ngày đơn mới nhất của cả ba khách. Nhưng khi cô thêm cột total vào cùng câu lệnh, MySQL báo lỗi 1055 và từ chối chạy. Cô cần 3 dòng, mỗi dòng có đủ ngày lẫn số tiền, trong khi cả bảng chỉ có 6 dòng.
Hãy đoán trước: có cách nào giữ nguyên sáu dòng, gắn thêm cho mỗi dòng một số thứ tự riêng theo khách, rồi chỉ chọn dòng số 1 không? Cuối bài bạn sẽ có câu lệnh làm đúng điều đó.
Ôn nhanh bài trước: hai đơn có cùng total trong ORDER BY total DESC có thứ tự cố định không, và cách chữa là gì?
Đáp án: không được bảo đảm, nên thêm một khóa duy nhất như id vào cuối. Bài này dùng lại ý tưởng đó: ORDER BY xuất hiện bên trong cả hàm cửa sổ.
Mục tiêu bài học
Sau bài này bạn có thể:
- Giải thích khác biệt giữa
GROUP BY(gộp dòng) và hàm cửa sổ vớiPARTITION BY(giữ nguyên dòng, thêm cột). - Viết
ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)và dự đoán số thứ tự của từng dòng. - Lấy dòng đầu tiên của mỗi nhóm bằng cách bọc truy vấn trong một truy vấn con.
- Giải thích vì sao hàm cửa sổ không dùng được trực tiếp trong
WHERE.
Bức tranh lớn
Xuyên suốt bài, bảng orders là một lớp học và PARTITION BY chia lớp thành các tổ. Mỗi tổ tự xếp hàng và tự đánh số, học sinh vẫn đứng nguyên chỗ trong lớp. Còn GROUP BY giống như mỗi tổ chỉ cử một đại diện lên bảng. Dự án xuyên suốt là danh sách gọi điện cho khách của nhóm chăm sóc, bảng orders có sáu đơn của Ann, Binh và Chi.
Hình dưới trả lời một câu hỏi: cùng một bảng sáu dòng, GROUP BY và hàm cửa sổ cho ra kết quả khác nhau thế nào?
Đang tải sơ đồ…
Cách đọc hình: đi từ ô xanh dương xuống hai nhánh. Nhánh trái là GROUP BY: ba dòng, mỗi khách một dòng, nên ô đỏ nhắc rằng thông tin của từng đơn đã mất. Nhánh phải là hàm cửa sổ: vẫn sáu dòng và thêm một cột tính theo nhóm, ô xanh lá.
Chỗ ẩn dụ không còn đúng: tổ trong lớp học là cố định, còn nhóm của PARTITION BY chỉ tồn tại trong lúc câu lệnh chạy, và bạn có thể chia lại theo cột khác ở câu lệnh sau.
Khái niệm cốt lõi
Bài này có bốn khái niệm.
1. Hàm cửa sổ giữ nguyên dòng
Hàm cửa sổ là hàm tính trên một nhóm dòng nhưng kết quả được gắn lại cho từng dòng của nhóm, không gộp. Cú pháp đặc trưng là hàm(...) OVER (...). Vì không gộp, bạn vẫn dùng được mọi cột của dòng gốc.
2. PARTITION BY chia dòng thành các nhóm
PARTITION BY customer chia các dòng theo giá trị customer. Hàm được tính riêng trong từng nhóm và bắt đầu lại ở nhóm kế tiếp. Nếu bỏ PARTITION BY, cả bảng là một nhóm duy nhất.
3. ORDER BY bên trong OVER quyết định thứ tự trong nhóm
ROW_NUMBER() OVER (PARTITION BY customer ORDER BY placed_at DESC) đánh số từng nhóm theo ngày giảm dần. ORDER BY này chỉ ảnh hưởng cách tính, không sắp xếp kết quả cuối cùng. Muốn kết quả có thứ tự, vẫn cần ORDER BY ở ngoài cùng.
4. Hàm cửa sổ chỉ chạy sau WHERE
WHERE chạy trước khi hàm cửa sổ được tính, nên không viết hàm cửa sổ hay tên cột được tính từ nó trong WHERE. Muốn lọc theo số thứ tự, tính nó trong truy vấn con rồi lọc ở truy vấn ngoài.
Ví dụ đầu tiên
Dựng bảng orders với sáu đơn [Đã chạy: MySQL 8.0.44]:
CREATE TABLE orders (
id INT PRIMARY KEY,
customer VARCHAR(20) NOT NULL,
total INT NOT NULL,
placed_at DATE NOT NULL
);
INSERT INTO orders VALUES
(1, 'Ann', 500, '2026-03-01'),
(2, 'Ann', 1200, '2026-03-05'),
(3, 'Binh', 300, '2026-03-02'),
(4, 'Binh', 300, '2026-03-09'),
(5, 'Binh', 800, '2026-03-12'),
(6, 'Chi', 450, '2026-03-03');
Trước hết là cách mà Mai thử, GROUP BY, lấy được ngày mới nhất nhưng không kèm được total:
SELECT customer, MAX(placed_at) AS last_order
FROM orders
GROUP BY customer;
+----------+------------+
| customer | last_order |
+----------+------------+
| Ann | 2026-03-05 |
| Binh | 2026-03-12 |
| Chi | 2026-03-03 |
+----------+------------+
Hàm cửa sổ cho từng dòng thêm tổng chi của khách, mà không mất dòng nào:
SELECT id, customer, total,
SUM(total) OVER (PARTITION BY customer) AS customer_spent
FROM orders
ORDER BY id;
Dự đoán trước khi xem kết quả: có bao nhiêu dòng, và customer_spent của đơn 4 là bao nhiêu?
Kết quả [Đã chạy: MySQL 8.0.44]:
+----+----------+-------+----------------+
| id | customer | total | customer_spent |
+----+----------+-------+----------------+
| 1 | Ann | 500 | 1700 |
| 2 | Ann | 1200 | 1700 |
| 3 | Binh | 300 | 1400 |
| 4 | Binh | 300 | 1400 |
| 5 | Binh | 800 | 1400 |
| 6 | Chi | 450 | 450 |
+----+----------+-------+----------------+
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
SUM(total) OVER (PARTITION BY customer) | Chia dòng theo khách, rồi cộng total trong mỗi nhóm và gắn lại cho từng dòng | Mỗi đơn vẫn còn, kèm tổng chi của khách của nó |
AS customer_spent | Đặt tên cho cột mới | Tên dễ đọc, dùng được ở ORDER BY |
ORDER BY id | Sắp kết quả cuối cùng theo mã đơn | Dễ so với bảng gốc |
Đơn 4 của Binh nhận 1400, vì ba đơn của Binh có tổng 300 + 300 + 800. Cả sáu dòng vẫn còn đó.
Bây giờ đánh số đơn của mỗi khách, đơn mới nhất là số 1 [Đã chạy: MySQL 8.0.44]:
SELECT id, customer, placed_at,
ROW_NUMBER() OVER (PARTITION BY customer ORDER BY placed_at DESC) AS rn
FROM orders
ORDER BY customer, rn;
+----+----------+------------+----+
| id | customer | placed_at | rn |
+----+----------+------------+----+
| 2 | Ann | 2026-03-05 | 1 |
| 1 | Ann | 2026-03-01 | 2 |
| 5 | Binh | 2026-03-12 | 1 |
| 4 | Binh | 2026-03-09 | 2 |
| 3 | Binh | 2026-03-02 | 3 |
| 6 | Chi | 2026-03-03 | 1 |
+----+----------+------------+----+
Lấy đúng đơn số 1, bọc trong truy vấn con [Đã chạy: MySQL 8.0.44]:
SELECT id, customer, total, placed_at
FROM (
SELECT o.*,
ROW_NUMBER() OVER (PARTITION BY customer ORDER BY placed_at DESC) AS rn
FROM orders o
) t
WHERE rn = 1
ORDER BY customer;
+----+----------+-------+------------+
| id | customer | total | placed_at |
+----+----------+-------+------------+
| 2 | Ann | 1200 | 2026-03-05 |
| 5 | Binh | 800 | 2026-03-12 |
| 6 | Chi | 450 | 2026-03-03 |
+----+----------+-------+------------+
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
SELECT o.*, ROW_NUMBER() OVER (...) AS rn FROM orders o | Tính số thứ tự rn cho mọi đơn trong từng nhóm | Truy vấn trong tạo ra cột rn |
) t | Đặt tên cho bảng tạm vừa tạo | MySQL bắt buộc bảng suy ra có alias |
WHERE rn = 1 | Giữ dòng số 1 của mỗi nhóm | rn đã tồn tại ở truy vấn ngoài, nên lọc được |
Xem từng bước
Animation dưới đây cho sáu đơn chia thành ba nhóm và đánh số. Hãy trả lời câu dự đoán trước khi bấm bước đầu tiên.
Đang tải Animation…
Bảng trace bằng chữ [Đã chạy: MySQL 8.0.44, sáu đơn, đánh số theo ngày giảm dần]:
| Đơn | Khách | Ngày đặt | rn | Được giữ khi rn = 1 |
|---|---|---|---|---|
| 2 | Ann | 5/3 | 1 | Giữ |
| 1 | Ann | 1/3 | 2 | Loại |
| 5 | Binh | 12/3 | 1 | Giữ |
| 4 | Binh | 9/3 | 2 | Loại |
| 3 | Binh | 2/3 | 3 | Loại |
| 6 | Chi | 3/3 | 1 | Giữ |
Bạn vừa làm được: đánh số riêng cho từng nhóm và chọn dòng đầu của mỗi nhóm mà vẫn giữ đủ cột của dòng gốc.
Concept Check
Dùng bảng orders sáu đơn ở đầu bài.
Câu 1. SUM(total) OVER (PARTITION BY customer) trên bảng sáu dòng cho kết quả có bao nhiêu dòng?
- A. 3 dòng, vì mỗi khách được gộp thành một dòng.
- B. 1 dòng, vì tổng cộng chỉ có một số duy nhất.
- C. 6 dòng, vì hàm cửa sổ giữ nguyên mọi dòng và thêm một cột.
- D. 2 dòng, vì khách có nhiều đơn nhất được giữ lại.
Câu 2. ROW_NUMBER() OVER (PARTITION BY customer ORDER BY placed_at DESC) cho đơn 3 của Binh (ngày 2/3) số nào?
- A. 1, vì đây là đơn có
idnhỏ nhất của Binh. - B. 3, vì đơn 3 là đơn cũ nhất trong ba đơn của Binh.
- C. 2, vì đơn 3 đứng giữa ba đơn của Binh theo
id. - D. 6, vì số thứ tự chạy liên tục qua mọi nhóm.
Câu 3. Vì sao WHERE ROW_NUMBER() OVER (...) = 1 báo lỗi?
- A. Vì
ROW_NUMBERchỉ dùng được vớiPARTITION BYở cuối câu lệnh. - B. Vì
WHEREchạy trước khi hàm cửa sổ được tính, nên chưa có giá trị để so. - C. Vì dấu
=không so sánh được với số nguyên. - D. Vì hàm cửa sổ chỉ dùng được trong
HAVING.
Đáp án và giải thích:
- Câu 1: C. A và D mô tả
GROUP BY. B sai vì tổng được tính theo từng nhóm, không phải toàn bảng. - Câu 2: B. A và C lấy
idlàm tiêu chí, nhưng số thứ tự theoORDER BY placed_at DESC. D sai vì số thứ tự bắt đầu lại từ 1 ở mỗi nhóm. - Câu 3: B. A bịa ra điều kiện về vị trí. C sai vì phép so sánh hợp lệ. D sai vì
HAVINGcũng chạy trước hàm cửa sổ.
Làm cùng mình
Mai muốn mỗi dòng đơn hiển thị kèm tổng chi của khách đó và số thứ tự của đơn trong nhóm khách, đơn mới nhất là số 1. Một câu lệnh, sáu dòng.
Bước 1. Vẽ ra giấy
Trước khi gõ lệnh, hãy kẻ bảng sáu đơn và tự điền hai cột: tổng chi của khách, và số thứ tự theo ngày đặt giảm dần trong từng khách.
Bước 2. Điền chỗ trống
Câu lệnh còn thiếu hai mệnh đề:
SELECT id, customer, total,
SUM(total) OVER (
-- TODO 1: chia các dòng thành từng nhóm theo khách
) AS customer_spent,
ROW_NUMBER() OVER (
PARTITION BY customer
-- TODO 2: đánh số đơn mới nhất của mỗi khách là 1
) AS rn
FROM orders
ORDER BY customer, rn;
Gợi ý cho chỗ trống thứ nhất: chia theo cột customer. Cho chỗ trống thứ hai: sắp theo ngày đặt giảm dần.
Bước 3. Chạy và đối chiếu
Dự đoán trước: đơn nào nhận rn bằng 2 trong nhóm Binh? Rồi đối chiếu với mục Lời giải và đáp án.
Bước 4. Vẽ lại ra giấy
Ghi một câu: vì sao ORDER BY bên trong OVER không thay được ORDER BY ở cuối câu lệnh.
Thử phá nó đi
Các lỗi người mới hay gặp với PARTITION BY [Đã chạy: MySQL 8.0.44]:
| Code lỗi | Hiện tượng | Nguyên nhân | Cách sửa |
|---|---|---|---|
SELECT customer, total, MAX(placed_at) ... GROUP BY customer | ERROR 1055 | total không nằm trong GROUP BY cũng không được tổng hợp | Dùng hàm cửa sổ rồi lọc |
WHERE ROW_NUMBER() OVER (...) = 1 | ERROR 3593 | WHERE chạy trước hàm cửa sổ | Tính trong truy vấn con, lọc ở ngoài |
ROW_NUMBER() OVER (ORDER BY placed_at DESC) | Số thứ tự chạy 1 đến 6 qua mọi khách | Thiếu PARTITION BY nên cả bảng là một nhóm | Thêm PARTITION BY customer |
FROM (SELECT ...) không có alias | ERROR 1248 | Bảng suy ra bắt buộc có tên | Thêm AS t |
Lỗi thứ nhất [Đã chạy: MySQL 8.0.44]:
SELECT customer, total, MAX(placed_at) AS last_order
FROM orders
GROUP BY customer;
SELECT 'done' AS marker;
+--------+
| marker |
+--------+
| done |
+--------+
ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'shop.orders.total' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by
Lỗi thứ hai, done chứng minh lệnh sau vẫn chạy [Đã chạy: MySQL 8.0.44]:
SELECT id
FROM orders
WHERE ROW_NUMBER() OVER (PARTITION BY customer ORDER BY placed_at DESC) = 1;
SELECT 'done' AS marker;
+--------+
| marker |
+--------+
| done |
+--------+
ERROR 3593 (HY000): You cannot use the window function 'row_number' in this context.'
Lỗi thứ ba, nhóm không được chia nên số thứ tự chạy liên tục [Đã chạy: MySQL 8.0.44]:
SELECT id, placed_at,
ROW_NUMBER() OVER (ORDER BY placed_at DESC) AS rn
FROM orders
ORDER BY id;
+----+------------+----+
| id | placed_at | rn |
+----+------------+----+
| 1 | 2026-03-01 | 6 |
| 2 | 2026-03-05 | 3 |
| 3 | 2026-03-02 | 5 |
| 4 | 2026-03-09 | 2 |
| 5 | 2026-03-12 | 1 |
| 6 | 2026-03-03 | 4 |
+----+------------+----+
Hiểu lầm quan trọng nhất ở đây là nghĩ WHERE đứng cạnh SELECT thì dùng được số thứ tự vừa tính. Hình dưới đối chiếu mô hình sai với mô hình đúng.
Đang tải sơ đồ…
Cách đọc hình: hai chuỗi độc lập. Chuỗi đỏ là niềm tin sai, dẫn đến lỗi 3593. Chuỗi xanh là thực tế: số thứ tự chỉ tồn tại sau khi hàm cửa sổ chạy, nên phải tính nó trong truy vấn con rồi mới lọc ở ngoài.
Tự tay làm
Dùng chung bảng orders sáu đơn của bài. Mỗi bài nêu đầu vào, kết quả mong đợi và tiêu chí đạt.
Cơ bản
Bài 1. Điền hai cột. Đầu vào: câu lệnh dưới đây trên bảng đầu bài.
SUM(total) OVER (PARTITION BY customer) AS customer_spent và ROW_NUMBER() OVER (PARTITION BY customer ORDER BY placed_at DESC) AS rn
| Đơn | customer_spent | rn |
|---|---|---|
| 1 | ? | ? |
| 2 | ? | ? |
| 3 | ? | ? |
| 4 | ? | ? |
| 5 | ? | ? |
| 6 | ? | ? |
Tiêu chí đạt: đúng cả mười hai ô. Gợi ý: tổng chi của Binh là tổng ba đơn, và số thứ tự bắt đầu lại ở mỗi khách.
Vận dụng
Bài 2. Hai đơn mới nhất của mỗi khách. Đầu vào: bảng orders. Liệt kê tối đa hai đơn mới nhất của mỗi khách, nhóm theo khách, đơn mới hơn đứng trước. Tiêu chí đạt: năm dòng, là đơn 2, 1, 5, 4, 6. Gợi ý: đổi điều kiện lọc rn ở truy vấn ngoài.
Thử thách (tùy chọn)
Bài 3. Tổng cộng dồn theo khách. Đầu vào: bảng orders. Cho mỗi đơn, hiển thị tổng tiền các đơn của cùng khách từ đơn đầu tiên đến chính đơn đó (theo ngày đặt tăng dần). Tiêu chí đạt: dãy cộng dồn là 500, 1700, 300, 600, 1400, 450. Gợi ý: thêm ORDER BY vào trong OVER.
Tự kiểm tra
Trả lời bằng lời của bạn, không nhìn lại bài. Mỗi câu ghi mục tiêu được kiểm tra.
- (Mục tiêu 1, nhớ lại) Khác biệt lớn nhất về số dòng kết quả giữa
GROUP BYvà hàm cửa sổ là gì? - (Mục tiêu 2, dự đoán) Số thứ tự của đơn 6 của Chi là bao nhiêu, và vì sao không phải 6?
- (Mục tiêu 3, dự đoán trạng thái) Lọc
WHERE rn = 1ở truy vấn ngoài cho bao nhiêu dòng, và là những đơn nào? - (Mục tiêu 4, giải thích cho bạn chưa học) Vì sao không viết được
WHERE ROW_NUMBER() OVER (...) = 1? - (Mục tiêu 2, tìm lỗi) Ai đó bỏ
PARTITION BY customerkhỏi câu lệnh đánh số đơn. Số thứ tự thay đổi thế nào?
Điều cần nhớ
- Hàm cửa sổ giữ nguyên dòng và thêm cột;
GROUP BYgộp dòng. PARTITION BYchia nhóm; hàm được tính lại từ đầu ở mỗi nhóm.ORDER BYbên trongOVERchỉ quyết định cách tính, không sắp xếp kết quả cuối.- Hàm cửa sổ chạy sau
WHERE; muốn lọc theo kết quả của nó, tính trong truy vấn con.
Cheat sheet:
| Muốn | Viết | Ghi chú |
|---|---|---|
| Tổng theo nhóm, giữ dòng | SUM(x) OVER (PARTITION BY g) | Không mất dòng nào |
| Đánh số trong nhóm | ROW_NUMBER() OVER (PARTITION BY g ORDER BY d DESC) | Bắt đầu lại từ 1 ở mỗi nhóm |
| Dòng đầu của mỗi nhóm | Bọc trong truy vấn con rồi WHERE rn = 1 | Bắt buộc có alias cho bảng suy ra |
| Tổng cộng dồn | SUM(x) OVER (PARTITION BY g ORDER BY d) | Cẩn thận khi có dòng bằng nhau |
| Xếp hạng có đồng hạng | RANK() hoặc DENSE_RANK() | ROW_NUMBER() không cho đồng hạng |
Hình tóm tắt của bài là sơ đồ ở mục Bức tranh lớn.
Câu hỏi để dành cho bài sau: điều kiện lọc thường kết hợp nhiều mệnh đề với nhau. Khi viết status = 'active' OR status = 'pending' AND country = 'VN', MySQL nhóm các mệnh đề như thế nào, và kết quả có đúng ý bạn không?
Đào sâu
Phần này dành cho bạn tò mò.
- Cơ chế bên trong. Cùng giá trị sắp xếp thì
ROW_NUMBERvẫn bắt buộc đánh số khác nhau, cònRANKvàDENSE_RANKcho đồng hạng. Ba đơn của Binh cótotallà 300, 300, 800 [Đã chạy: MySQL 8.0.44]:
SELECT id, total,
ROW_NUMBER() OVER (ORDER BY total) AS rn,
RANK() OVER (ORDER BY total) AS rk,
DENSE_RANK() OVER (ORDER BY total) AS dr
FROM orders
WHERE customer = 'Binh'
ORDER BY rn;
+----+-------+----+----+----+
| id | total | rn | rk | dr |
+----+-------+----+----+----+
| 3 | 300 | 1 | 1 | 1 |
| 4 | 300 | 2 | 1 | 1 |
| 5 | 800 | 3 | 3 | 2 |
+----+-------+----+----+----+
Hai đơn 300 nhận rn 1 và 2, nhưng thứ tự giữa hai đơn này không được bảo đảm vì ORDER BY total có dòng bằng nhau, giống bài ORDER BY. Muốn cố định, thêm id vào ORDER BY bên trong OVER.
- So sánh với lựa chọn khác. Khi có
ORDER BYtrongOVER, khung mặc định tính đến mọi dòng bằng nhau cùng lúc. Cộtdefault_framedưới đây cho hai đơn 300 cùng giá trị 600, cònby_rowsđếm từng dòng một [Đã chạy: MySQL 8.0.44]:
SELECT id, total,
SUM(total) OVER (ORDER BY total) AS default_frame,
SUM(total) OVER (ORDER BY total ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS by_rows
FROM orders
WHERE customer = 'Binh'
ORDER BY id;
+----+-------+---------------+---------+
| id | total | default_frame | by_rows |
+----+-------+---------------+---------+
| 3 | 300 | 600 | 300 |
| 4 | 300 | 600 | 600 |
| 5 | 800 | 1400 | 1400 |
+----+-------+---------------+---------+
Vì vậy khi cộng dồn trên cột có giá trị lặp, hãy chỉ rõ ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, hoặc thêm cột duy nhất vào ORDER BY.
- Hiểu lầm nâng cao.
PARTITION BYcòn xuất hiện trongCREATE TABLEvới nghĩa hoàn toàn khác: chia bảng thành các phần lưu trữ. Truy vấn lọc theo cột phân vùng chỉ đọc phần liên quan [Đã chạy: MySQL 8.0.44. Có thể khác nhau: kế hoạch thực thi]. Cộtpartitionscho thấy chỉ phầnpmaxđược đọc:
CREATE TABLE log_p (
id INT NOT NULL,
created DATE NOT NULL,
PRIMARY KEY (id, created)
)
PARTITION BY RANGE (YEAR(created)) (
PARTITION p2025 VALUES LESS THAN (2026),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
INSERT INTO log_p VALUES (1, '2025-06-01'), (2, '2026-03-01');
EXPLAIN SELECT * FROM log_p WHERE created >= '2026-01-01';
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+--------------------------+
| 1 | SIMPLE | log_p | pmax | index | PRIMARY | PRIMARY | 7 | NULL | 1 | 100.00 | Using where; Using index |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+--------------------------+
- Giới hạn của bài. Bài này chưa nói đến khung cửa sổ
ROWSvàRANGEngoài ví dụ trên, hay các hàmLAG,LEAD,NTILE. Hàm cửa sổ cũng cần MySQL 8.0 trở lên. - Đọc thêm. MySQL 8.0 Reference Manual, mục Window Functions: https://dev.mysql.com/doc/refman/8.0/en/window-functions.html và mục Partitioning: https://dev.mysql.com/doc/refman/8.0/en/partitioning.html
Bài tiếp theo
Bài sau là AND, OR, NOT: ghép nhiều điều kiện trong WHERE, thứ tự ưu tiên và vì sao thiếu dấu ngoặc có thể sửa nhầm dòng thật. Mini-dự án nối tiếp: lọc danh sách tài khoản của kho shop theo nhiều điều kiện cùng lúc.
Lời giải và đáp án
Làm cùng mình, Bước 2. Hai mệnh đề cần điền, rồi kết quả:
SUM(total) OVER (PARTITION BY customer) AS customer_spent
ORDER BY placed_at DESC
SELECT id, customer, total,
SUM(total) OVER (PARTITION BY customer) AS customer_spent,
ROW_NUMBER() OVER (
PARTITION BY customer
ORDER BY placed_at DESC
) AS rn
FROM orders
ORDER BY customer, rn;
+----+----------+-------+----------------+----+
| id | customer | total | customer_spent | rn |
+----+----------+-------+----------------+----+
| 2 | Ann | 1200 | 1700 | 1 |
| 1 | Ann | 500 | 1700 | 2 |
| 5 | Binh | 800 | 1400 | 1 |
| 4 | Binh | 300 | 1400 | 2 |
| 3 | Binh | 300 | 1400 | 3 |
| 6 | Chi | 450 | 450 | 1 |
+----+----------+-------+----------------+----+
Trong nhóm Binh, đơn 4 (9/3) nhận rn bằng 2. ORDER BY trong OVER chỉ dùng để tính số thứ tự, nên kết quả cuối vẫn cần ORDER BY customer, rn ở ngoài cùng.
Bài 1. Dùng kết quả bước trên, xếp theo id:
| Đơn | customer_spent | rn | Giải thích |
|---|---|---|---|
| 1 | 1700 | 2 | Ann tổng 500 + 1200; đơn 1 cũ hơn đơn 2 |
| 2 | 1700 | 1 | Đơn 2 mới nhất của Ann |
| 3 | 1400 | 3 | Binh tổng 300 + 300 + 800; đơn 3 cũ nhất |
| 4 | 1400 | 2 | Đơn 4 ở giữa |
| 5 | 1400 | 1 | Đơn 5 mới nhất của Binh |
| 6 | 450 | 1 | Chi chỉ có một đơn |
Bài 2.
SELECT id, customer, total, placed_at
FROM (
SELECT o.*,
ROW_NUMBER() OVER (PARTITION BY customer ORDER BY placed_at DESC) AS rn
FROM orders o
) t
WHERE rn <= 2
ORDER BY customer, placed_at DESC;
+----+----------+-------+------------+
| id | customer | total | placed_at |
+----+----------+-------+------------+
| 2 | Ann | 1200 | 2026-03-05 |
| 1 | Ann | 500 | 2026-03-01 |
| 5 | Binh | 800 | 2026-03-12 |
| 4 | Binh | 300 | 2026-03-09 |
| 6 | Chi | 450 | 2026-03-03 |
+----+----------+-------+------------+
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
WHERE rn <= 2 | Giữ hai dòng đầu của mỗi nhóm | Đổi từ rn = 1, chỉ khác ở điều kiện |
ORDER BY customer, placed_at DESC | Sắp kết quả cuối theo khách rồi ngày giảm | Chi chỉ có một đơn nên chỉ ra một dòng |
Bài 3.
SELECT id, customer, total,
SUM(total) OVER (PARTITION BY customer ORDER BY placed_at) AS running
FROM orders
ORDER BY customer, placed_at;
+----+----------+-------+---------+
| id | customer | total | running |
+----+----------+-------+---------+
| 1 | Ann | 500 | 500 |
| 2 | Ann | 1200 | 1700 |
| 3 | Binh | 300 | 300 |
| 4 | Binh | 300 | 600 |
| 5 | Binh | 800 | 1400 |
| 6 | Chi | 450 | 450 |
+----+----------+-------+---------+
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
SUM(total) OVER (PARTITION BY customer ORDER BY placed_at) | Cộng total từ đơn đầu của khách đến đơn hiện tại | ORDER BY trong OVER biến tổng của nhóm thành tổng cộng dồn |
Hai đơn của Ann ra 500 rồi 1700, ba đơn của Binh ra 300, 600, 1400. Vì ngày đặt không trùng, khung mặc định không gây nhầm lẫn.
Tự kiểm tra.
GROUP BYgiảm còn một dòng mỗi nhóm, hàm cửa sổ giữ nguyên số dòng.- 1, vì Chi là một nhóm riêng và số thứ tự bắt đầu lại từ 1 ở mỗi nhóm.
- 3 dòng: đơn 2, 5 và 6, mỗi khách một dòng.
- Vì
WHEREchạy trước, lúc đó hàm cửa sổ chưa được tính. Phải tính trong truy vấn con rồi lọc ở ngoài. - Cả bảng thành một nhóm, số thứ tự chạy liên tục từ 1 đến 6 theo ngày đặt giảm dần, không còn bắt đầu lại ở mỗi khách.