Bảng xếp hạng có hai người cùng 90 điểm. Người tiếp theo nên đứng hạng 4 hay hạng 3? Nếu yêu cầu chưa được chốt, đổi tên hàm trong SQL có thể âm thầm đổi số người được chọn vào danh sách trao thưởng hoặc báo cáo.
Bài đối chiếu PostgreSQL 17 và SQLite ngày 07/11/2026. Ví dụ chạy bằng Python 3.9.6 với SQLite 3.54.0 trong bộ nhớ; chưa chạy PostgreSQL server, ORM hoặc benchmark. Điểm và ID đều là dữ liệu minh họa, không phải bảng xếp hạng thực tế.
Chốt cách xử lý đồng điểm
RANK giữ cùng thứ hạng cho các hàng ngang nhau theo ORDER BY của cửa sổ, rồi để khoảng trống sau nhóm đồng hạng. DENSE_RANK cũng giữ đồng hạng nhưng số tiếp theo liên tục. ROW_NUMBER cấp số riêng cho từng hàng.
Với dãy 100, 90, 90, 70, RANK là 1, 2, 2, 4; DENSE_RANK là 1, 2, 2, 3. Nếu cần đúng hai bản ghi thay vì mọi người thuộc hai mức điểm đầu, đó là một yêu cầu khác.
1. So sánh trên cùng một bảng
Chạy file Python sau để tạo database riêng trong bộ nhớ:
import sqlite3
db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE demo_scores(id INTEGER PRIMARY KEY, score INTEGER NOT NULL);
INSERT INTO demo_scores VALUES (1, 100), (2, 90), (3, 90), (4, 70);
""")
rows = db.execute("""
SELECT id,
RANK() OVER (ORDER BY score DESC) AS r,
DENSE_RANK() OVER (ORDER BY score DESC) AS d,
ROW_NUMBER() OVER (ORDER BY score DESC, id) AS n
FROM demo_scores
ORDER BY score DESC, id
""").fetchall()
assert rows == [(1, 1, 1, 1), (2, 2, 2, 2),
(3, 2, 2, 3), (4, 4, 3, 4)]
print(rows)
ORDER BY bên ngoài quyết định thứ tự trình bày kết quả. ORDER BY trong OVER quyết định nhóm ngang nhau của phép xếp hạng. Hai vị trí có thể phục vụ hai mục đích khác nhau.
ID được dùng làm tiêu chí phụ cho ROW_NUMBER để chọn thứ tự ổn định trong fixture. RANK và DENSE_RANK chỉ nhìn score, nên hai người 90 điểm vẫn là một nhóm đồng hạng.
2. Đừng thêm ID vào mọi cửa sổ theo thói quen
Nếu thêm ID vào ORDER BY của RANK, hai hàng cùng điểm nhưng khác ID không còn ngang nhau theo toàn bộ khóa sắp xếp. Nối tiếp file để thấy ảnh hưởng:
broken_ties = db.execute("""
SELECT id, RANK() OVER (ORDER BY score DESC, id)
FROM demo_scores ORDER BY score DESC, id
""").fetchall()
assert broken_ties == [(1, 1), (2, 2), (3, 3), (4, 4)]
top_groups = db.execute("""
WITH ranked AS (
SELECT id, score, DENSE_RANK() OVER (ORDER BY score DESC) AS position
FROM demo_scores
)
SELECT id FROM ranked WHERE position <= ? ORDER BY score DESC, id
""", (2,)).fetchall()
assert top_groups == [(1,), (2,), (3,)]
db.close()
Hai mức điểm đầu có ba người, không phải hai người. Test cần kiểm tra số hàng và ID được chọn, không chỉ kiểm tra cột position có giá trị từ 1 đến 2.
CTE tách bước tính thứ hạng khỏi bước lọc nó. Đừng mặc định alias của window function có thể được dùng trực tiếp trong WHERE cùng cấp. Khi chuyển sang query builder, đọc SQL sinh ra để xác nhận cấu trúc tương đương.
Kiểm tra quy tắc trước khi nối vào endpoint
Thêm ca toàn bộ người cùng điểm, chỉ một người và bảng rỗng. Nếu xếp hạng riêng từng nhóm, bổ sung PARTITION BY theo khóa nhóm và fixture có ít nhất hai nhóm để phát hiện xếp hạng toàn bảng ngoài ý muốn.
Điểm trong demo NOT NULL. Với dữ liệu nullable hoặc nhiều tiêu chí nghiệp vụ, chốt cách sắp xếp và khái niệm ngang nhau trước. Không suy ra quy tắc NULL giống nhau ở mọi hệ quản trị chỉ từ fixture SQLite này.
Chọn hàm theo câu hỏi
Dùng RANK cho cách đánh số có khoảng trống sau đồng hạng. Dùng DENSE_RANK khi cần đếm các nhóm giá trị liên tiếp. Dùng ROW_NUMBER khi cần số thứ tự riêng và đã chốt tiêu chí phá hòa.
Bài chưa kiểm tra index hoặc chi phí sắp xếp trên bảng lớn. Sau khi assertions đúng với quy tắc đồng hạng, mới đo kế hoạch truy vấn của hệ quản trị thật. Một câu SQL nhanh nhưng chọn sai người vẫn không đáp ứng yêu cầu của báo cáo.


