Case study database migration này ghi lại hành trình chúng tôi giúp một công ty logistics chuyển 10 triệu records từ MySQL sang PostgreSQL — và điều quan trọng nhất: zero downtime. Hệ thống vẫn phục vụ 3.000 users đồng thời trong suốt quá trình migration.
Bối cảnh dự án
Khách hàng: Công ty logistics vận chuyển hàng hóa, hoạt động tại 25 tỉnh thành Việt Nam.
Hệ thống:
- Ứng dụng quản lý vận đơn (Laravel PHP)
- 3.000 users đồng thời (nhân viên kho, tài xế, khách hàng)
- 10 triệu vận đơn, 2 triệu khách hàng, 500.000 tuyến đường
- Database MySQL 5.7 trên VPS 16GB RAM
- Hoạt động 24/7 (không có “giờ nghỉ” cho maintenance)
Tại sao phải chuyển sang PostgreSQL?
| Vấn đề với MySQL 5.7 | PostgreSQL giải quyết |
|---|---|
| JSON search chậm (không index được JSON) | JSONB với GIN index — query JSON nhanh 50x |
| Không có partial index | Partial index giảm dung lượng index 60% |
| Full-text search tiếng Việt kém | pg_trgm + unaccent hỗ trợ tốt tiếng Việt |
| Window functions hạn chế | Window functions đầy đủ cho báo cáo |
| GIS support yếu | PostGIS — tìm kho gần nhất, tính khoảng cách |
| Concurrent writes lock table | MVCC — ghi không block đọc |
| End-of-life MySQL 5.7 (10/2023) | PostgreSQL 16 — support đến 2028 |
Yêu cầu:
- Zero downtime migration
- Data integrity 100% (không mất record nào)
- Rollback plan (quay lại MySQL nếu có vấn đề)
- Hoàn thành trong 2 tuần
Chiến lược: Dual-Write Migration
Chúng tôi chọn chiến lược Dual-Write thay vì Big Bang migration:
Big Bang (không chọn)
MySQL → Stop app → Export data → Import PostgreSQL → Start app
Downtime: 4–8 giờ
Risk: Cao — nếu lỗi phải rollback toàn bộ
Dual-Write (đã chọn)
Phase 1: MySQL (primary) → Replicate → PostgreSQL (shadow)
Phase 2: App ghi vào cả MySQL + PostgreSQL
Phase 3: Đọc từ PostgreSQL, verify với MySQL
Phase 4: PostgreSQL (primary), tắt MySQL
Downtime: 0
Risk: Thấp — rollback bất cứ lúc nào
Quy trình chi tiết: 4 giai đoạn
Giai đoạn 1: Chuẩn bị (3 ngày)
Ngày 1–2: Schema conversion
Chuyển đổi schema MySQL → PostgreSQL:
| MySQL | PostgreSQL | Lưu ý |
|---|---|---|
| AUTO_INCREMENT | SERIAL / BIGSERIAL | Tương đương |
| TINYINT(1) | BOOLEAN | MySQL dùng 0/1, PostgreSQL dùng true/false |
| DATETIME | TIMESTAMP | PostgreSQL hỗ trợ timezone |
| ENUM | VARCHAR + CHECK constraint | Hoặc tạo custom TYPE |
| JSON | JSONB | JSONB indexable, nhanh hơn JSON |
| UNSIGNED INT | INT + CHECK (col >= 0) | PostgreSQL không có UNSIGNED |
| DOUBLE | DOUBLE PRECISION | Tương đương |
| TEXT (utf8mb4) | TEXT | PostgreSQL mặc định UTF-8 |
Công cụ sử dụng: pgloader — tự động convert schema và migrate data.
Ngày 3: Initial data load
# pgloader tự động chuyển schema + data
pgloader mysql://user:pass@localhost/logistics \
postgresql://user:pass@pgserver/logistics
- 10 triệu records: load trong 45 phút
- Verify: so sánh row count tất cả bảng
- Kết quả: 100% rows match
Giai đoạn 2: Dual-Write Setup (3 ngày)
Ngày 4–5: Code changes
Thêm dual-write layer vào application:
// Simplified concept
class DualWriteRepository {
public function create($data) {
// Ghi vào MySQL (primary)
$mysqlResult = $this->mysql->create($data);
// Async ghi vào PostgreSQL
Queue::dispatch(new SyncToPostgres($data));
return $mysqlResult;
}
}
Ngày 6: Verification system
Tạo hệ thống so sánh tự động:
- Cron job mỗi 5 phút: so sánh 1.000 random records giữa MySQL và PostgreSQL
- Alert nếu có sai lệch
- Dashboard hiển thị sync status real-time
Giai đoạn 3: Shadow Read + Verify (5 ngày)
Ngày 7–11: Chạy parallel
- App vẫn đọc từ MySQL (primary)
- Đồng thời đọc từ PostgreSQL (shadow) — so sánh kết quả
- Log mọi sai lệch
Kết quả verify:
| Ngày | Records so sánh | Sai lệch | Nguyên nhân |
|---|---|---|---|
| Ngày 7 | 288.000 | 12 | Timezone conversion |
| Ngày 8 | 288.000 | 3 | Floating point precision |
| Ngày 9 | 288.000 | 0 | — |
| Ngày 10 | 288.000 | 0 | — |
| Ngày 11 | 288.000 | 0 | — |
Sai lệch ngày 7–8 đều do khác biệt data type giữa MySQL và PostgreSQL → fix trong schema conversion.
Giai đoạn 4: Cutover (2 ngày)
Ngày 12: Switch read to PostgreSQL
- 10:00 — Chuyển read traffic sang PostgreSQL (1% canary)
- 11:00 — Tăng lên 10%
- 14:00 — Tăng lên 50%
- 17:00 — 100% read từ PostgreSQL
- Write vẫn vào cả MySQL + PostgreSQL (safety net)
Ngày 13: Confirm + decommission MySQL
- 24 giờ 100% PostgreSQL — zero issues
- Tắt dual-write, PostgreSQL là primary duy nhất
- Giữ MySQL read-only thêm 7 ngày (rollback insurance)
- Ngày 20: Tắt MySQL server
Kết quả
Performance
| Metric | MySQL 5.7 | PostgreSQL 16 | Cải thiện |
|---|---|---|---|
| Query trung bình | 45ms | 12ms | 3.75x |
| JSON search | 2.800ms | 35ms | 80x |
| Báo cáo phức tạp | 15 giây | 2.1 giây | 7x |
| Concurrent writes | 800/giây | 2.400/giây | 3x |
| GIS query (tìm kho) | N/A (manual) | 8ms | Mới |
| Full-text search VN | 500ms | 25ms | 20x |
Business impact
| Metric | Trước | Sau | Thay đổi |
|---|---|---|---|
| Thời gian tạo vận đơn | 3.2 giây | 0.8 giây | -75% |
| Báo cáo hàng ngày | 45 phút | 5 phút | -89% |
| Tìm kho gần nhất | Manual (15 phút) | Tự động (< 1 giây) | Tự động hóa |
| Downtime trong migration | 0 phút | — | Zero downtime |
| Data loss | 0 records | — | 100% integrity |
Chi phí
| Hạng mục | Chi phí |
|---|---|
| Tư vấn + planning | 15 triệu VND |
| Implementation (2 tuần) | 35 triệu VND |
| PostgreSQL server (VPS 16GB) | 2.5 triệu VND/tháng |
| Tổng one-time | 50 triệu VND |
| Tiết kiệm hàng tháng | ~8 triệu VND (giảm nhân sự xử lý báo cáo thủ công) |
| ROI | Hoàn vốn trong 6.3 tháng |
Bài học kinh nghiệm
1. Đừng “Big Bang” migration
Downtime migration cho hệ thống 24/7 là không chấp nhận được. Dual-write tốn công hơn nhưng an toàn hơn 100x. Chi phí thêm cho dual-write setup (~30% tổng project) là xứng đáng.
2. Data type differences quan trọng hơn bạn nghĩ
MySQL TINYINT(1) → PostgreSQL BOOLEAN. MySQL UNSIGNED INT → PostgreSQL INT + CHECK constraint. Mỗi khác biệt nhỏ đều có thể gây data corruption nếu bỏ qua.
3. Verify, verify, verify
So sánh data giữa 2 database liên tục trong 5 ngày. Đừng tin rằng “tool migration xử lý hết” — luôn verify bằng automated comparison.
4. Giữ rollback option
Giữ MySQL chạy thêm 7 ngày sau cutover. Chi phí: ~600.000 VND. Giá trị: vô giá (peace of mind).
5. Timing matters
Cutover nên bắt đầu vào ngày traffic thấp (thứ 7). Tăng dần từ 1% → 10% → 50% → 100%. Không bao giờ switch 0% → 100% một lần.
FAQ — Câu hỏi thường gặp
Migration database mất bao lâu?
Tùy quy mô: Database < 1GB (vài triệu records): 1–2 tuần. Database 1–10GB: 2–4 tuần. Database > 10GB: 4–8 tuần. Thời gian chủ yếu cho testing và verification, không phải cho việc copy data.
Có nhất thiết phải đổi database không?
Không. Nếu MySQL đang hoạt động tốt và bạn không cần tính năng đặc biệt của PostgreSQL, đừng đổi. Migration chỉ nên làm khi có lý do business rõ ràng (như case study này: cần JSONB, GIS, và better concurrency).
Kết luận
Database migration từ MySQL sang PostgreSQL là dự án phức tạp nhưng hoàn toàn khả thi với zero downtime nếu có chiến lược đúng. Dual-write approach tuy tốn công hơn Big Bang, nhưng loại bỏ rủi ro downtime và data loss.
Nếu doanh nghiệp bạn đang cân nhắc migration database hoặc cần tư vấn xây dựng hệ thống với kiến trúc database phù hợp, hãy liên hệ Trinh Digital để được tư vấn chi tiết.