Back to blog

Thiết Kế Schema Database — Từ Yêu Cầu Đến Bảng

databasepostgresqlsqlschema-designbackend
Thiết Kế Schema Database — Từ Yêu Cầu Đến Bảng

Giới Thiệu

Bạn cần thêm ba bảng mới cho tính năng mới. Tên bảng nên số nhiều hay số ít? Có cần created_at cho mọi bảng không? Dùng UUID hay auto-increment ID? Những quyết định nhỏ này tích lũy nhanh chóng — schema thiết kế kém dẫn đến query chậm, data anomalies, và tính năng khó xây dựng.

Bài viết này hướng dẫn cách tiếp cận có hệ thống cho thiết kế schema — từ thu thập yêu cầu đến tạo bảng production-ready, với hai case study thực tế.

Điều kiện tiên quyết: Bài viết này dựa trên Cơ Bản Về Cơ Sở Dữ Liệu Quan Hệ. Bạn nên hiểu tables, primary keys, foreign keys, constraints, và data types trước khi tiếp tục.

Bạn Sẽ Học Được Gì

✅ Thu thập yêu cầu và xác định entities từ business rules
✅ Thiết kế relationships one-to-one, one-to-many, và many-to-many
✅ Áp dụng normalization (1NF, 2NF, 3NF) với ví dụ thực tế
✅ Biết khi nào denormalize — và tại sao đó là chiến lược, không phải lười biếng
✅ Chọn naming conventions nhất quán cho toàn bộ codebase
✅ Triển khai soft deletes, audit columns, và timestamps
✅ Quyết định giữa UUIDs và auto-increment IDs
✅ Sử dụng JSON/JSONB columns một cách chiến lược
✅ Lên kế hoạch schema evolution và backward compatibility
✅ Thiết kế hai schema thực tế: blog platform và SaaS multi-tenant


Từ Yêu Cầu Đến Entities

Trích Xuất Entities và Relationships

Đọc yêu cầu và tìm danh từ (entities) và động từ (relationships):

Yêu cầu: "Users viết blog posts. Mỗi post thuộc một category
nhưng có thể có nhiều tags. Users có thể comment trên posts.
Posts có thể published hoặc draft."

Entities: Users, Posts, Categories, Tags, Comments

Relationships:

  • User → Post (one-to-many)
  • Category → Post (one-to-many)
  • Post ↔ Tag (many-to-many)
  • Post → Comment (one-to-many)

Entity vs Attribute

Trường hợpEntity hay Attribute?Lý do
Email người dùngAttributeGiá trị đơn giản, không có relationships
Địa chỉTùy trường hợpTái sử dụng → entity. Duy nhất per user → attribute
Category sản phẩmEntityCó attributes riêng, được query độc lập
Trạng thái đơn hàngAttribute (enum)Tập giá trị cố định

Quy tắc: Nếu có attributes riêng, được query độc lập, hoặc tham gia relationships — tạo bảng. Nếu là giá trị đơn giản chỉ có ý nghĩa trong context parent — tạo column.


Relationships Deep Dive

One-to-One

Dùng khi tách dữ liệu nhạy cảm, chia bảng lớn, hoặc thêm extension tùy chọn:

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    username VARCHAR(50) NOT NULL UNIQUE
);
 
CREATE TABLE profiles (
    id SERIAL PRIMARY KEY,
    user_id INT NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
    bio TEXT,
    avatar_url VARCHAR(500)
);

UNIQUE trên user_id đảm bảo mỗi user chỉ có một profile.

One-to-Many

Relationship phổ biến nhất. Foreign key nằm ở bên "many":

CREATE TABLE posts (
    id SERIAL PRIMARY KEY,
    user_id INT NOT NULL REFERENCES users(id),
    title VARCHAR(200) NOT NULL,
    content TEXT NOT NULL
);
 
-- ❌ KHÔNG NÊN: Lưu nhiều IDs trong array
-- ALTER TABLE users ADD COLUMN post_ids INT[];
 
-- ✅ NÊN: Foreign key ở bảng "many" (như trên)

Many-to-Many

Cần junction table (bảng trung gian):

CREATE TABLE tags (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50) NOT NULL UNIQUE,
    slug VARCHAR(50) NOT NULL UNIQUE
);
 
