0

Checklist SQL

Chào mọi người, hiện tại mình đang khá gấp rút làm đồ án tốt nghiệp và tranh thủ giải quyết nốt mấy task nhỏ để hoàn thiện project ném vào CV. Trong quá trình cày cuốc, mình nhận ra việc "vừa làm, vừa học, vừa viết" là cách tốt nhất để nhớ lâu kiến thức, đặc biệt là để chuẩn bị cho các buổi phỏng vấn Backend Intern trong tương lai.

Hôm nay mình sẽ tổng hợp lại bộ checklist SQL kinh điển mà các nhà tuyển dụng hay xoáy sâu vào. Để lý thuyết không bị khô khan, mình sẽ dùng luôn hệ thống DB của dự án mình đang xây dựng (hệ thống quản lý lịch đặt cắt tóc) làm ví dụ minh hoạ xuyên suốt bài viết nhé. Bắt đầu ha!

1. Vì sao backend (dù có ORM) vẫn phải vững SQL ?

SQL (Structured Query Language) là ngôn ngữ tiêu chuẩn để giao tiếp với cơ sở dữ liệu quan hệ (như MySQL, PostgreSQL...). Đối với một Backend Developer, ứng dụng của bạn (dù viết bằng Java, Node.js,...) không thể tự lưu trữ dữ liệu vĩnh viễn. SQL chính là ngôn ngữ chung để code của bạn ra lệnh cho database: "Hãy lưu thông tin khách hàng này lại, hoặc lấy cho tôi lịch cắt tóc của ngày mai"

Tớ đã có suy nghĩ : "Bây giờ toàn dùng framework hỗ trợ ORM như Hibernate/JPA, mọi thao tác đều qua object, framework tự sinh query hết rồi cần gì cày SQL thuần làm cái gì???"

Tuy nhiên, thực tế dự án lại chứng minh điều ngược lại:

  • N+1 Query: ORM tiện nhưng đôi khi nó lại là một chiếc hộp đen. Nếu ta không hiểu cách DB liên kết dữ liệu, việc dùng ORM sai cách sinh ra hàng chục câu query thừa thĩa cho một request đơn giản.

  • Tối ưu performance: khi dữ liệu chỉ có vài nghìn dòng, query viết sao cũng được. Nhưng khi dữ liệu phình to một câu lệnh thiếu tối ưu có thể làm lag treo cả hệ thống. Để debug và tối ưu, ta phải buộc rà lại cải file nhưng rà là chưa đủ mà phải hiểu được nó có ý gì ra sao mà còn fix được

Vậy nên hiểu cách database hoạt động không chỉ là qua ải phỏng vấn mà là kỹ năng sinh tồn bắt buộc của một backend dev trong thời đại AI phát triển này nữa.

2. Các loại JOIN

Bản chất JOIN là để kết hợp dữ liệu từ nhiều bẳng lại với nhau

vd: Thông tin khách hàng ở bảng khach_hang còn lịch cắt nằm ở bảng lich_cat. Để biết ai đặt lịch ngày nào, ta phải JOIN hai bảng này lại với nhau qua ID

Có 4 loại JOIN cơ bản

  • INNER JOIN: Lấy phần giao nhau (dữ liệu khớp ở 2 bảng)
  • LEFT JOIN: Giữ toàn bộ dữ liệu bảng bên trái (dù bảng phải không có)
  • RIGHT JOIN: Giữ toàn bộ dữ liệu bảng bên phải
  • FULL JOIN: Giữ toàn bộ giữ liệu của cả 2 bảng

VD- INNER JOIN: lấy danh sách những khách hàng có đặt lịch

SELECT kh.ten_khach_hang , ld.ngay_dat
FROM khach_hang kh
INNER JOIN lich_dat ld
ON kh.id = ld.khach_hang_id

Thường thì mọi người khi tắt INNER JOIN = JOIN

VD- Tìm những khách hàng CHƯA Từng đặt lịch(để gửi mã khuyến mãi nhằm lôi kéo )

