Các bài trước trong module 11 đã đi qua từng công cụ riêng lẻ (Claude Code, OpenCode, Gemini CLI, Cursor) kết nối với Postgres MCP. Bài này ghép lại thành một workflow đầu-cuối hoàn chỉnh, mô phỏng đúng tình huống bạn gặp hàng ngày: nhận một yêu cầu tính năng mới, cần đổi schema, viết migration an toàn, và xác nhận hiệu năng sau khi deploy — tất cả có AI hỗ trợ ở từng bước nhưng con người vẫn giữ quyền quyết định cuối cùng ở các điểm rủi ro cao. Đây là quy trình mình áp dụng thực tế cho các thay đổi schema có ảnh hưởng tới production, không phải lý thuyết suông.
Tổng quan workflow: từ yêu cầu tính năng đến schema đã deploy
Trước khi đi vào từng bước kỹ thuật, cần hiểu rõ bức tranh tổng thể để biết AI nên tham gia ở đâu và con người phải chốt ở đâu. Một thay đổi schema an toàn, có AI hỗ trợ, thường đi qua 5 giai đoạn:
- Nhận yêu cầu nghiệp vụ (ví dụ: "cần lưu lịch sử thay đổi giá sản phẩm để hiển thị biểu đồ giá theo thời gian") — giai đoạn này con người làm chủ, AI có thể hỗ trợ hỏi lại để làm rõ requirement mơ hồ.
- Khám phá schema hiện tại và lên phương án thay đổi — AI đọc trực tiếp database qua MCP, đề xuất 1-2 phương án thiết kế kèm trade-off, con người chọn phương án.
- Sinh migration script có thể rollback — AI viết, con người review kỹ trước khi cho chạy ở bất kỳ môi trường nào ngoài local.
- Áp dụng migration theo thứ tự môi trường: local → staging → production, có kiểm tra ở mỗi bước.
- Xác nhận hiệu năng sau migration — AI phân tích lại execution plan, index coverage, đảm bảo thay đổi không làm chậm các query quan trọng khác.
Điểm mấu chốt xuyên suốt: MCP cho AI khả năng chạm vào database thật, nhưng không có nghĩa AI nên tự quyết định khi nào chạm và chạm gì. Ranh giới rõ ràng nhất nên đặt ở bước 3 và 4 — AI được tự do đọc, tự do đề xuất, nhưng thao tác viết ở staging/production luôn cần một con người review và bấm nút chạy.
Ví dụ tình huống xuyên suốt bài này: team cần thêm khả năng lưu lịch sử thay đổi giá (price_history) cho bảng products hiện có, phục vụ tính năng "biểu đồ giá theo thời gian" trên trang sản phẩm.
Mẹo: Viết lại 5 giai đoạn này thành một checklist trong file
CONTRIBUTING.mdhoặcCLAUDE.md/AGENTS.mdcủa repo, ghi rõ giai đoạn nào AI được tự chủ hoàn toàn và giai đoạn nào bắt buộc có approval từ người review. Khi checklist tồn tại thành văn bản, cả AI (nếu đọc được file cấu hình) và người mới trong team đều tuân theo cùng một chuẩn, không phụ thuộc vào việc từng người tự nhớ quy tắc bất thành văn.
Bước 1: khám phá schema hiện tại và lên phương án thay đổi cùng AI
Trước khi viết bất kỳ dòng migration nào, cần hiểu rõ schema hiện tại liên quan đến bảng products — không chỉ cột và kiểu dữ liệu, mà cả các ràng buộc, trigger, và các bảng khác đang tham chiếu tới nó, vì thay đổi có thể ảnh hưởng dây chuyền.
Prompt khởi đầu:
Kết nối MCP postgres-dev. Đọc đầy đủ cấu trúc bảng "products": column, type,
default value, constraint, index, và trigger (nếu có). Liệt kê tất cả bảng khác
có foreign key tham chiếu tới products.id.
Yêu cầu nghiệp vụ: cần lưu lịch sử thay đổi giá của sản phẩm (mỗi lần giá đổi,
lưu lại giá cũ, giá mới, thời điểm đổi) để vẽ biểu đồ giá theo thời gian trên
trang sản phẩm. Đề xuất 2 phương án thiết kế schema khác nhau, kèm trade-off
về hiệu năng đọc/viết và độ phức tạp khi query.
Với yêu cầu này, agent sau khi đọc schema thật thường đề xuất hai hướng kinh điển:
Phương án A — bảng lịch sử riêng (audit table pattern):
CREATE TABLE product_price_history (
id BIGSERIAL PRIMARY KEY,
product_id UUID NOT NULL REFERENCES products(id),
old_price NUMERIC(10,2),
new_price NUMERIC(10,2) NOT NULL,
changed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
changed_by UUID REFERENCES users(id)
);
CREATE INDEX idx_price_history_product_time
ON product_price_history (product_id, changed_at DESC);
Ưu điểm: query lịch sử giá nhanh, không ảnh hưởng gì tới bảng products chính, dễ mở rộng thêm field (ví dụ lý do đổi giá). Nhược điểm: cần thêm logic ghi vào cả hai bảng mỗi lần đổi giá (qua trigger hoặc ở tầng application), có nguy cơ ghi thiếu nếu có đường update giá không đi qua logic chuẩn.
Phương án B — dùng trigger tự động ghi audit, tách khỏi logic application:
CREATE OR REPLACE FUNCTION log_price_change() RETURNS trigger AS $$
BEGIN
IF NEW.price IS DISTINCT FROM OLD.price THEN
INSERT INTO product_price_history (product_id, old_price, new_price)
VALUES (OLD.id, OLD.price, NEW.price);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_log_price_change
AFTER UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION log_price_change();
Ưu điểm: không thể quên ghi log dù có bao nhiêu đường code khác nhau update giá (an toàn hơn về tính đúng đắn dữ liệu). Nhược điểm: logic ẩn trong database, dev mới join team dễ không biết trigger này tồn tại nếu không đọc kỹ schema hoặc doc; debug khó hơn một chút vì hành vi không nằm trong code application.
Đây là quyết định kiến trúc thật, cần con người chọn dựa trên văn hoá team (team quen quản lý logic ở application layer hay chấp nhận logic ở database layer), không nên để AI tự quyết định thay.
Mẹo: Khi AI đề xuất nhiều phương án kèm trade-off, luôn yêu cầu thêm câu hỏi "phương án nào dễ rollback hơn nếu sau này team quyết định bỏ tính năng này?" — tiêu chí "dễ tháo ra" thường bị bỏ qua khi so sánh phương án, nhưng lại rất quan trọng cho maintainability dài hạn.
Bước 2: sinh migration có thể rollback và review trước khi áp dụng
Giả sử team chọn Phương án A (bảng riêng, ghi qua application code) vì muốn giữ logic tường minh ở tầng service. Bước tiếp theo là sinh migration hoàn chỉnh, kèm khả năng rollback rõ ràng.
Prompt:
Dựa trên phương án A đã chọn, viết migration đầy đủ theo format [công cụ migration
team đang dùng — ví dụ Prisma Migrate, Flyway, hoặc raw SQL file có up/down].
Migration cần:
1. Tạo bảng product_price_history với đúng cấu trúc đã thống nhất
2. Backfill dữ liệu ban đầu: với mỗi product hiện có, tạo 1 dòng lịch sử với
old_price = NULL, new_price = giá hiện tại, changed_at = products.created_at
3. Kèm file/script rollback xoá bảng an toàn (kiểm tra bảng rỗng hoặc có flag
force trước khi DROP)
Trước khi viết, chạy SELECT COUNT(*) FROM products để biết quy mô backfill cần xử lý.
Việc yêu cầu agent chạy SELECT COUNT(*) trước khi viết backfill là chi tiết nhỏ nhưng quan trọng — nếu bảng products có 50 triệu dòng, chiến lược backfill cần khác hẳn (batch theo lô, tránh lock lâu) so với bảng chỉ vài nghìn dòng.
Với quy mô nhỏ, migration file điển hình:
-- up.sql
BEGIN;
CREATE TABLE product_price_history (
id BIGSERIAL PRIMARY KEY,
product_id UUID NOT NULL REFERENCES products(id),
old_price NUMERIC(10,2),
new_price NUMERIC(10,2) NOT NULL,
changed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_price_history_product_time
ON product_price_history (product_id, changed_at DESC);
INSERT INTO product_price_history (product_id, old_price, new_price, changed_at)
SELECT id, NULL, price, created_at FROM products;
COMMIT;
-- down.sql
BEGIN;
DROP TABLE IF EXISTS product_price_history;
COMMIT;
Với quy mô lớn (hàng chục triệu dòng), agent nên được yêu cầu viết bản backfill theo batch để tránh transaction quá dài gây lock kéo dài và phình WAL (write-ahead log):
DO $$
DECLARE
batch_size INT := 5000;
rows_affected INT;
BEGIN
LOOP
INSERT INTO product_price_history (product_id, old_price, new_price, changed_at)
SELECT p.id, NULL, p.price, p.created_at
FROM products p
WHERE NOT EXISTS (
SELECT 1 FROM product_price_history h WHERE h.product_id = p.id
)
LIMIT batch_size;
GET DIAGNOSTICS rows_affected = ROW_COUNT;
RAISE NOTICE 'Backfilled % rows', rows_affected;
EXIT WHEN rows_affected = 0;
COMMIT;
END LOOP;
END $$;
Checklist review bắt buộc trước khi cho migration này chạy ở staging (chưa nói tới production):
- Migration có bọc transaction đúng cách không, và transaction có quá dài (nguy cơ lock lâu) không?
- Rollback script có thực sự an toàn — có xoá nhầm dữ liệu người dùng đã tạo thêm sau khi migration up chạy không?
- Index có được tạo với
CONCURRENTLYnếu bảng đang có traffic viết không (chú ý:CREATE INDEX CONCURRENTLYkhông thể chạy trong transaction, cần tách riêng khỏi blockBEGIN...COMMITnếu dùng)? - Backfill có xử lý đúng trường hợp dữ liệu NULL/edge case đã phát hiện ở bước introspect trước đó không?
Mẹo: Luôn yêu cầu agent tự liệt kê rõ "các giả định mình đã đưa ra khi viết migration này" ngay dưới migration script — ví dụ "giả định products.created_at luôn có giá trị, giả định price không âm". Đọc danh sách giả định này nhanh hơn nhiều so với việc bạn tự đọc lại toàn bộ SQL để suy ra agent đã giả định gì, và giúp bắt lỗi sai giả định trước khi chạy thật.
Bước 3: xác nhận execution plan và index coverage sau migration
Migration chạy thành công không đồng nghĩa với "xong việc". Bước cuối cùng, thường bị bỏ qua nhất trong thực tế, là xác nhận thay đổi schema không gây tác dụng phụ tới các query khác đang chạy trên bảng liên quan, và các query mới (phục vụ tính năng biểu đồ giá) thực sự dùng đúng index đã tạo.
Prompt cho bước xác nhận:
Migration tạo bảng product_price_history và trigger/index liên quan đã chạy xong
trên postgres-staging. Hãy:
1. Chạy EXPLAIN ANALYZE cho query lấy lịch sử giá 1 sản phẩm trong 12 tháng gần nhất,
xác nhận index idx_price_history_product_time có được dùng (Index Scan, không phải
Seq Scan)
2. Kiểm tra lại 3 query quan trọng nhất hiện có trên bảng products (lấy từ file
docs/critical-queries.md) để xác nhận thời gian chạy không bị chậm hơn so với
trước migration
3. Kiểm tra kích thước bảng product_price_history sau backfill và ước lượng tốc độ
tăng trưởng theo thời gian dựa trên tần suất đổi giá lịch sử
Kết quả EXPLAIN ANALYZE cho query lịch sử giá nên cho thấy Index Scan rõ ràng:
Index Scan using idx_price_history_product_time on product_price_history
(cost=0.42..8.44 rows=12 width=24) (actual time=0.031..0.045 rows=8 loops=1)
Index Cond: (product_id = '3fa85f64-5717-4562-b3fc-2c963f66afa6'::uuid)
Filter: (changed_at >= (now() - '12 mons'::interval))
Nếu thấy Seq Scan thay vì Index Scan dù đã tạo index, nguyên nhân phổ biến nhất là bảng còn quá nhỏ (planner PostgreSQL chọn Seq Scan hợp lý khi bảng chỉ vài trăm dòng vì đọc tuần tự rẻ hơn dùng index) — không phải lỗi, chỉ cần theo dõi lại khi bảng lớn dần theo thời gian thật.
Với việc kiểm tra tác dụng phụ lên query cũ trên products, nếu team chọn Phương án B (trigger) ở bước 1, đây là lúc quan trọng để xác nhận trigger không làm chậm đáng kể các câu UPDATE hàng loạt hiện có (ví dụ batch job đồng bộ giá từ hệ thống ERP mỗi đêm):
Chạy EXPLAIN ANALYZE cho batch job update giá hiện có (500 sản phẩm một lần,
xem file jobs/syncPricesFromErp.ts để biết đúng câu UPDATE), so sánh thời gian
chạy trước và sau khi có trigger trg_log_price_change.
Nếu trigger làm batch job chậm đáng kể (ví dụ tăng gấp đôi thời gian), đây là lúc cân nhắc lại quyết định ở bước 1 — có thể chuyển sang ghi log bất đồng bộ (queue) thay vì đồng bộ trong cùng transaction, tuỳ mức độ chấp nhận được của team về độ trễ dữ liệu lịch sử.
Mẹo: Lưu lại kết quả
EXPLAIN ANALYZE"trước" và "sau" migration vào file trong repo (ví dụdocs/perf-baselines/products-2026-08.md) thay vì chỉ xem qua rồi bỏ. Khi có vấn đề hiệu năng phát sinh vài tháng sau, bạn có baseline thật để so sánh, thay vì phải đoán "hồi đó query này có chậm vậy không".
Tips
- Luôn tách rõ ba loại quyền MCP theo môi trường trong toàn bộ workflow:
postgres-dev(đọc/viết tự do),postgres-staging(đọc/viết có kiểm soát, dùng để test migration thật),postgres-prod-readonly(chỉ đọc, dùng để verify sau khi migration đã chạy qua kênh chính thức như CI/CD). - Không bao giờ để agent tự chạy migration trực tiếp lên production qua MCP, dù đã review kỹ ở staging — luôn đi qua pipeline CI/CD có approval, MCP chỉ nên dùng để đọc/verify trên production.
- Với mọi migration ảnh hưởng tới bảng lớn, luôn yêu cầu agent ước lượng thời gian chạy dự kiến (dựa trên số dòng và loại thao tác) trước khi bạn quyết định chạy giờ nào — tránh chạy migration nặng vào giờ cao điểm.
- Lưu lại toàn bộ prompt và output quan trọng của workflow (phương án đề xuất, migration, kết quả EXPLAIN trước/sau) vào một file/PR description — đây chính là audit trail và tài liệu thiết kế cho quyết định schema, hữu ích hơn nhiều một commit message một dòng.
- Định kỳ review lại các trigger/logic ẩn trong database (như Phương án B) trong một buổi kỹ thuật của team, đảm bảo kiến thức không chỉ tồn tại trong đầu người đã yêu cầu AI viết nó.
Mẹo: Biến 3 bước trong bài này (khám phá & lên phương án, sinh migration & review, verify sau migration) thành một template PR checklist chuẩn cho mọi thay đổi schema từ nay về sau — dù người thực hiện dùng AI hay tự viết tay, checklist giữ cho chất lượng review đồng nhất và giảm phụ thuộc vào việc một cá nhân "nhớ" đủ các bước an toàn cần làm.