CREATE TABLE post_tags (
    post_id INT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
    tag_id INT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
    PRIMARY KEY (post_id, tag_id)
);

Junction table với attributes — khi relationship có dữ liệu riêng:

CREATE TABLE enrollments (
    user_id INT NOT NULL REFERENCES users(id),
    course_id INT NOT NULL REFERENCES courses(id),
    enrolled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    grade DECIMAL(5, 2) CHECK (grade >= 0 AND grade <= 100),
    PRIMARY KEY (user_id, course_id)
);

Self-Referencing Relationships

-- Categories phân cấp
CREATE TABLE categories (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    parent_id INT REFERENCES categories(id) ON DELETE SET NULL
);

Normalization: 1NF, 2NF, 3NF

Normalization tổ chức dữ liệu để giảm redundancy và ngăn anomalies. Mục tiêu: mỗi fact được lưu đúng một lần.

First Normal Form (1NF)

Quy tắc: Mỗi column chứa giá trị nguyên tử. Không có repeating groups.

-- ❌ Vi phạm 1NF: Giá trị phân tách bằng dấu phẩy
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    products VARCHAR(500)  -- "Laptop, Mouse, Keyboard"
);
 
-- ❌ Vi phạm 1NF: Columns lặp lại
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    product_1 VARCHAR(100),
    product_2 VARCHAR(100),
    product_3 VARCHAR(100)  -- Nếu 4 sản phẩm thì sao?
);
 
-- ✅ Thỏa 1NF: Bảng riêng cho order items
CREATE TABLE order_items (
    id SERIAL PRIMARY KEY,
    order_id INT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
    product_id INT NOT NULL REFERENCES products(id),
    quantity INT NOT NULL CHECK (quantity > 0)
);

Second Normal Form (2NF)

Quy tắc: 1NF + mọi non-key column phụ thuộc vào toàn bộ primary key.

-- ❌ Vi phạm 2NF: product_name chỉ phụ thuộc vào product_id
CREATE TABLE order_items (
    order_id INT NOT NULL,
    product_id INT NOT NULL,
    product_name VARCHAR(200),  -- Partial dependency!
    quantity INT NOT NULL,
    PRIMARY KEY (order_id, product_id)
);
 
-- ✅ Thỏa 2NF: Product data trong bảng riêng
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    price DECIMAL(10, 2) NOT NULL
);
 
CREATE TABLE order_items (
    order_id INT NOT NULL REFERENCES orders(id),
    product_id INT NOT NULL REFERENCES products(id),
    quantity INT NOT NULL,
    unit_price DECIMAL(10, 2) NOT NULL,  -- Snapshot giá tại thời điểm mua
    PRIMARY KEY (order_id, product_id)
);

Third Normal Form (3NF)

Quy tắc: 2NF + không có transitive dependencies.

-- ❌ Vi phạm 3NF: country phụ thuộc vào zip_code, không phải customer id
CREATE TABLE customers (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    zip_code VARCHAR(20),
    city VARCHAR(100),
    country VARCHAR(100)  -- Phụ thuộc vào zip_code
);
 
-- ✅ Thỏa 3NF: Tách location data
CREATE TABLE zip_codes (
    zip_code VARCHAR(20) PRIMARY KEY,
    city VARCHAR(100) NOT NULL,
    country VARCHAR(100) NOT NULL
);
 
CREATE TABLE customers (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    zip_code VARCHAR(20) REFERENCES zip_codes(zip_code)
);

Với hầu hết ứng dụng, 3NF là mục tiêu phù hợp. Bắt đầu ở 3NF, sau đó denormalize khi đo được vấn đề hiệu suất cụ thể.


Khi Nào Denormalize

Denormalization thêm redundancy có chủ ý để cải thiện hiệu suất đọc. Đó là trade-off có chủ đích, không phải lười biếng.

Pattern 1: Cache Aggregates

-- Thay vì đếm posts mỗi lần load trang:
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    post_count INT NOT NULL DEFAULT 0  -- Denormalized
);
 