SELECT kh.ten_khach_hang, kh.so_dien_thoai
FROM khach_hang kh
LEFT JOIN lich_dat ld
ON kh.id = ld.khach_hang_id
WHERE ld.id ÍS NULL

khi dùng LEFT JOIN, nếu khách hàng ở bảng trái không có lịch đặt tương ứng ở bảng phải db sẽ tự động điền NULL cho các cột đặt lịch đặt. Ta chỉ cần dùng WHERE ld.id IS NULL là lọc được

Để dễ hiểu hơn, đặt vào hệ thống salon ha có 2 bảng như sau

Gồm những người đã đăng ký tài khoản trên hệ thống : bảng khach_hang

id ten_khach_hang
1 Hoàn
2 Công
3 Nam

Bảng lich_dat gồm các lịch được tạo ra. có 1 khách vãng lai không có tài khoản vừa ghé quán cắt tóc và được nhân viên ghi tạm vào lịch sử với khach_hang_id là 99

id_lich khach_hang_id dich_vu
101 1 Cắt tóc
102 99 Gội đầu

Bây giờ ta sẽ xem 4 loại JOIN 2 bảng như trên sẽ thế nào khi ta nối chúng qua cột khach_hang_id

2.1 INNER JOIN (Lấy phần giao nhau)

Quy tắc: chit lấy những ai vừa có tên trong bảng khách hàng vừa có lịch đặt. Ai thiếu 1 trong 2 điều kiện trên -> LOẠI

=> Chỉ có khách hàng Hoàn là thảo mãn (vừa có id =1 ở bảng khách hàng và vừa có id =101 ở bảng lịch đặt). Công và Nam bị loại vì không có lịch. Khách vãng lai (id=99) bị loại vì không có thông tin trong bảng khách hàng

ten_khach_hang id_lich dich_vu
Hoàn 101 Cắt tóc

2.2 LEFT JOIN (bảo kê bảng bên trái) Quy tắc: Lấy toàn bộ dữ liệu bảng khach_hang làm gốc. Đứa nào có lịch thì ghép vào, đứa nào không lịch thì phần lịch cứ để trống NULL. Không quan tâm bảng lịch bị thừa thiếu gì

=> Kết quả: Hoàn, Công, Nam đều được gọi tên, khách vãng lai bị ngó lơ

ten_khach_hang id_lich dich_vu
Hoàn 101 Cắt tóc
Công NULL NULL
Nam NULL NULL

2.3 RIGHT JOIN (bảo kê bảng bên phải) Quy tắc: Ngược lại với cái trên thì cái này lấy toàn bộ lịch đặt làm gốc kịch nào khớp với khách thì hiện tên khách lịch nào của khách vãng lai thì phần tên sẽ để NULL

=> Kết quả: Lấy ra lịch 101 của Hoàn và lịch 102 của khách vãng lai, Công và Nam bị ngó lơ vì không có lịch

ten_khach_hang id_lich dich_vu
Hoàn 101 Cắt tóc
NULL 102 Gội đầu

2.4 FULL JOIN (lấy tất tần tật) Quy tắc: Bắt tay hào bình, gom hết dữ liệu của cả 2 bảng. Ai khớp thì ghép ai thiếu bên nào điền NULL bên đó, không bỏ ai phía sau :>>

=> Kết quả: Gom cả Hoàn, Công, Nam và lịch của khách vãng lai lại với nhau

ten_khach_hang id_lich dich_vu
Hoàn 101 Cắt tóc
Công NULL NULL
Nam NULL NULL
NULL 102 Gội đầu

HÚ HÚ Bảng nào bảng trái, bảng nào bảng phải???? Đơn giản thôi ae, bảng nào ghi trước thì bảng đó bảng trái còn ghi sau kaf bảng phải

  • Bảng TRÁI (Left Table): Là bảng được gọi tên ĐẦU TIÊN, nằm ngay sau chữ FROM.
  • Bảng PHẢI (Right Table): Là bảng được gọi tên THỨ HAI, nằm ngay sau chữ JOIN.

3. Phân biệt WHERE và HAVING

