PostgreSQL không chỉ là nơi lưu dữ liệu. Đây là nền tảng để người mới hiểu cách một hệ thống kinh doanh biến đơn hàng, khách hàng và sản phẩm thành dữ liệu có thể kiểm soát.
Bài viết xây dựng một database bán hàng hoàn chỉnh ở quy mô học tập nhưng tuân theo những nguyên tắc có thể mang vào dự án thật. Sau khi hoàn thành, người đọc có thể cài PostgreSQL, tạo cấu trúc dữ liệu, nạp dữ liệu mẫu, viết truy vấn doanh thu và tự kiểm tra kết quả.
Mục lục
- PostgreSQL là gì và vì sao phù hợp để bắt đầu?
- Sản phẩm đầu ra: database bán hàng có thể truy vấn
- Cài đặt PostgreSQL và kiểm tra kết nối
- Tạo schema và bốn bảng cốt lõi
- Nạp bộ dữ liệu mẫu có chủ đích
- Viết truy vấn doanh thu có thể kiểm chứng
- Kiểm thử dữ liệu trước khi tin báo cáo
- Năm lỗi thường gặp và cách chẩn đoán
- Từ bài thực hành đến môi trường vận hành
- Bài tập mở rộng và tiêu chí hoàn thành
PostgreSQL là gì và vì sao phù hợp để bắt đầu?
PostgreSQL là hệ quản trị cơ sở dữ liệu quan hệ mã nguồn mở, nổi bật nhờ tính nhất quán, khả năng mở rộng và hệ sinh thái mạnh. Dữ liệu được tổ chức thành bảng, liên kết bằng khóa và được truy vấn bằng SQL. Điểm quan trọng với người mới là PostgreSQL vừa đủ thân thiện để học, vừa đủ nghiêm ngặt để hình thành tư duy làm việc đúng với dữ liệu.
Trong doanh nghiệp, PostgreSQL có thể phục vụ website giao dịch, hệ thống nội bộ, kho dữ liệu nhỏ và lớp lưu trữ cho ứng dụng AI. Khi dữ liệu tăng, hệ thống vẫn cung cấp index, transaction, JSONB, partitioning, replication và nhiều cơ chế vận hành mà một database production cần có.
Sản phẩm đầu ra: database bán hàng có thể truy vấn
Database thực hành gồm bốn thực thể: khách hàng, sản phẩm, đơn hàng và chi tiết đơn hàng. Một khách hàng có nhiều đơn hàng; một đơn hàng có nhiều dòng sản phẩm. Mô hình này đủ gần thực tế để học khóa ngoại, transaction và báo cáo doanh thu mà không làm người mới bị ngợp.
Đầu ra cuối cùng là báo cáo doanh thu theo ngày và theo khách hàng. Mỗi con số đều có thể truy ngược về dòng đơn hàng nguồn, giúp người học hiểu rằng báo cáo tốt phải có khả năng kiểm chứng chứ không chỉ hiển thị kết quả đẹp.

Cài đặt PostgreSQL và kiểm tra kết nối
Trên Windows hoặc macOS, có thể dùng bộ cài chính thức kèm pgAdmin. Trên Ubuntu, cài bằng trình quản lý gói. Trong môi trường dự án, Docker giúp cả nhóm dùng cùng phiên bản và cấu hình.
docker run --name postgres-sales \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=sales_db \
-p 5432:5432 -d postgres:17
psql -h localhost -U postgres -d sales_db
SELECT version();
Nếu câu lệnh cuối trả về phiên bản PostgreSQL, kết nối đã sẵn sàng. Không nên dùng tài khoản quản trị cho ứng dụng thật; phần phân quyền sẽ được xử lý ở bài chuyên sâu riêng.

