Hiển thị mã sinh viên, họ tên và 3 ký tự cuối của mã sinh viên.
Hàm mở rộng, CASE & Subquery
Mục tiêu: Sau Tuần 4, sinh viên không chỉ biết viết truy vấn để lấy dữ liệu, mà có thể biến đổi dữ liệu, phân loại dữ liệu và tạo ra các cột tính toán từ truy vấn con.
01 · TỔNG QUAN TUẦN 4
Từ truy vấn dữ liệu đến xử lý dữ liệu
Đây là bước chuyển từ truy vấn dữ liệu sang xử lý và tạo thông tin từ dữ liệu. Sau khi hoàn thành, sinh viên có thể chuyển đổi kiểu dữ liệu, trích xuất chuỗi, xây dựng logic điều kiện với CASE, viết subquery trong FROM/SELECT kết hợp alias, và kiểm tra sự tồn tại dữ liệu liên quan bằng EXISTS/NOT EXISTS.
02 · HÀM CAST()
CAST()
CAST() dùng để chuyển đổi một giá trị từ kiểu dữ liệu này sang kiểu dữ liệu khác (ví dụ: VARCHAR ➜ INT, DATETIME ➜ DATE, DECIMAL ➜ INT, VARCHAR ➜ DATE).Cú pháp
CAST(expression AS target_data_type)expression: Giá trị hoặc biểu thức cần chuyển. target_data_type: Kiểu dữ liệu muốn chuyển sang.
Ví dụ cơ bản
Chuyển chuỗi thành ngày giờ:
SELECT CAST('2025-04-10' AS DATETIME) AS Ngay;Kết quả: 2025-04-10 00:00:00.000
Chuyển số thực thành số nguyên:
SELECT CAST(123.456 AS INT) AS SoNguyen;Kết quả: 123
Các kiểu dữ liệu thường gặp
| Kiểu | Ý nghĩa | Ví dụ |
|---|---|---|
INT | Số nguyên | CAST(123 AS INT) |
VARCHAR(n) | Chuỗi | CAST('Hello' AS VARCHAR(50)) |
DATE | Ngày | CAST('2024-11-14' AS DATE) |
DATETIME | Ngày + giờ | CAST('2024-11-14 10:20:30' AS DATETIME) |
DECIMAL(p,s) | Số thập phân | CAST(123.456 AS DECIMAL(10,2)) |
NUMERIC(p,s) | Số thập phân | CAST(123.456 AS NUMERIC(10,2)) |
FLOAT | Số thực | CAST(123.456 AS FLOAT) |
CHAR(n) | Chuỗi cố định | CAST('Hello' AS CHAR(10)) |
CAST() trong bài toán thực tế
Bảng NhanVien(MaNV, HoTen, Luong) với Luong lưu dạng DECIMAL(12,2), muốn hiển thị dưới dạng số nguyên:
SELECT
MaNV,
HoTen,
CAST(Luong AS INT) AS Luong
FROM NhanVien;Làm tròn cần chú ý
SELECT CAST(123.99 AS INT);Kết quả: 123
CAST(... AS INT) không phải là hàm ROUND - Nó cắt phần thập phân, không làm tròn. Đây là điểm rất dễ nhầm.03 · HÀM GETDATE()
GETDATE()
GETDATE() trả về ngày và giờ hiện tại của hệ thống SQL Server tại thời điểm truy vấn được thực thi. Đây là hàm dành riêng cho SQL Server (T-SQL).SELECT GETDATE() AS NgayGioHienTai;Kết hợp GETDATE() và CAST()
Nếu chỉ cần ngày hiện tại, không cần giờ:
SELECT CAST(GETDATE() AS DATE) AS NgayHienTai;Ví dụ tính tuổi
Giả sử NgaySinh = '2000-08-15'. Lấy ngày hiện tại bằng SELECT GETDATE() AS HomNay;
DATEADD() - Tính tuổi chính xác đến từng ngày
DATEADD(datepart, number, date) cộng (hoặc trừ nếu number âm) một khoảng thời gian vào một giá trị ngày, trả về ngày mới - datepart có thể là YEAR, MONTH, DAY...SELECT DATEADD(YEAR, -18, GETDATE()) AS Ngay18NamTruoc;Muốn kiểm tra "sinh viên phải từ 18 tuổi trở lên", thay vì trừ năm cho năm (dễ sai lệch quanh ngày sinh nhật), hãy so sánh NgaySinh với "ngày tròn 18 tuổi tính từ hôm nay":
SELECT MaSV, HoTen, NgaySinh
FROM SinhVien
WHERE NgaySinh <= DATEADD(YEAR, -18, GETDATE());Cách này chính xác đến từng ngày vì so sánh trực tiếp 2 mốc ngày tháng, không quy tròn về năm như YEAR(GETDATE()) - YEAR(NgaySinh) - Cách quy tròn theo năm vẫn dùng được khi đề chỉ cần ước lượng gần đúng, nhưng sẽ sai lệch vài tháng quanh dịp sinh nhật.
GETDATE() - NgaySinh >= 18 (trừ trực tiếp 2 giá trị ngày rồi so với số 18) trông giống hướng đúng nhưng sai hoàn toàn - Hiệu số 2 ngày trong SQL Server là số ngày chênh lệch, so với 18 chỉ tương đương kiểm tra "đã qua 18 ngày kể từ khi sinh", không phải 18 năm.04 · HÀM LEFT() VÀ RIGHT()
LEFT() và RIGHT()
LEFT() lấy một số ký tự từ bên trái của chuỗi, RIGHT() lấy từ bên phải.
LEFT(chuoi, so_ky_tu)
RIGHT(chuoi, so_ky_tu)SELECT LEFT('abcdef', 3); -- abc
SELECT RIGHT('abcdef', 3); -- defVí dụ với mã sinh viên
Giả sử MaSV = '22520001', bảng SinhVien(MaSV, HoTen). Lấy 3 ký tự đầu (mã khóa):
SELECT
MaSV,
HoTen,
LEFT(MaSV, 3) AS Khoa
FROM SinhVien;Lấy 3 ký tự cuối (số thứ tự):
SELECT
MaSV,
RIGHT(MaSV, 3) AS SoThuTu
FROM SinhVien;Với MaSV = 22520001: Khoa = 225, SoThuTu = 001.
So sánh nhanh
| Hàm | Lấy từ | Ví dụ | Kết quả |
|---|---|---|---|
LEFT() | Bên trái | LEFT('abcdef',3) | abc |
RIGHT() | Bên phải | RIGHT('abcdef',3) | def |
05 · CASE...END
CASE...END
CASE là cấu trúc điều kiện trong SQL, có chức năng tương tự tư duy IF / ELSE IF / ELSE trong các ngôn ngữ lập trình.Cú pháp dạng điều kiện
CASE
WHEN condition1 THEN result1
WHEN condition2 THEN result2
ELSE result_other
ENDSELECT
HoTen,
Luong,
CASE
WHEN Luong >= 20000000 THEN N'Cao'
WHEN Luong >= 10000000 THEN N'Trung bình'
ELSE N'Thấp'
END AS MucLuong
FROM NhanVien;Ví dụ phân loại khách hàng
Phân loại theo TongChiTieu: ≥ 10.000.000 ➜ VIP, ≥ 5.000.000 ➜ Thân thiết, còn lại ➜ Thường.
CASE kiểm tra theo đúng thứ tự từ trên xuống - dừng lại ở điều kiện đầu tiên đúng, giống thứ tự các băng ở trên.
SELECT
MaKH,
HoTen,
CASE
WHEN TongChiTieu >= 10000000 THEN N'VIP'
WHEN TongChiTieu >= 5000000 THEN N'Thân thiết'
ELSE N'Thường'
END AS PhanLoai
FROM KhachHang;Thứ tự điều kiện - Rất quan trọng
Câu sau sai về logic:
CASE
WHEN TongChiTieu >= 5000000 THEN N'Thân thiết'
WHEN TongChiTieu >= 10000000 THEN N'VIP'
ELSE N'Thường'
END15.000.000 >= 5.000.000 đã đúng ngay ở điều kiện đầu tiên, nên 15.000.000 sẽ bị phân loại "Thân thiết" thay vì "VIP". Quy tắc: Điều kiện cụ thể/cao hơn nên đặt trước điều kiện rộng hơn (>=10M ➜ >=5M ➜ Còn lại).CASE dạng so sánh giá trị
CASE expression
WHEN value1 THEN result1
WHEN value2 THEN result2
ELSE result_other
ENDVí dụ phân loại giới tính:
SELECT
MaNV,
HoTen,
CASE GioiTinh
WHEN 1 THEN N'Nam'
WHEN 0 THEN N'Nữ'
ELSE N'Không xác định'
END AS PhanLoaiGioiTinh
FROM NhanVien;Hai dạng CASE
| Dạng | Khi nào dùng? |
|---|---|
CASE WHEN điều kiện | Điều kiện phức tạp |
CASE expression WHEN giá trị | So sánh một giá trị với nhiều giá trị |
CASE không chỉ dùng trong SELECT
Có thể dùng CASE trong SELECT, ORDER BY, và cả WHERE/GROUP BY khi cần:
ORDER BY
CASE
WHEN TrangThai = N'Ưu tiên' THEN 1
ELSE 2
END;KIỂM TRA NHANH
Trong CASE WHEN nhiều điều kiện lồng theo mức (ví dụ phân loại lương), nên đặt điều kiện nào trước?
CASE dừng lại ở điều kiện đầu tiên đúng, nên điều kiện cụ thể/cao hơn phải đặt trước, nếu không mọi dòng sẽ rơi vào nhánh rộng phía trên.06 · ALIAS VÀ SUBQUERY
Alias và Subquery cơ bản
SELECT SV.MaSV, SV.HoTen
FROM SinhVien SV;Alias cho cột:
SELECT
HoTen AS TenSinhVien
FROM SinhVien;Alias cho bảng (có thể bỏ AS):
SELECT
SV.MaSV,
SV.HoTen
FROM SinhVien AS SV;Subquery là gì?
SELECT, FROM, WHERE, HAVING.SELECT *
FROM SinhVien
WHERE Diem > (
SELECT AVG(Diem)
FROM SinhVien
);Đọc: "Tìm sinh viên có điểm lớn hơn điểm trung bình của tất cả sinh viên." Tư duy: Tính AVG(Diem) trước, rồi lấy những dòng có Diem > AVG.
07 · SUBQUERY TRONG FROM VÀ SELECT
Subquery nâng cao
Đây là nội dung trọng tâm mới của Tuần 4.
Subquery trong FROM + Alias
SELECT Alias1.Cot1, Alias1.Cot2
FROM
(
SELECT ...
FROM ...
WHERE ...
) AS Alias1
WHERE ...;SQL tạo ra một bảng kết quả tạm thời từ subquery, sau đó truy vấn bên ngoài sử dụng bảng đó:
SELECT *
FROM
(
SELECT
MaSV,
HoTen,
Diem
FROM SinhVien
) AS SV
WHERE SV.Diem >= 8;FROM phải có tên - FROM (SELECT ...) không đủ, cần FROM (SELECT ...) AS SV.Subquery trong SELECT + Alias
Đây là phần đặc biệt quan trọng của Tuần 4.
SELECT
<cột chính>,
(
SELECT <biểu thức>
FROM <bảng liên quan>
WHERE <điều kiện liên kết>
) AS <bí danh>
FROM <bảng chính>;Ví dụ - Đếm số lần bán của từng sản phẩm
Bảng SanPham(MaSP, TenSP), CTHD(MaHD, MaSP, SoLuong). Đề bài: "với mỗi sản phẩm, cho biết sản phẩm đã xuất hiện bao nhiêu lần trong chi tiết hóa đơn."
SELECT
SP.TenSP,
(
SELECT COUNT(*)
FROM CTHD
WHERE CTHD.MaSP = SP.MaSP
) AS SoLanBan
FROM SanPham SP;| Sản phẩm | Số lần bán |
|---|---|
| Laptop | 5 |
| Chuột | 12 |
| Bàn phím | 7 |
Correlated Subquery
Trong ví dụ trên, WHERE CTHD.MaSP = SP.MaSP tham chiếu đến SP.MaSP của truy vấn bên ngoài - Đây là correlated subquery (truy vấn con tương quan): Subquery được tính lại riêng cho từng dòng của truy vấn ngoài.
So sánh với subquery thường
Subquery thường chạy độc lập, không phụ thuộc dòng ngoài:
SELECT *
FROM SanPham
WHERE Gia >
(
SELECT AVG(Gia)
FROM SanPham
);Correlated subquery phụ thuộc vào từng dòng ngoài (SP.MaSP ở ví dụ trên).
Khi nào nên dùng Correlated Subquery?
Rất phù hợp với câu hỏi dạng "với mỗi A, tính một giá trị liên quan đến A" - Ví dụ: Mỗi sản phẩm có bao nhiêu lần bán? mỗi sinh viên có bao nhiêu môn đã đăng ký? mỗi nhân viên tham gia bao nhiêu dự án? mỗi khách hàng có bao nhiêu hóa đơn? mỗi phòng ban có bao nhiêu nhân viên?
Ví dụ - Quản lý giáo vụ
Bảng SINHVIEN(MaSV, HoTen), DANGKY(MaSV, MaHP). Đề bài: "hiển thị mỗi sinh viên và số học phần đã đăng ký."
SELECT
SV.MaSV,
SV.HoTen,
(
SELECT COUNT(*)
FROM DangKy DK
WHERE DK.MaSV = SV.MaSV
) AS SoHocPhan
FROM SinhVien SV;Kết hợp CASE + Subquery
Đề bài: "phân loại sinh viên dựa trên số học phần đã đăng ký."
SELECT
SV.MaSV,
SV.HoTen,
CASE
WHEN
(
SELECT COUNT(*)
FROM DangKy DK
WHERE DK.MaSV = SV.MaSV
) >= 20
THEN N'Đăng ký nhiều'
WHEN
(
SELECT COUNT(*)
FROM DangKy DK
WHERE DK.MaSV = SV.MaSV
) >= 15
THEN N'Bình thường'
ELSE N'Đăng ký ít'
END AS PhanLoai
FROM SinhVien SV;EXISTS và NOT EXISTS
EXISTS kiểm tra 1 subquery có trả về ít nhất 1 dòng hay không, kết quả chỉ là TRUE/FALSE - Không quan tâm subquery trả về giá trị gì hay bao nhiêu dòng, khác hẳn IN phải so khớp trực tiếp với danh sách giá trị cụ thể.SELECT ...
FROM BangA a
WHERE EXISTS (
SELECT * FROM BangB b WHERE b.KhoaNgoai = a.KhoaChinh
);Ví dụ - "Tìm sinh viên đã đăng ký ít nhất 1 học phần" (dùng lại SinhVien, DangKy ở trên):
SELECT SV.MaSV, SV.HoTen
FROM SinhVien SV
WHERE EXISTS (
SELECT * FROM DangKy DK WHERE DK.MaSV = SV.MaSV
);Đảo ngược thành NOT EXISTS để tìm sinh viên chưa đăng ký học phần nào:
SELECT SV.MaSV, SV.HoTen
FROM SinhVien SV
WHERE NOT EXISTS (
SELECT * FROM DangKy DK WHERE DK.MaSV = SV.MaSV
);EXISTS/NOT EXISTS luôn là correlated subquery (giống Correlated Subquery ở trên) - Mệnh đề WHERE DK.MaSV = SV.MaSV tham chiếu đến dòng ngoài, nên subquery được tính lại riêng cho từng sinh viên. Đây chính là công cụ đứng sau cấu trúc "hai lớp NOT EXISTS" đã học ở Tuần 3 để giải bài toán "tất cả" - EXISTS/NOT EXISTS một lớp dùng cho câu hỏi "có tồn tại hay không" đơn giản hơn, còn hai lớp lồng nhau mới diễn đạt được "với mọi".
NOT EXISTS hơn NOT IN? Nếu danh sách con của NOT IN chứa dù chỉ 1 giá trị NULL, cả câu NOT IN sẽ trả về rỗng (không có dòng nào) - Một lỗi rất khó phát hiện. NOT EXISTS không so khớp giá trị trực tiếp nên hoàn toàn không gặp phải bẫy này, luôn cho kết quả đúng.KIỂM TRA NHANH
Cột MaSV trong bảng DangKy có khả năng chứa NULL. Nên dùng cách nào để tìm sinh viên chưa đăng ký học phần nào cho an toàn nhất?
NOT IN sẽ âm thầm trả về rỗng nếu danh sách con dính NULL, còn NOT EXISTS chỉ kiểm tra "có dòng khớp hay không" nên không bị ảnh hưởng bởi NULL.08 · TƯ DUY TỔNG HỢP
Nhận diện từ khóa & Cheat Sheet
| Từ khóa đề bài | Kiến thức |
|---|---|
| Chuyển kiểu dữ liệu | CAST() |
| Ngày hiện tại | GETDATE() |
| Cộng/trừ thời gian vào 1 ngày, tính tuổi chính xác | DATEADD() |
| Ký tự đầu / ký tự cuối | LEFT() / RIGHT() |
| Phân loại / "Nếu... thì..." | CASE WHEN |
| Tên tạm của bảng/cột | Alias |
| Truy vấn bên trong truy vấn | Subquery |
| Tính cho từng đối tượng | Correlated Subquery |
| Truy vấn con trong FROM | Derived Table + Alias |
| Truy vấn con trong SELECT | Scalar / Correlated Subquery |
| "Có tồn tại / không tồn tại..." | EXISTS / NOT EXISTS |
Bốn tuần đầu của IT004
Tuần 1: Tạo & thao tác dữ liệu. Tuần 2: Truy vấn & kết hợp dữ liệu. Tuần 3: Truy vấn nâng cao ("tất cả" / nhóm / TOP). Tuần 4: Biến đổi & tính toán trên dữ liệu bằng CAST, CASE, Subquery và EXISTS/NOT EXISTS.
09 · BÀI TẬP THỰC HÀNH
Thực hành tổng hợp
Quản lý giáo vụ - Câu 1 ➜ 50
Xem đề bài đầy đủ, file đính kèm và đáp án chi tiết (cần mã truy cập) của BTTH4.
Sau Tuần 4, đây là các bài tự luyện thêm để rèn tư duy - Không thuộc bài nộp BTTH4. Mỗi bài có gợi ý ẩn - Bấm để mở.
Hiển thị ngày hiện tại chỉ dưới dạng DATE.
Gợi ý
Phân loại sinh viên theo điểm: ≥8.5 ➜ Xuất sắc, ≥7.0 ➜ Tốt, ≥5.0 ➜ Đạt, <5.0 ➜ Không đạt.
Gợi ý
Hiển thị mỗi sinh viên và số học phần đã đăng ký.
Gợi ý
Hiển thị mỗi sinh viên, số học phần đã đăng ký và phân loại: ≥20 ➜ Đăng ký nhiều, 15-19 ➜ Bình thường, <15 ➜ Đăng ký ít.
Gợi ý
Hiển thị mã sinh viên, họ tên của những sinh viên chưa đăng ký học phần nào.
