Chế độ lập kế hoạch thiết kế schema Postgres giúp bạn xác định thực thể, ràng buộc, index và migrations trước khi sinh mã, giảm việc viết lại sau này.

Nếu bạn xây dựng endpoints và models trước khi hình dạng cơ sở dữ liệu rõ ràng, thường bạn sẽ phải viết lại cùng những chức năng nhiều lần. Ứng dụng hoạt động cho bản demo, rồi dữ liệu thực và các trường hợp cạnh xuất hiện và mọi thứ trở nên mong manh.
Hầu hết các lần viết lại xuất phát từ ba vấn đề có thể đoán trước:
Mỗi vấn đề đều buộc phải thay đổi lan toả qua code, tests và ứng dụng khách.
Lập kế hoạch schema Postgres nghĩa là quyết hợp đồng dữ liệu trước rồi sinh mã tương ứng. Trong thực tế điều này là viết ra các thực thể, quan hệ, và vài truy vấn quan trọng, rồi chọn ràng buộc, index và chiến lược migration trước khi bất kỳ công cụ nào scaffold bảng và CRUD.
Điều này càng quan trọng khi bạn dùng nền tảng tạo mã nhanh như Koder.ai, nơi bạn có thể sinh nhiều mã nhanh chóng. Sinh nhanh thì tốt, nhưng tin cậy hơn nhiều khi schema đã được ổn định. Models và endpoints sinh ra sẽ cần ít chỉnh sửa sau này.
Những gì thường sai khi bỏ qua bước lập kế hoạch:
Kế hoạch schema tốt là đơn giản: mô tả bằng ngôn ngữ thường về thực thể, phác thảo bảng và cột, các ràng buộc và index chính, và chiến lược migration cho phép bạn thay đổi an toàn khi sản phẩm lớn lên.
Lập kế hoạch schema hiệu quả nhất khi bạn bắt đầu từ những gì app phải nhớ và những gì người dùng cần làm với dữ liệu đó. Viết mục tiêu trong 2–3 câu đơn giản. Nếu bạn không thể giải thích một cách đơn giản, có khả năng bạn sẽ tạo thêm bảng không cần thiết.
Tiếp theo, tập trung vào các hành động tạo hoặc thay đổi dữ liệu. Những hành động này là nguồn thực sự của các hàng và chúng tiết lộ điều gì cần được validate. Nghĩ bằng động từ, không phải danh từ.
Ví dụ, một app đặt chỗ có thể cần tạo booking, lên lịch lại, huỷ, hoàn tiền, và nhắn khách hàng. Các động từ đó nhanh chóng gợi ra những gì phải lưu (khung giờ, thay đổi trạng thái, số tiền) trước khi bạn đặt tên bảng.
Ghi lại các đường đọc (read paths) nữa, vì phần đọc định hướng cấu trúc và indexing sau này. Liệt kê các màn hình hoặc báo cáo người dùng thực sự dùng và cách họ cắt lát dữ liệu: “Lịch đặt của tôi” sắp theo ngày và lọc theo trạng thái, tìm kiếm admin theo tên khách hoặc mã booking, doanh thu hàng ngày theo địa điểm, và view audit xem ai thay đổi gì lúc nào.
Cuối cùng, ghi các yêu cầu phi chức năng làm thay đổi lựa chọn schema, như lịch sử audit, soft deletes, tách multi-tenant, hoặc quy tắc quyền riêng tư (ví dụ giới hạn ai được thấy thông tin liên hệ).
Nếu bạn dự định sinh mã sau bước này, những ghi chú này trở thành prompt mạnh mẽ. Chúng nêu rõ điều cần thiết, điều có thể thay đổi, và điều phải có thể tìm kiếm. Nếu dùng Koder.ai, viết điều này trước khi sinh mã sẽ khiến Chế độ Lập kế hoạch hiệu quả hơn vì nền tảng làm việc từ yêu cầu thực thay vì đoán mò.
Trước khi chạm tới bảng, viết mô tả bằng ngôn ngữ thường về những gì app lưu. Bắt đầu bằng danh sách các danh từ bạn lặp lại: user, project, message, invoice, subscription, file, comment. Mỗi danh từ là một thực thể tiềm năng.
Rồi thêm một câu cho mỗi thực thể trả lời: nó là gì và tại sao tồn tại? Ví dụ: “A Project là một workspace mà user tạo để nhóm công việc và mời người khác.” Điều này tránh các bảng mơ hồ như data, items, hoặc misc.
Quyền sở hữu là quyết định lớn tiếp theo và ảnh hưởng đến hầu hết truy vấn. Với mỗi thực thể, quyết:
Giờ quyết cách nhận diện bản ghi. UUID rất tốt khi bản ghi có thể được tạo từ nhiều nơi (web, mobile, background jobs) hoặc khi bạn không muốn ID dễ đoán. Bigint nhỏ hơn và nhanh hơn. Nếu cần định danh thân thiện cho người dùng, giữ nó riêng (ví dụ project_code ngắn, unique trong tài khoản) thay vì ép làm primary key.
Cuối cùng, viết quan hệ bằng câu trước khi vẽ sơ đồ: user có nhiều project, project có nhiều message, và user có thể thuộc nhiều project. Đánh dấu mỗi liên kết là bắt buộc hay tuỳ chọn, ví dụ “một message phải thuộc project” so với “một invoice có thể thuộc project”. Những câu này là nguồn sự thật khi sinh mã sau này.
Khi thực thể đã rõ bằng ngôn ngữ thường, biến mỗi cái thành một bảng với các cột phản ánh sự thật bạn cần lưu.
Bắt đầu với tên và kiểu bạn sẽ giữ. Chọn quy ước nhất quán: tên cột snake_case, cùng kiểu cho cùng một ý tưởng, và primary key có quy tắc rõ ràng. Với timestamp, ưu tiên timestamptz để múi giờ không gây bất ngờ. Với tiền, dùng numeric(12,2) (hoặc lưu cents bằng integer) thay vì float.
Với cột trạng thái, dùng enum Postgres hoặc cột text kèm CHECK để giá trị được kiểm soát.
Quyết cột nào bắt buộc vs tuỳ chọn bằng cách dịch luật ra NOT NULL. Nếu một giá trị bắt buộc để hàng có ý nghĩa, đặt NOT NULL. Nếu thực sự không biết hoặc không áp dụng, cho phép null.
Một tập cột mặc định thực tế để lên kế hoạch:
id (uuid hoặc bigint, chọn một và nhất quán)created_at và updated_atdeleted_at chỉ khi bạn thực sự cần soft deletes và khả năng restorecreated_by khi cần audit rõ người làm gìQuan hệ many-to-many hầu như luôn trở thành các bảng join. Ví dụ, nếu nhiều user có thể cộng tác trên một app, tạo app_members với app_id và user_id, rồi ép uniqueness trên cặp để tránh trùng.
Suy nghĩ về lịch sử sớm. Nếu biết sẽ cần versioning, lên kế hoạch bảng bất biến như app_snapshots, mỗi hàng là một phiên bản lưu liên kết đến apps bằng app_id và có created_at.
Ràng buộc là lan can của schema. Quyết các luật phải đúng bất kể dịch vụ, script hay công cụ admin nào chạm tới DB.
Bắt đầu với định danh và quan hệ. Mỗi bảng cần primary key, và cột “belongs to” nên là foreign key thực sự, không chỉ integer mà bạn hy vọng khớp.
Rồi thêm unique nơi trùng lặp gây hại, như hai account cùng email hoặc hai line item cùng (order_id, product_id).
Các ràng buộc giá trị cao nên lên kế hoạch sớm:
amount >= 0, status IN ('draft','paid','canceled'), hoặc rating BETWEEN 1 AND 5.Hành vi cascade là nơi lập kế hoạch giúp bạn sau này. Hỏi xem người dùng mong gì. Nếu một customer bị xóa, đơn hàng của họ thường không nên biến mất. Điều đó chỉ ra dùng restrict deletes và giữ lịch sử. Với dữ liệu phụ thuộc như order line items, cascade từ order xuống items có thể hợp lý vì items vô nghĩa nếu thiếu parent.
Khi bạn sinh models và endpoints, các ràng buộc này trở thành yêu cầu rõ ràng: lỗi nào phải xử lý, trường nào bắt buộc, và trường hợp cạnh nào là không thể xảy ra do thiết kế.
Index nên trả lời một câu hỏi: điều gì cần nhanh cho người dùng thực sự.
Bắt đầu với các màn hình và API bạn dự định phát hành trước. Trang danh sách lọc theo status và sắp theo mới nhất có nhu cầu khác so với trang chi tiết tải quan hệ liên quan.
Viết 5–10 mẫu truy vấn bằng ngôn ngữ thường trước khi chọn index. Ví dụ: “Hiển thị hóa đơn của tôi trong 30 ngày qua, lọc theo trả/ chưa trả, sắp theo created_at” hoặc “Mở project và liệt kê tasks theo due_date.” Điều này giúp lựa chọn index bám sát sử dụng thực tế.
Một bộ index khởi đầu tốt thường bao gồm cột foreign key dùng cho joins, cột lọc phổ biến (như status, user_id, created_at), và một hai index composite cho các truy vấn nhiều bộ lọc ổn định, như (account_id, created_at) khi bạn luôn lọc theo account_id rồi sắp theo thời gian.
Thứ tự trong index composite quan trọng. Đặt cột bạn lọc thường xuyên nhất (và có tính phân biệt cao) lên trước. Nếu bạn luôn lọc theo tenant_id ở mọi request, nó thường cần ở đầu nhiều index.
Tránh index mọi thứ “phòng hờ”. Mỗi index thêm gánh nặng khi INSERT và UPDATE và có thể gây hại hơn một truy vấn hiếm hơi chậm.
Lên kế hoạch tìm kiếm văn bản riêng. Nếu chỉ cần “contains” đơn giản, ILIKE có thể đủ ban đầu. Nếu search là cốt lõi, lên kế hoạch full-text search (tsvector) sớm để không phải thiết kế lại sau.
Schema không “xong” khi bạn tạo bảng đầu tiên. Nó thay đổi mỗi khi thêm tính năng, sửa lỗi, hoặc hiểu rõ hơn về dữ liệu. Nếu quyết chiến lược migration trước, bạn tránh được các viết lại đau đớn sau khi sinh mã.
Giữ quy tắc đơn giản: thay đổi DB bằng các bước nhỏ, từng tính năng một. Mỗi migration nên dễ review và an toàn chạy ở mọi môi trường.
Hầu hết lỗi phá vỡ xuất phát từ đổi tên hoặc xoá cột, hoặc thay đổi kiểu. Thay vì làm mọi thứ một lần, lên kế hoạch con đường an toàn:
Cách này mất nhiều bước hơn, nhưng thực tế nhanh hơn vì giảm downtime và vá khẩn cấp.
Seed data cũng là phần của migration. Quyết cái bảng tham chiếu nào là “luôn có” (roles, statuses, countries, plan types) và làm cho chúng có thể dự đoán được. Đặt các INSERT/UPDATE cho những bảng này vào migration riêng để mọi dev và mọi deploy nhận được kết quả giống nhau.
Đặt kỳ vọng sớm:
Rollback không luôn là một “down migration” hoàn hảo. Đôi khi rollback tốt nhất là restore từ backup. Nếu bạn dùng Koder.ai, cũng nên quyết khi nào dựa vào snapshots để phục hồi nhanh, đặc biệt trước thay đổi rủi ro.
Hãy tưởng tượng một SaaS nhỏ nơi mọi người tham gia team, tạo project, và theo dõi task.
Bắt đầu bằng liệt kê thực thể và chỉ các trường cần thiết ngày đầu:
Quan hệ rõ ràng: một team có nhiều projects, một project có nhiều tasks, và user tham gia team qua team_members. Tasks thuộc project và có thể được gán cho user.
Giờ thêm vài ràng buộc ngăn lỗi thường gặp:
Index nên khớp màn hình thực tế. Ví dụ, nếu danh sách task lọc theo project và state và sắp theo mới nhất, lên kế hoạch index như tasks (project_id, state, created_at DESC). Nếu “My tasks” là view quan trọng, index như tasks (assignee_user_id, state, due_date) có thể giúp.
Với migrations, giữ lần đầu an toàn và cơ bản: tạo bảng, primary keys, foreign keys, và các unique constraint cốt lõi. Thay đổi tốt tiếp theo là thứ bạn thêm sau khi thấy cách dùng thực tế, như giới thiệu soft delete (deleted_at) trên tasks và điều chỉnh index “active tasks” để bỏ các hàng đã xoá.
Hầu hết viết lại xảy ra vì schema ban đầu thiếu luật và chi tiết sử dụng thực tế. Một lần lập kế hoạch tốt không phải là sơ đồ hoàn hảo mà là phát hiện bẫy sớm.
Một lỗi phổ biến là giữ các quy tắc quan trọng chỉ trong code ứng dụng. Nếu một giá trị phải là unique, bắt buộc, hoặc trong khoảng, database cũng nên ép. Nếu không, một background job, một endpoint mới, hoặc import thủ công có thể bỏ qua logic của bạn.
Một thiếu sót thường gặp khác là xem index là vấn đề sau này. Thêm index sau khi ra mắt thường thành phỏng đoán, và bạn có thể index nhầm thứ trong khi truy vấn chậm thực sự là một join hoặc lọc theo status.
Bảng many-to-many cũng là nguồn lỗi âm thầm. Nếu bảng join không ngăn trùng, bạn có thể lưu cùng một quan hệ hai lần và mất thời gian debug “tại sao user này có hai vai trò?”.
Cũng dễ tạo bảng trước rồi nhận ra cần audit logs, soft deletes, hoặc event history. Những bổ sung đó lan toả vào endpoints và báo cáo.
Cuối cùng, cột JSON hấp dẫn vì “linh hoạt”, nhưng nó loại bỏ kiểm tra và làm indexing khó hơn. JSON hợp lý cho payload thay đổi thực sự, không phải cho các trường nghiệp vụ cốt lõi.
Trước khi sinh code, chạy danh sách kiểm tra nhanh này:
Dừng lại và đảm bảo kế hoạch đủ để sinh mã mà không phải đi truy hỏi liên tục. Mục tiêu không phải hoàn hảo mà là phát hiện các khoảng trống gây viết lại sau này: quan hệ thiếu, quy tắc không rõ, và index không khớp cách app dùng.
Dùng kiểm tra tiền bay (pre-flight) nhanh này:
amount >= 0 hoặc trạng thái hợp lệ).Bài kiểm tra tỉnh táo nhanh: giả sử một đồng đội vào ngày mai. Họ có thể xây endpoints đầu tiên mà không phải hỏi “cái này có thể null không?” hay “xảy ra gì khi xóa?” mỗi giờ không?
Khi kế hoạch đọc rõ và các luồng chính hợp lý trên giấy, biến nó thành thứ có thể chạy được: schema thực và migrations.
Bắt đầu với migration ban đầu tạo bảng, type (nếu dùng enum), và các ràng buộc cần thiết. Giữ lần đầu nhỏ nhưng đúng. Nạp một ít seed data và chạy các truy vấn app thực sự cần. Nếu một flow cảm thấy vụng về, sửa schema khi lịch sử migration còn ngắn.
Sinh models và endpoints chỉ sau khi bạn có thể test vài hành động end-to-end với schema đã có (create, update, list, delete, và một hành động nghiệp vụ thực). Sinh mã nhanh nhất khi bảng, khóa và đặt tên đủ ổn định để bạn không phải đổi tên mọi thứ ngày hôm sau.
Vòng lặp thực tế giữ viết lại thấp:
Quyết sớm điều bạn validate ở DB vs tầng API. Đặt quy tắc cố định ở database (foreign keys, unique constraints, check constraints). Giữ quy tắc mềm ở API (feature flags, giới hạn tạm thời, và logic cross-table phức tạp thay đổi thường xuyên).
Nếu dùng Koder.ai, cách hợp lý là đồng ý về các thực thể và migrations trong Chế độ Lập kế hoạch trước, rồi sau đó sinh backend Go + PostgreSQL. Khi thay đổi trượt, snapshot và rollback có thể giúp bạn quay về phiên bản tốt đã biết nhanh chóng trong khi điều chỉnh kế hoạch schema.
Lập kế hoạch schema trước. Nó thiết lập hợp đồng dữ liệu ổn định (bảng, khóa, ràng buộc) để các model và endpoint sinh ra không phải đổi tên và viết lại liên tục sau này.
Trên thực tế: ghi lại các thực thể, quan hệ và truy vấn chính, rồi khóa các ràng buộc, index và migration trước khi sinh mã.
Viết 2–3 câu mô tả ứng dụng cần nhớ gì và người dùng cần làm gì.\n\nRồi liệt kê:\n\n- Các hành động tạo/thay đổi dữ liệu (động từ)\n- Các màn hình/báo cáo người dùng sẽ dùng (đường dẫn đọc)\n- Nhu cầu phi chức năng như lịch sử audit, soft deletes, đa thuê, và quyền riêng tư\n\nCách này đủ rõ để thiết kế bảng mà không làm thừa.
Bắt đầu bằng việc liệt kê các danh từ lặp đi lặp lại (user, project, invoice, task). Với mỗi danh từ, thêm một câu: nó là gì và vì sao tồn tại.
Nếu bạn không thể mô tả rõ, có khả năng sẽ tạo ra bảng mơ hồ như items hoặc misc và hối hận sau này.
Chọn một chiến lược ID nhất quán cho toàn schema.\n\n- UUID: tốt khi bản ghi có thể được tạo từ nhiều nơi (web/mobile/jobs) hoặc khi bạn không muốn ID có thể dự đoán\n- bigint: nhỏ hơn, nhanh hơn một chút, và đơn giản khi mọi thứ được tạo bởi server\n\nNếu cần định danh thân thiện với con người, thêm một cột riêng (ví dụ project_code) thay vì dùng nó làm khóa chính.
Quyết theo từng quan hệ dựa trên mong đợi của người dùng và những gì cần được lưu giữ.\n\nQuy tắc phổ biến:\n\n- Giữ lịch sử: dùng RESTRICT/NO ACTION khi xóa parent sẽ làm mất dữ liệu quan trọng (ví dụ khách hàng → đơn hàng)\n- Cascade an toàn: dùng CASCADE khi hàng con không có ý nghĩa nếu thiếu parent (ví dụ đơn hàng → line items)\n\nQuyết sớm vì nó ảnh hưởng tới hành vi API và các trường hợp cạnh.
Đưa các quy tắc cố định vào database để mọi trình ghi (API, script, import, admin) tuân thủ.\n\nƯu tiên:\n\n- PRIMARY KEY cho mọi bảng\n- FOREIGN KEY cho mọi cột “belongs to”\n- UNIQUE nơi trùng lặp gây hại thực sự (email, (team_id, user_id) trong bảng join)\n- CHECK cho quy tắc đơn giản (số tiền >= 0, trạng thái hợp lệ)\n- cho các trường bắt buộc để một hàng có ý nghĩa
Bắt đầu từ các mẫu truy vấn thực tế, không phải phỏng đoán.\n\nGhi 5–10 truy vấn bằng tiếng thường (lọc + sắp xếp), rồi index cho chúng:\n\n- Cột foreign key dùng để join\n- Bộ lọc phổ biến như status, user_id, created_at\n- Một vài index composite khớp mẫu ổn định (ví dụ (account_id, created_at))\n\nTránh index mọi thứ; mỗi index sẽ làm chậm INSERT và UPDATE.
Tạo bảng join với hai foreign key và một ràng buộc unique composite.\n\nVí dụ mẫu:\n\n- team_members(team_id, user_id, role, joined_at)\n- Thêm UNIQUE (team_id, user_id) để tránh trùng\n\nCách này ngăn các lỗi tinh vi như “tại sao user này xuất hiện hai lần?” và giữ truy vấn sạch.
Mặc định dùng:\n\n- timestamptz cho timestamp (ít bất ngờ về múi giờ)\n- numeric(12,2) hoặc lưu tiền bằng số nguyên cents (tránh float)\n- Giá trị trạng thái được ép qua enum Postgres hoặc CHECK\n\nGiữ kiểu dữ liệu nhất quán giữa các bảng để joins và validate dễ dự đoán.
Dùng các migration nhỏ, dễ review và tránh thay đổi phá vỡ chỉ trong một bước.\n\nCon đường an toàn:\n\n- Thêm cột mới trước (nullable nếu cần)\n- Backfill dữ liệu\n- Ghi đồng thời vào cũ và mới tạm thời nếu app phải hoạt động liên tục\n- Chuyển đọc sang cột mới\n- Xoá cột cũ sau\n\nQuyết trước cách xử lý dữ liệu seed/reference để mọi môi trường giống nhau.
NOT NULL