T
Trinh Digital
Xây dựng Hệ thống

Thiết kế database: Những nguyên tắc nền tảng tránh 'hối hận' sau này

Trinh Digital · · 12 phút đọc

Thiết kế database là bước quan trọng nhất nhưng bị bỏ qua nhiều nhất trong phát triển phần mềm. Giống như xây nhà — nếu móng yếu, dù sơn phết đẹp cũng sẽ nứt tường sau vài năm. Theo nghiên cứu của IBM Systems Sciences Institute, chi phí sửa lỗi thiết kế database ở giai đoạn production đắt gấp 100 lần so với sửa ở giai đoạn thiết kế. Một khảo sát trên Stack Overflow cho thấy 34% developer thừa nhận đã gặp vấn đề nghiêm trọng do thiết kế database sai từ đầu.

Lead magnet: Tải miễn phí Database Design Checklist & ERD Template — checklist 30 điểm kiểm tra trước khi “đóng gói” thiết kế database.

Tại sao thiết kế database quan trọng?

Hậu quả của database thiết kế sai

Vấn đềTriệu chứngChi phí sửa
Data redundancyCùng thông tin lưu ở 5 nơi, update 1 nơi quên 4 nơi5–20 triệu VND (refactor)
Data inconsistencyCùng khách hàng có 3 tên khác nhau trong hệ thốngKhông thể sửa hoàn toàn nếu đã mất data gốc
Performance bottleneckQuery chạy 10 giây thay vì 0.1 giây10–50 triệu VND (optimization)
Scale limitHệ thống không chịu nổi 10.000 users đồng thời50–200 triệu VND (re-architect)
Feature limitationKhông thể thêm tính năng mới vì data model không hỗ trợ20–100 triệu VND (migration)

Ví dụ thực tế

Trường hợp: Một sàn TMĐT lưu địa chỉ giao hàng trực tiếp trong bảng orders:

orders: id, customer_name, phone, address, city, product, quantity, price

Vấn đề phát sinh:

  • Khách hàng đổi số điện thoại → phải update tất cả đơn cũ
  • Muốn thêm tính năng “lưu nhiều địa chỉ giao hàng” → phải redesign toàn bộ
  • Báo cáo “doanh thu theo khách hàng” sai vì cùng 1 người nhưng ghi tên khác nhau
  • Khi có 1 triệu đơn → query báo cáo chạy 30 giây

Nguyên tắc 1: Normalization (Chuẩn hóa dữ liệu)

Normalization là gì?

Normalization là quá trình tổ chức dữ liệu để giảm thiểu redundancy (dữ liệu trùng lặp) và dependency (phụ thuộc không cần thiết).

3 dạng chuẩn phổ biến

First Normal Form (1NF): Không có giá trị lặp

Sai:

OrderIDProducts
1Áo thun, Quần jean, Giày
2Áo sơ mi, Cà vạt

Đúng:

OrderIDProduct
1Áo thun
1Quần jean
1Giày
2Áo sơ mi
2Cà vạt

Quy tắc: Mỗi cell chỉ chứa 1 giá trị. Không lưu danh sách trong 1 cột.

Second Normal Form (2NF): Phụ thuộc vào toàn bộ primary key

Sai:

OrderIDProductIDProductNameProductPriceQuantity
1P001Áo thun200.0002
2P001Áo thun200.0001

ProductNameProductPrice phụ thuộc vào ProductID, không phụ thuộc vào OrderID.

Đúng: Tách thành 2 bảng:

products: ProductID, ProductName, ProductPrice order_items: OrderID, ProductID, Quantity

Third Normal Form (3NF): Không phụ thuộc bắc cầu

Sai:

CustomerIDNameCityCityPopulation
1Nguyễn Văn AHCM9.000.000
2Trần Thị BHN8.000.000

CityPopulation phụ thuộc vào City, không phụ thuộc trực tiếp vào CustomerID.