Cả WHERE và HAVING sứ mệnh của cả 2 đứa nó đều là bộ lọc dữ liệu. Điểm khác biệt mang tính sống còn giữa chúng là thời điểm lọc

  • WHERE (lọc trước): Lọc từng dòng dữ liệu TRƯỚC KHI đem đi gom nhóm (GROUP BY). Tuyệt đối không được dùng WHERE đi kèm với các hàm tính toán tổng hợp như COUNT(), SUM(), MAX(),.....
  • HAVING (lọc sau): Lọc các nhóm dữ liệu SAU KHI được gom nhóm. Nó sinh ra để đi cặp với GROUP BY và để xử lý hàm tính toán

Ví dụ: Yêu cầu "tìm ra những khách hàng VVIP (những người đã đã cắt trên 5 lần)"

SELECT khach_hang_id, COUNT(*) as tong_so_lan_cat
FROM lich_cat
WHERE trang_thai = "da_cat"
GROUP BY khach_hang_id
HAVING COUNT(*)>5;

HÚ HÚ Cơ sở dữ liệu độc câu query theo thứ tự nào ?????

FROM -> JOIN->WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> LIMIT

4. UNION và UNION ALL (Ghép nối kết quả)

Nếu JOIN là ghép dữ liệu theo chiều ngang (thêm cột và kết quả) thì UNION là ghép kết quả của hai hay nhiều câu query theo chiều dọc (thêm dòng)

Điều kiện để dùng UNION là các câu query phải trả về số lượng cột bằng nhau và có dùng kiểu dữ liệu. Nhưng điểm ăn tiền khi đi phỏng vấn là bạn phân biệt được 2 loại này:

  • UNION: Ghép dữ liệu và loại bỏ các dòng trùng lặp (DB sẽ phải thực hiện thêm thao tác quét lại toàn bộ kết quả để check trùng -> tốn tài nguyên, chạy chậm hơn)
  • UNION ALL: Ghép dữ liệu và giữ nguyên tất cả kể cả trùng (cứ bồi cứ đắp thêm cho nhau -> chạy nhanh hơn)

ví dụ: Sắp tới là mùng 8/3 cửa hàng cần lấy danh sách toàn bộ sdt của cả khách hàng và cả nhân viên luôn để chạy tool gửi tin nhắn SMS khuyến mãi hàng loạt

SELECT ten, so_dien_thoai, 'Khach Hang' AS vai_tro
FROM khach_hang

UNION ALL

SELECT ten, so_dien_thoai, 'Nhan Vien' AS vai_tro
FROM nhan_vien;

5. Subquery (truy vấn lồng) và CTE

Subquery hiểu đơn giản là một câu lệnh SELECT nằm trọn bên trong một câu SQL khác. NÓ giúp bạn giải quyết các bài toán logic nhiều bước một cách khá tự nhiên

Ví dụ: Sếp yêu cầu bạn lấy danh sách dịch vụ có giá cao hơn mức giá trung bình của cả cửa hàng

Lúc này ta không thể tự nhậm tính bên ngoài để viết giá trung bình bằng 1 con số cụ thể vào mệnh đề WHERE được bạn phải lồng thêm query cho db tính cho

SELECT ten_dich_vu, gia_tien
FROM dich_vu
WHERE gia_tien > (
        SELECT AVG(gia_tien) FROM dich_vu
        );

Ưu điểm: dễ hình dung theo flow suy nghĩ thông thường Nhược điểm: Rất dễ dính vào vấn đề hiệu suất khi ta lạm dụng vào subquery đặc biệt nhét vào trong mệnh đề SELECT hoặc lồng nhiều tầng, DB nó sẽ phải hoạt động chạy đi chạy lại cái query con cho từng dòng lệnh của query cha. bảng có 1 triệu dòng thì nó chạy query con 1 triệu lần -> sập server

Vậy giải pháp là gì???? Dùng DTE : DTE cho phép bạn định nghĩa 1 bảng tạm thời ở ngya trên đầu câu lệnh. Câu lệnh sẽ được chia thành từng block nhỏ, để tối ưu tốc độ hơn, thường bắt đầu bằng từ khoá WITH

thử lại vd trên bằng CTE ha:

