Типы данных и NULL

Предпосылки: реляционная модель (таблица, строка, столбец, типизация столбцов).

Реляционная модель | Выражения

Тип столбца определяет не только «какие символы хранятся», но и какие операции допустимы и как они себя ведут. Числа можно складывать и сравнивать по величине. Текст можно искать по подстроке, но сложение бессмысленно (хотя конкатенация — склейка — возможна). А когда значение неизвестно, SQL использует специальный маркер NULL, который ведёт себя не так, как nil в Ruby или None в Python.

Зачем нужна строгая типизация

Допустим, данные о сотрудниках лежат в CSV-файле. Все столбцы — текст. Какие проблемы возникнут?

Зарплаты хранятся как строки. Сравнение '9000' > '70000' — TRUE, потому что текстовое сравнение работает посимвольно: '9' > '7'. При сортировке Евгений с зарплатой 55000 окажется перед Глебом с 70000 — '55000' < '70000', потому что '5' < '7', хотя числовое значение 55000 меньше 70000 лишь на 15000. Результат совпал случайно — текстовое сравнение не учитывает разрядность. Для корректного сравнения зарплат нужен числовой тип.

Цена товара записана как 19.99. После серии операций — сложений и умножений — вместо ожидаемых 199.90 получается 199.89999999999998. Числа с плавающей точкой (real, double precision) быстры, но неточны. Для денег нужен numeric — тип с точной десятичной арифметикой.

Дата найма записана как текст '15.03.2021'. Сортировка по этому столбцу не даст хронологический порядок — '15.03.2021' окажется перед '01.07.2020', потому что '1' < '0' — FALSE. Для дат нужен тип date, который понимает календарь.

Числовые типы

integer (или int) — целое число, 4 байта, диапазон примерно ±2 миллиарда. bigint — 8 байт, примерно ±9.2 × 10¹⁸. numeric(precision, scale) — число с точной десятичной арифметикой, без ошибок округления; используется для денег и финансовых расчётов. real и double precision — числа с плавающей точкой, быстрые, но с ошибками округления.

Практическое правило: для денег — numeric, для большинства задач — integer или bigint, для научных вычислений — double precision.

Текстовые типы

text — строка произвольной длины. Основной строковый тип в PostgreSQL. varchar(n) (от variable character, «строка переменной длины») — строка с ограничением длины (максимум n символов). char(n) — строка фиксированной длины, добивается пробелами; на практике используется редко.

В PostgreSQL text и varchar без указания длины работают практически одинаково1.

Дата и время

date — только дата: 2025-03-15. timestamp — дата и время без часового пояса (полное имя в стандарте SQL — timestamp without time zone). timestamptz — PostgreSQL-сокращение для timestamp with time zone — дата и время с учётом часового пояса: PostgreSQL хранит значение в UTC и конвертирует при выводе.

Практическое правило: почти всегда timestamptz, не timestamp. Без часового пояса рано или поздно появляются баги при смене зон или переходе на летнее время.

Boolean

boolean — логический тип: TRUE, FALSE и… NULL. Третье «значение» — не ошибка, а следствие того, как SQL обрабатывает отсутствие информации.

NULL — отсутствие значения

NULL в SQL означает «значение неизвестно». Не ноль, не пустая строка, не false — именно «мы не знаем». Если у сотрудника Дины salary = NULL, это не значит «зарплата ноль», это значит «мы не знаем, какая у неё зарплата».

 id | name   | salary
----+--------+--------
  1 | Анна   |  90000
  2 | Борис  |  60000
  5 | Дина   |  NULL

В отличие от nil в Ruby (где nil == nil возвращает true) или None в Python, SQL-евский NULL — не значение, а маркер. Его нельзя сравнить ни с чем, включая другой NULL.

Трёхзначная логика

В обычной логике сравнение даёт true или false. В SQL появляется третий вариант — NULL. Это трёхзначная логика (three-valued logic).

Любое сравнение с NULL даёт NULL:

NULL = NULL   -->  NULL  (не TRUE)
NULL = 1      -->  NULL  (не FALSE)
NULL > 5      -->  NULL
NULL <> 5     -->  NULL

Логика: если значение неизвестно, результат сравнения тоже неизвестен. Ментальная модель: NULL — это запечатанный конверт с неизвестным содержимым. Нельзя сказать, равно ли содержимое чему-то, пока конверт не вскрыт.

IS NULL — единственный способ проверки

Проверить, является ли значение NULL, можно только через IS NULL и IS NOT NULL:

SELECT name FROM employees WHERE salary IS NULL;
 name
------
 Дина

Конструкция WHERE salary = NULL не работает — salary = NULL возвращает NULL для любой строки, и ни одна строка не проходит фильтр.

Логические операторы и NULL

Логические операторы AND, OR, NOT с NULL ведут себя по принципу «короткого замыкания»: если один операнд уже определяет результат, второй неважен.

TRUE  AND NULL  -->  NULL    -- результат зависит от неизвестного
FALSE AND NULL  -->  FALSE   -- FALSE AND что-угодно = FALSE
TRUE  OR  NULL  -->  TRUE    -- TRUE OR что-угодно = TRUE
FALSE OR  NULL  -->  NULL    -- результат зависит от неизвестного
NOT NULL        -->  NULL    -- отрицание неизвестности = неизвестность

Принцип: если один операнд уже решает исход независимо от второго — результат определён. Если нет — результат NULL.

WHERE и NULL

WHERE пропускает строку только если результат условия — TRUE. И NULL, и FALSE отсеиваются одинаково.

SELECT name FROM employees WHERE salary <> 90000;
 name
---------
 Борис
 Вера
 Глеб
 Евгений

Дина не попала: NULL <> 90000 даёт NULL, строка отсеяна. Если нужны строки с неизвестной зарплатой, условие нужно дополнить:

WHERE salary <> 90000 OR salary IS NULL

Это правило всплывает снова и снова: NULL проваливает любое сравнение, и строка молча исчезает из результата.

NULL в разных контекстах

NULL ведёт себя по-разному в зависимости от контекста — это одна из самых контринтуитивных особенностей SQL:

КонтекстДва NULL считаются…Почему
= в WHEREнесравнимыми (NULL = NULL NULL)оператор сравнения
DISTINCTодинаковыми (дубликат)специальное правило дедупликации
GROUP BYодной группойаналогично DISTINCT
ORDER BY«наибольшими» (в PostgreSQL по умолчанию)конвенция сортировки, переопределяемая через NULLS FIRST/LAST

DISTINCT и GROUP BY используют не обычное сравнение =, а механизм «IS NOT DISTINCT FROM», который считает два NULL одинаковыми. Это непоследовательно с поведением =, но так определён стандарт SQL.

Подробности поведения NULL в каждом контексте — в соответствующих файлах querying/.

Sources


Реляционная модель | Выражения

Footnotes

  1. В стандарте SQL и в ряде СУБД (MySQL, Oracle, SQL Server) типы TEXT/CLOB исторически были ограничены: нельзя использовать в индексах, в GROUP BY, сравнивать через =. В PostgreSQL text — полноценный тип без таких ограничений, внутренне идентичный varchar без указания длины.