SQL thường được giới thiệu bằng vài câu SELECT rời rạc, nhưng người mới sẽ hiểu nhanh hơn khi tự tạo một cơ sở dữ liệu nhỏ và theo dõi dữ liệu từ lúc định nghĩa bảng đến lúc trả lời câu hỏi kinh doanh. Bài viết xây dựng cửa hàng sách trên PostgreSQL, nạp dữ liệu mẫu và hoàn thành báo cáo đầu tiên.
Mục lục
- Bài toán thực tế
- Sản phẩm hoàn thành
- Kiến thức cần dùng
- Chuẩn bị môi trường
- Dữ liệu thực hành
- Xây dựng từng bước
- Chương trình hoàn chỉnh
- Kiểm tra kết quả
- Lỗi thường gặp
- Đưa vào thực tế
- Bài tập thực hành
- Project mở rộng
- Tổng kết
- Tài liệu tham khảo
1. Bài toán thực tế

Một cửa hàng sách lưu danh mục sản phẩm và giao dịch trong bảng tính. Khi hai nhân viên cùng cập nhật, mã sách bị trùng, giá bán bị nhập thành chữ và không ai biết phiên bản nào mới nhất. Chủ cửa hàng muốn chuyển dữ liệu vào hệ quản trị cơ sở dữ liệu để tìm kiếm và tổng hợp ổn định hơn.
Project tập trung vào ba câu hỏi rất cụ thể: đang có bao nhiêu đầu sách, sách nào có giá cao nhất và doanh thu của từng giao dịch là bao nhiêu. Phạm vi nhỏ giúp người mới quan sát rõ vai trò của bảng, kiểu dữ liệu, khóa chính và truy vấn.
2. Sản phẩm hoàn thành
Sản phẩm cuối cùng là database bookshop_lab gồm bảng categories, books và sales. Người học tạo cấu trúc bằng SQL, chèn dữ liệu, sửa một bản ghi, truy vấn danh mục và tính doanh thu.
Mỗi file có một nhiệm vụ rõ ràng: 01_create_tables.sql tạo schema, 02_seed.sql nạp mẫu, 03_queries.sql chứa câu hỏi và 04_checks.sql kiểm tra dữ liệu.
- Cài và kết nối PostgreSQL bằng DBeaver hoặc pgAdmin.
- Hiểu database, schema, table, row và column.
- Dùng CREATE TABLE, INSERT, SELECT, UPDATE và DELETE có điều kiện.
- Biết vì sao khóa chính và constraint ngăn dữ liệu lỗi.
- Lưu project SQL có thể chạy lại từ đầu.
3. Kiến thức cần dùng
SQL là ngôn ngữ khai báo. Người viết mô tả dữ liệu cần lấy, còn PostgreSQL chọn cách đọc bảng và thực thi. Một câu lệnh SQL thường kết thúc bằng dấu chấm phẩy; từ khóa không phân biệt hoa thường nhưng nên viết nhất quán.
Kiểu dữ liệu là cam kết về giá trị được phép lưu. bigint phù hợp cho mã số, varchar cho chuỗi có giới hạn, numeric cho tiền và timestamptz cho thời điểm có múi giờ. Chọn kiểu đúng giúp phát hiện lỗi sớm thay vì sửa trong từng báo cáo.
PRIMARY KEY bảo đảm mỗi dòng có định danh duy nhất. NOT NULL buộc trường quan trọng có giá trị; CHECK bảo vệ quy tắc đơn giản như giá không âm; FOREIGN KEY ngăn một cuốn sách tham chiếu đến danh mục không tồn tại.
4. Chuẩn bị môi trường

