NULL – отсутствующие значения.
Не путать с нулевым значением! NULL – это не число, не равно пробелу, пустой ссылке, Неопределено.
NULL – типообразующее значение, т.е. есть тип NULL и единственное значение этого типа.
NULL значения появляются в запросе в следующих ситуациях:
а) Внешнее соединение, при котором не было найдено соответствующей записи в другой таблице (при левом – во второй, при правом – в первой, при полном – в обоих)
б) Обращение к реквизитам элементов для группы и наоборот.
в) NULL в списке полей выборки (ВЫБРАТЬ)
г) Обращение к реквизитам для битой ссылки
ЕСТЬ NULL используется в операторе ВЫБРАТЬ (как бы проверя, что значение это есть пустое ( Значение ЕСТЬ NULL )):
Код 1C v 8.х
ВЫБОР
КОГДА Значение ЕСТЬ NULL ТОГДА РезультатЕслиNULL
ИНАЧЕ Значение
КОНЕЦ
еще пример:
Код 1C v 8.х
ВЫБРАТЬ
СправочникНоменклатуры.Наименование,
ВЫБОР КОГДА УчетНоменклатурыОстатки.КоличествоОстаток ЕСТЬ NULL ТОГДА 0
ИНАЧЕ УчетНоменклатурыОстатки.КоличествоОстаток КАК КоличествоОстаток
ИЗ
Справочник.Номенклатура КАК СправочникНоменклатуры
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.УчетНоменклатуры.Остатки КАК УчетНоменклатурыОстатки
ПО УчетНоменклатурыОстатки.Номенклатура = СправочникНоменклатуры.Ссылка
ГДЕ
СправочникНоменклатуры.ЭтоГруппа = ЛОЖЬ
Функция ЕСТЬNULL (значение, РезультатЕслиNULL) возвращает значение своего первого параметра, в случае если он не равен NULL, и значение второго параметра в противном случае
Является свернутым ВЫБОР…КОНЕЦ, но ЕСТЬNULL предпочтительнее.
Код 1C v 8.х
ВЫБРАТЬ
ЕСТЬNULL(Справочник.Номенклатура.Артикул, "---") КАК Артикул,
Справочник.Номенклатура.Представление КАК Номенклатура
еще пример:
Код 1C v 8.х
ВЫБРАТЬ
СправочникНоменклатуры.Наименование,
ЕСТЬNULL(УчетНоменклатурыОстатки.КоличествоОстаток, 0) КАК КоличествоОстаток
ИЗ
Справочник.Номенклатура КАК СправочникНоменклатуры
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.УчетНоменклатуры.Остатки КАК УчетНоменклатурыОстатки
ПО УчетНоменклатурыОстатки.Номенклатура = СправочникНоменклатуры.Ссылка
ГДЕ
СправочникНоменклатуры.ЭтоГруппа = ЛОЖЬ
В данном примере получаются все элементы справочника номенклатуры, после чего, для каждой номенклатуры из регистра накопления получаются текущие остатки. Т.к. для номенклатуры, по которой отсутствуют остатки, виртуальная таблица остатков не запись вернет, то в результате соединения в поле «УчетНоменклатурыОстатки.КоличествоОстаток» будут значения NULL для номенклатуры, по которой не было остатков. Для того чтобы вместо значения NULL в результате запроса присутствовало значение 0, мы использовали функцию ЕСТЬNULL(), которая осуществит желаемую замену.
ЕСТЬNULL отличается от ВЫБОР по следующим причинам:
а) При ЕСТЬNULL лучше читается запрос (проще)
б) При ЕСТЬNULL, если проверяется сложное выражение, то работает быстрее, поскольку вычисляется один раз
в) При ЕСТЬNULL выражение замены приводится к типу проверяемого выражения, если оно имеет тип Строка (длина) или Число (разрядность).
Нельзя проверять значения на NULL обычным равенством, потому что в SQL действует трехзначная логика – Истина, Ложь, NULL, и результатом такого сравнения будет UNKNOWN, что в 1С 8.0 аналогично ЛОЖЬ.
NULL <> 0, поэтому при левых внешних соединениях спр. Номенклатура с таблицами остатков, цен, Контрагентов со взаиморасчетами при отсутствии таких записей там будет NULL, который не равен 0. Лучшее решение – ЕСТЬNULL
Содержание:
1. Тип значения Null
2. Методы Есть Null и ЕстьNull
1. Тип значения Null
Тип значения Null – это переменная или поле с отсутствующим значением. Если реквизит объекта имеет один определенный тип, например тип «Справочник.Договоры», то незаполненное значение будет равно пустой ссылке в запросе «Справочник.Договоры.ПустаяСсылка». Если реквизит имеет мультитип, то незаполненное значение данного реквизита будет равно значению «Неопределено», так как используемые в этом реквизите типы известны, но неизвестно, какой из пустых типов нужно использовать в данном случае. Для примера, в документе «ЗаказКлиента» реквизит «ДокументОснование» может быть и «ЗаданиеТорговомуПредставителю» и «КоммерческоеПредложениеКлиенту». В случаях же, когда вообще неизвестно, какие типы мы хотим получить, значением свойства является Null. В качестве примера, в котором в запросе есть Null, можно рассмотреть левое соединение таблиц, когда в присоединяемой таблице отсутствуют значения, удовлетворяющие условию соединения.
Пример кода: присоединение к таблице контрагентов таблицы договоров
Рис. 1 Левое соединение таблиц
Если у контрагента отсутствует договор, то при выполнении запроса получим следующие значения:
Как видим в запросе в 1С есть Null и это отображается пустой строкой.
В запросе сравнение любого значения с типом значения Null дает результат Ложь. Даже условие Null = Null является ложным.
2. Методы Есть Null и ЕстьNull
Для правильной обработки данных ситуаций созданы такие методы, как:
· Есть Null – это выражение условия, проверяющее, является ли выбранное значение типом значения Null. При проверке следует применять его. Сравнение вида (ДоговорКонтрагента.Ссылка = Null) не даст корректного результата.
Пример кода:
Рис. 2 Пример кода для проверки типа значения Null
Для корректной работы запроса обязательно следует отслеживать места, где есть Null в запросе 1С, и заменять данное значение на какой-то определенный тип.
· ЕстьNull
Этот метод позволяет менять значение Null на нужное нам значение.
Пример кода для запроса в 1С с ЕстьNull
Рис. 3 Код для ЕстьNull в запросе
Отсутствующую строку с помощью метода ЕстьNull в 1С можно заменить на пустую строку, отсутствующую цифру на ноль, ссылку на пустую ссылку, дату на пустую дату. Например ЕстьNull(“ДоговорыКонтрагентов.Номер”, “”), ЕстьNull(“ДоговорыКонтрагентов.СуммаДоговора”, 0), ЕстьNull(“ДоговорыКонтрагентов.Партнер”, Значение(Справочник.Партнеры.ПустаяСсылка)), ЕстьNull(“ДоговорыКонтрагентов.Дата”, ДАТАВРЕМЯ(1,1,1,0,0,0)).
Тип значения Null также может уже содержаться в используемом в запросе реквизите, попав туда в результате некорректной записи программным кодом, смене типа реквизита в конфигураторе либо при неправильном обмене данными между базами. В этих случаях тоже можно воспользоваться выражением ЕстьNull в запросе, но лучше исправлять такие реквизиты.
Специалист компании «Кодерлайн»
Александр Суворов
Автор Allen Browne, ноябрь 1999 г.
http://allenbrowne.com/casu-12.html
Перевод: А. Артамонов, октябрь 2011г.
Вот некоторые распространенные ошибки обращения с Null -ами, которые допускают новички. Если у вас нет ясности насчет Null’ов, сначала прочитайте Nulls: Do I need them?
Ошибка № 1: Null-ы в критериях
Если вы вводите условии отбора под полем в конструкторе запросов, он возвращает только совпадающие с образцом записи. Null-ы при этом исключаются из отбора.
Например, скажем, у вас есть таблица компаний и адресов. Вам нужны два запроса: один, который выдает местные компании, другой ― все остальные. В строке условий отбора в первом запросе под полем Город вы вписываете:
«Бобруйск»
и во втором запросе:
Not «Бобруйск»
Неправильно! Ни один запрос не включит записи, у которых город Null.
Решение
Используйте Is Null. Во втором запросе, чтобы достичь желаемого результата, условия отбора должны выглядеть как:
Is Null Or Not «Бобруйск»
Примечание: Запросы DDL (языка определения данных) обращаются с Null-ами иначе. Например, Null-ы учитываются в таком типе запроса:
ALTER TABLE Table1 ADD CONSTRAINT chk1 CHECK (99 < (SELECT Count(*) FROM Table2 WHERE Table2.State <> ‘TX’));
Ошибка № 2: Null-ы в выражениях
Результатом вычислений, включающих Null, обычно является Null. Например, новички иногда вводят выражение в источник данных текстового поля, чтобы вывести остаток к оплате:
=[СуммаКОплате] — [СуммаУплаченная]
Проблема в том, что если не было оплачено ничего, СуммаУплаченная является Null-ом, и в текстовом поле ничего не отображается.
Решение
Используйте функцию Nz(), чтобы указать значение для Null-а:
= Nz([СуммаКОплате], 0) — Nz([СуммаУплаченная], 0)
Ошибка № 3: Null-ы во внешних ключах
В то время, как Аксесс запрещает Null-ы в первичных ключах, он разрешает Null-ы во внешних. В большинстве случаев стоит явно запрещать эту возможность, чтобы избежать висящих ссылок.
В типичной таблице для накладных, строки накладной хранятся в таблице НакладнаяСтроки, соединенной с таблицей Накладные по НакладнаяКод. Вы создаете связь между Накладные.НакладнаяКод и НакладнаяСтроки.НакладнаяКод с поддержкой ссылочной целостности. Этого недостаточно!
Если вы не установили свойство Обязательное поля НакладнаяКод в таблице НакладнаяСтроки на Да, Аксесс разрешает Null-ы. Чаще всего это случается, когда пользователь начинает добавлять строки в подформе, не создав сперва саму накладную в основной форме. Так как у этих записей нет соответствующей им записи в основной форме, эти висящие записи больше нигде не показываются снова. Пользователь уверен, что программа их удалила, хотя на самом деле они находятся в таблице.
Решение
Всегда выставляйте значение свойства Обязательное на Да в конструкторе таблиц, если только вам не нужна возможность иметь Null-ы во внешних ключах.
Ошибка № 4: Null-ы и типы, отличные от Variant.
В Visual Basic единственным типом данных, который может содержать Null является Variant. Когда бы вы ни присваивали значение поля в переменную с типом не-Variant, всегда нужно учитывать возможность, что в поле может содержаться Null. Посмотрите, что в этом коде модуля формы может пойти не так:
Dim strName as String
Dim lngID As Long
strName = Me.Отчество
lngID = Me.КодКлиента
Когда поле Отчество содержит Null, попытка присвоить Null переменной строкового типа породит ошибку.
Аналогично, присвоение значения КодКлиента числовой переменной может вызвать ошибку. Даже если КодКлиента является первичным ключом, код небезопасен: первичный ключ содержит Null в новой записи.
Решения
(a) Используйте тип Variant, если вам необходимо работать с Null-ами.
(b) Используйте функцию Nz(), чтобы указать значение вместо Null. Например::
strName = Nz(Me.Отчество, «»)
lngID = Nz(Me.КодКлиента, 0)
Error 5: Сравнение с Null
Выражение:
If [Фамилия] = Null Then
будет бессмысленно, так как никогда не может быть истинно. Даже если фамилия является Null-ом, VBA полагает, что вы спросили::
Является ли Неизвестное равным Неизвестному?
и всегда отвечает: “Откуда мне знать, равны ли ваши неизвестные друг другу? Здесь мы опять наблюдаем распространение Null: результат не является ни истинным ни ложным.
Решение
Используйте функцию IsNull():
If IsNull([Фамилия]) Then
Ошибка № 6: Забыли, что Null ни Истина ни Ложь.
Выполняют ли эти две конструкции одну и ту же работу?
(a) If [Фамилия] = «Иванов» Then
MsgBox «Это Иванов”
Else
MsgBox «Это не Иванов»
End If
(b) If [Фамилия] <> «Иванов» Then
MsgBox «Это не Иванов»
Else
MsgBox «Это Иванов”
End If
Когда фамилия является Null-ом, эти два куска кода противоречат друг другу. В обоих случаях, первая часть If не срабатывает, и начинается выполнение Else, что приводит к противоречащим друг другу сообщениям.
Решения
(a) Учитывайте все три возможных результата сравнения — True, False, и Null:
If [Фамилия] = «Иванов» Then
MsgBox «Это Иванов”
ElseIf [Фамилия] <> «Иванов» Then
MsgBox «Это не Иванов”
Else
MsgBox «Мы не знаем, Иванов это или нет»
End If
(b) В некоторых случаях, функция Nz() позволит обработать два случая разом. Например, рассматривать Null и нулевую строку одинаково:
If Len(Nz([Фамилия],»»)) = 0 Then
Значение NULL возвращается в том случае, когда реквизита попросту нет. Тип в этом случае будет тоже NULL. Например, вы объединяете две таблицы через левое соединение. В том случае, когда для левой таблицы не будет найдено ни одного значения в правой, вернется NULL.
Проверку на данное значение можно осуществить при помощи конструкции «ЕСТЬ NULL» и «ЕСТЬNULL()». В первом случае возвращается Истина или Ложь. Во втором случае можно сразу задать другое значение в том случае, когда возвращается NULL.
В приведенном ниже запросе 1С 8.3 будут возвращен список контактных лиц тех партнеров, у кого не задан сегмент.
ВЫБРАТЬ КонтактныеЛицаПартнеров.Ссылка ИЗ Справочник.КонтактныеЛицаПартнеров КАК КонтактныеЛицаПартнеров ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.СегментыПартнеров КАК СегментыПартнеров ПО КонтактныеЛицаПартнеров.Владелец = СегментыПартнеров.Родитель ГДЕ СегментыПартнеров.Ссылка ЕСТЬ NULL
ЕСТЬ NULL в запросе
С помощью оператора ЕСТЬ NULL можно проверить является ли значение NULL:
123456789101112 ВЫБРАТЬ ПродажаУслуг.НомерКАК Номер, ПродажаУслуг.ПокупательКАК Покупатель, ПродажаУслуг.Покупатель.НаименованиеКАК ПокупательНаименование, ПродажаУслуг.Покупатель.КодКАК ПокупательКод, ВЫБОР КОГДАПродажаУслуг.Покупатель.Код ЕСТЬ NULL ТОГДАИСТИНА ИНАЧЕЛОЖЬ КОНЕЦ КАК ЭтоNULLИЗ Документ.ПродажаУслугКАК ПродажаУслуг
Если покупатель в документе не заполнен, то запрос вернет следующий результат:
ЕСТЬ NULL можно использовать в условии:
1234567 ВЫБРАТЬ ПродажаУслуг.НомерКАК Номер, ПродажаУслуг.ПокупательКАК ПокупательИЗ Документ.ПродажаУслугКАК ПродажаУслугГДЕ ПродажаУслуг.Покупатель.Код ЕСТЬ NULL
Для проверки, что значение не равно NULL можно использовать ЕСТЬ НЕ NULL или НЕ ЕСТЬ NULL:
1234567 ВЫБРАТЬ ПродажаУслуг.НомерКАК Номер, ПродажаУслуг.ПокупательКАК ПокупательИЗ Документ.ПродажаУслугКАК ПродажаУслугГДЕ ПродажаУслуг.Покупатель.Код ЕСТЬ НЕ NULL
Функция NULL в MySQL
В системе MySQL есть ряд функций, позволяющих результативно работать с NULL. Это IFNULL, NULLIF и COALESCE.
- IFNULL может принять два параметра: возвращает первый аргумент, если он не является NULL, в обратном случае — возвращает второй аргумент.
- NULLIF также может принять два аргумента: если они равны, то функция возвращает NULL, в обратном случае — возвращает первый аргумент. Эта функция также будет эффективна, если в вашей таблице в столбце есть пустые строки со значением NULL.
- COALESCE может принимать список аргументов и возвращать первый аргумент не-NULL. Например, эту функцию можно применять для базы контактных данных с потенциальной возможностью в зависимости от важности информации в порядке Телефон — Электронная почта — N/A.
Отступление
Как вы помните, некоторые элементы справочника Города не имеют реквизита ОтличительныйПризнак. Функция ТИПЗНАЧЕНИЯ для таких элементов выдаёт NULL.
Как можно сделать отбор таких элементов в запросе? Для этого предусмотрен специальный логический оператор ЕСТЬ NULL (не путать с функцией ЕСТЬNULL, которую мы рассмотрим ниже). Вы читаете ознакомительную версию урока, полноценные уроки находятся здесь.
Вот пример его использования:
Отлично.
Но есть и такие элементы (Сан-Паулу), у которых реквизит ОтличительныйПризнак (составного типа) просто не заполнен и равен специальному значению НЕОПРЕДЕЛЕНО.
Чтобы отобрать такие записи следует использовать другую конструкцию:
Но сравнение с НЕОПРЕДЕЛЕНО для определения пустых (не заполненных) реквизитов будет работать только для составных типов.
Кстати, у логического оператора ЕСТЬ NULL форма отрицания выглядит следующим образом:
Функция ЕСТЬNULL
Функция предназначена для замены значения NULL на другое значение.
Мы помним, что значение NULL возвращается в том случае, если запрашиваемый реквизит (поле, свойство) не существует.
Как например, реквизит ОтличительныйПризнак для групп справочника Города:
Функция ЕСТЬNULL поможет нам вывести другое значение в том случае, если это значение равно NULL. Вы читаете ознакомительную версию урока, полноценные уроки находятся здесь. Пусть в данном случае это будет строка «Такого реквизита нет!»:
Получается, что если первый параметр функции ЕСТЬNULL не равен NULL, то возвращается он. Если же он равен NULL, то возвращается второй параметр.
Отличия функции ЕСТЬNULL() от ЕСТЬ NULL
Как вы могли увидеть из предыдущих примеров, в обоих случаях запрос возвращает одни и те же данные. Функция ЕСТЬNULL() является сокращенным вариантом ВЫБОР КОГДА … ЕСТЬ NULL … КОНЕЦ, но она все же будет предпочтительнее по следующим причинам:
- Функция EСТЬNULL() оптимизирует запрос. Она считывается один раз, поэтому при проверке сложного выражения, запрос отработает быстрее.
- Функция EСТЬNULL() сокращает конструкцию, за счет чего запрос становится более читабельным.
- При выполнении функции EСТЬNULL() выражение замены приводится к типу проверяемого выражения для типов строковых типов (к длине строки) и числовых (к разрядности).
Теги
Время на прочтение
8 мин
Количество просмотров 20K
Навскидку многим кажется, что они знакомы с поведением NULL-значений в PostgreSQL, однако иногда неопределённые значения преподносят сюрпризы. Мы с коллегами написали статью на основе моего доклада с PGConf.Russia 2022 — он был полностью посвящён особенностям обработки NULL-значений в Postgres.
NULL простыми словами
Что такое SQL база данных? Согласно одному из определений, это просто набор взаимосвязанных таблиц. А что такое NULL? Обратимся к простому бытовому примеру: все мы задаём друг другу дежурный вопрос: «Как дела?». Часто мы получаем в ответ: «Да ничего…» Вот это «ничего» нам и нужно положить в базу данных — NULL: неопределённое, некорректное или неизвестное значение.
Допустим, вы суммируете две колонки, и в сотой по счёту записи наткнулись на NULL. Что тогда делать? Или возвращать ошибку, потому что так нельзя, или всё-таки как-то выполнить сложение и идти дальше. Сообщество решило в пользу второго варианта и закрепило это в стандартах языка SQL. Также договорились, что данные любого типа могут оказаться NULL, написали специальные функции и операции для обработки NULL-значений.
NULL может оказаться в столбце с любым типом данных и попасть на вход к любому оператору или функции. Соответственно, все операторы и функции как-то обрабатывают NULL, но результат обработки иногда оказывается неожиданным.
Какие значения не являются NULL?
Давайте теперь посмотрим, что не есть NULL. Ноль — это просто ноль, не NULL. Пустая строка — это пустая строка в Postgres, в отличие от Oracle. Пустой массив, пустой JSON, массив NULL-значений, пустой диапазон — это не NULL. Сложные типы, включающие NULL, уже не являются NULL.
Есть, правда, одно исключение: запись, собранная из NULL-значений, является NULL. Это сделано для совместимости со стандартом языка SQL. Однако, «под капотом» Postgres функции и операторы считают запись, состоящую из NULL-значений, NOT NULL. Ниже приведены результаты обработки такой записи для некоторых из них:
сount(row(NULL)) посчитает такую запись;
num_nulls(row(NULL)) выдаст ноль;
row(NULL) IS DISTINCT FROM NULL выдаст TRUE.
Ещё удивительнее пример с записями, содержащими NULL:
row(NULL::int, ‘Bob’::TEXT) IS NULL ожидаемо выдаст FALSE, но
row(NULL::int, ‘Bob’::TEXT) IS NOT NULL тоже выдаст FALSE!
Тем не менее, это поведение не является багом и описано в документации.
Операции с NULL
Почти все бинарные операции с NULL — сложить, вычесть, умножить, конкатенировать — дают на выходе NULL. С этим стоит быть осторожнее. Если вы к строке или к JSON конкатенируете что-то, оказавшееся NULL, то получаете на выходе NULL. А если вы ещё и сделали UPDATE в базу данных, выйдет совсем нехорошо.
Тем не менее, логическая операция TRUE OR NULL на выходе даёт TRUE. FALSE AND NULL даёт в результате FALSE. То есть существуют некоторые исключения из общего правила.
Операции сравнения
Операции сравнения — больше, меньше, больше или равно — c NULL на выходе дают NULL. При этом и сам NULL не равен самому себе. Впрочем, в PostgreSQL есть параметр transform_null_equals, который по умолчанию выключен. Если его включить, то NULL будет равен NULL.
Для проверки любого значения на NULL в Postgres предусмотрен специальный оператор — … IS NULL, … IS NOT NULL. Также может быть непривычно, что при сравнении булевых переменных с NULL или при применении оператора равенства помимо значений TRUE и FALSE возможно ещё и неизвестное значение. При этом оператор IS (NOT) UNKNOWN — это аналог IS (NOT) NULL для булевых переменных.
Операторы IS TRUE или IS FALSE для булевых переменных дают или TRUE, или FALSE. NULL в результате их применения получиться не может. Использование оператора IS TRUE позволяет писать более надёжный код, чем обычное сравнение = TRUE, которое может выдать не учтённое программистом NULL-значение и пойти «не туда».
Что если нам нужно сравнить два значения X и Y, считая, что NULL-значения равны друг другу? Можно самому написать конструкцию из логических операторов, но существует уже готовый оператор X IS (NOT) DISTINCT FROM Y. Правда, планировщик PostgreSQL плохо понимает этот оператор и может выдавать долгие планы выполнения для запросов с ним.
Cпециальные функции для работы с NULL
Обратимся к специальным функциям для работы с NULL. Всем известная coalesce возвращает первый NOT NULL аргумент. Есть nullif, есть num_nulls — этой функции можно дать сколько угодно аргументов, она посчитает количество NULL-значений. С помощью функции num_nonnulls можно посчитать NOT NULL значения.
Как правило, функции с произвольным числом аргументов игнорируют NULL. Такие функции, как greatest, concat его просто проигнорируют. При этом функция создания массивов включит NULL-значение во вновь образованный массив, за этим надо следить.
NULL и агрегатные функции
Что касается агрегатных функций, то array_agg, json_agg включают NULL в агрегат, а конкатенация строки не может вставить NULL-значение в середину строки, и поэтому она NULL игнорирует.
Статистические функции min, max, sum игнорируют NULL, а вот с выражением Count всё хитро. Count по конкретному полю посчитает только строки, где выражение NOT NULL, а вот Count со звёздочкой посчитает всё, включая NULL-значения.
Что со всем этим делать? Можно почитать в справке или потестировать, как функция обрабатывает NULL-значения. А лучше использовать выражение FILTER и в явном виде исключить все NULL-значения.
NULL и пользовательские функции
Теперь о пользовательских функциях. При создании пользовательской функции по умолчанию включен режим CALLED ON NULL INPUT, то есть при наличии NULL среди аргументов функция вызовется и будет обрабатывать это значение. Если вам это не нужно, можно использовать RETURNS NULL ON NULL INPUT либо STRICT — в этом случае функция, обнаружив NULL хотя бы в одном аргументе, сразу возвращает NULL и дальше вообще не думает — для экономии времени.
Многие системные функции в PostgreSQL определены именно как STRICT, поэтому стали возможны некоторые математические казусы. Например, NULL можно разделить на ноль, и в результате вы получите NULL — вместо ошибки деления на ноль. NULL в степени ноль тоже является NULL, хотя в математике любое число в нулевой степени, даже если это сам ноль, даёт единицу. Непонятно, правильно ли такое поведение с философской точки зрения, но вроде пока никто не жаловался.
Группировка и сортировка
Если говорить о группировке, то она считает все NULL-значения одинаковыми, так как это делает оператор IS NOT DISTINCT FROM. При сортировке есть специальные подвыражения, в которых можно указать NULLS FIRST или NULLS LAST. По умолчанию выбирается NULLS LAST, то есть считается, что неопределённые значения больше всех остальных чисел.
Сортировка работает так при создании выборки, индекса, в агрегатных функциях и оконных функциях.
NULL и записи
Когда мы формируем запись из нескольких значений, то сравниваются все NOT NULL значения. Если найдётся различие, то результат будет FALSE. Если все NOT NULL значения совпадают, и нашёлся NULL, то будет NULL.
Сравнение на больше/меньше выполняется по другим правилам. Как только попадётся не совпадающее значение, тогда оно будет больше или меньше, а если обнаружится NULL, то будет NULL.
NULL и диапазоны
С бинарными операциями разобрались, но что если у нас тернарная операция? Например, SELECT NOW BETWEEN NULL AND NULL. Получится, ожидаемо, NULL.
Однако, точно такое же выражение, сформулированное через диапазоны, неожиданно даёт TRUE. Да, с точки зрения Postgres здесь и сейчас мы находимся в неопределённом промежутке времени!
Согласно стандарту SQL, все диапазонные типы — не только временные, а все вообще —воспринимают границу NULL как бесконечность.
Я полюбопытствовал и выяснил, что промежуток от минус бесконечности до плюс бесконечности входит в промежуток от NULL до NULL, а обратное — неверно.
Выходит, что NULL здесь даже несколько больше, чем бесконечность.
Также я попытался проверить, входит ли NULL в промежуток от минус бесконечности до плюс бесконечности. Оказалось, что это неизвестно. Это контринтуитивный для меня момент: мне казалось, что полный диапазон значений от минус до плюс бесконечности должен включать в себя любое значение, в том числе и неопределённое. Но нет, в PostgreSQL это не так.
Откуда в запросах появляются NULL-значения?
Во-первых, они попадают в базу данных при вставке или обновлении и продолжают храниться в столбцах таблиц.
Во-вторых, они записываются в базу как результат подзапроса не нашедшего ни одной строки — в этом случае подзапрос вернёт вам NULL.
В-третьих, NULL-значения могут появляться в результате операции объединения LEFT JOIN.
В-четвёртых, NULL-значения появлются как результат некоторых функций при некоторых условиях.
В-пятых, их можно создать вручную, например, при использовании конструкции CASE. В каком-то хитром запросе вы можете указать, что при определённых условиях получится неизвестное значение.
Структура базы данных и NULL
Во-первых, можно запретить хранение NULL-значений в столбце. Есть специальное ограничение (constraint) NOT NULL. Крайне рекомендую так и поступать — всегда запрещать хранение NULL-значений, если только вы не планируете хранить и обрабатывать NULL именно в этом столбце.
При определении ограничения (constraint) тоже есть одна особенность: если условие возвращает NULL, это считается допустимым, и такая запись может быть вставлена.
Например, ограничение Foreign key позволяет в дочерней таблице вставить запись со ссылкой, которая является NULL. Это будет допустимо.
Ограничение CHECK (price > 0) даст вам вставить в таблицу поле для Price со значением, равным NULL.
Ограничение unique позволяет создать несколько записей со значением NULL. Правда, в PostgreSQL 14 уже появилось специальное «заклинание», которое может запретить несколько записей с NULL.
Как NULL хранится внутри записи БД?
NULL вообще не хранится среди полей записи, но если там есть хотя бы одно NULL-значение, то создаётся битовая карта неопределённых значений, которая называется t_bits. Стоит запомнить, что самое первое NULL-значение влечёт за собой создание такой карты и расход некоторого количества места.
Правда, все дополнительные NULL-значения в этой записи вы уже храните бесплатно и достаточно компактно. Если вы действительно экономите каждый байт в своей базе данных, хранение NULL — правильное занятие.
NULL и индексы
Postgres хранит NULL-значения в btree-индексах. Этим он отличается от Oracle. Также Postgres может использовать такой индекс при поиске записей по NULL-значению.
Тем не менее, хранение NULL-значений в индексе для вас бесполезно, если у вас нет такого типа запросов (они довольно редки, и их можно проверить в представлении pg_stats_statements).
Также пользы не будет при большом количестве NULL-значений в индексе, и следовательно, плохой селективности. В этом случае будет дешевле сделать последовательное сканирование таблицы, а не возиться с индексом.
Вот пример случая с большим числом NULL-значений в таблице. У вас есть внешний ключ (foreign key) на какую-то родительскую таблицу, но реальная ссылка используется редко, и в основном в дочерней таблице NULL-значения.
Или же у вас может быть какой-то хитрый функциональный индекс, который часто возвращает NULL. Здесь у нас пример по JSONB-ключу key1, а если у вас в JSON этот ключ встречается нечасто, то и большинство значений будет NULL.
Если у вас NULL-значений много, то вам поможет перестроение индекса на частичный с условием WHERE <ваше поле или выражение> IS NOT NULL. То есть мы просто выкидываем такие значения из нашего индекса. Это принесёт ряд улучшений:
-
во-первых, сокращается размер индекса на дисках, в том числе на репликах и бэкапах;
-
во-вторых, уменьшится количество записей в журнал предзаписи (WAL);
-
в-третьих, освободится место в оперативной памяти и улучшится кэширование.
В моей практике были случаи, когда реально удавалось сократить размер индекса на порядок.
В поиске таких индексов поможет представление pg_stats. В нём есть поле null_frac, которое показывает долю NULL-значений в столбцах таблиц. То есть с помощью этого представления можно определить, есть ли у вас кандидаты на оптимизацию.
Сценарий аккуратного переезда вполне очевиден:
-
создаёте новый частичный индекс;
-
по представлению pg_stat_user_indexes убеждаетесь, что запросы переехали на новый индекс;
-
удаляете старый индекс.
Выводы
-
Значение NULL может преподнести некоторые сюрпризы, если вы к нему не готовы.
-
Стоит проверить, как работают с NULL вызываемые вами функции и ваш код.
-
Запрещайте NULL там, где вы не планируете его использовать явным образом.
-
Проверяйте ваши индексы на наличие NULL-значений — возможно, за счёт оптимизаций удастся сэкономить некоторое количество памяти и ресурсов процессора.
Полезные ссылки
В статье Хаки Бенита рассматриваются как раз такие переполненные NULL-значениями индексы, есть SQL запрос для их поиска в вашей базе данных и практический результат перестроения.
Классическая статья Брюса Момжиана (Bruce Momjian) под названием «NULLs Make Things Easier?» доступна здесь.
Также рекомендуем ознакомиться с книгой Егора Рогова «PostgreSQL 14 изнутри».