WITH GiaTrungBinh AS(
        SELECT AVG(gia_tien) as muc_gia_tb
        FROM dich_vu
)
SELECT dv.ten_dich_vu , dv.gia_tien
FROM dich_vu dv
JOIN GiaTrungBinh gtb
     ON dv.gia_tien > gtb.muc_gia_tb

6. Index và Partition

6.1. Database Index (chỉ mục)

Hãy tưởng tượng bạn đang cầm một cuốn sách 10000 trang và cần tìm tất cả các trang có nhắc đến từ "Java"

  • Nếu không có mục lục: Bạn phải lật từng trang một từ đầu đến cuối để đọc (Table scan)
  • Nếu có mục lục. Ta chỉ việc lật ra mục lục , tìm chữ "Java" thấy nó ghi ở trang 45 , 103. Ta lật thẳng đến trang đó (gọi là index lookup)

Ví dụ: bảng khach_hang có 1 triệu người. Khi nhân viên tìm khách hàng bằng số điện thoại

SELECT * FROM khach_hang WHERE so_dien_thoai = "0987654321"

Nếu cột so_dien_thoai không được đánh index, DB sẽ quét trọn vẹn 1 triệu dòng. Để giải cứu hệ thống ta cần tạo index

CREATE INDEX idx_so_dien_thoai ON khach_hang(so_dien_thoai)

HÚ HÚ Nếu index giúp mình tìm kiếm nhanh như vậy, tại sao không đánh index cho tất cả các cột trong bảng ?????

Vì index là một sự đánh đổi. Nó làm tăng tốc độ đọc SELECT nhưng lại giảm tốc độ ghi INSERT, UPDATE, DELETE , vì mỗi khi có dữ liệu mới thêm vào, DB không chỉ ghi vào bảng mà còn phải mất công update lại cái mục lục đó nữa. Do đó chỉ đánh index những cột hay dùng trong mệnh đề WHERE, JOIN hoặc ORDER BY thôi.

6.2. Partitioning (phân mảnh)

Khi dữ liệu của bạn không chỉ là 1 triệu mà lên tới vài chục, vài trăm triệu dòng thì cái mục lục cũng trở nên quá bự và sẽ chậm chạp. Lúc này ta dùng chiêu "Chia để trị" (partitioning)

Bản chất của partition là chia một bảng vật lý khổng lồ thành nhiều phần nhỏ hơn, nhưng mặt logic backedn của bạn vẫn nhìn nó như một bảng duy nhất

Ví dụ : Bảng lich_dat cua salon sau 5 năm hoạt dộng đã quá nặng. Ta có thể dùng Partition bảng này theo năm

  • lich_dat_2024
  • lich_dat_2025
  • lich_dat_2026

Khi code backend của bạn query tìm lịch cắt tóc của ngày hôm qua. DB sẽ tự hiểu và chỉ mò vào đúng cái phân vùng lich_dat_2026 để quét, bỏ qua hoàn toàn dữ liêu cũ của các năm trước . Tốc độ lại nhanh nhưu dự án mới mới

7. Tối ưu Query cơ bản

Đến phần này, nếu bạn đã ok các phần trên thì internview họ sẽ muốn xem thói quen gõ code hàng ngày của bạn. Duới đây là luật bất thành văn để code DB không bị các sếp chê là NGUU

**1. Không lạm dụng SELECT ***

ĐỪng tiện tay lấy hết tất cả các cột nếu response chỉ cần tên và sdt, việc kéo dư thừa dữ liệu rác từ DB lên Backedn sẽ tốn băng thông mạng và ngốn RAM của server một cách mô ích

