# CS50 SQL của Harvard: bảy tuần học thiết kế bảng trước khi đụng vào dữ liệu khách hàng

> Nếu sắp phải đọc database của khách, thiết kế lại bảng và viết truy vấn an toàn, bạn có thể luyện cả ba việc đó trong khoá học miễn phí của David J. Malan và Carter Zenke.

Bản gốc: https://fdetimes.net/vi/sach-khoa-hoc/khoa-hoc-cs50-sql-harvard-sql-cho-fde/

Harvard có một khoá học chỉ dạy đúng một ngôn ngữ, và nó miễn phí. Trang giới thiệu của CS50's Introduction to Databases with SQL, thường gọi tắt là CS50 SQL, nói thẳng rằng khoá học này dành trọn cho SQL.

Bạn không cần là sinh viên Harvard mới học được: toàn bộ bài giảng có trên OpenCourseWare, còn chứng chỉ qua edX là lựa chọn riêng nếu bạn muốn.

Với người muốn làm FDE, đây không phải một khoá "học cho biết". Dự án ở site khách hàng thường bắt đầu từ một database bạn không viết ra và một bảng tính không ai còn nhớ ai đã tạo. Thiết kế bảng sai thì mọi thứ xây trên đó, từ dashboard tới agent, đều sai theo.

## Hai người dạy, bảy tuần, một ngôn ngữ

Hai giảng viên của khoá là David J. Malan và Carter Zenke. Harvard Online còn ghi Zenke là Preceptor tại Harvard Extension School. Khoá kéo dài 7 tuần, đánh số từ Tuần 0 đến Tuần 6, theo thứ tự Querying, Relating, Designing, Writing, Viewing, Optimizing và Scaling.

Thứ tự này hợp lý với người học. Phải đọc được dữ liệu và hiểu các bảng nối với nhau ra sao thì bạn mới tự thiết kế, ghi dữ liệu, tự động hoá và tối ưu được.

Khoá học dùng SQLite trước vì nó gọn và dễ mang theo, đến cuối mới giới thiệu PostgreSQL và MySQL. Bài tập được làm trên dữ liệu thực, không phải bảng đồ chơi.

CS50 SQL cũng khác CS50x. CS50x dạy khoa học máy tính nói chung, với C, Python, SQL và JavaScript, còn CS50 SQL chỉ đào sâu vào SQL. Bạn có thể học hai khoá cùng lúc hoặc chỉ học CS50 SQL.

## Một công ty bị đếm thành hai: bài học của Tuần 2

Phần đáng tiền nhất nằm ở Tuần 2, Designing. Khoá học dạy bạn mô hình hoá thực thể và quan hệ ngoài đời bằng bảng, chọn kiểu dữ liệu phù hợp, rồi chuẩn hoá để loại bỏ trùng lặp và giảm khả năng sai sót. Ghi chú bài giảng gọi việc tách dữ liệu như thế là normalizing và dùng ER diagram để vẽ các quan hệ.

Thử hình dung một khách hàng logistics gửi bạn file đơn hàng, mỗi dòng ghi lại tên công ty mua hàng. Trong 1.000 dòng, cùng một khách hàng có thể xuất hiện dưới dạng "Công ty ABC" ở chỗ này và "Cty ABC" ở chỗ khác. Khi đó báo cáo doanh thu theo khách hàng tách một công ty thành hai, và không câu truy vấn nào cứu được.

Chuẩn hoá giải quyết đúng chỗ này. Bạn tạo bảng `customers` có `id` làm khoá chính, còn bảng `orders` chỉ lưu `customer_id`. Bài giảng Tuần 2 nêu hai ràng buộc cần nhớ: khoá chính phải là duy nhất, và giá trị khoá ngoại phải có mặt trong cột khoá chính của bảng liên quan.

Có một bước không được bỏ qua: khoá ngoại không tự gộp "Công ty ABC" với "Cty ABC". Trước khi nạp dữ liệu, bạn vẫn phải làm sạch các biến thể tên, thống nhất chúng thành một dòng trong `customers`, rồi ánh xạ từng đơn hàng cũ về đúng `id` đó.

Sau bước làm sạch, trên SQLite, một phiên bản tối giản có thể trông như sau:

```sql
PRAGMA foreign_keys = ON;

CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);

CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
created_at TEXT,
FOREIGN KEY (customer_id) REFERENCES customers(id)
);

INSERT INTO customers (id, name) VALUES (1, 'Công ty ABC');
INSERT INTO orders (customer_id, created_at) VALUES (1, '2026-10-01');  -- hợp lệ
INSERT INTO orders (customer_id, created_at) VALUES (99, '2026-10-01'); -- bị từ chối
```

Dòng cuối trỏ tới khách hàng số 99 không tồn tại, nên database từ chối nó thay vì để lỗi lọt vào báo cáo. Tên công ty giờ chỉ nằm ở một chỗ, sửa một lần là mọi đơn hàng đều đúng theo.

**Điểm mấu chốt:** Ràng buộc trong schema bắt lỗi dữ liệu từ trước khi nó kịp vào báo cáo của khách.

Khi bắt đầu một dự án mới, việc nên làm đầu tiên là vẽ ER diagram từ database của khách. Nếu thấy một bảng chứa cả tên khách hàng lẫn chi tiết đơn hàng, đó là chỗ bạn cần hỏi trước khi viết dòng code nào.

## View lo logic nghiệp vụ, index lo tốc độ

