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

Một bàn làm việc với tờ tra cứu SQL in trên một trang, laptop đang mở một truy vấn và một cốc cà phê

Đâ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

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), 简体中文