SQL cho phân tích dữ liệu · Hướng dẫn · 30 tháng 10, 2026 · 15 phút đọc
SQL cheat sheet cho data analyst: mọi mệnh đề và hàm kèm ví dụ chạy được

Đây là phần SQL mà một data analyst dùng hằng ngày, gói trong một trang: các mệnh đề của truy vấn, bộ lọc, hàm tổng hợp, JOIN, CASE, xử lý NULL, truy vấn con và CTE, hàm cửa sổ (window function), cùng những hàm ngày giờ và hàm chuỗi bạn dùng nhiều nhất. Mỗi phần có một bảng tra cứu ngắn và một ví dụ kèm kết quả thật, để bạn thấy cú pháp thực sự trả về gì thay vì phải đoán.
Mọi ví dụ đều chạy trên cơ sở dữ liệu hãng hàng không của khóa SQL trên Nomto: 104 sân bay, 9 mẫu máy bay, 33.121 chuyến bay từ 12/7 đến 10/9/2026, 262.788 đơn đặt chỗ; tiền tính bằng đô la Mỹ và thời gian theo giờ Moscow. Cú pháp là PostgreSQL. Bạn có thể dán bất kỳ truy vấn nào vào phòng học SQL của Nomto và chạy nguyên như vậy.
Các bảng được dùng bên dưới
| Bảng | Mỗi dòng là | Các cột dùng ở đây |
|---|---|---|
airports |
một sân bay | airport_code, airport_name, city, timezone |
aircrafts |
một mẫu máy bay | aircraft_code, model, range (km) |
flights |
một chuyến khởi hành trong một ngày | flight_id, flight_no, scheduled_departure, actual_departure, departure_airport, arrival_airport, status, aircraft_code |
bookings |
một đơn đặt chỗ | book_ref, book_date, total_amount |
tickets |
một hành khách trong một đơn đặt chỗ | ticket_no, book_ref, passenger_name, contact_data (JSON) |
ticket_flights |
một chặng bay trên một vé | ticket_no, flight_id, fare_conditions, amount |
Khung truy vấn: các mệnh đề và thứ tự của chúng
| Mệnh đề | Làm gì | Chạy thứ |
|---|---|---|
SELECT |
các cột và biểu thức cần trả về | 5 |
FROM / JOIN |
dòng lấy từ đâu | 1 |
WHERE |
giữ những dòng thỏa điều kiện | 2 |
GROUP BY |
mỗi nhóm một dòng | 3 |
HAVING |
giữ những nhóm thỏa điều kiện | 4 |
ORDER BY |
sắp xếp kết quả | 6 |
LIMIT |
giữ N dòng đầu tiên | 7 |
Bạn viết chúng theo thứ tự ở cột đầu tiên của bảng, nhưng cơ sở dữ liệu chạy chúng theo thứ tự ở cột cuối. Đó là lý do bí danh định nghĩa trong SELECT dùng được trong ORDER BY nhưng không dùng được trong WHERE.
SELECT model, range
FROM aircrafts
WHERE range >= 5000
ORDER BY range DESC
LIMIT 3;
| model | range |
|---|---|
| Boeing 777-300 | 11100 |
| Boeing 767-300 | 7900 |
| Airbus A319-100 | 6700 |
Bộ lọc (WHERE)
| Toán tử | Ví dụ | Ghi chú |
|---|---|---|
=, <>, <, >= |
range >= 5000 |
văn bản đặt trong nháy đơn |
AND, OR, NOT |
a AND (b OR c) |
AND ưu tiên hơn OR: hãy dùng ngoặc |
IN |
status IN ('Departed', 'Arrived') |
viết tắt cho nhiều OR |
BETWEEN |
x BETWEEN 1 AND 10 |
bao gồm cả hai đầu |
LIKE / ILIKE |
city LIKE 'Novo%' |
% là chuỗi ký tự bất kỳ, _ là một ký tự; ILIKE không phân biệt hoa thường |
IS NULL |
actual_departure IS NULL |
không bao giờ viết = NULL |
SELECT COUNT(*) FILTER (WHERE status IN ('Departed', 'Arrived')) AS flown,
COUNT(*) FILTER (WHERE scheduled_departure BETWEEN '2026-08-01' AND '2026-08-07 23:59:59') AS first_week_aug,
COUNT(*) FILTER (WHERE actual_departure IS NULL) AS not_departed
FROM flights;
| flown | first_week_aug | not_departed |
|---|---|---|
| 16765 | 3798 | 16348 |
FILTER (WHERE ...) cho phép đặt ba bộ lọc khác nhau trong một truy vấn; mỗi COUNT chỉ thấy các dòng của riêng nó. Với khoảng ngày, >= '2026-08-01' AND < '2026-08-08' an toàn hơn BETWEEN, vì bạn không phải nhớ thêm 23:59:59.
Hàm tổng hợp
| Hàm | Trả về |
|---|---|
COUNT(*) |
số dòng |
COUNT(col) |
số giá trị khác NULL |
COUNT(DISTINCT col) |
số giá trị khác nhau, khác NULL |
SUM(col), AVG(col) |
tổng, trung bình (bỏ qua NULL) |
MIN(col), MAX(col) |
nhỏ nhất, lớn nhất (dùng được cả với ngày và văn bản) |
SELECT COUNT(*) AS bookings,
COUNT(DISTINCT DATE_TRUNC('day', book_date)) AS days,
SUM(total_amount) AS revenue,
ROUND(AVG(total_amount), 2) AS avg_booking,
MIN(total_amount) AS min_b,
MAX(total_amount) AS max_b
FROM bookings;
| bookings | days | revenue | avg_booking | min_b | max_b |
|---|---|---|---|---|---|
| 262788 | 56 | 207669809.00 | 790.26 | 34.00 | 12045.00 |
GROUP BY và HAVING
Mọi cột trong SELECT phải nằm trong GROUP BY hoặc bên trong một hàm tổng hợp. WHERE lọc dòng trước khi nhóm; HAVING lọc nhóm sau đó.
SELECT city, COUNT(*) AS airports
FROM airports
GROUP BY city
HAVING COUNT(*) > 1
ORDER BY airports DESC, city;
| city | airports |
|---|---|
| Moscow | 3 |
| Ulyanovsk | 2 |
Xem thêm về sự khác biệt trong bài GROUP BY và HAVING.
JOIN
| JOIN | Giữ lại |
|---|---|
JOIN (INNER JOIN) |
chỉ những dòng khớp ở cả hai phía |
LEFT JOIN |
mọi dòng của bảng bên trái; NULL ở chỗ bên phải không có dòng khớp |
RIGHT JOIN |
ngược lại của LEFT JOIN (hiếm khi dùng: hãy đổi chỗ hai bảng) |
FULL JOIN |
mọi dòng của cả hai phía |
CROSS JOIN |
mọi tổ hợp dòng |
LEFT JOIN ... WHERE right.key IS NULL |
những dòng không có dòng khớp (anti join) |
Tra một giá trị từ bảng khác:
SELECT f.flight_no, a.model
FROM flights f
JOIN aircrafts a ON a.aircraft_code = f.aircraft_code
WHERE f.flight_id = 1;
| flight_no | model |
|---|---|
| PG0405 | Airbus A321-200 |
Tìm những dòng không có cặp, ở đây là mẫu máy bay không bao giờ bay:
SELECT a.model
FROM aircrafts a
LEFT JOIN flights f ON f.aircraft_code = a.aircraft_code
WHERE f.flight_id IS NULL;
| model |
|---|
| Airbus A320-200 |
Cái bẫy cần nhớ: khi một dòng khớp với nhiều dòng, nó bị lặp lại một lần cho mỗi dòng khớp. Cộng bookings.total_amount sau khi JOIN với tickets cho ra 343.206.192 thay vì con số đúng 207.669.809, vì những đơn đặt chỗ có nhiều vé bị tính nhiều lần. Bài JOIN trong SQL giải thích chi tiết hiện tượng nhân dòng này.
CASE: logic nếu-thì
CASE WHEN condition1 THEN value1
WHEN condition2 THEN value2
ELSE default
END
Các điều kiện được kiểm tra từ trên xuống, điều kiện đúng đầu tiên thắng, nên hãy đặt điều kiện hẹp nhất lên trước.
SELECT CASE WHEN total_amount >= 1000 THEN 'large'
WHEN total_amount >= 300 THEN 'medium'
ELSE 'small'
END AS size,
COUNT(*) AS bookings
FROM bookings
GROUP BY size
ORDER BY bookings DESC;
| size | bookings |
|---|---|
| medium | 130009 |
| small | 68305 |
| large | 64474 |
SUM(CASE WHEN ... THEN 1 ELSE 0 END) là cách đếm có điều kiện chạy được ở mọi hệ quản trị; COUNT(*) FILTER (WHERE ...) làm điều tương tự trong PostgreSQL.
Xử lý NULL
| Biểu thức | Làm gì |
|---|---|
col IS NULL / IS NOT NULL |
cách duy nhất đúng để kiểm tra NULL |
COALESCE(a, b, ...) |
giá trị khác NULL đầu tiên |
NULLIF(a, b) |
NULL nếu a = b, ngược lại là a (tiện để tránh chia cho 0) |
COUNT(col) |
bỏ qua NULL; COUNT(*) thì không |
SELECT flight_no, scheduled_departure, status,
COALESCE(CAST(actual_departure AS VARCHAR), 'not departed') AS actual
FROM flights
WHERE flight_no = 'PG0405'
AND scheduled_departure BETWEEN '2026-08-10' AND '2026-08-13'
ORDER BY scheduled_departure;
| flight_no | scheduled_departure | status | actual |
|---|---|---|---|
| PG0405 | 2026-08-10 09:35:00 | Arrived | 2026-08-10 09:37:00 |
| PG0405 | 2026-08-11 09:35:00 | Arrived | 2026-08-11 09:38:00 |
| PG0405 | 2026-08-12 09:35:00 | On Time | not departed |
Một phép tính phần trăm an toàn, trả về NULL thay vì báo lỗi nếu bảng rỗng:
SELECT ROUND(100.0 * COUNT(*) FILTER (WHERE status = 'Cancelled') / NULLIF(COUNT(*), 0), 2) AS cancelled_pct
FROM flights;
| cancelled_pct |
|---|
| 1.25 |
Hãy viết 100.0, không phải 100: với hai số nguyên, một số hệ quản trị thực hiện phép chia nguyên và trả về 0.
Truy vấn con và CTE
Truy vấn con là một truy vấn nằm bên trong truy vấn khác. Số đơn đặt chỗ có giá trị trên mức trung bình:
SELECT COUNT(*) AS above_avg
FROM bookings
WHERE total_amount > (SELECT AVG(total_amount) FROM bookings);
| above_avg |
|---|
| 87224 |
CTE (WITH) đặt tên cho một truy vấn con để bạn đọc truy vấn từ trên xuống dưới. Số chuyến bay mỗi ngày, rồi giá trị nhỏ nhất, trung bình và lớn nhất của các số đếm theo ngày đó:
WITH daily AS (
SELECT CAST(scheduled_departure AS DATE) AS day, COUNT(*) AS flights
FROM flights
GROUP BY day
)
SELECT MIN(flights) AS min_day, ROUND(AVG(flights), 1) AS avg_day, MAX(flights) AS max_day
FROM daily;
| min_day | avg_day | max_day |
|---|---|---|
| 493 | 543 | 568 |
Hãy dùng CTE mỗi khi bạn cần tổng hợp hai lần (trung bình của các số đếm theo ngày, tỷ trọng trên tổng) hoặc khi truy vấn dài hơn một màn hình.
Hàm cửa sổ
Hàm cửa sổ thêm một giá trị vào mỗi dòng mà không gộp các dòng lại. Dạng chung là function() OVER (PARTITION BY ... ORDER BY ... frame).
| Hàm | Dùng để |
|---|---|
ROW_NUMBER() |
top N mỗi nhóm, loại bỏ dòng trùng |
RANK(), DENSE_RANK() |
xếp hạng có đồng hạng (RANK để lại khoảng trống, DENSE_RANK thì không) |
LAG(col), LEAD(col) |
giá trị của dòng trước / dòng sau |
SUM(col) OVER (ORDER BY ...) |
tổng lũy kế |
AVG(col) OVER (ORDER BY ... ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) |
trung bình trượt 7 dòng |
SUM(col) OVER () |
tổng chung bên cạnh mỗi dòng (để tính tỷ trọng) |
So sánh với ngày hôm trước và tổng lũy kế trong cùng một truy vấn:
SELECT CAST(book_date AS DATE) AS day,
COUNT(*) AS bookings,
LAG(COUNT(*)) OVER (ORDER BY CAST(book_date AS DATE)) AS prev_day,
SUM(COUNT(*)) OVER (ORDER BY CAST(book_date AS DATE)) AS running_total
FROM bookings
GROUP BY day
ORDER BY day
LIMIT 4;
| day | bookings | prev_day | running_total |
|---|---|---|---|
| 2026-06-17 | 3 | NULL | 3 |
| 2026-06-18 | 7 | 3 | 10 |
| 2026-06-19 | 20 | 7 | 30 |
| 2026-06-20 | 74 | 20 | 104 |
Top 1 mỗi nhóm với ROW_NUMBER: sân bay khởi hành bận rộn nhất của từng mẫu máy bay.
SELECT *
FROM (
SELECT a.model, f.departure_airport, COUNT(*) AS flights,
ROW_NUMBER() OVER (PARTITION BY a.model ORDER BY COUNT(*) DESC) AS rn
FROM flights f
JOIN aircrafts a USING (aircraft_code)
GROUP BY a.model, f.departure_airport
) t
WHERE rn = 1
ORDER BY flights DESC
LIMIT 4;
| model | departure_airport | flights | rn |
|---|---|---|---|
| Sukhoi Superjet-100 | SVO | 1011 | 1 |
| Bombardier CRJ-200 | DME | 829 | 1 |
| Airbus A321-200 | LED | 793 | 1 |
| Cessna 208 Caravan | KJA | 488 | 1 |
Bạn không thể lọc theo hàm cửa sổ trong WHERE của cùng truy vấn, vì thế mới cần truy vấn con. Hướng dẫn đầy đủ: hàm cửa sổ trong SQL và tổng lũy kế (tiếng Anh).
Ngày và giờ
| Biểu thức | Kết quả |
|---|---|
CAST(ts AS DATE) |
bỏ phần giờ |
DATE_TRUNC('month', ts) |
thời điểm đầu tiên của tháng (cũng có 'week', 'day', 'hour') |
EXTRACT(MONTH FROM ts) |
số tháng; cũng có YEAR, DAY, HOUR, ISODOW (1 = thứ Hai) |
ts + INTERVAL '30 days' |
cộng trừ ngày |
date1 - date2 |
số ngày giữa hai ngày |
EXTRACT(EPOCH FROM ts1 - ts2) / 60 |
số phút giữa hai mốc thời gian |
Số chuyến bay mỗi tháng:
SELECT DATE_TRUNC('month', scheduled_departure) AS month, COUNT(*) AS flights
FROM flights
GROUP BY month
ORDER BY month;
| month | flights |
|---|---|
| 2026-07-01 00:00:00 | 10826 |
| 2026-08-01 00:00:00 | 16854 |
| 2026-09-01 00:00:00 | 5441 |
Ở đây tháng 7 và tháng 9 là tháng không trọn (lịch bay kéo dài từ 12/7 đến 10/9), điều đáng nói rõ mỗi khi bạn so sánh các tháng.
Cộng và trừ ngày:
SELECT book_ref, book_date,
book_date + INTERVAL '30 days' AS plus_30,
'2026-08-11'::DATE - CAST(book_date AS DATE) AS days_ago
FROM bookings
ORDER BY book_ref
LIMIT 2;
| book_ref | book_date | plus_30 | days_ago |
|---|---|---|---|
| 00000F | 2026-07-01 03:12:00 | 2026-07-31 03:12:00 | 41 |
| 000012 | 2026-07-10 09:02:00 | 2026-08-09 09:02:00 | 32 |
Những lần cất cánh trễ lâu nhất, tính bằng phút:
SELECT flight_no, scheduled_departure, actual_departure,
ROUND(EXTRACT(EPOCH FROM actual_departure - scheduled_departure) / 60) AS delay_min
FROM flights
WHERE status = 'Arrived'
ORDER BY delay_min DESC
LIMIT 3;
| flight_no | scheduled_departure | actual_departure | delay_min |
|---|---|---|---|
| PG0589 | 2026-07-25 15:30:00 | 2026-07-25 20:07:00 | 277 |
| PG0164 | 2026-07-25 15:25:00 | 2026-07-25 19:53:00 | 268 |
| PG0364 | 2026-07-15 11:45:00 | 2026-07-15 16:12:00 | 267 |
Với những ngày hoàn toàn không có dòng nào (một ngày không có đơn đặt chỗ đơn giản là không xuất hiện trong GROUP BY), xem bài về date spine (tiếng Anh).
Chuỗi
| Hàm | Ví dụ | Kết quả |
|---|---|---|
|| |
'PG' || '0405' |
PG0405 |
UPPER, LOWER |
LOWER('SVO') |
svo |
LENGTH |
LENGTH('Vnukovo') |
7 |
TRIM |
TRIM(' SVO ') |
SVO |
LEFT(s, n), SUBSTRING(s, start, len) |
LEFT('Moscow', 3) |
Mos |
POSITION(sub IN s) |
POSITION('/' IN 'Europe/Moscow') |
7 |
REPLACE(s, from, to) |
REPLACE('Asia/Ust-Nera', '-', ' ') |
Asia/Ust Nera |
SPLIT_PART(s, sep, n) |
SPLIT_PART('IVAN PETROV', ' ', 2) |
PETROV |
json ->> 'key' |
contact_data ->> 'phone' |
giá trị dưới dạng văn bản |
Tên hành khách được lưu bằng chữ in hoa theo dạng “TÊN HỌ”, còn thông tin liên hệ là JSON:
SELECT passenger_name,
LOWER(SPLIT_PART(passenger_name, ' ', 2)) AS last_lower,
contact_data ->> 'phone' AS phone
FROM tickets
ORDER BY ticket_no
LIMIT 3;
| passenger_name | last_lower | phone |
|---|---|---|
| VALERIY TIKHONOV | tikhonov | +70127117011 |
| EVGENIYA ALEKSEEVA | alekseeva | +70378089255 |
| ARTUR GERASIMOV | gerasimov | +70760429203 |
Tạo nhãn và cắt chuỗi tại một ký tự:
SELECT airport_name || ' (' || airport_code || ')' AS label,
SUBSTRING(timezone, 1, POSITION('/' IN timezone) - 1) AS region
FROM airports
WHERE city = 'Moscow'
ORDER BY airport_code;
| label | region |
|---|---|
| Domodedovo (DME) | Europe |
| Sheremetyevo (SVO) | Europe |
| Vnukovo (VKO) | Europe |
Các hàm chuỗi được trình bày đầy đủ trong bài hàm chuỗi trong SQL.
Những lỗi mà cheat sheet này giúp bạn tránh
= NULL. Luôn dùngIS NULL; phép so sánh bằng không trả về dòng nào và cũng không báo lỗi.LIMITkhông cóORDER BY. Bạn nhận được vài dòng nào đó, không phải những dòng đứng đầu.- Cộng tổng sau JOIN một-nhiều. Hãy tổng hợp phía “nhiều” trước, rồi mới JOIN.
- Phép chia nguyên.
100 * a / bcó thể làm tròn thành 0; hãy viết100.0. - Lọc hàm cửa sổ trong
WHERE. Hãy bọc nó trong truy vấn con hoặc CTE. BETWEENtrên mốc thời gian.'2026-08-07'nghĩa là nửa đêm; hãy dùng>=ngày bắt đầu và<ngày kế tiếp.
Khác biệt giữa các phương ngữ SQL
Mọi thứ ở đây là PostgreSQL. Phần lớn giống hệt trong các hệ quản trị khác; những khác biệt thường gặp là:
| Việc cần làm | PostgreSQL | MySQL | SQL Server |
|---|---|---|---|
| N dòng đầu tiên | LIMIT 10 |
LIMIT 10 |
TOP 10 / FETCH FIRST 10 ROWS ONLY |
| nối chuỗi | a || b |
CONCAT(a, b) |
a + b hoặc CONCAT(a, b) |
| cắt ngày về đầu kỳ | DATE_TRUNC('month', d) |
DATE_FORMAT(d, '%Y-%m-01') |
DATETRUNC(month, d) (từ 2022) |
| LIKE không phân biệt hoa thường | ILIKE |
LIKE (tùy collation) |
LIKE (tùy collation) |
| đếm có điều kiện | COUNT(*) FILTER (WHERE ...) |
SUM(CASE WHEN ... THEN 1 ELSE 0 END) |
SUM(CASE WHEN ... THEN 1 ELSE 0 END) |
Cách dùng cheat sheet này
Hãy để nó mở khi làm việc, nhưng đừng cố học thuộc. Bạn nhớ cú pháp nhờ dùng nó để trả lời những câu hỏi mình quan tâm. Trong phòng học SQL của Nomto, mọi truy vấn ở trên đều chạy trên cùng cơ sở dữ liệu hãng hàng không, nên hãy đổi một bộ lọc hay một cột nhóm và xem điều gì thay đổi. Nếu bạn mới bắt đầu, bài câu lệnh SQL cơ bản giải thích từng bước nửa đầu của trang này, còn bài tập SQL có lời giải cho bạn bài tập để áp dụng nửa sau.
Câu hỏi thường gặp
Data analyst cần biết những gì về SQL?
Nắm chắc: SELECT, WHERE, GROUP BY, HAVING, JOIN, CASE, xử lý NULL, truy vấn con và CTE, và các hàm ngày giờ. Sau đó là hàm cửa sổ (ROW_NUMBER, LAG, tổng lũy kế), thứ xuất hiện trong hầu hết các buổi phỏng vấn data analyst. Index, stored procedure và quản trị cơ sở dữ liệu ít quan trọng hơn nhiều với vị trí này.
Cheat sheet này có dùng được cho MySQL và SQL Server không?
Các mệnh đề, JOIN, hàm tổng hợp, CASE, COALESCE, truy vấn con, CTE và hàm cửa sổ hoạt động như nhau. Hàm ngày giờ, cách nối chuỗi và LIMIT thì khác; bảng phương ngữ ở trên liệt kê các trường hợp thường gặp.
Truy vấn con và CTE khác nhau thế nào?
Chúng làm cùng một việc. CTE đặt tên cho truy vấn con và đưa nó lên đầu, nên một truy vấn dài đọc được theo từng bước. Hãy dùng cách nào dễ đọc hơn; với mọi thứ có từ hai bước trở lên, đó thường là CTE.
Có chạy được các ví dụ này mà không cần cài cơ sở dữ liệu không?
Được. Hãy mở phòng học SQL của Nomto, dán truy vấn và chạy ngay trong trình duyệt trên cùng dữ liệu hãng hàng không.
Ngôn ngữ khác: English, Español, Português (Brasil), 简体中文