Đúng: Tách CityPopulation sang bảng cities.

Khi nào nên denormalize?

Normalization không phải lúc nào cũng tốt. Denormalization (ngược lại) được chấp nhận khi:

Khi nào denormalizeVí dụ
Read-heavy applicationDashboard báo cáo cần tổng hợp nhanh
Tránh JOIN phức tạpLưu customer_name trong orders để hiển thị nhanh
Caching dataMaterialized views cho báo cáo
Historical dataLưu giá sản phẩm tại thời điểm đặt hàng (không lấy giá hiện tại)

Quy tắc: Normalize first, denormalize for performance.

Nguyên tắc 2: Thiết kế ERD (Entity Relationship Diagram)

ERD là gì?

ERD là sơ đồ thể hiện các entity (thực thể) trong hệ thống và mối quan hệ giữa chúng. Đây là “bản vẽ kiến trúc” của database.

3 loại quan hệ

Quan hệKý hiệuVí dụ
One-to-One (1:1)1 — 1User — UserProfile
One-to-Many (1:N)1 — NCustomer — Orders
Many-to-Many (M:N)M — NStudents — Courses (cần bảng trung gian)

Ví dụ ERD cho website e-commerce

[customers] 1 ——— N [orders]
                        |
                        N
                        |
              [order_items] N ——— 1 [products]
                                      |
                                      N
                                      |
                            [product_categories] N ——— 1 [categories]

[customers] 1 ——— N [addresses]
[customers] 1 ——— N [reviews]
[products]  1 ——— N [reviews]
[products]  1 ——— N [product_images]

Các bảng chi tiết

customers:

  • id (PK, auto-increment)
  • email (UNIQUE, NOT NULL)
  • password_hash (NOT NULL)
  • full_name (NOT NULL)
  • phone (VARCHAR 15)
  • created_at (TIMESTAMP)
  • updated_at (TIMESTAMP)

orders:

  • id (PK, auto-increment)
  • customer_id (FK → customers.id)
  • status (ENUM: pending, confirmed, shipping, delivered, cancelled)
  • total_amount (DECIMAL 12,2)
  • shipping_address_id (FK → addresses.id)
  • note (TEXT, nullable)
  • created_at (TIMESTAMP)

order_items:

  • id (PK, auto-increment)
  • order_id (FK → orders.id)
  • product_id (FK → products.id)
  • quantity (INT, NOT NULL)
  • unit_price (DECIMAL 12,2) — giá tại thời điểm đặt hàng
  • subtotal (DECIMAL 12,2)

products:

  • id (PK, auto-increment)
  • name (VARCHAR 255, NOT NULL)
  • slug (VARCHAR 255, UNIQUE)
  • description (TEXT)
  • price (DECIMAL 12,2, NOT NULL)
  • compare_price (DECIMAL 12,2, nullable)
  • stock_quantity (INT, DEFAULT 0)
  • category_id (FK → categories.id)
  • is_active (BOOLEAN, DEFAULT true)
  • created_at (TIMESTAMP)

Nguyên tắc 3: Naming Convention (Quy tắc đặt tên)

Bảng quy tắc đặt tên

ElementConventionVí dụ đúngVí dụ sai
Tên bảngsnake_case, số nhiềuorder_itemsOrderItem, tblOrderItems
Tên cộtsnake_casecreated_atCreatedAt, createdAt
Primary keyididcustomer_id, CustomerID
Foreign key{table_singular}_idcustomer_idcust_id, fk_customer
Booleanis_, has_, can_is_activeactive, status
Timestamp_atcreated_at, deleted_atcreate_date, DateCreated
Indexidx_{table}_{columns}idx_orders_customer_idindex1

Quy tắc bổ sung

  • Không dùng reserved words: Tránh tên bảng/cột là order, user, group, table (thêm prefix: user_accounts)
  • Không viết tắt: quantity thay vì qty, description thay vì desc
  • Consistent: Chọn 1 convention và dùng xuyên suốt