Cài PostgreSQL 16 và một công cụ quản trị. Khi tạo kết nối local, host thường là localhost, cổng mặc định 5432 và database ban đầu là postgres. Tạo riêng database học tập thay vì thực hành trong database hệ thống.
Tạo thư mục project và chạy file theo thứ tự. Không đưa mật khẩu vào README hoặc Git; công cụ quản trị có thể lưu kết nối cục bộ.
CREATE DATABASE bookshop_lab;
-- Sau đó kết nối vào bookshop_lab
SELECT current_database(), current_user, version();5. Dữ liệu thực hành
Bảng categories chứa một dòng mỗi danh mục. Bảng books chứa một dòng mỗi đầu sách và tham chiếu categories. Bảng sales trong bài đơn giản hóa: mỗi dòng là một giao dịch bán một đầu sách, đủ để người mới quan sát phép tính số lượng nhân đơn giá.
Tên cột dùng snake_case và không chứa khoảng trắng. Giá dùng numeric(12,2) để tránh sai số số thực. created_at có giá trị mặc định để biết thời điểm bản ghi được tạo.
CREATE TABLE categories (
category_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
category_name varchar(100) NOT NULL UNIQUE
);
CREATE TABLE books (
book_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
category_id bigint NOT NULL REFERENCES categories(category_id),
title varchar(200) NOT NULL,
author varchar(150) NOT NULL,
unit_price numeric(12,2) NOT NULL CHECK (unit_price >= 0),
stock_quantity integer NOT NULL CHECK (stock_quantity >= 0)
);
CREATE TABLE sales (
sale_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
book_id bigint NOT NULL REFERENCES books(book_id),
sold_at timestamptz NOT NULL DEFAULT now(),
quantity integer NOT NULL CHECK (quantity > 0),
sale_price numeric(12,2) NOT NULL CHECK (sale_price >= 0)
);6. Xây dựng từng bước
Bước 1: Nạp dữ liệu mẫu
INSERT nên liệt kê tên cột để script không phụ thuộc thứ tự vật lý của bảng. RETURNING giúp xem khóa PostgreSQL vừa tạo.
INSERT INTO categories (category_name) VALUES
('Data'), ('AI'), ('Kinh doanh')
RETURNING *;
INSERT INTO books (category_id, title, author, unit_price, stock_quantity) VALUES
(1, 'SQL thực chiến', 'An Nguyen', 189000, 25),
(1, 'Data Engineering căn bản', 'Minh Tran', 249000, 18),
(2, 'AI cho doanh nghiệp', 'Lan Pham', 219000, 12);Kết quả mong đợi: Ba danh mục và ba đầu sách, mỗi sách có book_id riêng.
Bước 2: Đọc đúng cột cần thiết
SELECT * tiện khi khám phá nhanh nhưng không phù hợp cho truy vấn được tái sử dụng. Chọn rõ cột giúp kết quả ổn định khi bảng có thêm trường.
SELECT book_id, title, author, unit_price, stock_quantity
FROM books
ORDER BY title;Kết quả mong đợi: Danh mục sách được sắp xếp theo tên.
Bước 3: Lọc và sắp xếp
WHERE loại dòng trước khi trả kết quả. ORDER BY có thể dùng nhiều cột; DESC sắp giảm dần và ASC là tăng dần.
SELECT title, unit_price, stock_quantity
FROM books
WHERE unit_price >= 200000
AND stock_quantity > 0
ORDER BY unit_price DESC, title ASC;Kết quả mong đợi: Chỉ sách còn hàng và có giá từ 200.000 đồng.
Bước 4: Ghi nhận giao dịch và tính tiền
Biểu thức quantity * sale_price được tính trên từng dòng. Alias revenue làm tên cột kết quả dễ hiểu.
INSERT INTO sales (book_id, sold_at, quantity, sale_price) VALUES
(1, '2026-09-20 09:15:00+07', 2, 189000),
(2, '2026-09-20 10:20:00+07', 1, 249000);
SELECT sale_id, book_id, quantity, sale_price,
quantity * sale_price AS revenue
FROM sales;Kết quả mong đợi: Hai giao dịch với doanh thu lần lượt 378.000 và 249.000 đồng.
Bước 5: Cập nhật an toàn
UPDATE không có WHERE sẽ thay đổi toàn bộ bảng. Trước khi cập nhật, chạy SELECT với cùng điều kiện để xác nhận đúng dòng.
SELECT * FROM books WHERE book_id = 1;
UPDATE books
SET stock_quantity = stock_quantity - 2
WHERE book_id = 1
AND stock_quantity >= 2
RETURNING book_id, title, stock_quantity;Kết quả mong đợi: Tồn kho sách SQL thực chiến giảm từ 25 xuống 23.
Đọc database như một mô hình của thế giới thật
Khi nhìn vào ba bảng bookshop, hãy tập thói quen mô tả bằng câu: một category có nhiều book, một book có nhiều sale và mỗi sale trong phiên bản đơn giản chỉ tham chiếu một book. Câu mô tả này giúp phát hiện thiết kế chưa biểu diễn được hóa đơn nhiều sản phẩm. SQL bắt đầu từ mô hình, không bắt đầu từ câu lệnh.
Dùng công cụ quản trị để mở diagram hoặc đọc information_schema. Người học nên chỉ ra cột nào là định danh, cột nào mô tả, cột nào là số đo và cột nào liên kết bảng. Nếu không giải thích được vai trò từng cột, việc viết truy vấn sẽ nhanh chóng trở thành thử-sai.
Quan sát tác động của từng lệnh
INSERT làm số dòng tăng, UPDATE giữ số dòng nhưng thay giá trị, DELETE làm số dòng giảm và SELECT không thay dữ liệu. Sau mỗi thao tác, chạy một câu kiểm tra nhỏ. Thói quen này quan trọng hơn ghi nhớ cú pháp vì nó hình thành khả năng dự đoán tác động trước khi nhấn chạy.
Thử bao lệnh UPDATE trong BEGIN và ROLLBACK. Quan sát tồn kho thay đổi trong transaction rồi trở về giá trị cũ. Đây là cách an toàn để học các lệnh ghi và cũng là nền tảng xử lý sự cố khi script production sửa sai nhiều dòng.
Từ câu hỏi tiếng Việt đến SELECT
Viết yêu cầu “liệt kê sách Data còn hàng, giá dưới 300.000, đắt nhất trước” rồi gạch chân danh từ và điều kiện. Danh từ xác định bảng, thuộc tính xác định cột, điều kiện đi vào WHERE và yêu cầu thứ tự đi vào ORDER BY. Cách dịch có hệ thống giảm cảm giác SQL là tập ký hiệu khó nhớ.
Sau khi có kết quả, hỏi ngược: mỗi dòng đại diện cho gì, có thể trùng book_id không và có cuốn sách nào bị bỏ ngoài ý muốn không. Ba câu hỏi này sẽ theo người học suốt các chủ đề JOIN, GROUP BY và Window Function.
Dataset mẫu hoàn chỉnh để tự chạy
Ba bảng ban đầu cần đủ dữ liệu để người mới quan sát cả trường hợp bình thường lẫn biên. Thêm một sách hết hàng, một sách giá bằng 0 dùng cho tài liệu tặng, một category chưa có sách và một sách chưa phát sinh giao dịch. Những dòng này làm rõ sự khác biệt giữa INNER JOIN và LEFT JOIN ở phần báo cáo cuối.
Sau khi nạp, lưu kết quả SELECT dưới dạng ảnh hoặc đoạn output trong README. Người học phải biết bảng đúng trông như thế nào trước khi tiếp tục; nếu seed data sai, mọi truy vấn sau đều mất giá trị kiểm chứng.
INSERT INTO categories(category_name) VALUES ('Tài liệu miễn phí');
INSERT INTO books(category_id,title,author,unit_price,stock_quantity) VALUES
(1,'PostgreSQL từ cơ bản','Mai Le',159000,0),
(4,'SQL Cheat Sheet','TechData.AI',0,100);
SELECT c.category_name,b.title,b.unit_price,b.stock_quantity
FROM categories c LEFT JOIN books b ON b.category_id=c.category_id
ORDER BY c.category_name,b.title;Đọc kết quả truy vấn thay vì chỉ nhìn code
Truy vấn LEFT JOIN sẽ trả cả “Tài liệu miễn phí” dù category có thể chưa có sách ở thời điểm kiểm tra. Các cột từ books nhận NULL. Đây không phải dữ liệu lỗi; đó là cách SQL biểu diễn việc không tìm thấy bản ghi phía phải.
Với báo cáo sách và doanh thu, một cuốn chưa bán vẫn xuất hiện nhờ LEFT JOIN và SUM trả NULL trước khi COALESCE thành 0. Nếu dùng INNER JOIN, sách chưa bán biến mất. Lựa chọn JOIN phụ thuộc câu hỏi “báo cáo tất cả sách” hay “chỉ sách đã bán”.
SELECT b.title,
COALESCE(SUM(s.quantity),0) units_sold,
COALESCE(SUM(s.quantity*s.sale_price),0) revenue
FROM books b LEFT JOIN sales s ON s.book_id=b.book_id
GROUP BY b.book_id,b.title
ORDER BY revenue DESC,b.title;Thực hành giao dịch kho an toàn
Bán hàng gồm hai hành động: tạo sale và giảm stock. Nếu chỉ một hành động thành công, dữ liệu mất nhất quán. Transaction gom chúng thành một đơn vị. UPDATE dùng điều kiện stock_quantity >= quantity, sau đó ứng dụng phải kiểm tra số dòng được cập nhật.
Trong PostgreSQL, SELECT FOR UPDATE có thể khóa dòng sách trong transaction để hai phiên bán đồng thời không cùng đọc một lượng tồn cũ. Đây là bước nâng cao nhưng giúp người mới hiểu database không chỉ là nơi lưu bảng.
BEGIN;
SELECT book_id,stock_quantity FROM books
WHERE book_id=1 FOR UPDATE;
UPDATE books SET stock_quantity=stock_quantity-2
WHERE book_id=1 AND stock_quantity>=2;
INSERT INTO sales(book_id,quantity,sale_price)
SELECT 1,2,unit_price FROM books WHERE book_id=1;
COMMIT;Cách tự chấm project đầu tiên
Project đạt yêu cầu khi có thể xóa database, chạy lại các file theo README và nhận cùng schema, dữ liệu, báo cáo. Một người khác không cần hỏi tác giả vẫn tái tạo được kết quả. Đây là tiêu chuẩn nhỏ nhưng rất gần cách dự án thật được bàn giao.
Tự chấm bốn nhóm: tính đúng của constraint, khả năng chạy lại, sự rõ ràng của truy vấn và kiểm tra kết quả. Không cộng điểm chỉ vì câu lệnh dài; một SELECT ngắn giải quyết đúng câu hỏi có giá trị hơn nhiều đoạn code không kiểm chứng.
SELECT table_name FROM information_schema.tables
WHERE table_schema='public' ORDER BY table_name;
SELECT conrelid::regclass table_name,conname,contype
FROM pg_constraint
WHERE connamespace='public'::regnamespace
ORDER BY table_name,conname;7. Chương trình hoàn chỉnh

