FK
DB Consistency
schema • key • rule

Cách thiết kế database nhất quán, ít lệch dữ liệu

Nguyên tắc thiết kế database để giữ dữ liệu nhất quán: naming, primary key, foreign key, unique, not null và chuẩn hoá vừa đủ.

10 phút đọc16/06/2026

Thiết kế database tệ thường không nổ ngay ngày đầu. Vấn đề chỉ lộ ra sau vài tháng khi dữ liệu bắt đầu lệch, bảng này nói một kiểu, bảng kia nói một kiểu, code phải vá bằng if-else, migration ngày càng khó, và mỗi lần sửa business rule là lo hỏng dữ liệu cũ.

Nếu mục tiêu của bạn là một database dễ sống lâu, thì thứ cần ưu tiên đầu tiên không phải “tối ưu query” mà là tính nhất quán trong design.

Kết luận nhanh

Nếu chỉ cần nhớ một checklist ngắn khi design database:

  1. mỗi entity phải có một nguồn dữ liệu gốc rõ ràng
  2. naming phải nhất quán giữa bảng, cột và khóa
  3. mọi quan hệ quan trọng nên có foreign key
  4. các rule như duy nhất, bắt buộc, giới hạn giá trị phải đẩy xuống database bằng unique, not null, check
  5. đừng duplicate dữ liệu nếu chưa có lý do rõ ràng
  6. transaction phải bọc các thao tác cập nhật nhiều bảng

Làm đúng 6 điểm này sẽ tránh được phần lớn lỗi “data bị lệch” về sau.

Tính nhất quán trong database là gì?

Trong ngữ cảnh thiết kế database, tính nhất quán thường có 2 lớp:

  • consistency của dữ liệu: dữ liệu không mâu thuẫn, không orphan, không duplicate vô nghĩa
  • consistency của design: cách đặt tên, kiểu cột, cách tách bảng, cách lưu trạng thái giống nhau giữa các phần của hệ thống

Ví dụ không nhất quán:

  • bảng users dùng id, bảng khác lại dùng userId, bảng khác nữa dùng uid
  • chỗ thì lưu status = active, chỗ khác lại lưu is_active = 1
  • bảng orders xóa user rồi nhưng order.user_id vẫn còn trỏ tới record không tồn tại
  • email bị trùng vì chỉ check ở code mà không có unique constraint

Những lỗi kiểu này ban đầu nhìn nhỏ, nhưng càng nhiều bảng thì chi phí sửa càng lớn.

Khi nào database dễ mất nhất quán nhất?

Rủi ro tăng mạnh khi:

  • nhiều dev cùng sửa schema nhưng không có convention rõ
  • business rule nằm hết ở app, database gần như không có constraint
  • copy dữ liệu từ bảng này sang bảng khác cho “tiện query”
  • update nhiều bảng trong một flow nhưng không dùng transaction
  • soft delete, status, audit field mỗi bảng lưu một kiểu khác nhau

Đó là lý do database design nên có nguyên tắc ngay từ đầu, không nên để mỗi bảng tự tiến hoá theo kiểu riêng.

Nguyên tắc 1: Mỗi entity phải có một nguồn dữ liệu gốc

Một rule rất quan trọng là:

Một thông tin quan trọng chỉ nên có một nơi được xem là nguồn đúng nhất.

Ví dụ:

  • tên user nên lấy từ bảng users
  • giá sản phẩm hiện tại nên lấy từ bảng products
  • trạng thái đơn hàng nên có một cột chuẩn trong orders

Nếu cùng một thông tin nằm ở nhiều nơi mà không có lý do rõ ràng, sớm muộn sẽ lệch.

Ví dụ xấu:

users.email
orders.customer_email
invoices.customer_email

Nếu đây đều là “email hiện tại của user” thì rất dễ lệch. Chỉ nên duplicate khi bạn cố ý snapshot dữ liệu lịch sử, ví dụ email tại thời điểm phát hành invoice.

Nguyên tắc 2: Naming phải nhất quán ngay từ đầu

Consistency trong naming giúp giảm rất nhiều lỗi code và migration.

Nên chốt sớm:

  • dùng snake_case hay camelCase
  • primary key có luôn là id không
  • foreign key có luôn là xxx_id không
  • timestamp có luôn là created_at, updated_at không

