Học cách viết prompt cho Claude Code để thực hiện migrations PostgreSQL an toàn: mẫu expand-contract, backfill, kế hoạch rollback và những kiểm tra cần làm ở staging trước khi phát hành.

Một thay đổi schema PostgreSQL trông đơn giản cho đến khi gặp lưu lượng thực và dữ liệu thực. Phần rủi ro thường không phải là SQL, mà là khi mã app, trạng thái database và thời điểm triển khai không còn khớp nhau.
Hầu hết thất bại là thực tế và đau đầu: một deploy hỏng vì mã cũ chạm tới cột mới, một migration khóa một bảng nóng và số timeout tăng vọt, hoặc một thay đổi “nhanh” im lặng xóa hoặc ghi đè dữ liệu. Ngay cả khi không có crash, bạn có thể đưa ra lỗi tinh vi như default sai, constraint bị vỡ, hoặc index chưa kịp xây xong.
Các migration do AI sinh ra thêm một lớp rủi ro nữa. Công cụ có thể tạo SQL hợp lệ nhưng vẫn không an toàn cho workload, quy mô dữ liệu hoặc quy trình phát hành của bạn. Chúng cũng có thể đoán sai tên bảng, bỏ sót các khóa chạy lâu, hoặc lờ đi rollback vì down migrations khó. Nếu bạn dùng Claude Code cho migrations, bạn cần rào chắn và ngữ cảnh cụ thể.
Khi bài viết này nói một thay đổi là “an toàn”, nó có ba nghĩa:
Mục tiêu là làm cho migrations trở thành công việc thường nhật: dự đoán được, có thể kiểm tra và nhàm chán.
Bắt đầu với vài quy tắc bất di bất dịch. Chúng giúp model tập trung và giúp bạn không phát hành thay đổi chỉ chạy trên laptop.
Tách công việc thành các bước nhỏ. Một thay đổi schema, một backfill dữ liệu, một thay đổi app và một bước dọn dẹp là các rủi ro khác nhau. Gộp chúng lại khiến khó phát hiện chỗ hỏng và khó rollback.
Ưu tiên thay đổi bổ sung trước khi làm tiêu cực. Thêm cột, index hoặc bảng thường rủi ro thấp. Đổi tên hoặc drop object là nơi dễ xảy ra sự cố. Làm phần an toàn trước, chuyển app, rồi chỉ khi chắc chắn không còn dùng mới loại bỏ cái cũ.
Làm cho app chịu được hai dạng schema trong một thời gian. Code nên có khả năng đọc cả cột cũ lẫn cột mới trong suốt rollout. Điều này tránh cuộc đua khi một số server chạy code mới trong khi database vẫn cũ (hoặc ngược lại).
Đối xử với migrations như code production, chứ không phải script nhanh. Ngay cả khi bạn xây với nền tảng như Koder.ai (backend Go với PostgreSQL, cộng với client React hoặc Flutter), database được chia sẻ cho mọi thứ. Sai lầm rất đắt đỏ.
Nếu bạn muốn một tập quy tắc ngắn để đặt ở đầu mọi yêu cầu tạo SQL, dùng kiểu như:
Một ví dụ thực tế: thay vì đổi tên cột mà app phụ thuộc, hãy thêm cột mới, backfill chậm, deploy code đọc mới rồi fallback về cũ, và chỉ sau đó xóa cột cũ.
Claude có thể viết SQL khá tốt từ yêu cầu mơ hồ, nhưng migration an toàn cần ngữ cảnh. Đối xử prompt như một brief thiết kế nhỏ: cho thấy hiện trạng, giải thích điều gì không được phá vỡ và định nghĩa “an toàn” cho rollout của bạn.
Bắt đầu bằng việc dán chỉ các sự thật database có liên quan. Bao gồm định nghĩa bảng cùng các index và constraint quan trọng (primary key, unique, foreign key, check, trigger). Nếu có bảng liên quan, dán các đoạn đó nữa. Một trích đoạn nhỏ và chính xác ngăn model đoán tên hoặc bỏ sót constraint quan trọng.
Thêm qui mô thực tế. Số hàng, kích thước bảng, tốc độ ghi và lưu lượng đỉnh ảnh hưởng kế hoạch. “200M rows và 1k writes/sec” khác nhiều so với “20k rows và chủ yếu đọc.” Cũng cho biết phiên bản Postgres và cách migrations chạy trong hệ thống của bạn (một transaction hay nhiều bước).
Mô tả cách ứng dụng sử dụng dữ liệu: các truy vấn read/write quan trọng và các background job. Ví dụ: “API đọc theo email”, “worker cập nhật status”, hoặc “report scan theo created_at”. Đây là yếu tố quyết định bạn cần expand/contract, feature flags, và mức an toàn của backfill.
Cuối cùng, rõ ràng về ràng buộc và kết quả mong muốn. Một cấu trúc đơn giản hiệu quả:
Yêu cầu cả SQL lẫn run plan buộc model nghĩ tới thứ tự, rủi ro, và những gì cần kiểm tra trước khi phát hành.
Mẫu expand/contract thay đổi database PostgreSQL mà không làm vỡ app khi thay đổi đang diễn ra. Thay vì chuyển đổi một lần rủi ro, bạn làm cho database hỗ trợ cả hai dạng cũ và mới trong một khoảng thời gian.
Nghĩ đơn giản: thêm yếu tố mới an toàn (expand), chuyển dần traffic và dữ liệu, rồi chỉ khi chắc chắn, loại bỏ phần cũ (contract). Điều này đặc biệt hữu ích khi dùng trợ giúp AI vì nó buộc bạn lên kế hoạch cho phần giữa thường lộn xộn.
Một flow thực tế trông như sau:
Dùng pattern này khi người dùng có thể vẫn ở phiên bản app cũ trong khi database thay đổi. Bao gồm deployment đa instance, mobile app cập nhật chậm, hoặc bất kỳ release nào migration có thể kéo dài hàng phút hoặc hàng giờ.
Một chiến thuật hữu ích là lên kế hoạch cho hai release. Release 1 làm expand + compatibility để không gì vỡ nếu backfill chưa xong. Release 2 làm contract chỉ sau khi xác nhận code mới và dữ liệu mới đã sẵn sàng.
Sao chép mẫu này và điền vào các ngoặc. Nó ép Claude Code tạo SQL bạn có thể chạy, các kiểm tra để chứng minh nó hoạt động, và kế hoạch rollback thực tế.
You are helping me plan a PostgreSQL expand-contract migration.
Context
- App: [what the feature does, who uses it]
- Database: PostgreSQL [version if known]
- Table sizes: [rough row counts], write rate: [low/medium/high]
- Zero/near-zero downtime required: [yes/no]
Goal
- Change: [describe the schema change]
- Current schema (relevant parts):
[paste CREATE TABLE or \d output]
- How the app will change (expand phase and contract phase):
- Expand: [new columns/indexes/triggers, dual-write, read preference]
- Contract: [when/how we stop writing old fields and remove them]
Hard safety requirements
- Prefer lock-safe operations. Avoid full table rewrites on large tables when possible.
- If any step can block writes, call it out explicitly and suggest alternatives.
- Use small, reversible steps. No “big bang” changes.
Deliverables
1) UP migration SQL (expand)
- Use clear comments.
- If you propose indexes, tell me if they should be created CONCURRENTLY.
- If you propose constraints, tell me whether to add them NOT VALID then VALIDATE.
2) Verification queries
- Queries to confirm the new schema exists.
- Queries to confirm data is being written to both old and new structures (if dual-write).
- Queries to estimate whether the change caused bloat/slow queries/locks.
3) Rollback plan (realistic)
- DOWN migration SQL (only if it is truly safe).
- If down is not safe, write a rollback runbook:
- how to stop the app change
- how to switch reads back
- what data might be lost or need re-backfill
4) Runbook notes
- Exact order of operations (including app deploy steps).
- What to monitor during the run (errors, latency, deadlocks, lock waits).
- “Stop/continue” checkpoints.
Output format
- Separate sections titled: UP.sql, VERIFY.sql, DOWN.sql (or ROLLBACK.md), RUNBOOK.md
Hai dòng thêm hữu dụng:
RISK: blocks writes, cùng đề xuất thời điểm chạy (off-peak vs anytime).Những thay đổi nhỏ vẫn có thể gây hại nếu chúng khóa lâu, rewrite bảng lớn, hoặc fail giữa chừng. Khi dùng Claude Code cho migrations, hãy yêu cầu SQL tránh rewrite và giữ app hoạt động trong khi database bắt kịp.
Thêm cột nullable thường an toàn. Thêm cột với default không-null có thể rủi ro trên các phiên bản Postgres cũ vì nó có thể rewrite toàn bảng.
Cách an toàn hơn là thay đổi hai bước: thêm cột NULL không default, backfill theo lô, rồi đặt default cho hàng mới và thêm NOT NULL khi dữ liệu đã sạch.
Nếu bạn bắt buộc phải áp default ngay, yêu cầu giải thích về hành vi khóa cho phiên bản Postgres của bạn và phương án fallback nếu thời gian chạy lâu hơn dự kiến.
Với index trên bảng lớn, yêu cầu CREATE INDEX CONCURRENTLY để đọc/ghi tiếp tục. Cũng nhắc rằng nó không thể chạy trong transaction block, nghĩa là công cụ migration của bạn cần bước không trong transaction.
Với foreign key, con đường an toàn thường là thêm constraint với NOT VALID trước, rồi validate sau. Điều này làm bước ban đầu nhanh hơn trong khi vẫn áp constraint cho các ghi mới.
Khi siết ràng buộc (NOT NULL, UNIQUE, CHECK), yêu cầu “clean first, enforce second.” Migration nên phát hiện hàng xấu, sửa chúng, rồi bật ràng buộc.
Nếu bạn cần checklist ngắn bỏ vào prompt, giữ nó súc tích:
Backfill là nơi hầu hết đau đầu migration bộc lộ, chứ không phải ALTER TABLE. Prompt an toàn coi backfill như job được điều khiển: có thể đo lường, có thể restart, và nhẹ nhàng với production.
Bắt đầu với các kiểm tra chấp nhận dễ chạy và khó tranh cãi: số hàng dự kiến, mục tiêu tỉ lệ null, và vài spot-check (ví dụ so sánh giá trị cũ vs mới cho 20 ID ngẫu nhiên).
Rồi yêu cầu kế hoạch batching. Batching giữ khóa ngắn và giảm bất ngờ. Một yêu cầu tốt chỉ rõ:
Yêu cầu idempotency vì backfill có thể fail giữa chừng. SQL nên an toàn khi chạy lại mà không nhân đôi hay làm hỏng dữ liệu. Mẫu hay dùng là “update chỉ khi new_col IS NULL” hoặc quy tắc xác định đầu vào luôn cho cùng đầu ra.
Cũng mô tả cách app giữ đúng trong khi backfill chạy. Nếu ghi mới vẫn tới liên tục, bạn cần cầu nối: dual-write trong code, trigger tạm thời, hoặc read-fallback (đọc mới nếu có, nếu không đọc cũ). Nói rõ cách bạn có thể deploy an toàn.
Cuối cùng, xây khả năng pause và resume. Yêu cầu tracking tiến độ và checkpoint, như bảng nhỏ lưu last processed ID và truy vấn báo tiến độ (rows updated, last ID, thời gian bắt đầu).
Ví dụ: bạn thêm users.full_name từ first_name và last_name. Một backfill an toàn chỉ cập nhật những hàng full_name IS NULL, chạy theo range ID, ghi lại last updated ID, và giữ đăng ký mới nhất đúng nhờ dual-write cho tới khi chuyển đổi xong.
Kế hoạch rollback không chỉ là “viết down migration.” Nó gồm hai vấn đề: undo thay đổi schema và xử lý dữ liệu thay đổi trong khi phiên bản mới hoạt động. Rollback schema thường khả thi. Rollback dữ liệu thường không, trừ khi bạn đã dự trù sẵn.
Hãy rõ ràng về ý nghĩa rollback cho thay đổi của bạn. Nếu bạn drop cột hoặc rewrite dữ liệu inplace, yêu cầu câu trả lời thực tế như: "Rollback phục hồi tương thích app, nhưng dữ liệu gốc không thể lấy lại nếu không có snapshot." Sự trung thực này giữ an toàn cho bạn.
Yêu cầu trigger rollback rõ ràng để không có tranh cãi khi sự cố. Ví dụ:
Yêu cầu cả gói rollback, không chỉ SQL: DOWN SQL nếu an toàn, các bước app để giữ tương thích, và cách dừng background job.
Mẫu prompt gói thường đủ:
Produce a rollback plan for this migration.
Include: down migration SQL, app config/code switches needed for compatibility, and the exact order of steps.
State what can be rolled back (schema) vs what cannot (data) and what evidence we need before deciding.
Include rollback triggers with thresholds.
Trước khi phát hành, chụp một "safety snapshot" nhẹ để so sánh trước và sau:
Cũng rõ ràng khi không nên rollback. Nếu bạn chỉ thêm cột nullable và app đang dual-write, sửa tiến lên (hotfix code, tạm dừng backfill, rồi resume) thường an toàn hơn revert và tạo thêm drift.
AI viết SQL nhanh, nhưng nó không nhìn thấy database production của bạn. Hầu hết lỗi xảy ra khi prompt mơ hồ và model tự điền vào chỗ trống.
Bẫy phổ biến là bỏ qua schema hiện tại. Nếu bạn không dán định nghĩa bảng, index và constraint, SQL có thể nhắm vào cột không tồn tại hoặc bỏ sót một quy tắc duy nhất khiến backfill trở nên chậm và khóa nặng.
Một sai lầm khác là phát hàng expand, backfill và contract trong một deploy. Điều đó lấy mất cửa thoát. Nếu backfill chạy lâu hoặc lỗi giữa chừng, bạn kẹt với app mong trạng thái cuối cùng.
Vấn đề hay gặp nhất:
Ví dụ cụ thể: “đổi tên cột và cập nhật app.” Nếu plan sinh ra rename và backfill trong cùng một transaction, backfill chậm có thể giữ khóa và phá traffic. Một prompt an toàn ép dùng lô nhỏ, timeout rõ ràng và truy vấn verify trước khi loại bỏ đường cũ.
Staging là nơi bạn tìm các vấn đề không hiện trên DB dev nhỏ: khóa lâu, null bất ngờ, index bị quên, và đường code bị bỏ sót.
Trước hết, kiểm tra schema khớp với plan sau migration: cột, kiểu, default, constraint và index. Quan sát nhanh không đủ—một index thiếu có thể biến backfill an toàn thành thảm họa.
Rồi chạy migration trên dataset thực tế. Lý tưởng là một bản sao production gần nhất với dữ liệu nhạy cảm đã mask. Nếu không làm được, ít nhất hãy mô phỏng volume production và các hotspot (bảng lớn, row rộng, bảng nhiều index). Ghi lại thời gian cho từng bước để biết mong đợi ở production.
Checklist staging ngắn:
Cuối cùng, test luồng người dùng thật, không chỉ SQL. Tạo, cập nhật và đọc record liên quan thay đổi. Nếu dùng expand/contract, xác nhận cả hai schema hoạt động cho tới khi dọn dẹp cuối cùng.
Giả sử bạn có cột users.name lưu tên đầy đủ như "Ada Lovelace." Bạn muốn first_name và last_name, nhưng không thể làm vỡ signups, profile hay trang admin trong khi thay đổi triển khai.
Bắt đầu với bước expand an toàn ngay cả khi không có code thay đổi: thêm cột nullable, giữ cột cũ, và tránh khóa dài.
ALTER TABLE users ADD COLUMN first_name text;
ALTER TABLE users ADD COLUMN last_name text;
Rồi cập nhật hành vi app để hỗ trợ cả hai schema. Trong Release 1, app nên đọc từ cột mới khi có, fallback về name khi null, và ghi cả hai để dữ liệu mới nhất nhất quán.
Tiếp theo là backfill. Chạy job theo lô nhỏ, cập nhật một lượng hàng mỗi lần, ghi tiến độ và có thể pause an toàn. Ví dụ: update users where first_name is null theo thứ tự ID tăng dần, 1.000 hàng một lần, và log số hàng thay đổi.
Trước khi siết ràng buộc, validate ở staging:
first_name và last_name và vẫn set namenameusers không chậm rõ rệtRelease 2 chuyển sang chỉ đọc cột mới. Chỉ sau đó mới thêm constraint (ví dụ SET NOT NULL) và drop name, lý tưởng là trong deploy tách biệt muộn hơn.
Về rollback, giữ cho nó đơn giản. App tiếp tục đọc name trong quá trình chuyển đổi, và backfill có thể dừng. Nếu cần rollback Release 2, chuyển đọc về name và giữ các cột mới đến khi ổn định.
Đối xử mỗi thay đổi như một runbook nhỏ. Mục tiêu không phải prompt hoàn hảo, mà là một quy trình ép ra các chi tiết đúng: schema, constraint, run plan và rollback.
Chuẩn hóa các thông tin bắt buộc trong mọi yêu cầu migration:
Quyết định ai sở hữu từng bước trước khi ai đó chạy SQL. Phân chia rõ ràng tránh "mọi người nghĩ người khác đã làm": dev sở hữu prompt và code migration, ops sở hữu thời điểm chạy production và monitoring, QA verify staging và edge case, và một người quyết go/no-go.
Nếu bạn xây app qua chat, mô tả trình tự trước khi sinh SQL có ích. Với teams dùng Koder.ai, Planning Mode là nơi hợp lý để viết sequence đó, và snapshots cộng rollback giảm diện hỏa khi xảy ra bất ngờ trong rollout.
Sau khi ship, lên lịch thực hiện cleanup (contract) ngay khi còn nhớ ngữ cảnh, để các cột cũ và code tương thích không tồn tại lửng lơ nhiều tháng.
Một thay đổi schema trở nên rủi ro khi mã ứng dụng, trạng thái cơ sở dữ liệu và thời điểm triển khai không còn khớp nhau.
Các lỗi phổ biến:
Sử dụng phương pháp expand/contract an toàn:
Cách này giúp cả phiên bản ứng dụng cũ và mới cùng hoạt động trong quá trình rollout.
Mô hình AI có thể tạo ra SQL hợp lệ nhưng không an toàn cho workload của bạn.
Rủi ro thường gặp liên quan tới AI:
Xử lý đầu ra của AI như bản nháp: luôn yêu cầu kế hoạch chạy, kiểm tra và bước rollback.
Dán vào prompt chỉ những sự thật migration cần:
CREATE TABLE liên quan (cộng với index, FK, UNIQUE/CHECK, trigger)Quy tắc mặc định: tách chúng ra.
Một chuỗi thực tế:
Gộp tất cả làm một sẽ khiến lỗi khó chẩn đoán và quay lui.
Ưu tiên theo mẫu:
ADD COLUMN ... NULL không default (nhanh)NOT NULL chỉ sau khi xác minhThêm default không-null ngay lập tức có thể gây rewrite toàn bộ bảng trên một số phiên bản Postgres. Nếu cần default ngay, yêu cầu phần giải thích về hành vi khóa và phương án dự phòng.
Hãy yêu cầu:
CREATE INDEX CONCURRENTLY cho bảng lớn/nóngTrong bước verify, so sánh EXPLAIN trước/sau ở staging để đảm bảo index được dùng.
Dùng NOT VALID trước, rồi VALIDATE sau:
NOT VALID để bước ban đầu ít gây gián đoạnVALIDATE CONSTRAINT riêng khi bạn có thể giám sát nóCách này vẫn áp dụng FK cho các ghi mới, đồng thời bạn kiểm soát khi validation tốn tài nguyên được thực hiện.
Một backfill tốt là theo lô, idempotent và có thể resume.
Yêu cầu thực tế:
WHERE new_col IS NULL)Mục tiêu rollback mặc định: phục hồi tính tương thích của app nhanh, ngay cả khi dữ liệu không hoàn toàn khôi phục.
Một kế hoạch rollback thực tế gồm:
Thường cách an toàn nhất là chuyển đọc trở lại trường cũ trong khi giữ các cột mới nguyên vẹn.
Điều này ngăn model đoán mò và ép mô tả thứ tự bước thực hiện.
Thiết kế này giúp backfill chịu được traffic thực tế.