Nên viết `SELECT id, so_dien_thoai FROM khach_hang;

2. Tuyện đối không bọc hàm

  • Bạn muốn tìm tất cả lịch cắt tóc 2026
  • Cách hay viết SELECT * FROM lich_dat WHERE YEAR(ngay_cat)=2026; ->Không nên, DB buộc phải tính toán lại cột này cho từng dòng dữ liệu một, cái mục index ở phần 6 coi như vô hiệu hoá
  • Cách nên viết SELECT * FROM lich_dat WHERE ngay_cat >= '2026-01-01' AND ngay_cat <='2026-12-31;'

3. luôn dùng khi LIMIT khi mò chạm đến data

Tưởng tượng hệ thốn của bạn sau 5 năm có 10 triệu lượt khách. Một ngày đẹp trời, code bị lỗi, bạn mở công cụ quản lý DB (như DBeaver hay MySQL Workbench) lên để xem cấu trúc bảng lich_dat trông như thế nào nhằm fix bug.

  • Nếu bạn gõ SELECT * FROM lich_dat;(Không có LIMIT): Bạn đang ra lệnh cho cơ sở dữ liệu: "Khuân hết 10 triệu tờ hóa đơn ra đây cho tao xem".

    • Hậu quả: Con server DB phải vắt kiệt 100% CPU và RAM để gom 10 triệu dòng dữ liệu, sau đó nhồi nhét gửi qua mạng internet về máy tính của bạn.
    • Kết cục: Máy tính của bạn bị treo cứng đơ, server DB của công ty bị sập (quá tải), website khách đang đặt lịch cũng bị lỗi 500 luôn. Ngày mai bạn được gọi lên phòng nhân sự uống trà
  • Nếu bạn kẹp thêm LIMIT 10: SELECT * FROM lich_dat LIMIT 10;

    • Bạn đang bảo DB: "Tao chỉ cần xem mẫu cấu trúc bảng thôi, mày vớ đại 10 tờ hóa đơn trên cùng đưa tao xem là được".
    • Kết cục: Server DB nhón tay lấy đúng 10 dòng mất 0.001 giây, trả về ngay lập tức. Cực kỳ an toàn!

Chốt lại: LIMIT dùng để bảo vệ hệ thống khỏi những pha "lỡ tay" lôi quá nhiều dữ liệu về máy khi bạn chỉ muốn xem qua cấu trúc hoặc data mẫu. 4. Câu lệnh EXPLAIN EXPLAIN dịch ra là "giải thích"

Khi gõ một câu lệnh, bạn chỉ ra lệnh cho DB đi tìm gì làm gì, DB sẽ lẳng lặng đi tìm và ném cho bạn kết quả cho bạn. khổ nỗi bạn không hề biết DB đã dùng cách gì để tìm

  • Dùng index
  • Hay quét toàn bộ bảng

Vậy làm sao để bắt được long mạch mình có đang ngu không??

Lúc này cần EXPLAIN vào trước EXPLAIN SELECT * FROM khach_hang WHERE so_dien_thoai = '0987654321';

DB sẽ không tìm dl nữa mà nó sẽ ném cho mình một bản thiết kế hành động của nó, trong đấy nó khai báo cho mình

  • "Ê Hoàn, tao sắp quét từ trên xuống dưới cái bảng khach_hang đấy nhé" (Nếu nó hiện ra chữ type: ALL hoặc Table Scan). -> Dấu hiệu query tồi, cần tạo thêm Index!
  • "Ê Hoàn, tao thấy có Index ở cột số điện thoại, tao sẽ dùng cái mục lục này để tìm cho lẹ" (Nếu nó hiện ra chữ type: ref hoặc Index Lookup). -> Dấu hiệu query đã được tối ưu tốt!

THU CUỐI

Vậy là chúng ta đã đi qua bộ checklist SQL từ JOIN, WHERE/HAVING, cho đến tận tầng như Index, Partition và Tối ưu hóa. Hy vọng bài viết này sẽ giúp các bạn tự tin hơn khi đối mặt với các nhà tuyển dụng.

SQL không chỉ là viết đúng cú pháp cho code chạy, mà là tư duy quản trị hệ thống và hiểu cách dữ liệu vận hành bên dưới. Chúc các bạn sớm apply thành công và có những dòng code thật mượt mà!

Nếu thấy bài viết hữu ích, đừng ngại để lại 1 upvote để mình có động lực viết tiếp những bài chia sẻ kinh nghiệm Backend khác trong thời gian tới nhé! Cảm ơn mọi người! 😉


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí