Mở đầu — vì sao bài này quan trọng
Có một sự thật mà không ai nói với bạn khi bạn mới bắt đầu học thiết kế dữ liệu: cái schema đẹp nhất mà bạn vẽ ra hôm nay, chỉ sáu tháng sau sẽ trông "sai" trong mắt chính bạn. Không phải vì bạn kém, mà vì thế giới xung quanh cái schema đó đã thay đổi. Sản phẩm thêm tính năng mới, đội marketing muốn phân khúc khách hàng theo cách khác, luật bảo vệ dữ liệu cá nhân ra đời, hoặc đơn giản là một truy vấn đang chạy chậm dần vì bảng đã có vài chục triệu dòng.
Trong các bài trước, chúng ta đã học cách thiết kế một mô hình dữ liệu — từ entity, attribute, khóa, quan hệ cho đến chuẩn hóa và mô hình vật lý. Nhưng thiết kế chỉ là ảnh chụp tại một thời điểm. Bài 44 này nói về chuyển động: làm sao để một schema đang phục vụ hàng nghìn người dùng có thể tiến hóa mà không làm sập hệ thống, không làm mất dữ liệu, và không khiến cả đội phát triển phải thức trắng đêm.
Với vai trò Business Analyst (BA), bạn hiếm khi là người trực tiếp chạy lệnh migration. Nhưng bạn chính là người phải hiểu rằng một thay đổi nghiệp vụ tưởng chừng đơn giản — "cho phép một khách hàng có nhiều số điện thoại" — thực chất kéo theo cả một chiến lược tiến hóa schema phía sau. Nếu bạn không lường trước, bạn sẽ hứa với khách hàng những điều mà kỹ thuật không thể giao đúng hạn. Hiểu schema evolution giúp bạn ước lượng đúng độ phức tạp, giao tiếp đúng với đội dev, và bảo vệ chất lượng dữ liệu qua từng lần thay đổi.
Khái niệm cốt lõi
Schema evolution là gì
Schema evolution (tiến hóa lược đồ) là quá trình thay đổi cấu trúc cơ sở dữ liệu theo thời gian một cách có kiểm soát: thêm bảng, thêm/xóa cột, đổi kiểu dữ liệu, tách bảng, gộp bảng, thêm ràng buộc... trong khi hệ thống vẫn đang hoạt động và dữ liệu cũ vẫn phải được bảo toàn.
Điểm mấu chốt: trong môi trường thực tế, bạn không có quyền "làm lại từ đầu". Cơ sở dữ liệu đang chứa dữ liệu thật của người dùng thật. Mọi thay đổi phải cộng dồn (incremental) lên trạng thái hiện có, chứ không phải thay thế toàn bộ.
Vì sao schema phải thay đổi
Sáu nhóm nguyên nhân phổ biến nhất, dựa trên dàn ý gốc của bài:
- Tính năng mới (new features): Sản phẩm thêm chức năng "ví điểm thưởng", kéo theo bảng
loyalty_points,point_transactions. - Thay đổi nghiệp vụ (business changes): Trước đây mỗi đơn hàng thuộc một cửa hàng, giờ công ty mở mô hình marketplace nên đơn hàng có thể chứa sản phẩm từ nhiều nhà bán.
- Tối ưu hiệu năng (performance optimization): Thêm cột đã tính sẵn (denormalized), thêm index, tách bảng nóng.
- Yêu cầu tuân thủ (compliance requirements): Nghị định 13/2023 về bảo vệ dữ liệu cá nhân (PDPD) buộc phải mã hóa số CCCD, thêm cột
consent_at,data_retention_until. - Trả nợ kỹ thuật / tái cấu trúc (refactoring tech debt): Cột
statustừng là chuỗi tự do "đã giao/Đã Giao/delivered", giờ chuẩn hóa về một bảng tra cứu. - Tích hợp và mở rộng: Kết nối với hệ thống mới đòi thêm cột định danh ngoài (external ID).
Migration là công cụ để evolution xảy ra
Nếu evolution là "cái gì thay đổi", thì migration (di trú lược đồ) là "cách thay đổi đó được thực thi". Một migration là một đơn vị thay đổi được viết thành script, được đánh phiên bản, có thể chạy tiến (up) và lý tưởng là chạy lùi (down/rollback).
Các đội hiện đại dùng công cụ như Flyway, Liquibase, Alembic (Python), Rails Active Record Migrations, hay migration của Laravel/Prisma. Nguyên tắc chung giống nhau:
- Mỗi thay đổi là một file có số thứ tự hoặc timestamp:
V001__create_customers.sql,V002__add_phone_table.sql. - Có một bảng đặc biệt (ví dụ
schema_version/flyway_schema_history) ghi lại migration nào đã chạy. - Migration chạy theo đúng thứ tự, đúng một lần, trên mọi môi trường (dev → staging → production) — đảm bảo mọi môi trường luôn ở cùng một trạng thái schema.
Ba loại thay đổi theo mức độ rủi ro
Đây là khung tư duy quan trọng nhất bạn cần nhớ:
- Additive (bổ sung) — an toàn nhất: thêm bảng mới, thêm cột cho phép NULL, thêm index. Code cũ không biết đến thứ mới này nên vẫn chạy bình thường.
- Destructive (phá hủy) — nguy hiểm: xóa cột, xóa bảng, đổi tên. Nếu code cũ còn tham chiếu, hệ thống sẽ lỗi ngay.
- Transformative (biến đổi) — phức tạp nhất: đổi kiểu dữ liệu, tách/gộp cột, thay đổi ràng buộc. Vừa phải chuyển đổi dữ liệu hiện có, vừa phải giữ tương thích.
Backward compatibility và expand–contract
Nguyên tắc vàng để evolution không gây downtime là expand–contract (còn gọi là parallel change). Ý tưởng: thay vì thay đổi trực tiếp, bạn mở rộng schema để cả cấu trúc cũ và mới cùng tồn tại, chuyển ứng dụng dần sang cấu trúc mới, rồi mới thu hẹp bằng cách bỏ cấu trúc cũ. Nhờ đó tại mọi thời điểm, phiên bản code đang chạy đều tương thích với schema đang có — bạn có thể deploy dần dần thay vì phải tắt hệ thống.
Tình huống thực tế
Tình huống 1 — Tiki tách trường "họ tên" thành "họ" và "tên"
Bối cảnh: Một sàn thương mại điện tử lớn tại Việt Nam (giả định dựa trên mô hình Tiki) ban đầu lưu tên khách hàng trong một cột duy nhất full_name VARCHAR(255). Sau ba năm, đội CRM muốn cá nhân hóa email theo tên gọi ("Chào anh Khang" thay vì "Chào Đỗ Khang"), nên cần tách thành last_name và first_name.
Diễn giải: Đội dữ liệu áp dụng expand–contract qua bốn migration:
- Expand: thêm hai cột mới
first_name,last_namecho phép NULL — thay đổi additive, không ảnh hưởng gì. - Backfill: viết script tách 12 triệu dòng
full_nametheo quy tắc "chữ cuối cùng là tên gọi", chạy theo lô (batch) 50.000 dòng mỗi lần để không khóa bảng quá lâu. Những trường hợp nhập nhằng (tên nước ngoài, tên một chữ) được đánh dấu để rà soát tay. - Chuyển ứng dụng: code mới ghi đồng thời cả
full_namelẫn hai cột mới, đọc từ cột mới. Cột cũ vẫn được duy trì phòng khi phải rollback. - Contract: sau hai tuần chạy ổn định, xóa cột
full_name.
Tình huống 2 — MoMo thêm ràng buộc mã hóa số CCCD do tuân thủ Nghị định 13
Bối cảnh: Một ví điện tử (giả định theo mô hình MoMo) lưu số CMND/CCCD ở cột national_id VARCHAR(12) dạng văn bản thô. Khi Nghị định 13/2023 về bảo vệ dữ liệu cá nhân có hiệu lực, đội pháp chế yêu cầu: dữ liệu định danh phải được mã hóa, và phải ghi nhận thời điểm khách hàng đồng ý xử lý dữ liệu.
Diễn giải: Đây là thay đổi transformative trên dữ liệu nhạy cảm, nên phải cực kỳ cẩn trọng:
- Thêm cột mới
national_id_encrypted VARBINARYvàconsent_at TIMESTAMP(additive, an toàn). - Chạy job mã hóa toàn bộ ~8 triệu bản ghi bằng khóa quản lý riêng, ghi vào cột mới.
- Đối chiếu (validate) bằng cách giải mã mẫu 10.000 bản ghi so với bản gốc để chắc chắn không sai lệch.
- Chuyển toàn bộ code sang đọc/ghi cột mã hóa, rồi mới xóa cột
national_idthô — và xóa cả trong các bản backup theo chính sách lưu giữ.
Tình huống 3 — Grab chuyển quan hệ đơn hàng từ one-to-many sang many-to-many
Bối cảnh: Một nền tảng giao đồ ăn (giả định theo mô hình GrabFood/ShopeeFood) ban đầu mô hình hóa "một đơn hàng thuộc một nhà hàng" (orders.restaurant_id). Khi ra mắt tính năng "gộp nhiều nhà hàng trong một đơn giao", ràng buộc một-nhiều này không còn đúng.
Diễn giải: Đây là thay đổi nghiệp vụ làm thay đổi cả cardinality của quan hệ — loại migration khó nhất:
- Tạo bảng trung gian
order_restaurants(order_id, restaurant_id)để biểu diễn quan hệ nhiều-nhiều. - Backfill: với mỗi đơn cũ, chèn đúng một dòng vào bảng mới dựa trên
restaurant_idhiện có — dữ liệu lịch sử được bảo toàn nguyên vẹn. - Giữ tạm cột
restaurant_idcũ như "nhà hàng chính" trong giai đoạn quá độ để báo cáo cũ không vỡ. - Chuyển dần các truy vấn báo cáo và API sang bảng mới, rồi mới gỡ cột cũ.
Hướng dẫn từng bước
Đây là quy trình chuẩn để lập kế hoạch và thực thi một schema migration an toàn:
- Xác định động cơ và phân loại thay đổi. Ghi rõ lý do (feature/business/performance/compliance/refactor) và xếp vào additive, destructive hay transformative. Việc phân loại quyết định toàn bộ mức độ thận trọng phía sau.
- Đánh giá ảnh hưởng (impact analysis). Cột/bảng này đang được những đối tượng nào tham chiếu: ứng dụng nào, báo cáo nào, job ETL nào, view nào? Một cột tưởng bỏ được có thể đang nuôi một dashboard của ban giám đốc.
- Chọn chiến lược. Nếu là additive đơn giản, chạy thẳng. Nếu là destructive/transformative, dùng expand–contract để tránh downtime.
- Viết migration có phiên bản và có rollback. Mỗi thay đổi một file, đặt tên rõ ràng, kèm script down khi khả thi. Với thao tác không đảo ngược được (như xóa cột), coi backup là phương án rollback.
- Xử lý backfill dữ liệu theo lô. Đừng
UPDATEcả chục triệu dòng trong một transaction — nó khóa bảng và có thể làm treo hệ thống. Chia lô nhỏ, chạy giờ thấp điểm, theo dõi tải.
- Kiểm thử trên môi trường giống production. Chạy migration trên bản sao dữ liệu thật ở staging, đo thời gian chạy và kiểm tra khóa bảng. Một migration chạy 2 giây trên 1.000 dòng có thể mất 40 phút trên 40 triệu dòng.
- Triển khai theo thứ tự expand → deploy code → backfill → validate → contract. Không gộp các bước nhạy cảm vào cùng một lần release.
- Xác thực sau migration (post-validation). Đếm số dòng, so tổng kiểm tra (checksum), đối chiếu mẫu. Ghi lại bằng chứng — đặc biệt quan trọng với migration do compliance.
- Cập nhật tài liệu. Data dictionary, sơ đồ ERD và tài liệu nghiệp vụ phải phản ánh schema mới. Migration chưa được ghi tài liệu là migration chưa hoàn tất.
Lỗi thường gặp & mẹo
Lỗi: Thêm cột NOT NULL không có giá trị mặc định trên bảng lớn. Lệnh này buộc database ghi lại từng dòng và có thể khóa bảng nhiều phút. Mẹo: thêm cột cho phép NULL trước, backfill, rồi mới áp ràng buộc NOT NULL.
Lỗi: Đổi tên cột trực tiếp (RENAME COLUMN). Code cũ vẫn gọi tên cũ sẽ lỗi ngay khi deploy. Mẹo: dùng expand–contract — tạo cột mới, ghi song song, chuyển đọc, rồi xóa cột cũ.
Lỗi: Migration không có rollback. Khi production hỏng lúc 2 giờ sáng mà không có đường lùi, thiệt hại nhân đôi. Mẹo: luôn viết script down; với thao tác phá hủy, đảm bảo có backup ngay trước khi chạy.
Lỗi: Chạy migration lần đầu trên production. Không bao giờ. Luôn thử trên staging có dữ liệu quy mô tương đương.
Lỗi: Backfill trong một transaction khổng lồ. Gây khóa bảng, đầy transaction log, treo hệ thống. Mẹo: chia lô, thêm độ trễ nhỏ giữa các lô.
Lỗi: Quên các "khách hàng" của schema. View, stored procedure, báo cáo BI, pipeline ETL cũng phụ thuộc vào cột bạn định xóa. Mẹo: lập danh sách phụ thuộc trước khi xóa bất cứ thứ gì.
Mẹo cho BA: Khi viết user story chạm tới dữ liệu, hãy thêm mục "tác động lên schema" và "chiến lược migration" ngay trong tài liệu phân tích. Điều này giúp đội dev ước lượng đúng và giúp bạn quản lý kỳ vọng của bên nghiệp vụ.
Bài tập thực hành
- Phân loại thay đổi. Cho các yêu cầu sau, hãy xếp mỗi cái vào additive / destructive / transformative và giải thích: (a) thêm cột
email_verified BOOLEAN; (b) đổiprice INT(đồng) sangprice DECIMAL(15,2); (c) xóa cộtfax_number; (d) táchaddressthànhprovince,district,ward.
- Thiết kế expand–contract. Một hệ thống đặt lịch khám ở phòng khám có cột
patient_phone VARCHAR(15). Nghiệp vụ đổi thành "một bệnh nhân có thể có nhiều số điện thoại". Hãy viết ra trình tự các migration (đặt tên file kiểuV00x__...) để thực hiện an toàn không downtime, kèm bước backfill.
- Lập kế hoạch impact analysis. Bạn được yêu cầu xóa cột
legacy_customer_code. Liệt kê ít nhất 5 loại "khách hàng" của cột này mà bạn phải kiểm tra trước khi xóa, và cách bạn xác minh chúng không còn dùng cột đó.
- Ước lượng rủi ro. Viết một đoạn ngắn (5–7 câu) giải thích cho một product owner không rành kỹ thuật vì sao yêu cầu "cho phép đơn hàng thuộc nhiều cửa hàng" lại cần nhiều bước và không thể xong trong một bản release.
Tóm tắt
Schema không phải là bản vẽ đóng khung treo tường — nó là một sinh thể sống, thay đổi liên tục theo tính năng, nghiệp vụ, hiệu năng và tuân thủ. Nghệ thuật của schema evolution là làm cho những thay đổi đó xảy ra có kiểm soát: đánh phiên bản bằng migration, phân loại theo mức rủi ro (additive / destructive / transformative), và ưu tiên nguyên tắc expand–contract để không bao giờ đặt hệ thống vào trạng thái không tương thích.
Ba tình huống — tách tên trên sàn TMĐT, mã hóa CCCD theo Nghị định 13, và chuyển đơn hàng sang quan hệ nhiều-nhiều — cho thấy cùng một khuôn mẫu: mở rộng, backfill theo lô, chuyển ứng dụng, xác thực, rồi mới thu hẹp. Với vai trò BA, giá trị của bạn không nằm ở việc gõ câu lệnh SQL, mà ở chỗ nhìn ra một yêu cầu nghiệp vụ tưởng nhỏ thực chất kéo theo lộ trình tiến hóa schema như thế nào — để ước lượng đúng, giao tiếp đúng, và bảo vệ chất lượng dữ liệu qua từng lần thay đổi. Ở Bài 45 tiếp theo, chúng ta sẽ mở rộng góc nhìn này sang các mẫu tích hợp dữ liệu giữa nhiều hệ thống.