Một convention phổ biến và dễ sống lâu:

users
orders
order_items

id
user_id
order_id
created_at
updated_at
deleted_at

Điểm quan trọng không phải là bạn chọn kiểu nào, mà là đã chọn thì giữ cho đồng đều.

Nguyên tắc 3: Dùng primary key và foreign key rõ ràng

Primary key là định danh chính của record. Foreign key là cách database hiểu quan hệ giữa các bảng.

Ví dụ:

CREATE TABLE users (
  id BIGINT PRIMARY KEY,
  email VARCHAR(255) NOT NULL UNIQUE
);

CREATE TABLE orders (
  id BIGINT PRIMARY KEY,
  user_id BIGINT NOT NULL,
  created_at TIMESTAMP NOT NULL,
  CONSTRAINT fk_orders_user
    FOREIGN KEY (user_id) REFERENCES users(id)
);

Lợi ích thực tế của foreign key:

  • chặn record mồ côi
  • giúp migration và query rõ nghĩa hơn
  • ép dev tôn trọng quan hệ dữ liệu thay vì update bừa

Không phải hệ thống nào cũng bật full FK ở mọi nơi, nhưng nếu bỏ FK thì bạn phải có lý do thật rõ, không nên bỏ chỉ vì “cho nhanh”.

Nguyên tắc 4: Rule quan trọng phải nằm ở database, không chỉ ở code

Nhiều đội chỉ check ở API:

  • email không được trùng
  • status chỉ được là một số giá trị
  • một dòng bắt buộc phải có user_id

Nhưng nếu chỉ check ở code:

  • job nền có thể ghi sai
  • script import có thể ghi sai
  • service khác có thể bypass rule

Vì vậy các rule core nên đẩy xuống database:

  • NOT NULL
  • UNIQUE
  • CHECK
  • DEFAULT

Ví dụ:

