Cách đây khá lâu, tôi đã trả lời một câu hỏi về NULL trên Stack Exchange có tựa đề, "Tại sao chúng ta không cho phép NULL?" Tôi có những chia sẻ của tôi về những kẻ trộm thú cưng và niềm đam mê, và nỗi sợ hãi về NULLs khá cao trong danh sách của tôi. Một đồng nghiệp gần đây đã nói với tôi, sau khi bày tỏ sở thích buộc một chuỗi rỗng thay vì cho phép NULL:
"Tôi không thích xử lý null trong mã."
Tôi xin lỗi, nhưng đó không phải là lý do chính đáng. Cách lớp trình bày xử lý với các chuỗi trống hoặc NULL không phải là trình điều khiển cho thiết kế bảng và mô hình dữ liệu của bạn. Và nếu bạn cho phép "thiếu giá trị" trong một số cột, theo quan điểm logic, liệu "thiếu giá trị" có được biểu thị bằng một chuỗi có độ dài bằng 0 hay không? Hoặc tệ hơn, giá trị mã thông báo như 0 hoặc -1 cho số nguyên hoặc 1900-01-01 cho ngày?
Itzik Ben-Gan gần đây đã viết một loạt bài trên NULLs và tôi thực sự khuyên bạn nên xem qua tất cả:
- NULL phức tạp - Phần 1
- NULL phức tạp - Phần 2
- NULL phức tạp - Phần 3, Thiếu các tính năng tiêu chuẩn và các lựa chọn thay thế T-SQL
- NULL phức tạp - Phần 4, Thiếu ràng buộc duy nhất tiêu chuẩn
Nhưng mục đích của tôi ở đây là ít phức tạp hơn một chút, sau khi chủ đề đưa ra một câu hỏi Stack Exchange khác:“Thêm trường tự động ngay bây giờ vào bảng hiện có”. Ở đó, người dùng đã thêm một cột mới vào bảng hiện có, với mục đích tự động điền nó với ngày / giờ hiện tại. Họ tự hỏi liệu họ có nên để NULL trong cột đó cho tất cả các hàng hiện có hay đặt giá trị mặc định (có lẽ là 1900-01-01, mặc dù chúng không rõ ràng).
Ai đó có kinh nghiệm có thể dễ dàng lọc ra các hàng cũ dựa trên giá trị mã thông báo — sau cùng, làm sao ai đó có thể tin rằng một số loại doodad Bluetooth được sản xuất hoặc mua vào ngày 1900-01-01? Chà, tôi đã thấy điều này trong các hệ thống hiện tại, nơi chúng sử dụng một số ngày có âm tùy ý trong các chế độ xem để hoạt động như một bộ lọc ma thuật, chỉ hiển thị các hàng có giá trị đáng tin cậy. Trên thực tế, trong mọi trường hợp tôi đã thấy cho đến nay, ngày trong mệnh đề WHERE là ngày / giờ khi cột (hoặc ràng buộc mặc định của nó) được thêm vào. Cái nào cũng tốt; đó có thể không phải là cách tốt nhất để giải quyết vấn đề, nhưng đó là a đường đi.
Tuy nhiên, nếu bạn không truy cập bảng qua chế độ xem, thì điều này ngụ ý về một đã biết giá trị vẫn có thể gây ra các vấn đề liên quan đến lôgic và kết quả. Vấn đề logic đơn giản là ai đó tương tác với bảng phải biết 1900-01-01 là không có thật, giá trị mã thông báo đại diện cho “không xác định” hoặc “không liên quan”. Đối với một ví dụ trong thế giới thực, tốc độ phát hành trung bình, tính bằng giây, đối với một tiền vệ từng chơi trong những năm 1970, trước khi chúng tôi đo lường hoặc theo dõi một thứ như vậy là bao nhiêu? 0 có phải là giá trị mã thông báo tốt cho "không xác định" không? Làm thế nào về -1? Hay 100? Lấy lại ngày tháng, nếu một bệnh nhân không có giấy tờ tùy thân được đưa vào bệnh viện và bất tỉnh, họ nên nhập ngày tháng năm sinh như thế nào? Tôi không nghĩ 1900-01-01 là một ý tưởng hay và chắc chắn đó không phải là một ý kiến hay khi đó nhiều khả năng là ngày sinh thực sự.
Ý nghĩa hiệu suất của giá trị mã thông báo
Từ góc độ hiệu suất, các giá trị giả mạo hoặc "mã thông báo" như 1900-01-01 hoặc 9999-21-31 có thể gây ra vấn đề. Hãy xem xét một vài trong số này với một ví dụ dựa trên câu hỏi gần đây được đề cập ở trên. Chúng tôi có bảng Tiện ích và sau một số lần trả lại bảo hành, chúng tôi đã quyết định thêm cột EnteredService nơi chúng tôi sẽ nhập ngày / giờ hiện tại cho các hàng mới. Trong một trường hợp, chúng tôi sẽ để tất cả các hàng hiện có là NULL và trong trường hợp khác, chúng tôi sẽ cập nhật giá trị cho ngày 1900-01-01 kỳ diệu của chúng tôi. (Hiện tại, chúng tôi sẽ bỏ bất kỳ loại nén nào ra khỏi cuộc trò chuyện.)
CREATE TABLE dbo.Widgets_NULL ( WidgetID int IDENTITY(1,1) NOT NULL, SerialNumber uniqueidentifier NOT NULL DEFAULT NEWID(), Description nvarchar(500), CONSTRAINT PK_WNULL PRIMARY KEY (WidgetID) ); CREATE TABLE dbo.Widgets_Token ( WidgetID int IDENTITY(1,1) NOT NULL, SerialNumber uniqueidentifier NOT NULL DEFAULT NEWID(), Description nvarchar(500), CONSTRAINT PK_WToken PRIMARY KEY (WidgetID) );
Bây giờ chúng tôi sẽ chèn 100.000 hàng giống nhau vào mỗi bảng:
INSERT dbo.Widgets_NULL(Description) OUTPUT inserted.Description INTO dbo.Widgets_Token(Description) SELECT TOP (100000) LEFT(OBJECT_DEFINITION(o.object_id), 250) FROM master.sys.all_objects AS o CROSS JOIN (SELECT TOP (50) * FROM master.sys.all_objects) AS o2 WHERE o.[type] IN (N'P',N'FN',N'V') AND OBJECT_DEFINITION(o.object_id) IS NOT NULL;
Sau đó, chúng tôi có thể thêm cột mới và cập nhật 10% giá trị hiện có với phân phối ngày hiện tại và 90% còn lại cho ngày mã thông báo của chúng tôi chỉ trong một trong các bảng:
ALTER TABLE dbo.Widgets_NULL ADD EnteredService datetime; ALTER TABLE dbo.Widgets_Token ADD EnteredService datetime; GO UPDATE dbo.Widgets_NULL SET EnteredService = DATEADD(DAY, WidgetID/250, '20200101') WHERE WidgetID > 90000; UPDATE dbo.Widgets_Token SET EnteredService = DATEADD(DAY, WidgetID/250, '20200101') WHERE WidgetID > 90000; UPDATE dbo.Widgets_Token SET EnteredService = '19000101' WHERE WidgetID <= 90000;
Cuối cùng, chúng ta có thể thêm các chỉ mục:
CREATE INDEX IX_EnteredService ON dbo.Widgets_NULL (EnteredService); CREATE INDEX IX_EnteredService ON dbo.Widgets_Token(EnteredService);
Dung lượng được sử dụng
Tôi luôn nghe nói “dung lượng ổ đĩa rẻ” khi chúng ta nói về các lựa chọn kiểu dữ liệu, phân mảnh và giá trị mã thông báo so với NULL. Mối quan tâm của tôi không quá nhiều về dung lượng đĩa mà những giá trị vô nghĩa bổ sung này chiếm. Hơn nữa, khi bảng được truy vấn, nó sẽ lãng phí bộ nhớ. Tại đây, chúng ta có thể hiểu nhanh về lượng không gian mà giá trị mã thông báo của chúng ta sử dụng trước và sau khi cột và chỉ mục được thêm vào:
Không gian dành riêng của bảng sau khi thêm cột và thêm chỉ mục. Không gian gần như tăng gấp đôi với các giá trị mã thông báo.
Thực thi truy vấn
Chắc chắn, ai đó sẽ đưa ra các giả định về dữ liệu trong bảng và truy vấn cột EnteredService như thể tất cả các giá trị ở đó đều hợp lệ. Ví dụ:
SELECT COUNT(*) FROM dbo.Widgets_Token WHERE EnteredService <= '20210101'; SELECT COUNT(*) FROM dbo.Widgets_NULL WHERE EnteredService <= '20210101';
Các giá trị mã thông báo có thể gây nhầm lẫn với các ước tính trong một số trường hợp, nhưng quan trọng hơn, chúng sẽ tạo ra kết quả không chính xác (hoặc ít nhất là không mong đợi). Đây là kế hoạch thực thi cho truy vấn đối với bảng có giá trị mã thông báo:
Kế hoạch thực thi cho bảng mã thông báo; lưu ý chi phí cao.
Và đây là kế hoạch thực thi cho truy vấn đối với bảng có NULLs:
Kế hoạch thực thi cho bảng NULL; ước tính sai, nhưng chi phí thấp hơn nhiều.
Điều tương tự sẽ xảy ra theo cách khác nếu truy vấn yêu cầu> ={some date} và 9999-12-31 được sử dụng làm giá trị ma thuật đại diện cho không xác định.
Một lần nữa, đối với những người tình cờ biết kết quả là sai, cụ thể là do bạn đã sử dụng giá trị mã thông báo, đây không phải là vấn đề. Nhưng những người khác không biết rằng — bao gồm các đồng nghiệp tương lai, những người kế thừa và duy trì mã khác, và thậm chí cả bạn trong tương lai với những thách thức về trí nhớ — có thể sẽ vấp phải.
Kết luận
Lựa chọn để cho phép NULL trong một cột (hoặc tránh hoàn toàn NULL) không được chuyển thành một quyết định dựa trên ý thức hệ hoặc dựa trên nỗi sợ hãi. Có những nhược điểm thực tế, hữu hình khi kiến trúc mô hình dữ liệu của bạn để đảm bảo rằng không có giá trị nào có thể là NULL hoặc sử dụng các giá trị vô nghĩa để đại diện cho một thứ mà có thể dễ dàng không được lưu trữ. Tôi không đề xuất mọi cột trong mô hình của bạn nên cho phép NULL; chỉ là bạn không phản đối ý tưởng trong tổng số NULL.