-- Giữ đồng bộ bằng trigger:
CREATE OR REPLACE FUNCTION update_user_post_count()
RETURNS TRIGGER AS $$
BEGIN
    IF TG_OP = 'INSERT' THEN
        UPDATE users SET post_count = post_count + 1 WHERE id = NEW.user_id;
    ELSIF TG_OP = 'DELETE' THEN
        UPDATE users SET post_count = post_count - 1 WHERE id = OLD.user_id;
    END IF;
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

Pattern 2: Snapshot Historical Data

-- Order items lưu thông tin product tại thời điểm mua
CREATE TABLE order_items (
    id SERIAL PRIMARY KEY,
    order_id INT NOT NULL REFERENCES orders(id),
    product_id INT NOT NULL REFERENCES products(id),
    product_name VARCHAR(200) NOT NULL,   -- Snapshot
    unit_price DECIMAL(10, 2) NOT NULL,   -- Snapshot
    quantity INT NOT NULL
);

Bảng Quyết Định Denormalize

Trường hợpDenormalize?Lý do
Dữ liệu thay đổi thường xuyên❌ KhôngKhó giữ đồng bộ
Bảng write-heavy❌ KhôngOverhead update quá cao
Bảng nhỏ (< 10k rows)❌ KhôngJoins đã đủ nhanh
Dashboard reporting✅ CóChấp nhận dữ liệu hơi cũ
Lịch sử đơn hàng✅ CóCần snapshot thời điểm

Naming Conventions

Phần tửConventionVí dụ
Tên bảngSố nhiều, snake_caseusers, order_items
Tên columnsnake_casefirst_name, created_at
Primary keysidid BIGSERIAL PRIMARY KEY
Foreign keys{table_singular}_iduser_id, category_id
BooleansPrefix is_ hoặc has_is_active, has_paid
TimestampsSuffix _atcreated_at, deleted_at
Indexesidx_{table}_{columns}idx_posts_user_id
-- ✅ NÊN
users, blog_posts, order_items
user_id, created_at, is_active
 
-- ❌ KHÔNG NÊN
User, blogPost, tbl_users
userId, fName, active

Soft Deletes vs Hard Deletes

Hard Delete

DELETE FROM users WHERE id = 1;  -- Xóa vĩnh viễn

Dùng khi: GDPR, dữ liệu tạm thời, không cần audit trail.

Soft Delete

CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    deleted_at TIMESTAMPTZ  -- NULL = active, timestamp = đã xóa
);
 
UPDATE users SET deleted_at = NOW() WHERE id = 1;  -- "Xóa"
SELECT * FROM users WHERE deleted_at IS NULL;        -- Query active users
UPDATE users SET deleted_at = NULL WHERE id = 1;     -- Khôi phục

Dùng khi: Audit trails, undo/restore, compliance, analytics.

Quan trọng: Nhớ thêm WHERE deleted_at IS NULL vào mọi query. Cân nhắc tạo view:

CREATE VIEW active_users AS
SELECT * FROM users WHERE deleted_at IS NULL;

Audit Columns

Thêm tracking columns vào mọi bảng business data:

CREATE TABLE posts (
    id BIGSERIAL PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    -- Audit columns
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
 
-- Trigger auto-update updated_at
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;
ColumnBắt buộc?Khi nào dùng
created_at✅ LuônMọi bảng
updated_at✅ LuônMọi bảng mutable
created_byNên cóHệ thống multi-user
deleted_atTùy chọnChỉ khi soft delete

UUIDs vs Auto-Increment IDs

Auto-Increment

CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,  -- 1, 2, 3, ...
    email VARCHAR(255) NOT NULL
);

Ưu: Nhỏ (8 bytes), insert nhanh, dễ đọc, index hiệu quả. Nhược: Dự đoán được (security risk), conflict khi merge databases.

UUIDs

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) NOT NULL
);

Ưu: Globally unique, không dự đoán được, generate client-side. Nhược: Lớn (16 bytes), insert chậm hơn, khó debug.

Hybrid Approach (Khuyến nghị)

CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,                                 -- Internal
    public_id UUID NOT NULL UNIQUE DEFAULT gen_random_uuid(),  -- API
    email VARCHAR(255) NOT NULL UNIQUE
);
-- API trả public_id, database join dùng id (integer nhanh hơn)
Use CaseAuto-IncrementUUID
Bảng internal✅
Public API IDs✅
Distributed systems✅
High-write tables✅