Tạo schema và bốn bảng cốt lõi
Schema giúp nhóm các đối tượng cùng nghiệp vụ và tránh để mọi bảng trong public. Kiểu numeric(12,2) phù hợp với số tiền hơn kiểu số thực vì tránh sai lệch nhị phân.
CREATE SCHEMA IF NOT EXISTS sales;
CREATE TABLE sales.customers (
customer_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
full_name varchar(120) NOT NULL,
email varchar(255) NOT NULL UNIQUE,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE sales.products (
product_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
product_name varchar(160) NOT NULL,
unit_price numeric(12,2) NOT NULL CHECK (unit_price >= 0),
active boolean NOT NULL DEFAULT true
);
CREATE TABLE sales.orders (
order_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id bigint NOT NULL REFERENCES sales.customers(customer_id),
order_date date NOT NULL DEFAULT current_date,
status varchar(20) NOT NULL CHECK (status IN ('pending','paid','cancelled'))
);
CREATE TABLE sales.order_items (
order_id bigint REFERENCES sales.orders(order_id) ON DELETE CASCADE,
product_id bigint REFERENCES sales.products(product_id),
quantity integer NOT NULL CHECK (quantity > 0),
unit_price numeric(12,2) NOT NULL CHECK (unit_price >= 0),
PRIMARY KEY (order_id, product_id)
);

Nạp bộ dữ liệu mẫu có chủ đích
Dữ liệu mẫu nên bao phủ nhiều trạng thái thay vì chỉ tạo vài dòng giống nhau. Ba đơn hàng dưới đây gồm đơn đã thanh toán, đang chờ và bị hủy để truy vấn có điều kiện thực tế.
INSERT INTO sales.customers(full_name,email) VALUES
('An Nguyễn','an@example.com'),('Bình Trần','binh@example.com');
INSERT INTO sales.products(product_name,unit_price) VALUES
('Bàn phím cơ',1500000),('Chuột không dây',650000),('Màn hình 27 inch',6200000);
INSERT INTO sales.orders(customer_id,order_date,status) VALUES
(1,'2026-09-18','paid'),(1,'2026-09-19','pending'),(2,'2026-09-19','cancelled');
INSERT INTO sales.order_items(order_id,product_id,quantity,unit_price) VALUES
(1,1,1,1500000),(1,2,2,650000),(2,3,1,6200000),(3,2,1,650000);
Giá được lưu lại trong order_items vì giá sản phẩm có thể thay đổi theo thời gian. Báo cáo lịch sử phải dùng giá tại thời điểm mua, không lấy giá hiện tại từ bảng sản phẩm.

Viết truy vấn doanh thu có thể kiểm chứng
Doanh thu chỉ tính đơn đã thanh toán. Điều kiện này thể hiện logic nghiệp vụ; nếu bỏ qua, đơn hủy và đơn chờ sẽ làm số liệu bị phóng đại.
SELECT o.order_date,
COUNT(DISTINCT o.order_id) AS paid_orders,
SUM(i.quantity * i.unit_price) AS revenue
FROM sales.orders o
JOIN sales.order_items i USING (order_id)
WHERE o.status = 'paid'
GROUP BY o.order_date
ORDER BY o.order_date;
Kết quả mong đợi cho ngày 2026-09-18 là một đơn hàng và doanh thu 2.800.000 đồng. Có thể truy ngược phép tính: 1 × 1.500.000 cộng 2 × 650.000.
Kiểm thử dữ liệu trước khi tin báo cáo
Kiểm thử tối thiểu gồm số dòng, tính duy nhất của khóa, giá trị null và đối soát tổng. Các truy vấn dưới đây phải trả về lần lượt 3 đơn hàng, 0 email trùng và 2.800.000 đồng doanh thu đã thanh toán.
SELECT COUNT(*) AS order_count FROM sales.orders;
SELECT email, COUNT(*) FROM sales.customers GROUP BY email HAVING COUNT(*) > 1;
SELECT SUM(quantity * unit_price) AS paid_revenue
FROM sales.order_items i JOIN sales.orders o USING(order_id)
WHERE o.status='paid';
Thói quen ghi rõ expected output giúp phát hiện dữ liệu sai sớm hơn. Trong pipeline production, các kiểm tra tương tự có thể được tự động hóa thành data quality test.
Năm lỗi thường gặp và cách chẩn đoán
Lỗi connection refused thường do dịch vụ chưa chạy hoặc sai cổng. Lỗi xác thực đến từ sai mật khẩu hay cấu hình pg_hba.conf. Lỗi “relation does not exist” thường do sai schema hoặc chữ hoa được đặt trong dấu ngoặc kép.
Hai lỗi khó thấy hơn là dùng float cho tiền và xóa bản ghi cha khi chưa định nghĩa hành vi khóa ngoại. Cách xử lý tốt là đọc toàn bộ thông báo lỗi, xác định câu SQL nhỏ nhất tái hiện vấn đề, rồi kiểm tra lần lượt kết nối, quyền, schema và kiểu dữ liệu.

Từ bài thực hành đến môi trường vận hành
Database production cần tách tài khoản ứng dụng khỏi tài khoản quản trị, bật backup, theo dõi dung lượng và ghi nhận truy vấn chậm. Migration phải được quản lý bằng mã nguồn; không chỉnh cấu trúc tùy hứng trực tiếp trên production.
Chuỗi kết nối nên lấy từ biến môi trường hoặc secret manager. Mật khẩu không được đưa vào Git. Mỗi lần thay đổi schema cần có kế hoạch rollback và kiểm tra trên môi trường staging.
Bài tập mở rộng và tiêu chí hoàn thành
Hãy thêm bảng thanh toán, cột tỉnh thành cho khách hàng và truy vấn ba sản phẩm có doanh thu cao nhất. Sau đó thử chèn số lượng âm, email trùng và đơn hàng tham chiếu khách hàng không tồn tại để quan sát constraint bảo vệ dữ liệu.
Bài làm đạt yêu cầu khi có file SQL chạy lại từ đầu trên database rỗng; kết quả doanh thu khớp phép tính thủ công; các trường hợp dữ liệu sai bị từ chối; README nêu mô hình, cách chạy, quyết định thiết kế và giới hạn hiện tại.
Kết luận
Một database bán hàng nhỏ đã cho thấy gần như toàn bộ nền tảng quan trọng: mô hình quan hệ, khóa, constraint, dữ liệu lịch sử, logic doanh thu và kiểm thử. Bước tiếp theo là đi sâu vào thiết kế bảng để lựa chọn data type và constraint có chủ đích, thay vì chỉ tạo cấu trúc đủ chạy.
TechData.AI - Leading the Future.
Tham khảo các khoá học theo link: https://techdata.ai/techdata-ai-course/
Hoàng Minh.