File truy vấn cuối tạo báo cáo danh mục sách kèm doanh thu. JOIN được dùng ở mức đơn giản để đưa tên sách vào giao dịch; bài JOIN chuyên sâu sẽ giải thích cardinality và các loại JOIN.
Chạy script trong transaction khi muốn thử nghiệm nhiều thay đổi. Nếu kết quả sai có thể ROLLBACK; khi đúng mới COMMIT.
BEGIN;
SELECT
b.book_id,
b.title,
b.unit_price,
b.stock_quantity,
COALESCE(SUM(s.quantity), 0) AS units_sold,
COALESCE(SUM(s.quantity * s.sale_price), 0) AS revenue
FROM books b
LEFT JOIN sales s ON s.book_id = b.book_id
GROUP BY b.book_id, b.title, b.unit_price, b.stock_quantity
ORDER BY revenue DESC, b.title;
COMMIT;8. Kiểm tra kết quả
Đầu tiên kiểm tra số dòng và khóa duy nhất. Sau đó thử chèn giá âm hoặc giao dịch tham chiếu book_id không tồn tại để quan sát constraint từ chối dữ liệu.
Đối chiếu tổng doanh thu bằng phép tính tay: 2 × 189.000 + 1 × 249.000 = 627.000 đồng. Một bài học SQL chỉ hoàn thành khi người học giải thích được kết quả, không chỉ chạy được câu lệnh.
SELECT COUNT(*) AS book_count FROM books;
SELECT book_id, COUNT(*) FROM books GROUP BY book_id HAVING COUNT(*) > 1;
SELECT SUM(quantity * sale_price) AS total_revenue FROM sales;
-- Câu lệnh này phải thất bại vì CHECK constraint
-- INSERT INTO books (category_id,title,author,unit_price,stock_quantity)
-- VALUES (1,'Sách lỗi','Test',-1000,1);9. Lỗi thường gặp

