Các quản trị viên cơ sở dữ liệu và lập trình viên thường gặp phải một vấn đề phổ biến nhưng dễ bị bỏ qua trong schema của họ: các trường được đánh dấu là nullable nhưng thực tế không bao giờ chứa giá trị null. Tình huống này thường phát sinh trong quá trình migration cơ sở dữ liệu khi các nhóm thêm các trường mới dưới dạng nullable để tránh khóa bảng, nhưng sau đó quên cập nhật schema một khi tất cả dữ liệu đã được điền đầy đủ.
Mô Hình Migration Tạo Ra Vấn Đề
Vấn đề này xuất phát từ quy trình migration cơ sở dữ liệu tiêu chuẩn. Khi thêm các trường mới vào cơ sở dữ liệu production, các nhóm thường đánh dấu chúng là nullable ban đầu để ngăn chặn việc khóa bảng kéo dài trong quá trình triển khai. Sau khi trường được thêm vào, logic ứng dụng sẽ điền giá trị cho các bản ghi mới, và một quy trình backfill sẽ điền dữ liệu cho các hàng hiện có. Tuy nhiên, bước cuối cùng là làm cho trường đó không thể nullable thường bị quên, khiến schema ở trạng thái không nhất quán.
Điều này tạo ra cái mà các lập trình viên gọi là lời nói dối thầm lặng trong schema cơ sở dữ liệu. Trường này có vẻ tùy chọn đối với bất kỳ ai đọc schema, nhưng trong thực tế, nó luôn chứa dữ liệu. Sự không khớp này có thể dẫn đến nhầm lẫn giữa các thành viên trong nhóm và các kiểm tra null không cần thiết trong mã ứng dụng.
Các Bước Thường Thấy Trong Quy Trình Migration
- Thêm trường mới dưới dạng nullable (tránh khóa bảng)
- Cập nhật logic ứng dụng để điền dữ liệu vào trường
- Chạy job backfill cho các bản ghi hiện có
- Thường bị quên: Cập nhật schema để biến trường thành non-nullable
- Kết quả: Trường vẫn giữ trạng thái nullable vô thời hạn mặc dù không bao giờ chứa giá trị null
Mối Quan Ngại Của Cộng Đồng Về Phát Hiện Tự Động
Mặc dù có các công cụ để xác định những trường có vấn đề này bằng cách quét các cột nullable có zero giá trị null, cộng đồng lập trình viên đã nêu ra những mối quan ngại quan trọng về cách tiếp cận này. Một số cho rằng chỉ vì một trường hiện tại không có giá trị null không có nghĩa là nó nên được làm thành non-nullable.
Một cột nullable nhưng không bao giờ null thực sự không nói lên điều gì cả. Điều đó giống như nói rằng một cột ngày sinh không bao giờ trước năm 1970 trong dữ liệu hiện tại nên được hạn chế chỉ những năm sau ngày đó.
Sự khác biệt nằm ở việc hiểu ý định ban đầu đằng sau thiết kế trường. Các trường luôn được dự định là bắt buộc nhưng vẫn nullable do các artifact migration khác với các trường thực sự tùy chọn nhưng tình cờ được điền trong dữ liệu hiện tại.
Giải Pháp Kỹ Thuật Và Cách Khắc Phục
Các hệ thống cơ sở dữ liệu đã phát triển để giải quyết một số thách thức migration này. Ví dụ, PostgreSQL đã hỗ trợ việc thêm hiệu quả các cột non-null với giá trị mặc định kể từ phiên bản 11, loại bỏ nhu cầu về cách tiếp cận nullable-first trong nhiều trường hợp. Đối với các cơ sở dữ liệu không hỗ trợ tính năng này, các lập trình viên đã tìm ra các giải pháp sáng tạo như sử dụng check constraints cho phép các giá trị null hiện có trong khi ngăn chặn các giá trị mới.
Một số nhóm đã áp dụng các quy trình tốt hơn để ngăn chặn vấn đề hoàn toàn, bao gồm sử dụng danh sách kiểm tra cho các thay đổi schema và tạo các ticket theo dõi để đảm bảo việc chuyển đổi từ nullable sang non-nullable xảy ra trong các bản phát hành tiếp theo.
Lộ trình cải tiến PostgreSQL
- PostgreSQL v11 (2018): Đã thêm khả năng tránh việc viết lại bảng cho
ALTER TABLE ... ADD COLUMN
với các giá trị mặc định cột không null - Phiên bản hiện tại: PostgreSQL v17
- Hạn chế: Các giá trị mặc định phải là không biến đổi (các giá trị tĩnh hoạt động được, nhưng các hàm như
timeofday()
vẫn yêu cầu khóa bảng)
Tác Động Rộng Lớn Đến Chất Lượng Code
Sự hiện diện của các trường nullable không cần thiết ảnh hưởng nhiều hơn chỉ thiết kế cơ sở dữ liệu. Mã ứng dụng phải tính đến các giá trị null tiềm năng ngay cả khi chúng không bao giờ xảy ra trong thực tế. Điều này dẫn đến lập trình phòng thủ có thể không cần thiết và có thể tạo ra các giả định sai về tính toàn vẹn dữ liệu.
Các nhóm làm việc với hệ thống legacy thường thấy vấn đề này được nhân lên trên hàng trăm trường, khiến việc phân biệt giữa dữ liệu thực sự tùy chọn và các trường lẽ ra đã được đánh dấu là bắt buộc từ lâu trở nên khó khăn. Tình huống trở nên đặc biệt thách thức trong các ứng dụng single-tenant nơi các tenant khác nhau có thể có các mô hình điền dữ liệu khác nhau.
Giải pháp thay thế cho SQL Server đối với các trường có thể null
-- Thêm ràng buộc kiểm tra với NOCHECK để cho phép các giá trị null hiện có
ALTER TABLE foo WITH NOCHECK
ADD CONSTRAINT CheckNotnull CHECK (id IS NOT NULL)
-- Các thao tác insert/update mới không thể null, nhưng các giá trị null hiện có vẫn được giữ lại
-- Các giá trị null hiện có có thể được cập nhật theo thời gian mà không chặn các thao tác khác
Tiến Về Phía Trước Với Tính Toàn Vẹn Schema
Chìa khóa để giải quyết vấn đề này nằm ở việc coi thiết kế schema như một trách nhiệm liên tục thay vì một nhiệm vụ thiết lập một lần. Mặc dù các công cụ tự động có thể giúp xác định các ứng viên tiềm năng để dọn dẹp, phán đoán của con người vẫn cần thiết trong việc xác định xem một trường có thực sự nên được làm thành non-nullable dựa trên yêu cầu kinh doanh và ngữ nghĩa dữ liệu.
Các đánh giá schema thường xuyên và quy trình migration được cải thiện có thể ngăn chặn những sự không nhất quán này tích tụ theo thời gian. Mục tiêu là đảm bảo rằng các schema cơ sở dữ liệu phản ánh chính xác mô hình dữ liệu dự định, làm cho hệ thống đáng tin cậy hơn và dễ bảo trì hơn cho các nhóm phát triển tương lai.
Tham khảo: Nullable but not null