SQL cho phân tích dữ liệu · Hướng dẫn · 30 tháng 10, 2026 · 15 phút đọc
ORDER BY trong SQL: sắp xếp tăng dần, giảm dần, nhiều cột, NULLS LAST và top N

ORDER BY sắp xếp các dòng trong kết quả truy vấn. Bạn viết nó ở cuối truy vấn, liệt kê các cột cần sắp xếp và thêm ASC (tăng dần, mặc định) hoặc DESC (giảm dần) sau mỗi cột: ORDER BY range DESC đưa máy bay có tầm bay xa nhất lên đầu. Với nhiều cột, cột thứ hai chỉ phân định thứ tự khi cột thứ nhất bằng nhau. Không có ORDER BY, cơ sở dữ liệu trả về các dòng theo thứ tự không được bảo đảm, nên mọi truy vấn cần thứ tự, và mọi truy vấn có LIMIT, đều cần nó. Hai chi tiết hay làm người ta vấp nhất: NULL nằm ở đâu (mỗi hệ một kiểu, nên hãy viết NULLS FIRST hoặc NULLS LAST), và các giá trị bằng nhau ở ranh giới của top N.
Mọi ví dụ chạy trên cơ sở dữ liệu hãng hàng không của khóa SQL trên Nomto, với cú pháp PostgreSQL trên DuckDB (động cơ của Sân tập SQL trên Nomto), và mọi bảng kết quả đều là kết quả thật. Cú pháp tương đương cho SQL Server và MySQL, hai hệ rất phổ biến ở các công ty Việt Nam, được ghi chú ở từng phần.
Cú pháp ORDER BY
SELECT columns
FROM table
WHERE condition
ORDER BY column1 [ASC | DESC] [NULLS FIRST | NULLS LAST],
column2 [ASC | DESC] ...
LIMIT n;
ORDER BY đứng sau WHERE, GROUP BY và HAVING, và trước LIMIT. Nó cũng là một trong những bước cuối cùng cơ sở dữ liệu thực hiện, nên nó dùng được bí danh (alias) từ SELECT (xem phần bên dưới).
Sắp xếp tăng dần và giảm dần
Chín mẫu máy bay, sắp theo tầm bay từ ngắn nhất đến xa nhất:
SELECT model, range
FROM aircrafts
ORDER BY range;
| model | range |
|---|---|
| Cessna 208 Caravan | 1200 |
| Bombardier CRJ-200 | 2700 |
| Sukhoi Superjet-100 | 3000 |
| Boeing 737-300 | 4200 |
| Airbus A321-200 | 5600 |
| Airbus A320-200 | 5700 |
| Airbus A319-100 | 6700 |
| Boeing 767-300 | 7900 |
| Boeing 777-300 | 11100 |
ORDER BY range giống hệt ORDER BY range ASC. Muốn xa nhất lên đầu, viết ORDER BY range DESC. Số được sắp theo giá trị, ngày giờ từ sớm nhất đến muộn nhất, còn chuỗi theo bảng chữ cái (kèm vài lưu ý ở phần sắp xếp chuỗi, đặc biệt với tiếng Việt).
ORDER BY nhiều cột
Liệt kê nhiều cột, cách nhau bằng dấu phẩy. Các dòng được sắp theo cột đầu tiên; cột thứ hai chỉ được dùng khi cột đầu bằng nhau, và cứ thế. Mỗi cột có chiều riêng. Máy bay nhóm theo hãng sản xuất từ A đến Z, và trong mỗi hãng theo tầm bay từ xa nhất đến ngắn nhất:
SELECT split_part(model, ' ', 1) AS maker, model, range
FROM aircrafts
ORDER BY maker, range DESC;
| maker | model | range |
|---|---|---|
| Airbus | Airbus A319-100 | 6700 |
| Airbus | Airbus A320-200 | 5700 |
| Airbus | Airbus A321-200 | 5600 |
| Boeing | Boeing 777-300 | 11100 |
| Boeing | Boeing 767-300 | 7900 |
| Boeing | Boeing 737-300 | 4200 |
| Bombardier | Bombardier CRJ-200 | 2700 |
| Cessna | Cessna 208 Caravan | 1200 |
| Sukhoi | Sukhoi Superjet-100 | 3000 |
Lỗi hay gặp là viết ORDER BY maker, range DESC và nghĩ cả hai cột đều giảm dần. DESC chỉ áp dụng cho cột ngay trước nó; ở đây maker vẫn tăng dần.
Sắp theo bí danh, vị trí cột hoặc biểu thức
ORDER BY có thể tham chiếu đến:
- bí danh cột từ
SELECT:makerở trên không phải cột thật, mà là bí danh của một biểu thức; - một biểu thức:
ORDER BY split_part(model, ' ', 1)cho cùng kết quả; - số thứ tự cột:
ORDER BY 2 DESCsắp theo cột thứ hai trong danh sáchSELECT; - một cột hoàn toàn không được chọn:
SELECT city FROM airports ORDER BY timezonelà hợp lệ.
Sắp xếp kết quả đã nhóm theo giá trị tổng hợp, bằng bí danh hoặc vị trí:
SELECT status, COUNT(*) AS flights
FROM flights
GROUP BY status
ORDER BY flights DESC; -- or ORDER BY 2 DESC, or ORDER BY COUNT(*) DESC
| status | flights |
|---|---|
| Arrived | 16707 |
| Scheduled | 15383 |
| On Time | 518 |
| Cancelled | 414 |
| Departed | 58 |
| Delayed | 41 |
Vị trí (ORDER BY 2) tiện khi khám phá dữ liệu, nhưng dễ vỡ trong truy vấn lưu lại: thêm một cột vào đầu SELECT là thứ tự âm thầm thay đổi. Trong những gì người khác sẽ đọc, hãy dùng tên.
Hai giới hạn cần biết. Bí danh dùng một mình thì được, nhưng không phải lúc nào cũng dùng được bên trong biểu thức: PostgreSQL từ chối ORDER BY flights * 2 khi flights là bí danh. Và với SELECT DISTINCT, PostgreSQL yêu cầu mọi biểu thức trong ORDER BY phải có mặt trong danh sách chọn (“for SELECT DISTINCT, ORDER BY expressions must appear in select list”); DuckDB dễ dãi hơn, nhưng đừng dựa vào đó.
Thứ tự tùy chỉnh với CASE
Thứ tự bảng chữ cái của các trạng thái chẳng có ý nghĩa gì; người đọc muốn thấy theo vòng đời chuyến bay. Gán cho mỗi giá trị một con số bằng CASE rồi sắp theo nó:
SELECT status, COUNT(*) AS flights
FROM flights
GROUP BY status
ORDER BY CASE status
WHEN 'Scheduled' THEN 1
WHEN 'On Time' THEN 2
WHEN 'Delayed' THEN 3
WHEN 'Departed' THEN 4
WHEN 'Arrived' THEN 5
ELSE 6
END;
| status | flights |
|---|---|
| Scheduled | 15383 |
| On Time | 518 |
| Delayed | 41 |
| Departed | 58 |
| Arrived | 16707 |
| Cancelled | 414 |
ELSE đẩy mọi giá trị bất ngờ xuống cuối thay vì lên đầu. Cùng mẹo đó ghim một giá trị lên trên cùng: ORDER BY city = 'Moscow' DESC, city liệt kê Moscow trước, phần còn lại theo bảng chữ cái (trong PostgreSQL và DuckDB, true xếp sau false, nên cần DESC). MySQL còn có ORDER BY FIELD(status, 'Scheduled', 'On Time', ...), hàm không tồn tại ở nơi khác. Thêm nhiều mẫu CASE trong bài CASE WHEN trong SQL.
NULLS FIRST và NULLS LAST
Những chuyến bay nào cất cánh trễ nhất? 16.348 chuyến chưa cất cánh, nên actual_departure của chúng, và độ trễ tính ra, là NULL:
SELECT flight_no, departure_airport, scheduled_departure,
EXTRACT(EPOCH FROM actual_departure - scheduled_departure) / 60 AS delay_min
FROM flights
ORDER BY delay_min DESC NULLS LAST
LIMIT 5;
| flight_no | departure_airport | scheduled_departure | delay_min |
|---|---|---|---|
| PG0589 | PEE | 2026-07-25 15:30:00 | 277 |
| PG0164 | DME | 2026-07-25 15:25:00 | 268 |
| PG0364 | KRR | 2026-07-15 11:45:00 | 267 |
| PG0568 | OVS | 2026-08-09 16:15:00 | 260 |
| PG0096 | URJ | 2026-07-30 16:35:00 | 258 |
Đổi thành NULLS FIRST (với LIMIT 3), đầu danh sách là những chuyến hoàn toàn không có số liệu độ trễ:
| flight_no | departure_airport | scheduled_departure | delay_min |
|---|---|---|---|
| PG0402 | DME | 2026-09-05 12:25:00 | NULL |
| PG0405 | DME | 2026-08-12 09:35:00 | NULL |
| PG0404 | DME | 2026-08-12 19:05:00 | NULL |
Nếu bạn không viết gì, kết quả phụ thuộc vào hệ quản trị:
| hệ quản trị | NULL khi ASC | NULL khi DESC |
|---|---|---|
| PostgreSQL, Oracle | cuối | đầu |
| DuckDB | cuối | cuối |
| MySQL, SQL Server, SQLite | đầu | cuối |
Vì vậy cùng ORDER BY delay_min DESC LIMIT 5 trả về top 5 thật trong DuckDB nhưng năm giá trị NULL trong PostgreSQL. PostgreSQL, Oracle, DuckDB và SQLite (từ 3.30) chấp nhận NULLS FIRST/NULLS LAST. MySQL và SQL Server thì không; ở đó hãy sắp theo một cờ trước: ORDER BY delay_min IS NULL, delay_min DESC trong MySQL, hoặc ORDER BY CASE WHEN delay_min IS NULL THEN 1 ELSE 0 END, delay_min DESC trong SQL Server. Bài NULL trong SQL nói kỹ hơn về cách NULL hoạt động.
ORDER BY với LIMIT: top N dòng
ORDER BY cộng LIMIT là cách lấy “top N”. Bốn điểm đến có nhiều chuyến nhất từ sân bay Sheremetyevo (SVO):
SELECT arrival_airport, COUNT(*) AS flights
FROM flights
WHERE departure_airport = 'SVO'
GROUP BY arrival_airport
ORDER BY flights DESC, arrival_airport
LIMIT 4;
| arrival_airport | flights |
|---|---|
| LED | 305 |
| BZK | 183 |
| GOJ | 122 |
| PES | 122 |
Giá trị bằng nhau ở ranh giới
Ba sân bay GOJ, PES và ULV đều có 122 chuyến, và chỉ hai trong số đó lọt vào top 4. Không có cột sắp xếp thứ hai arrival_airport, cơ sở dữ liệu được tự do chọn hai cái bất kỳ, và nó làm đúng như thế: năm lần chạy truy vấn chỉ với ORDER BY flights DESC cho ra năm cặp khác nhau ở vị trí 3 và 4 (PES và ULV, GOJ và PES, ULV và PES, GOJ và ULV, PES và GOJ). Một báo cáo thay đổi mỗi lần làm mới là một lỗi. Có hai cách sửa:
- Thêm cột phân định để thứ tự là duy nhất, như ở trên. Kết quả ổn định, nhưng việc cắt vẫn tùy tiện: ULV bị loại chỉ vì tên của nó.
- Giữ mọi dòng bằng nhau. PostgreSQL 13+ và Oracle hỗ trợ
FETCH FIRST 4 ROWS WITH TIES, SQL Server cóTOP 4 WITH TIES. DuckDB và MySQL không có cả hai; hãy dùngRANK():
SELECT arrival_airport, flights
FROM (
SELECT arrival_airport, COUNT(*) AS flights,
RANK() OVER (ORDER BY COUNT(*) DESC) AS rnk
FROM flights
WHERE departure_airport = 'SVO'
GROUP BY arrival_airport
) t
WHERE rnk <= 4
ORDER BY flights DESC, arrival_airport;
| arrival_airport | flights |
|---|---|
| LED | 305 |
| BZK | 183 |
| GOJ | 122 |
| PES | 122 |
| ULV | 122 |
Năm dòng, vì ba sân bay cùng đứng thứ 3. Để lấy top N trong từng nhóm (ví dụ top 3 tuyến của mỗi sân bay), xem bài hàm cửa sổ (window function) trong SQL.
LIMIT ở các hệ khác
| hệ quản trị | top 3 |
|---|---|
| PostgreSQL, DuckDB, MySQL, SQLite | ORDER BY range DESC LIMIT 3 |
| SQL Server | SELECT TOP 3 ... ORDER BY range DESC |
| SQL chuẩn (PostgreSQL, Oracle 12c+, SQL Server, DuckDB) | ORDER BY range DESC FETCH FIRST 3 ROWS ONLY |
Phân trang với OFFSET
OFFSET bỏ qua một số dòng trước khi LIMIT bắt đầu đếm. Trang 2 của danh sách máy bay, ba dòng mỗi trang:
SELECT model, range
FROM aircrafts
ORDER BY range DESC
LIMIT 3 OFFSET 3;
| model | range |
|---|---|
| Airbus A320-200 | 5700 |
| Airbus A321-200 | 5600 |
| Boeing 737-300 | 4200 |
Dạng chuẩn OFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY trả về cùng các dòng và chạy được cả trong SQL Server, nơi OFFSET bắt buộc đi kèm ORDER BY. Phân trang chỉ có nghĩa khi thứ tự là duy nhất: nếu có giá trị bằng nhau, một dòng có thể xuất hiện ở hai trang hoặc không ở trang nào. Trên bảng lớn, offset sâu chạy chậm vì cơ sở dữ liệu vẫn phải tạo ra rồi bỏ qua mọi dòng trước đó; các ứng dụng thường chuyển sang phân trang “keyset” (WHERE range < last_seen_value ORDER BY range DESC LIMIT 3).
Sắp xếp chuỗi: số lưu dạng chuỗi, chữ hoa thường và tiếng Việt
Chuỗi được so sánh từng ký tự một, nên số lưu dạng chuỗi bị sắp “sai”. Số ghế trên Airbus A319 là chuỗi như 1A hay 10C:
SELECT seat_no FROM seats WHERE aircraft_code = '319' ORDER BY seat_no LIMIT 8;
| seat_no |
|---|
| 10A |
| 10B |
| 10C |
| 10D |
| 10E |
| 10F |
| 11A |
| 11B |
Hàng 10 đứng trước hàng 1 vì “1” theo sau là “0” xếp trước “1” theo sau là “A”. Hãy sắp theo phần số, rồi đến chữ cái:
SELECT seat_no, fare_conditions
FROM seats
WHERE aircraft_code = '319'
ORDER BY CAST(regexp_extract(seat_no, '^[0-9]+') AS INTEGER), right(seat_no, 1)
LIMIT 8;
| seat_no | fare_conditions |
|---|---|
| 1A | Business |
| 1C | Business |
| 1D | Business |
| 1F | Business |
| 2A | Business |
| 2C | Business |
| 2D | Business |
| 2F | Business |
Trong PostgreSQL, thay regexp_extract(seat_no, '^[0-9]+') bằng substring(seat_no FROM '^[0-9]+'). Cùng vấn đề này xảy ra với mã số và số tiền được nạp vào dưới dạng chuỗi: '10' < '9'. Cách sửa thật sự là lưu chúng dưới dạng số.
Chữ hoa chữ thường là bất ngờ thứ hai. DuckDB so sánh chuỗi theo mã ký tự, nên mọi chữ hoa đứng trước mọi chữ thường:
SELECT x FROM (VALUES ('banana'), ('Cherry'), ('apple'), ('Apple')) t(x)
ORDER BY x;
| x |
|---|
| Apple |
| Cherry |
| apple |
| banana |
PostgreSQL với collation en_US thông dụng trả về apple, Apple, banana, Cherry; MySQL và SQL Server với collation mặc định không phân biệt hoa thường coi apple và Apple là bằng nhau. Để có cùng thứ tự ở mọi nơi, hãy sắp theo lower(x), kèm x làm cột phân định:
| x |
|---|
| Apple |
| apple |
| banana |
| Cherry |
Sắp xếp tên tiếng Việt
Sắp theo mã ký tự còn làm hỏng thứ tự tiếng Việt: “Đ” có mã lớn hơn mọi chữ Latin cơ bản, nên Đà Nẵng rơi xuống sau Vũng Tàu:
SELECT city
FROM (VALUES ('Vũng Tàu'), ('Đà Nẵng'), ('An Giang'), ('Huế'), ('Bình Dương'), ('Cần Thơ')) t(city)
ORDER BY city;
| city |
|---|
| An Giang |
| Bình Dương |
| Cần Thơ |
| Huế |
| Vũng Tàu |
| Đà Nẵng |
Chỉ định collation tiếng Việt và “Đ” trở về đúng chỗ, ngay sau “D”:
SELECT city
FROM (VALUES ('Vũng Tàu'), ('Đà Nẵng'), ('An Giang'), ('Huế'), ('Bình Dương'), ('Cần Thơ')) t(city)
ORDER BY city COLLATE vi;
| city |
|---|
| An Giang |
| Bình Dương |
| Cần Thơ |
| Đà Nẵng |
| Huế |
| Vũng Tàu |
Ở các hệ khác: SQL Server dùng ORDER BY city COLLATE Vietnamese_CI_AS, PostgreSQL dùng collation ICU ORDER BY city COLLATE "vi-x-icu", còn MySQL 8 có utf8mb4_vi_0900_ai_ci. Nếu cột lưu họ tên đầy đủ, nhớ rằng người Việt thường sắp danh sách theo tên (chữ cuối), không theo họ, nên bạn cần tách tên ra một cột hoặc biểu thức riêng trước khi sắp.
Thứ tự ngẫu nhiên
Để chọn dòng ngẫu nhiên, hãy sắp theo một số ngẫu nhiên:
SELECT member_id, signup_channel
FROM loyalty_members
ORDER BY random()
LIMIT 3;
| member_id | signup_channel |
|---|---|
| 17473 | app |
| 12177 | web |
| 12825 | web |
Ba dòng của bạn sẽ khác, và đó chính là mục đích. Hàm này là random() trong PostgreSQL, DuckDB và SQLite, RAND() trong MySQL, còn SQL Server dùng ORDER BY NEWID(). Sắp xếp cả một bảng lớn chỉ để giữ ba dòng rất tốn kém; để lấy mẫu từ bảng lớn, PostgreSQL và DuckDB có TABLESAMPLE (DuckDB còn có USING SAMPLE).
Lỗi thường gặp
- Dựa vào thứ tự “tự nhiên”. Không có
ORDER BY, thứ tự là bất cứ thứ gì engine thấy tiện, và nó có thể đổi giữa các lần chạy hoặc sau một lần cập nhật. ORDER BYtrong truy vấn con hoặc view. Hầu hết hệ quản trị không cam kết giữ thứ tự đó ở truy vấn ngoài (SQL Server từ chối nó nếu không cóTOP). Hãy sắp xếp ở truy vấn ngoài cùng.DESCtrên một cột và nghĩ nó áp dụng cho tất cả. Mỗi cột cần chiều riêng.- Không có cột phân định trước
LIMIT. Giá trị bằng nhau làm top N và phân trang không ổn định. - Quên
NULLnằm ở đâu. ViếtNULLS LASThoặcNULLS FIRSTmỗi khi cột sắp xếp có thể rỗng. - Sắp xếp số lưu dạng chuỗi. Hãy ép kiểu, hoặc sửa kiểu của cột.
- Sắp xếp tiếng Việt không có collation. Tên bắt đầu bằng “Đ” sẽ bị đẩy xuống cuối.
Các quy tắc này xuất hiện lại trong bài câu lệnh SQL cơ bản và SQL cheat sheet.
Câu hỏi thường gặp
ORDER BY mặc định sắp xếp tăng hay giảm?
Tăng dần (ASC): số nhỏ nhất, ngày sớm nhất và đầu bảng chữ cái đứng trước. DESC đảo ngược lại. Vị trí mặc định của NULL tùy hệ quản trị, nên hãy nói rõ bằng NULLS FIRST hoặc NULLS LAST.
Làm sao ORDER BY nhiều cột?
Liệt kê chúng, cách nhau bằng dấu phẩy, mỗi cột có chiều riêng: ORDER BY maker ASC, range DESC. Cột thứ hai chỉ quyết định thứ tự giữa những dòng bằng nhau ở cột thứ nhất.
Có dùng bí danh cột trong ORDER BY được không?
Được. ORDER BY chạy sau SELECT, nên SELECT COUNT(*) AS flights ... ORDER BY flights DESC chạy được ở mọi hệ quản trị lớn. Bạn không dùng được bí danh trong WHERE, vì WHERE chạy trước SELECT.
Làm sao đưa giá trị NULL xuống cuối?
Thêm NULLS LAST sau cột: ORDER BY delay_min DESC NULLS LAST. Trong MySQL và SQL Server, vốn không có cú pháp này, hãy sắp theo cờ “là null” trước, ví dụ ORDER BY delay_min IS NULL, delay_min DESC trong MySQL.
Có ORDER BY theo cột không có trong SELECT được không?
Được, trừ khi dùng SELECT DISTINCT (trong PostgreSQL và SQL Server) hoặc khi cột đó không còn sau GROUP BY. Trong truy vấn có nhóm, bạn có thể sắp theo cột đã nhóm hoặc theo một hàm tổng hợp như COUNT(*).
Làm sao sắp xếp đúng thứ tự chữ cái tiếng Việt?
Dùng collation tiếng Việt: COLLATE vi trong DuckDB, COLLATE Vietnamese_CI_AS trong SQL Server, COLLATE "vi-x-icu" trong PostgreSQL. Không có nó, các tên bắt đầu bằng “Đ” bị xếp sau “Z”.
ORDER BY có làm truy vấn chậm đi không?
Sắp xếp tốn thời gian với kết quả lớn, nhất là khi không có LIMIT. Với ORDER BY ... LIMIT n, các hệ quản trị dùng kiểu sắp xếp top N chỉ giữ n dòng trong bộ nhớ, và một index trên cột sắp xếp có thể giúp bỏ qua hoàn toàn bước sắp xếp trong PostgreSQL, MySQL hay SQL Server.
Ngôn ngữ khác: English, Español, Português (Brasil), 简体中文