Преди много време отговорих на въпрос за NULL в Stack Exchange, озаглавен „Защо не трябва да разрешаваме NULL?“ Имам своя дял от раздразнения и страсти за домашни любимци, а страхът от NULL е доста високо в списъка ми. Наскоро един колега ми каза, след като изрази предпочитание да наложи празен низ, вместо да разреша NULL:
"Не обичам да се занимавам с нули в код."
Съжалявам, но това не е основателна причина. Начинът, по който слоят за презентация се справя с празни низове или NULL, не трябва да бъде драйверът за вашия дизайн на таблица и модел на данни. И ако допускате „липса на стойност“ в някаква колона, има ли значение за вас от логическа гледна точка дали „липсата на стойност“ е представена от низ с нулева дължина или NULL? Или по-лошо, стойност на токен като 0 или -1 за цели числа или 1900-01-01 за дати?
Ицик Бен-Ган наскоро написа цяла серия за NULL и силно препоръчвам да преминете през всичко това:
- Сложности NULL – част 1
- NULL сложности – част 2
- Сложности NULL – Част 3, Липсващи стандартни функции и алтернативи на T-SQL
- NULL сложности – част 4, липсва стандартно уникално ограничение
Но целта ми тук е малко по-малко сложна от това, след като темата се появи в различен въпрос на Stack Exchange:„Добавете автоматично поле сега към съществуваща таблица.“ Там потребителят добавяше нова колона към съществуваща таблица с намерението да я попълни автоматично с текущата дата/час. Те се чудеха дали трябва да оставят NULL в тази колона за всички съществуващи редове или да зададат стойност по подразбиране (като 1900-01-01, вероятно, въпреки че не са изрични).
Може да е лесно за някой запознат да филтрира стари редове въз основа на стойност на токена – в края на краищата, как някой би могъл да повярва, че някакъв вид Bluetooth doodad е произведен или закупен на 1900-01-01? Е, виждал съм това в настоящите системи, където те използват произволно звучаща дата в изгледите, за да действат като магически филтър, представяйки само редове, където може да се има доверие на стойността. Всъщност във всеки случай, който съм виждал досега, датата в клаузата WHERE е датата/часът, когато колоната (или нейното ограничение по подразбиране) е добавена. Което е добре; може би не е най-добрият начин за решаване на проблема, но е a начин.
Ако обаче нямате достъп до таблицата през изгледа, това намекване на известен стойност все още може да причини както логически, така и свързани с резултатите проблеми. Логическият проблем е просто, че някой, който взаимодейства с таблицата, трябва да знае, че 1900-01-01 е фалшива стойност на токен, представляваща „неизвестно“ или „неподходящо“. За пример от реалния свят, каква беше средната скорост на освобождаване в секунди за куотърбек, който играеше през 70-те години на миналия век, преди да измерим или проследим такова нещо? 0 е добра стойност на токена за „неизвестно“? Какво ще кажете за -1? Или 100? Връщайки се към датите, ако пациент без лична карта бъде приет в болницата и е в безсъзнание, какво трябва да въведе като дата на раждане? Не мисля, че 1900-01-01 е добра идея и със сигурност не е била добра идея, когато е по-вероятно да е истинска рождена дата.
Влияние върху производителността на стойностите на токена
От гледна точка на производителността, фалшивите или „токени“ стойности като 1900-01-01 или 9999-21-31 могат да доведат до проблеми. Нека разгледаме няколко от тях с пример, базиран свободно на скорошния въпрос, споменат по-горе. Имаме таблица с Widgets и след известно връщане на гаранцията решихме да добавим колона EnteredService, където ще въведем текущата дата/час за нови редове. В единия случай ще оставим всички съществуващи редове като NULL, а в другия ще актуализираме стойността до нашата магическа дата 1900-01-01. (Засега ще оставим всякакъв вид компресия извън разговора.)
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) );
Сега ще вмъкнем същите 100 000 реда във всяка таблица:
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;
След това можем да добавим новата колона и да актуализираме 10% от съществуващите стойности с разпределение на текущите дати, а останалите 90% към нашата символична дата само в една от таблиците:
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;
Накрая можем да добавим индекси:
CREATE INDEX IX_EnteredService ON dbo.Widgets_NULL (EnteredService); CREATE INDEX IX_EnteredService ON dbo.Widgets_Token(EnteredService);
Използвано пространство
Винаги чувам „дисковото пространство е евтино“, когато говорим за избор на типове данни, фрагментация и стойности на маркери срещу NULL. Притеснението ми не е толкова за дисковото пространство, което тези допълнителни безсмислени стойности заемат. Нещо повече, когато е запитана таблицата, това губи памет. Тук можем да получим бърза представа за това колко пространство заемат стойностите на нашите токени преди и след добавянето на колоната и индекса:
Запазено пространство на таблицата след добавяне на колона и добавяне на индекс. Пространството почти се удвоява със стойностите на символите.
Изпълнение на заявка
Неизбежно някой ще направи предположения за данните в таблицата и ще направи заявка към колоната EnteredService, сякаш всички стойности там са легитимни. Например:
SELECT COUNT(*) FROM dbo.Widgets_Token WHERE EnteredService <= '20210101'; SELECT COUNT(*) FROM dbo.Widgets_NULL WHERE EnteredService <= '20210101';
Стойностите на токена могат да се объркат с оценките в някои случаи, но по-важното е, че те ще дадат неправилни (или поне неочаквани) резултати. Ето плана за изпълнение на заявката към таблицата със стойности на токен:
План за изпълнение на таблицата с маркери; обърнете внимание на високата цена.
А ето и плана за изпълнение на заявката към таблицата с NULL:
План за изпълнение за таблицата NULL; грешна оценка, но много по-ниска цена.
Същото би се случило и по друг начин, ако заявката поиска>={някаква дата} и 9999-12-31 се използва като магическа стойност, представляваща неизвестно.
Отново, за хората, които случайно знаят, че резултатите са грешни, защото сте използвали стойности на токени, това не е проблем. Но всички останали, които не знаят това – включително бъдещи колеги, други наследници и поддържащи кода и дори бъдещи ви с предизвикателства с паметта – вероятно ще се спънат.
Заключение
Изборът за разрешаване на NULL в колона (или за избягване на NULL изцяло) не трябва да се свежда до идеологическо или базирано на страх решение. Има реални, осезаеми недостатъци на архитектурата на вашия модел на данни, за да се гарантира, че нито една стойност не може да бъде NULL, или използването на безсмислени стойности за представяне на нещо, което лесно би могло да не бъде съхранено изобщо. Не предлагам всяка колона във вашия модел да позволява NULL; само за да не се противопоставяте на идеята от NULL.