Sau bài đầu về cơ sở dữ liệu quan hệ, mình tiếp tục ôn lại các lệnh SQL cơ bản. Dù đã biết lý thuyết, nhưng khi thực hành hoặc bị hỏi, mình vẫn hay quên cú pháp hoặc nhầm lẫn. Viết bài này cũng là dịp để mình ghi nhớ kỹ hơn, nếu bạn cũng đang học SQL thì có thể tham khảo.
Mục tiêu của mình là viết sao cho dễ nuốt nhất có thể. Có chỗ mình sẽ chèn chút ví dụ đời thường, chỗ khác có vài câu vui vui để đỡ căng não. Mình từng vật lộn với JOIN cả tuần, nên nếu bạn thấy nó rối như mớ mì Ý, yên tâm: ai cũng từng như vậy.
Lưu ý: Trong bài này mình minh họa chủ yếu bằng PostgreSQL. Một số cú pháp có thể khác nhẹ so với MySQL (ví dụ SERIAL, DELETE ... USING, FULL OUTER JOIN).
Mẹo thực tế: Trong schema trên, mình dùng SERIAL cho khóa chính ở PostgreSQL để tự tăng ID cho đỡ phải nghĩ. Giá tiền dùng DECIMAL(10,2) để tránh lỗi làm tròn. Những thứ nhỏ nhỏ này về sau tiết kiệm kha khá thời gian debug.
Trước khi đi sâu vào các lệnh, mình muốn ôn lại một số khái niệm nền tảng mà ai học SQL cũng cần biết. Những thứ này tưởng chừng đơn giản nhưng rất quan trọng.
SQL có nhiều kiểu dữ liệu khác nhau tùy theo hệ quản trị. Mình sẽ nói về những kiểu phổ biến nhất:
Kiểu số:
INT hoặc INTEGER - Số nguyên (ví dụ: 42)
BIGINT - Số nguyên lớn (ví dụ: 9223372036854775807)
DECIMAL(p,s) hoặc NUMERIC(p,s) - Số thập phân chính xác (p: tổng chữ số, s: chữ số sau dấu phẩy)
FLOAT hoặc REAL - Số thực dấu phẩy động
Kiểu chuỗi:
CHAR(n) - Chuỗi có độ dài cố định n ký tự
VARCHAR(n) - Chuỗi có độ dài tối đa n ký tự
TEXT - Chuỗi dài không giới hạn
Kiểu ngày giờ:
DATE - Chỉ ngày (YYYY-MM-DD)
TIME - Chỉ giờ (HH:MM:SS)
DATETIME hoặc TIMESTAMP - Ngày và giờ
YEAR - Chỉ năm
Kiểu logic:
BOOLEAN hoặc BOOL - True/False
Kiểu nhị phân:
BLOB - Dữ liệu nhị phân lớn
JSON - Dữ liệu JSON (trong PostgreSQL, MySQL 5.7+)
Chọn kiểu dữ liệu phù hợp rất quan trọng. VARCHAR(100) cho tên người thì hợp lý, nhưng cho nội dung bài viết thì nên dùng TEXT. Sử dụng DECIMAL cho tiền tệ để tránh lỗi làm tròn.
Ví dụ trong schema của mình:
ID SERIAL PRIMARY KEY - ID tự tăng (PostgreSQL)
TenSach VARCHAR(100) - Tên sách tối đa 100 ký tự
Gia DECIMAL(10,2) - Giá tiền với 2 chữ số thập phân
SQL có nhiều loại toán tử để so sánh, tính toán, và kết hợp điều kiện:
Toán tử so sánh:
= - Bằng
!= hoặc <> - Khác
< - Nhỏ hơn
> - Lớn hơn
<= - Nhỏ hơn hoặc bằng
>= - Lớn hơn hoặc bằng
Toán tử logic:
AND - Và (cả hai điều kiện phải đúng)
OR - Hoặc (ít nhất một điều kiện đúng)
NOT - Phủ định (điều kiện ngược lại)
Toán tử đặc biệt:
IS NULL - Kiểm tra giá trị NULL
IS NOT NULL - Kiểm tra không NULL
LIKE - So khớp pattern (dùng với % và _)
IN - Kiểm tra trong danh sách
BETWEEN - Kiểm tra trong khoảng
EXISTS - Kiểm tra tồn tại subquery
Toán tử số học:
+ - Cộng
- - Trừ
* - Nhân
/ - Chia
% - Chia lấy dư
Thứ tự ưu tiên: số học > so sánh > logic. Dùng ngoặc đơn () để thay đổi thứ tự. Ví dụ: WHERE Gia > 100000 AND (TonKho < 10 OR TonKho > 100)
Ví dụ thực tế:
sql
-- Tìm sách giá từ 100k-200k và còn hàngSELECT * FROM Sach WHERE Gia BETWEEN 100000 AND 200000AND TonKho > 0;-- Tìm sách của tác giả bắt đầu bằng "Nguyễn"SELECT * FROM Sach WHERE TacGia LIKE 'Nguyễn%';-- Tìm sách giá rẻ (<150k) hoặc hết hàng (=0)SELECT * FROM Sach WHERE Gia < 150000 OR TonKho = 0;
GROUP BY dùng để nhóm các hàng có giá trị giống nhau trong một cột, thường kết hợp với các hàm tổng hợp như SUM, COUNT, AVG, MIN, MAX.
Ví dụ, tính tổng doanh thu theo sách:
sql
SELECT s.TenSach, SUM(d.SoLuong * s.Gia) AS TongDoanhThuFROM Sach sJOIN DonHang d ON s.ID = d.Sach_IDGROUP BY s.ID, s.TenSach;
tensach
tongdoanhthu
Web Dev Basics
180000.00
SQL Dễ Hiểu
240000.00
Lập trình Python
150000.00
GROUP BY sẽ tạo các nhóm dựa trên cột chỉ định, và các hàm tổng hợp tính toán trên mỗi nhóm. Nếu không có GROUP BY, hàm tổng hợp sẽ tính trên toàn bộ kết quả.
Gotcha: Với PostgreSQL, mọi cột xuất hiện trong SELECT (ngoài các hàm tổng hợp) đều phải có trong GROUP BY. Để an toàn và ổn định, hãy GROUP BY khóa chính (ví dụ s.ID) kèm theo các cột mô tả (ví dụ s.TenSach).
Thêm HAVING để lọc nhóm:
sql
SELECT s.TenSach, SUM(d.SoLuong * s.Gia) AS TongDoanhThuFROM Sach sJOIN DonHang d ON s.ID = d.Sach_IDGROUP BY s.ID, s.TenSachHAVING SUM(d.SoLuong * s.Gia) > 160000;
tensach
tongdoanhthu
Web Dev Basics
180000.00
SQL Dễ Hiểu
240000.00
HAVING khác WHERE ở chỗ WHERE lọc trước khi nhóm, HAVING lọc sau khi nhóm.
Ví dụ khác: Đếm số đơn hàng theo khách hàng, chỉ lấy những khách có hơn 1 đơn:
sql
SELECT k.Ten, COUNT(d.ID) AS SoDonFROM KhachHang kLEFT JOIN DonHang d ON k.ID = d.KhachHang_IDGROUP BY k.ID, k.TenHAVING COUNT(d.ID) > 1;
Result: 1 row updated. Sách ID 2 giờ có Gia = 130000.
Cập nhật nhiều cột:
sql
UPDATE Sach SET Gia = 155000, TonKho = 45 WHERE ID = 1;
Result: 1 row updated. Sách ID 1 giờ có Gia = 155000, TonKho = 45.
Quan trọng: Luôn dùng WHERE, nếu quên sẽ cập nhật toàn bộ bảng
Mẹo an toàn: Trước khi UPDATE/DELETE, hãy chạy một câu SELECT ... WHERE ... với đúng điều kiện để xem sẽ đụng vào những dòng nào. Làm vậy tránh những lần nghịch ngu :))).
Giả sử không có đơn hàng nào tham chiếu tới sách này (hoặc foreign key chưa bật / đã dùng ON DELETE CASCADE), lệnh sẽ xóa 1 dòng.
Result: 1 row deleted. Sách ID 3 đã bị xóa.
Xóa sách hết hàng:
sql
DELETE FROM Sach WHERE TonKho = 0;
Result: 0 rows deleted. Không có sách nào hết hàng trong dữ liệu mẫu.
Cũng nhớ dùng WHERE, nếu không sẽ xóa toàn bộ bảng
Deep dive: DELETE với JOIN (PostgreSQL):
sql
-- PostgreSQL: dùng USING để JOIN trong DELETEDELETE FROM Sach sUSING DonHang dWHERE s.ID = d.Sach_ID AND d.NgayMua < '2025-01-01';
Result: 0 rows deleted. Không có đơn hàng nào trước 2025.
Xóa sách đã được mua trước năm 2025. Trong PostgreSQL, nếu có foreign key từ DonHang -> Sach mà KHÔNG đặt ON DELETE CASCADE, lệnh có thể fail; thông thường nên xóa ở bảng con trước hoặc bật CASCADE.
Mẹo thực tế: Viết phiên bản SELECT trước để soi kết quả:
sql
SELECT s.*FROM Sach sUSING DonHang d -- (gợi ý cách tư duy, không phải cú pháp hợp lệ cho SELECT)WHERE s.ID = d.Sach_ID AND d.NgayMua < '2025-01-01';
Trong thực tế, bạn sẽ viết JOIN cho SELECT như bình thường, sau đó đổi sang DELETE ... USING khi đã chắc chắn.
Ví dụ về CASCADE: Nếu bảng DonHang có foreign key đến Sach với ON DELETE CASCADE, khi xóa sách, các đơn hàng liên quan sẽ tự động xóa.
sql
-- Giả sử có constraint CASCADEDELETE FROM Sach WHERE ID = 1; -- Sẽ xóa sách và các đơn hàng liên quan
Result: 1 row deleted from Sach, 1 row deleted from DonHang (do CASCADE).
Nếu không CASCADE, lệnh sẽ fail nếu có dữ liệu liên quan.
Dữ liệu của chúng ta được chia ra nhiều bảng (KhachHang, Sach, DonHang). JOIN là cú pháp "thần kỳ" giúp chúng ta kết hợp dữ liệu từ các bảng này lại thành một kết quả duy nhất.
Để dễ hình dung, hãy luôn nghĩ về biểu đồ Venn (biểu đồ tập hợp) khi nói đến JOIN.
Lời thú thật: JOIN là chỗ mình từng xoắn não nhất. Hãy dùng mental model đơn giản:
LEFT JOIN: coi bảng bên trái là chính chủ, luôn giữ lại mọi dòng bên trái; bên phải chỉ thêm được thì thêm, không thì để NULL.
INNER JOIN: chỉ giữ những cặp khớp nhau (giống giao nhau của hai tập).
RIGHT/FULL: giống LEFT nhưng đối xứng, hoặc lấy tất cả hai bên.
Pro tip: Viết SELECT trước với các cột và điều kiện JOIN rõ ràng. Khi kết quả đúng rồi, mới chuyển sang UPDATE/DELETE nếu cần.
Đây là loại JOIN phổ biến nhất. Nó chỉ trả về những hàng có khớp ở cả hai bảng.
Ví dụ: Lấy thông tin các đơn hàng đã được đặt thành công (tức là có Khách hàng khớp và Sách khớp).
sql
SELECT k.Ten, s.TenSach, d.NgayMuaFROM KhachHang kINNER JOIN DonHang d ON k.ID = d.KhachHang_IDINNER JOIN Sach s ON d.Sach_ID = s.ID;
Ten
TenSach
NgayMua
Lê Minh Khánh
Lập trình Python
2025-01-15
Hoàng Thị Lan
SQL Dễ Hiểu
2025-01-16
Lê Minh Khánh
Web Dev Basics
2025-01-17
Phân tích: Chú ý rằng khách hàng "Võ Quốc Huy" (ID 3) không xuất hiện trong kết quả này. Đó là vì khách hàng này không có hàng nào khớp trong bảng DonHang. INNER JOIN chỉ lấy phần giao của hai tập hợp.
Nhiều người sẽ nghĩ viết LEFT JOIN rồi lọc bằng WHERE như sau:
sql
-- Query này SAI về logicSELECT k.Ten, s.TenSachFROM KhachHang kLEFT JOIN DonHang d ON k.ID = d.KhachHang_IDLEFT JOIN Sach s ON d.Sach_ID = s.IDWHERE s.TenSach = 'SQL Dễ Hiểu'; -- CẠM BẪY Ở ĐÂY
Kết quả (Sai):
Ten
TenSach
Hoàng Thị Lan
SQL Dễ Hiểu
Tại sao sai? "Võ Quốc Huy" và "Lê Minh Khánh" đâu rồi?
LEFT JOIN chạy đúng, nó lấy cả "Võ Quốc Huy" (với s.TenSach là NULL) và "Lê Minh Khánh" (với s.TenSach là 'Lập trình Python'...).
Sau đó, mệnh đề WHERE chạy. Nó lọc sau khi đã join.
Hàng của "Võ Quốc Huy": WHERE NULL = 'SQL Dễ Hiểu' -> Sai. (Bị loại)
Hàng của "Lê Minh Khánh": WHERE 'Lập trình Python' = 'SQL Dễ Hiểu' -> Sai. (Bị loại)
Vô tình, LEFT JOIN của bạn đã bị biến thành INNER JOIN.
Khi dùng LEFT JOIN, nếu bạn muốn lọc trên bảng "bên phải" (DonHang, Sach), bạn phải đưa điều kiện đó vào mệnh đề ON.
sql
-- Query này ĐÚNG logicSELECT k.Ten, s.TenSachFROM KhachHang kLEFT JOIN DonHang d ON k.ID = d.KhachHang_IDLEFT JOIN Sach s ON d.Sach_ID = s.ID AND s.TenSach = 'SQL Dễ Hiểu'; -- Điều kiện lọc đặt ở ĐÂY
Kết quả (Đúng):
Ten
TenSach
Lê Minh Khánh
NULL
Hoàng Thị Lan
SQL Dễ Hiểu
Võ Quốc Huy
NULL
Phân tích:
SQL lấy "Lê Minh Khánh", tìm đơn hàng khớp ON ... AND s.TenSach = 'SQL Dễ Hiểu'. Không tìm thấy đơn nào khớp cả hai điều kiện, nên s.TenSach là NULL.
SQL lấy "Hoàng Thị Lan", tìm thấy đơn hàng khớp cả hai điều kiện.
SQL lấy "Võ Quốc Huy", không tìm thấy đơn hàng nào.
Vì là LEFT JOIN, tất cả 3 khách hàng đều được giữ lại.
Quy tắc vàng: Với LEFT JOIN, điều kiện lọc cho bảng "bên trái" (k) đặt ở WHERE. Điều kiện lọc cho bảng "bên phải" (d, s) phải đặt ở ON.
Định Nghĩa:RIGHT JOIN hoạt động ngược lại với LEFT JOIN. Nó giữ tất cả hàng từ bảng bên phải, và chỉ lấy hàng khớp từ bảng bên trái.
Ví Dụ Thực Tế:
sql
SELECT k.Ten AS KhachHang, s.TenSachFROM KhachHang kRIGHT JOIN DonHang d ON k.ID = d.KhachHang_IDRIGHT JOIN Sach s ON d.Sach_ID = s.IDORDER BY s.TenSach;
Kết Quả:
khachhang
tensach
null
JavaScript Advanced
Lê Minh Khánh
Lập trình Python
Hoàng Thị Lan
SQL Dễ Hiểu
Lê Minh Khánh
Web Dev Basics
Ghi Chú Thực Tế: Trong thực tế, RIGHT JOIN ít được dùng hơn LEFT JOIN. Lý do đơn giản: nếu bạn muốn giữ tất cả bảng phải, bạn có thể đổi thứ tự bảng và dùng LEFT JOIN thay vì RIGHT JOIN. Kết quả hoàn toàn giống nhưng rõ ràng hơn.
Định Nghĩa:FULL OUTER JOIN kết hợp cả LEFT JOIN và RIGHT JOIN. Nó trả về tất cả hàng từ cả hai bảng, với NULL cho những phần không khớp.
Lưu Ý:FULL OUTER JOINkhông hỗ trợ trong MySQL. Nếu bạn dùng MySQL, phải dùng UNION như sau:
sql
-- PostgreSQL (hỗ trợ FULL OUTER JOIN)SELECT k.Ten, s.TenSachFROM KhachHang kFULL OUTER JOIN DonHang d ON k.ID = d.KhachHang_IDFULL OUTER JOIN Sach s ON d.Sach_ID = s.ID;-- MySQL (phải dùng UNION thay vì FULL OUTER JOIN)SELECT k.Ten, s.TenSachFROM KhachHang kLEFT JOIN DonHang d ON k.ID = d.KhachHang_IDLEFT JOIN Sach s ON d.Sach_ID = s.IDUNIONSELECT k.Ten, s.TenSachFROM KhachHang kRIGHT JOIN DonHang d ON k.ID = d.KhachHang_IDRIGHT JOIN Sach s ON d.Sach_ID = s.ID;
Kết Quả Mong Đợi:
Ten
TenSach
Lê Minh Khánh
Lập trình Python
Hoàng Thị Lan
SQL Dễ Hiểu
Lê Minh Khánh
Web Dev Basics
Võ Quốc Huy
NULL
NULL
JavaScript Advanced
Hành Vi:FULL OUTER JOIN cho thấy toàn bộ dữ liệu từ cả hai bảng. Nó hữu ích khi bạn muốn kiểm tra "có cái gì không khớp không?" hoặc tìm những bản ghi bị mất trong quá trình migration.
⚠️ CẢNH BÁO:CROSS JOIN rất nguy hiểm! Nếu bạn có hai bảng với 1 triệu hàng mỗi bảng, kết quả sẽ có 1 tỷ hàng (1 triệu × 1 triệu). Server sẽ chết, memory cháy hết, query bị kill. Thường chỉ dùng CROSS JOIN khi bạn chắc chắn biết kích thước dữ liệu và có lý do rõ ràng (tạo bảng lịch, tất cả kombinasi yếu tố, v.v.).
Các lệnh cơ bản SELECT, INSERT, UPDATE, DELETE là nền tảng để làm việc với SQL. Kết hợp với JOIN, mình có thể truy vấn dữ liệu phức tạp từ nhiều bảng. Viết bài này giúp mình củng cố lại, hy vọng bạn cũng thấy hữu ích.
Ở bài tiếp theo, mình sẽ nói về các khái niệm nâng cao hơn như index, view, hoặc stored procedure. Nếu bạn vừa đấm được JOIN sau bài này, chúc mừng. Còn nếu vẫn thấy xoắn, không sao đọc lại, vẽ sơ đồ, và thử chạy SELECT từng bước. SQL phần lớn là luyện tay và soi kết quả.