- Kết nối nhầm database: Luôn chạy SELECT current_database() trước khi tạo bảng.
- Quên WHERE khi UPDATE hoặc DELETE: Chạy SELECT trước và dùng transaction.
- Dùng float cho tiền: Chọn numeric để tính toán chính xác.
- Tên bảng có dấu hoặc khoảng trắng: Dùng snake_case để tránh phải đặt trong dấu ngoặc kép.
- Chèn khóa ngoại không tồn tại: Nạp bảng cha trước bảng con và kiểm tra mã tham chiếu.
10. Đưa vào thực tế
Trong ứng dụng thật, schema được quản lý bằng migration thay vì sửa tay. Tài khoản ứng dụng không nên có quyền DROP TABLE. Backup phải được thử khôi phục định kỳ, bởi một file backup chưa kiểm tra chưa thể xem là phương án phục hồi.
Khi lượng giao dịch tăng, tách sale header và sale items để biểu diễn một hóa đơn có nhiều sách. Không nên mở rộng mô hình đơn giản bằng cách thêm book_1, book_2 vào cùng một dòng.
11. Bài tập thực hành

- Thêm cột isbn và bảo đảm không trùng.
- Viết truy vấn tìm năm cuốn sách đắt nhất còn hàng.
- Thêm ba giao dịch rồi tính tổng doanh thu.
- Thử xóa một category đang được books tham chiếu và giải thích lỗi.
- Tạo view book_sales_summary từ truy vấn hoàn chỉnh.
12. Project mở rộng
Mở rộng project thành hệ thống quản lý đơn hàng gồm customers, orders và order_items. Thêm trạng thái đơn, giảm giá và phương thức thanh toán. Đây là nền dữ liệu được sử dụng trong các bài JOIN và GROUP BY tiếp theo.
13. Tổng kết
Người mới không cần học toàn bộ SQL trong một lần. Việc tự tạo ba bảng, nạp dữ liệu và trả lời một câu hỏi doanh thu đã hình thành đầy đủ vòng đời cơ bản: định nghĩa, ghi, đọc, cập nhật và kiểm tra.
Sau bài này, bước tiếp theo là học SELECT, WHERE và ORDER BY sâu hơn trên một tập dữ liệu đủ lớn để xử lý điều kiện ngày, NULL, chuỗi và phân trang.
14. Tài liệu tham khảo
TechData.AI - Leading the Future.
Tham khảo các khoá học theo link: https://techdata.ai/techdata-ai-course/
Hoàng Minh.