CREATE TABLE users (
  id BIGINT PRIMARY KEY,
  email VARCHAR(255) NOT NULL UNIQUE,
  status VARCHAR(20) NOT NULL CHECK (status IN ('active', 'inactive', 'blocked')),
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

App vẫn cần validate để trả lỗi đẹp hơn, nhưng database mới là lớp chặn cuối cùng.

Nguyên tắc 5: Chuẩn hoá vừa đủ, đừng duplicate quá sớm

Một lỗi phổ biến là mới làm đã duplicate dữ liệu để “query cho tiện”.

Ví dụ:

  • lưu user_name trong orders
  • lưu product_price trong cart_items
  • lưu department_name ở nhiều bảng

Trừ khi bạn cố ý lưu snapshot lịch sử, phần lớn các trường kiểu này nên tham chiếu từ bảng gốc.

Mục tiêu là:

  • dữ liệu gốc có một nơi
  • dữ liệu dẫn xuất chỉ tạo khi thật sự cần

Chuẩn hoá quá mức cũng không tốt nếu query trở nên quá phức tạp. Nhưng với bài toán CRUD thông thường, ưu tiên chống lệch dữ liệu trước rồi mới tối ưu sau.

Nguyên tắc 6: Cùng một kiểu dữ liệu nên dùng cùng một format

Ví dụ:

  • ID chỗ dùng INT, chỗ dùng VARCHAR
  • tiền chỗ dùng FLOAT, chỗ dùng DECIMAL
  • status chỗ dùng string, chỗ dùng number
  • thời gian chỗ dùng local time, chỗ dùng UTC

Những khác biệt này làm code mapping rất mệt.

Một số rule thực dụng:

  • tiền nên dùng DECIMAL, tránh FLOAT
  • timestamp nên thống nhất UTC
  • foreign key phải cùng kiểu với primary key mà nó tham chiếu
  • trạng thái nên chốt một kiểu biểu diễn từ đầu

Nguyên tắc 7: Các thao tác cập nhật nhiều bảng phải có transaction

Đây là phần nhiều hệ thống bị lệch dữ liệu dù schema nhìn có vẻ ổn.

Ví dụ flow tạo đơn hàng:

  1. tạo record trong orders
  2. tạo nhiều record trong order_items
  3. trừ tồn kho
  4. ghi payment log

Nếu step 3 fail mà step 1 và 2 đã commit, data bắt đầu lệch.

Các flow kiểu này phải bọc trong transaction:

BEGIN;

-- insert orders
-- insert order_items
-- update inventory

COMMIT;

Consistency không chỉ đến từ schema, mà còn đến từ cách ghi dữ liệu.

Nguyên tắc 8: Soft delete, status, audit field phải có chuẩn chung

Một hệ thống dễ loạn rất nhanh nếu:

  • bảng này dùng is_deleted
  • bảng kia dùng deleted
  • bảng khác lại dùng deleted_at

Tương tự với audit field:

  • created_at
  • updated_at
  • created_by
  • updated_by

Không nhất thiết bảng nào cũng cần full audit, nhưng nếu đã dùng thì nên theo cùng một pattern.

Một kiểu thường dễ dùng:

created_at
updated_at
deleted_at

deleted_at IS NULL thường rõ nghĩa hơn cờ boolean kiểu is_deleted.

Nguyên tắc 9: Index phục vụ query, không thay thế design

Index rất quan trọng, nhưng không cứu được một schema đã lệch logic.

Ví dụ:

  • duplicate dữ liệu sai thì thêm index vẫn sai
  • quan hệ không rõ thì index không sửa được integrity
  • status lưu lung tung thì query nhanh hơn cũng không làm data đúng hơn

Thứ tự nên là:

  1. schema nhất quán
  2. constraint rõ
  3. query đúng
  4. rồi mới tối ưu bằng index

Một schema nhỏ nhưng nhất quán thường trông như thế nào?

Ví dụ:

CREATE TABLE users (
  id BIGINT PRIMARY KEY,
  email VARCHAR(255) NOT NULL UNIQUE,
  full_name VARCHAR(255) NOT NULL,
  status VARCHAR(20) NOT NULL CHECK (status IN ('active', 'inactive')),
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE orders (
  id BIGINT PRIMARY KEY,
  user_id BIGINT NOT NULL,
  status VARCHAR(20) NOT NULL CHECK (status IN ('pending', 'paid', 'cancelled')),
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE TABLE order_items (
  id BIGINT PRIMARY KEY,
  order_id BIGINT NOT NULL,
  product_id BIGINT NOT NULL,
  quantity INT NOT NULL CHECK (quantity > 0),
  unit_price DECIMAL(12,2) NOT NULL CHECK (unit_price >= 0),
  CONSTRAINT fk_order_items_order FOREIGN KEY (order_id) REFERENCES orders(id)
);

Điểm đáng chú ý ở đây không phải là schema quá “cao siêu”, mà là:

  • naming đồng đều
  • constraint rõ
  • relation rõ
  • kiểu dữ liệu nhất quán

Những lỗi design database rất hay gặp

1. Không có unique ở field đáng lẽ phải duy nhất

Ví dụ:

  • email user
  • order code
  • transaction reference

Chỉ check ở app là chưa đủ.

2. Trạng thái được lưu bằng nhiều kiểu khác nhau

Ví dụ:

  • active
  • 1
  • enabled

Ba cách này cùng chỉ một nghĩa nhưng làm query và code trở nên rối.

3. Dữ liệu gốc bị copy ra nhiều bảng

Nếu không phải snapshot có chủ đích, đây thường là nguồn gốc của inconsistency.

4. Thiếu transaction cho flow nhiều bước

Schema có đẹp nhưng flow ghi dữ liệu không atomic thì vẫn lệch như thường.

5. Foreign key cùng tên nhưng khác kiểu dữ liệu

Ví dụ users.idBIGINT nhưng orders.user_id lại là INT hoặc VARCHAR. Những mismatch kiểu này tạo ra lỗi khó chịu về sau.

Kết luận

Thiết kế database nhất quán không phải là làm schema thật hàn lâm. Mục tiêu thực tế hơn là:

  • dữ liệu không tự mâu thuẫn
  • business rule quan trọng được chặn ở database
  • mọi bảng đi theo cùng một convention
  • flow ghi dữ liệu nhiều bước không để lại trạng thái nửa vời

Nếu phải ưu tiên ít thứ nhất có thể, hãy chốt trước:

  1. naming convention
  2. primary key / foreign key
  3. not null, unique, check
  4. transaction cho các flow nhiều bảng

Chỉ bốn phần đó thôi đã giúp database dễ sống lâu hơn rất nhiều.