Tuần 4 và Tuần 5, Viewing và Optimizing, dạy hai công cụ mà người mới thường nhầm với nhau. View giúp tự động hoá các truy vấn hay lặp lại, còn index giúp truy vấn chạy nhanh hơn.

Ở site khách hàng, sự khác nhau này quyết định bạn cần làm gì. Khi đội vận hành ngày nào cũng hỏi "đơn nào trễ quá ba ngày", bạn nên viết một view để ai cũng dùng lại cùng một định nghĩa. Khi chính câu hỏi đó chạy chậm trên bảng lớn, bạn cần một index.

View giữ cho cả đội dùng chung một định nghĩa, còn index chỉ lo cho câu truy vấn trả kết quả nhanh hơn. Ngoài ra, khoá học còn hướng dẫn kết nối SQL với Python và Java, tức là cách code ứng dụng làm việc với database ngoài thực tế.

## Hai lỗi người mới hay mắc

Lỗi đầu tiên là quên dòng `PRAGMA foreign_keys = ON` khi làm với SQLite. Thiếu nó, SQLite không kiểm tra khoá ngoại, nên câu `INSERT` trỏ tới khách hàng số 99 ở trên sẽ lọt qua êm ru. Lời khuyên: bật nó ở mỗi kết nối, rồi cố tình chèn một dòng sai để chắc ràng buộc thật sự chạy.

Lỗi thứ hai là thấy index làm truy vấn nhanh thì thêm index cho mọi cột. Mỗi index phải được cập nhật mỗi lần ghi dữ liệu và chiếm thêm chỗ lưu trữ. Cách làm an toàn hơn là chỉ đánh index cho những cột thật sự hay nằm trong `WHERE` hoặc `JOIN` của các truy vấn chậm, và đo trước, đo sau.

## Từ SQLite lên database server, và cái bẫy SQL injection

Tuần 6, Scaling, nói rõ một điểm mà nhiều bản demo bỏ qua: SQLite là database nhúng, còn MySQL và PostgreSQL là database server. Bài giảng tuần này cũng nói về replication, tức là việc bạn sẽ gặp ngay khi hệ thống của khách có nhiều người dùng.

Cũng trong Tuần 6, khoá học dạy dùng prepared statement trong MySQL để chặn SQL injection. Với FDE, đây là kỹ năng bắt buộc. Nếu bạn xây một công cụ nhận input từ người dùng rồi ghép thẳng chuỗi đó vào câu SQL chạy trên dữ liệu thật của khách, một input độc hại là đủ để làm lộ cả bảng.

Thay vì ghép chuỗi, bạn dùng dấu `?` làm placeholder trong câu lệnh rồi truyền giá trị vào sau:

```sql
PREPARE find_orders FROM
'SELECT id, created_at FROM orders WHERE customer_id = ?';
SET @cid = 1;
EXECUTE find_orders USING @cid;
DEALLOCATE PREPARE find_orders;
```

Giá trị của `@cid` chỉ được coi là dữ liệu, không bao giờ trở thành một phần của câu lệnh. Đó là điều bạn nên kiểm tra đầu tiên khi review bất kỳ đoạn code nào chạm vào database của khách.

## Học theo thứ tự nào, và ghi gì vào CV?

Nếu đã viết SQL vài năm, bạn có thể lướt nhanh Tuần 0 và Tuần 1 rồi dành phần lớn thời gian cho Tuần 2 và Tuần 6, hai tuần gần nhất với công việc ở site khách hàng: thiết kế bảng và đưa database lên môi trường thật.

Nếu bạn mới chỉ dùng ORM, hãy học đủ cả bảy tuần theo thứ tự, vì các tuần sau dựa vào kiến thức của tuần trước.

Khi học xong, hãy đưa kết quả vào CV thay vì chỉ ghi "biết SQL". Bạn có thể viết một dòng mô tả cách bạn chuẩn hoá một bộ dữ liệu thực, đặt ràng buộc gì và thêm index nào, rồi gắn link repo.

Khi đọc job description FDE, nếu thấy những cụm như "data modeling" hay "customer data", bạn nên chuẩn bị sẵn một ví dụ chuẩn hoá và một ER diagram tự vẽ để mang vào buổi phỏng vấn. Đó là cách cụ thể nhất để cho thấy bạn đã làm, chứ không chỉ đã học.

Ở site khách hàng, câu hỏi đầu tiên thường không phải "model nào tốt nhất" mà là "bảng này có đáng tin không". CS50 SQL giúp bạn trả lời câu hỏi đó trước khi khách kịp hỏi.

**Thử ngay tuần này:**

- Xem bài giảng và đọc ghi chú Tuần 2 tại cs50.harvard.edu/sql, sau đó vẽ ER diagram cho một bảng tính mà bạn đang phải xử lý ở công ty
- Lấy một file CSV có tên khách hàng lặp lại, nạp vào SQLite, làm sạch tên, tách thành hai bảng nối nhau bằng khoá ngoại rồi thử chèn một dòng vi phạm ràng buộc
- Tìm trong codebase của bạn một chỗ ghép chuỗi để tạo câu SQL và viết lại bằng prepared statement

## Nguồn

- [CS50's Introduction to Databases with SQL](https://cs50.harvard.edu/sql/)

- [CS50's Introduction to Databases with SQL (Harvard Online)](https://pll.harvard.edu/node/23373)

- [Lecture 2 - CS50's Introduction to Databases with SQL](https://cs50.harvard.edu/sql/notes/2/)

- [Lecture 6 - CS50's Introduction to Databases with SQL](https://cs50.harvard.edu/sql/notes/6/)