Nguyên tắc 4: Data Types — Chọn đúng kiểu dữ liệu

Chọn data type phù hợp

Dữ liệuĐúngSaiLý do
Giá tiềnDECIMAL(12,2)FLOAT, DOUBLEFloat có lỗi làm tròn (200.000 → 199.999)
SĐTVARCHAR(15)INTSĐT có thể bắt đầu bằng 0, có dấu +
EmailVARCHAR(255)TEXTVARCHAR có index, TEXT không nên index
Mô tảTEXTVARCHAR(255)Mô tả có thể dài hơn 255 ký tự
Trạng tháiENUM hoặc TINYINTVARCHARENUM tiết kiệm dung lượng, tránh typo
Ngày thángDATE, DATETIMEVARCHARCho phép so sánh, sort, tính toán
UUIDCHAR(36) hoặc BINARY(16)VARCHAR(255)Tiết kiệm dung lượng, index hiệu quả
JSON dataJSON/JSONBTEXTCho phép query JSON trực tiếp

Lưu ý tiền VND

-- Đúng: DECIMAL cho tiền VND
price DECIMAL(15, 0)  -- 15 chữ số, không cần phần thập phân (VND không có xu)
-- Hoặc
price BIGINT  -- Lưu bằng đồng, chia 1000 khi hiển thị

-- Sai: FLOAT/DOUBLE
price FLOAT  -- 1.999.999 VND có thể thành 1.999.998.97

Nguyên tắc 5: Indexing — Tăng tốc query

Index là gì?

Index giống như mục lục sách — thay vì đọc hết 500 trang để tìm 1 từ, bạn tra mục lục và nhảy thẳng đến trang đó.

Khi nào cần index?

Nên đánh indexKhông nên đánh index
Cột dùng trong WHERE thường xuyênBảng < 1.000 rows
Cột dùng trong JOIN (foreign keys)Cột thường xuyên INSERT/UPDATE
Cột dùng trong ORDER BYCột có ít giá trị unique (boolean)
Cột UNIQUE (email, phone)Cột TEXT/BLOB

Loại index

LoạiUse caseVí dụ
B-Tree (default)So sánh =, <, >, BETWEEN, LIKE ‘abc%‘Hầu hết trường hợp
HashSo sánh = onlyLookup bằng ID
Full-textTìm kiếm textTìm sản phẩm theo tên
CompositeQuery với nhiều cộtWHERE status = ‘active’ AND city = ‘HCM’
Partial (PostgreSQL)Index có điều kiệnChỉ index orders WHERE status = ‘pending’

Ví dụ indexing cho e-commerce

-- Foreign keys (luôn index)
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
CREATE INDEX idx_order_items_order_id ON order_items(order_id);
CREATE INDEX idx_order_items_product_id ON order_items(product_id);

-- Tìm kiếm phổ biến
CREATE INDEX idx_products_category_id ON products(category_id);
CREATE INDEX idx_products_is_active ON products(is_active) WHERE is_active = true;

-- Composite index cho filter
CREATE INDEX idx_products_category_price ON products(category_id, price);

-- Full-text search
CREATE FULLTEXT INDEX idx_products_search ON products(name, description);

-- Unique constraints
CREATE UNIQUE INDEX idx_customers_email ON customers(email);

Nguyên tắc 6: Soft Delete vs Hard Delete

Hard Delete

DELETE FROM customers WHERE id = 123;
-- Dữ liệu bị xóa vĩnh viễn, không khôi phục được

Soft Delete (khuyến nghị)

-- Thêm cột deleted_at
ALTER TABLE customers ADD COLUMN deleted_at TIMESTAMP NULL DEFAULT NULL;

-- "Xóa" = đánh dấu
UPDATE customers SET deleted_at = NOW() WHERE id = 123;