JSON/JSONB Columns

Khi Nào Dùng JSONB

CREATE TABLE products (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    attributes JSONB DEFAULT '{}'::JSONB  -- Khác nhau per product type
);
-- Laptop: {"ram_gb": 16, "cpu": "Intel i7"}
-- Áo: {"size": "L", "color": "blue"}

Dùng cho: Variable attributes, user preferences, API responses.

Khi Nào KHÔNG Dùng JSONB

-- ❌ Core business data trong JSONB
CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    data JSONB  -- Không constraints, không FK, không type safety
);
 
-- ✅ Schema quan hệ đúng cách
CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    customer_id BIGINT NOT NULL REFERENCES customers(id),
    total DECIMAL(10, 2) NOT NULL CHECK (total >= 0)
);

Schema Evolution

Safe Migrations

-- ✅ AN TOÀN: Thêm nullable column
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
 
-- ✅ AN TOÀN: Thêm column có default
ALTER TABLE users ADD COLUMN is_verified BOOLEAN NOT NULL DEFAULT FALSE;
 
-- ❌ NGUY HIỂM: NOT NULL không có default
ALTER TABLE users ADD COLUMN required_field VARCHAR(100) NOT NULL;
-- ERROR: contains null values

Multi-Step Migration

-- 1. Thêm nullable
ALTER TABLE users ADD COLUMN email_verified BOOLEAN;
-- 2. Backfill
UPDATE users SET email_verified = FALSE WHERE email_verified IS NULL;
-- 3. Thêm NOT NULL
ALTER TABLE users ALTER COLUMN email_verified SET NOT NULL;
-- 4. Thêm default
ALTER TABLE users ALTER COLUMN email_verified SET DEFAULT FALSE;

Case Study 1: Blog Platform

-- Users
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    username VARCHAR(50) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    is_active BOOLEAN NOT NULL DEFAULT TRUE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
 
