IT Learning HubIT Learning Hub
THỰC HÀNH 04 · BTHT4

Hàm mở rộng, CASE & Subquery

5 tiếtDatabaseSQL Server

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

TUẦN 4 ├── Hàm mở rộng: CAST() · GETDATE() · DATEADD() · LEFT() · RIGHT() ├── CASE...END: Theo điều kiện · theo giá trị · phân loại dữ liệu ├── Alias + Subquery: Alias bảng/cột · subquery trong FROM · subquery trong SELECT · EXISTS/NOT EXISTS └── Bài tập tổng hợp: Quản lý giáo vụ (50 câ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ĩaVí dụ
INTSố nguyênCAST(123 AS INT)
VARCHAR(n)ChuỗiCAST('Hello' AS VARCHAR(50))
DATENgàyCAST('2024-11-14' AS DATE)
DATETIMENgày + giờCAST('2024-11-14 10:20:30' AS DATETIME)
DECIMAL(p,s)Số thập phânCAST(123.456 AS DECIMAL(10,2))
NUMERIC(p,s)Số thập phânCAST(123.456 AS NUMERIC(10,2))
FLOATSố thựcCAST(123.456 AS FLOAT)
CHAR(n)Chuỗi cố địnhCAST('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;
GETDATE() ➜ Ngày + giờ ➜ CAST(... AS DATE) ➜ Chỉ còn ngày

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;

Lưu ý: Không nên dùng phép trừ đơn giản giữa năm hiện tại và năm sinh để tính tuổi chính xác, vì còn phụ thuộc người đó đã qua sinh nhật trong năm hay chưa - Đây là sự khác biệt giữa xử lý dữ liệu đơn giản và xử lý dữ liệu đúng về mặt nghiệp vụ.

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());
Đủ 18 tuổi hôm nay ⟺ NgaySinh <= (Hôm nay - 18 năm) ⟺ 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.

Lỗi thường gặp: Viế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);  -- def

Ví 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.

LEFT(MaSV, 3)
22520001
➜ 225
RIGHT(MaSV, 3)
22520001
➜ 001

So sánh nhanh

HàmLấy từVí dụKết quả
LEFT()Bên tráiLEFT('abcdef',3)abc
RIGHT()Bên phảiRIGHT('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
END
SELECT
    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.

VIPTongChiTieu ≥ 10.000.000
Thân thiếtTongChiTieu ≥ 5.000.000
ThườngCòn lại

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'
END
Vì 15.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
END

Ví 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ạngKhi 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?

06 · ALIAS VÀ SUBQUERY

Alias và Subquery cơ bản

Alias là tên tạm thời đặt cho bảng hoặc cột trong truy vấn, giúp câu SQL dễ đọc và dễ dùng hơn trong biểu thức phức tạp.
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ì?

Subquery là một câu truy vấn nằm bên trong một câu truy vấn khác - Có thể xuất hiện trong 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 đó:

Bảng gốc ➜ Subquery ➜ Bảng kết quả tạm thời ➜ Alias ➜ Query bên ngoài
SELECT *
FROM
(
    SELECT
        MaSV,
        HoTen,
        Diem
    FROM SinhVien
) AS SV
WHERE SV.Diem >= 8;
SQL Server yêu cầu derived table/subquery trong 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ẩmSố lần bán
Laptop5
Chuột12
Bàn phím7

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.

Query ngoài ➜ SP.MaSP ➜ Subquery ➜ Tính riêng cho từng SP

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".

Vì sao ưu tiên 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?

08 · TƯ DUY TỔNG HỢP

Nhận diện từ khóa & Cheat Sheet

Từ khóa đề bàiKiến thức
Chuyển kiểu dữ liệuCAST()
Ngày hiện tạiGETDATE()
Cộng/trừ thời gian vào 1 ngày, tính tuổi chính xácDATEADD()
Ký tự đầu / ký tự cuốiLEFT() / RIGHT()
Phân loại / "Nếu... thì..."CASE WHEN
Tên tạm của bảng/cộtAlias
Truy vấn bên trong truy vấnSubquery
Tính cho từng đối tượngCorrelated Subquery
Truy vấn con trong FROMDerived Table + Alias
Truy vấn con trong SELECTScalar / Correlated Subquery
"Có tồn tại / không tồn tại..."EXISTS / NOT EXISTS
CAST() ➜ Chuyển kiểu dữ liệu GETDATE() ➜ Ngày + giờ hiện tại DATEADD() ➜ Cộng/trừ thời gian vào 1 ngày LEFT(x, n) ➜ n ký tự bên trái RIGHT(x, n) ➜ n ký tự bên phải CASE ... END ➜ Điều kiện / phân loại Alias ➜ Tên tạm cho bảng / cột Subquery ➜ Truy vấn bên trong truy vấn FROM (SELECT ...) AS T ➜ Derived Table SELECT (SELECT ...) AS X ➜ Cột tính toán bằng Subquery EXISTS (SELECT ...) ➜ Có dòng nào khớp không? NOT EXISTS (SELECT ...) ➜ Không có dòng nào khớp

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

BÀI TẬP CHÍNH THỨC · BTTH4

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.

Xem bài tập & đáp án ›
LUYỆN THÊM · KHÔNG BẮT BUỘC

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ở.

01

Hiển thị mã sinh viên, họ tên và 3 ký tự cuối của mã sinh viên.

Gợi ý
RIGHT()
02

Hiển thị ngày hiện tại chỉ dưới dạng DATE.

Gợi ý
GETDATE() + CAST()
03

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 ý
CASE
04

Hiển thị mỗi sinh viên và số học phần đã đăng ký.

Gợi ý
Correlated Subquery
05★

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 ý
CASE + Correlated Subquery
06

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.

Gợi ý
NOT EXISTS