-- Query chỉ lấy records chưa xóa
SELECT * FROM customers WHERE deleted_at IS NULL;

Ưu điểm soft delete:

  • Khôi phục được dữ liệu đã xóa
  • Giữ lịch sử cho audit
  • Không break foreign key constraints
  • Dễ implement “thùng rác” như Gmail

Nhược điểm:

  • Tất cả query phải thêm WHERE deleted_at IS NULL
  • Database lớn dần theo thời gian
  • Cần cleanup job định kỳ

Nguyên tắc 7: Audit Trail

Luôn lưu lại ai thay đổi gì, khi nào:

-- Mỗi bảng nên có
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_by INT REFERENCES users(id),
updated_by INT REFERENCES users(id)

Với dữ liệu nhạy cảm (tài chính, hợp đồng), tạo bảng audit log riêng:

CREATE TABLE audit_logs (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    table_name VARCHAR(100) NOT NULL,
    record_id BIGINT NOT NULL,
    action ENUM('INSERT', 'UPDATE', 'DELETE') NOT NULL,
    old_values JSON,
    new_values JSON,
    user_id INT REFERENCES users(id),
    ip_address VARCHAR(45),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Checklist thiết kế database

#Hạng mụcKiểm tra
1ERD đã vẽ và review
2Tất cả bảng đạt 3NF (hoặc denormalize có lý do)
3Naming convention nhất quán
4Data types phù hợp (đặc biệt tiền, ngày)
5Primary key cho mọi bảng
6Foreign keys với ON DELETE constraint
7Index trên foreign keys
8Index trên cột tìm kiếm thường xuyên
9UNIQUE constraint cho email, phone, slug
10NOT NULL cho các cột bắt buộc
11DEFAULT values hợp lý
12created_at, updated_at cho mọi bảng
13Soft delete (deleted_at) cho bảng quan trọng
14Charset UTF8MB4 cho tiếng Việt
15Seed data cho testing

FAQ — Câu hỏi thường gặp

Database bao nhiêu bảng là bình thường?

Tùy độ phức tạp: Blog đơn giản (5–10 bảng), E-commerce (20–40 bảng), ERP/CRM (50–200 bảng). Không có con số “chuẩn” — quan trọng là mỗi bảng có lý do tồn tại rõ ràng. Nếu bạn thấy 1 bảng có > 30 cột, có thể cần tách thành nhiều bảng.

Nên dùng auto-increment ID hay UUID?

Auto-increment ID cho hệ thống đơn giản, 1 database. UUID khi cần merge data từ nhiều nguồn, microservices, hoặc không muốn expose thứ tự ID ra ngoài (bảo mật). Hybrid: dùng auto-increment cho internal, UUID cho public-facing API.

Có nên lưu file/ảnh trong database?

Không nên lưu file trực tiếp trong database (chậm, tốn dung lượng, khó backup). Thay vào đó: lưu file trên cloud storage (S3, Google Cloud Storage), lưu URL/path trong database. Ngoại lệ: file < 1MB và cần transaction integrity.

Kết luận

Thiết kế database đúng từ đầu không mất nhiều thời gian hơn — nhưng tiết kiệm hàng trăm giờ sửa lỗi sau này. 7 nguyên tắc trong bài viết này cover 90% những gì bạn cần biết để thiết kế database cho một ứng dụng SME.

Nếu bạn đang bắt đầu dự án mới hoặc cần review thiết kế database hiện tại, hãy liên hệ Trinh Digital — chúng tôi giúp bạn thiết kế kiến trúc dữ liệu vững chắc, sẵn sàng scale khi doanh nghiệp phát triển.

#thiết kế#ERD#database#normalization
Chia sẻ: Z

Sẵn sàng chuyển đổi số cùng Trinh Digital?

Liên hệ ngay để nhận tư vấn miễn phí. Đội ngũ chuyên gia sẽ phân tích nhu cầu và đề xuất giải pháp tối ưu.

Zalo