-- Categories (phân cấp)
CREATE TABLE categories (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(100) NOT NULL UNIQUE,
    parent_id BIGINT REFERENCES categories(id) ON DELETE SET NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
 
-- Tags
CREATE TABLE tags (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(50) NOT NULL UNIQUE,
    slug VARCHAR(50) NOT NULL UNIQUE
);
 
-- Posts
CREATE TABLE posts (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL REFERENCES users(id),
    category_id BIGINT REFERENCES categories(id) ON DELETE SET NULL,
    title VARCHAR(200) NOT NULL,
    slug VARCHAR(200) NOT NULL UNIQUE,
    excerpt TEXT,
    content TEXT NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'draft'
        CHECK (status IN ('draft', 'published', 'archived')),
    published_at TIMESTAMPTZ,
    view_count INT NOT NULL DEFAULT 0,
    comment_count INT NOT NULL DEFAULT 0,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CHECK (status != 'published' OR published_at IS NOT NULL)
);
 
-- Post ↔ Tags
CREATE TABLE post_tags (
    post_id BIGINT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
    tag_id BIGINT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
    PRIMARY KEY (post_id, tag_id)
);
 
-- Comments (nested)
CREATE TABLE comments (
    id BIGSERIAL PRIMARY KEY,
    post_id BIGINT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
    user_id BIGINT NOT NULL REFERENCES users(id),
    parent_id BIGINT REFERENCES comments(id) ON DELETE CASCADE,
    content TEXT NOT NULL,
    is_approved BOOLEAN NOT NULL DEFAULT FALSE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
 
-- Indexes
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_status ON posts(status);
CREATE INDEX idx_posts_published_at ON posts(published_at DESC);
CREATE INDEX idx_comments_post_id ON comments(post_id);

Quyết định thiết kế: Categories phân cấp qua parent_id, counters denormalized (view_count, comment_count), CHECK constraint đảm bảo published posts có published_at, comments nested qua self-referencing FK.


Case Study 2: SaaS Multi-Tenant

-- Organizations (tenants)
CREATE TABLE organizations (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    slug VARCHAR(200) NOT NULL UNIQUE,
    plan VARCHAR(20) NOT NULL DEFAULT 'free'
        CHECK (plan IN ('free', 'pro', 'enterprise')),
    max_users INT NOT NULL DEFAULT 5,
    is_active BOOLEAN NOT NULL DEFAULT TRUE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
 
-- Users (global)
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    first_name VARCHAR(100),
    last_name VARCHAR(100),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
 
-- Memberships (many-to-many with roles)
CREATE TABLE organization_members (
    organization_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
    user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    role VARCHAR(20) NOT NULL DEFAULT 'member'
        CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
    joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (organization_id, user_id)
);
 
-- Projects
CREATE TABLE projects (
    id BIGSERIAL PRIMARY KEY,
    organization_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
    name VARCHAR(200) NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'active'
        CHECK (status IN ('active', 'archived', 'completed')),
    created_by BIGINT NOT NULL REFERENCES users(id),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (organization_id, name)
);
 
-- Tasks
CREATE TABLE tasks (
    id BIGSERIAL PRIMARY KEY,
    project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
    organization_id BIGINT NOT NULL REFERENCES organizations(id),
    title VARCHAR(200) NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'todo'
        CHECK (status IN ('todo', 'in_progress', 'review', 'done', 'cancelled')),
    priority VARCHAR(20) NOT NULL DEFAULT 'medium'
        CHECK (priority IN ('low', 'medium', 'high', 'urgent')),
    assigned_to BIGINT REFERENCES users(id),
    due_date DATE,
    created_by BIGINT NOT NULL REFERENCES users(id),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
 
-- Indexes
CREATE INDEX idx_org_members_user ON organization_members(user_id);
CREATE INDEX idx_projects_org ON projects(organization_id);
CREATE INDEX idx_tasks_org ON tasks(organization_id);
CREATE INDEX idx_tasks_assigned ON tasks(assigned_to);

Row-Level Security

ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
 
CREATE POLICY tasks_tenant_isolation ON tasks
    USING (
        organization_id IN (
            SELECT organization_id FROM organization_members
            WHERE user_id = current_setting('app.current_user_id')::BIGINT
        )
    );

Quyết định thiết kế: organization_id denormalized trong tasks cho RLS hiệu quả, composite UNIQUE trên projects cho phép tên trùng giữa các orgs, users global có thể thuộc nhiều orgs, RLS ngăn data leak ở database level.


Tổng Kết

✅ Bắt đầu từ yêu cầu — Trích xuất entities và relationships từ business rules
✅ Normalize đến 3NF — Mỗi fact lưu một lần, không partial hay transitive dependencies
✅ Denormalize có chiến lược — Cache aggregates, snapshot historical data, materialized views
✅ Naming nhất quán — Plural tables, snake_case columns, {table}_id cho FKs
✅ Audit columns mọi nơi — created_at, updated_at mọi bảng; deleted_at cho soft deletes
✅ Chọn IDs phù hợp — BIGSERIAL internal, UUID cho public API
✅ JSONB cho variable data — Product attributes, preferences; không cho core business data
✅ Lên kế hoạch evolution — Nullable trước, backfill, rồi constraints
✅ Multi-tenant cần isolation — Denormalize org_id, dùng Row-Level Security
✅ Index mọi foreign key — FK không index gây slow joins và cascades


Tiếp Theo

📚 Cơ Bản Về CSDL Quan Hệ — Ôn lại relational model, ACID, data types, constraints
📚 SQL Indexing Deep Dive (sắp ra) — B-tree indexes, composite indexes, EXPLAIN ANALYZE
📚 SQL Query Optimization Guide (sắp ra) — N+1 problems, JOIN strategies, pagination, profiling


Kết Luận

Schema design vừa là nghệ thuật vừa là khoa học. Normalization, constraints, data types là khoa học. Biết khi nào phá vỡ quy tắc — denormalization, JSONB, composite keys — là nghệ thuật.

Bắt đầu normalized. Denormalize chỉ khi đo được vấn đề hiệu suất thực sự. Dùng naming conventions nhất quán từ ngày đầu. Luôn thêm audit columns — bạn sẽ cảm ơn mình khi debug lúc 2 giờ sáng.

Schema là nền tảng của ứng dụng. Thiết kế đúng, mọi thứ khác sẽ dễ dàng hơn.

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.