Введение
Статья 393 кратко упомянула типы данных столбцов — в этой статье разберём их подробнее: основные категории (числа, строки, даты/время), и особенно тонкую, часто неправильно понимаемую концепцию NULL, упомянутую как подводный камень в статье 394.
Концепция
Числовые типы делятся на целочисленные (INTEGER, BIGINT — разной разрядности) и типы с плавающей/фиксированной точкой (DECIMAL/NUMERIC для точных вычислений, важных для денежных сумм; FLOAT/REAL для приближённых вычислений, где небольшая погрешность приемлема). Строковые типы различаются по фиксированной (CHAR(N), дополняется пробелами до точной длины N) или переменной (VARCHAR(N), до N символов без дополнения) длине. NULL представляет отсутствие значения или «неизвестность» — принципиально отличается от нулевого числа, пустой строки, или любого иного конкретного значения, и любая операция с участием NULL (сравнение, арифметика) обычно сама даёт NULL, а не TRUE/FALSE/конкретное число.
Пример кода: Типы данных SQL
-- Различие DECIMAL и FLOAT для денежных расчётов — критично важная деталь
CREATE TABLE accounts (
id INTEGER PRIMARY KEY,
balance DECIMAL(12, 2) -- точное представление, без проблем округления — для денег
-- balance_float FLOAT — НЕ используйте FLOAT для денег, проблема показана ниже
);
-- Проблема FLOAT: классическая иллюстрация неточности двоичного представления десятичных дробей
SELECT 0.1::FLOAT + 0.2::FLOAT; -- может дать 0.30000000000000004, а не точно 0.3 (синтаксис PostgreSQL)
-- NULL — особенности сравнения и обработки
SELECT * FROM customers WHERE email = NULL; -- НИКОГДА не вернёт строк, даже если email реально NULL
SELECT * FROM customers WHERE email IS NULL; -- ПРАВИЛЬНЫЙ способ проверки на NULL
-- NULL в арифметике и конкатенации — результат тоже NULL
SELECT 5 + NULL; -- результат: NULL
SELECT 'привет' || NULL; -- результат: NULL (конкатенация строк, синтаксис PostgreSQL/стандарт SQL)
-- COALESCE — замена NULL на конкретное значение по умолчанию
SELECT name, COALESCE(email, 'нет email') AS email_display FROM customers;
-- Типы дат и времени — DATE, TIME, TIMESTAMP, и работа с временными зонами
CREATE TABLE events (
id INTEGER PRIMARY KEY,
event_date DATE, -- только дата, без времени
event_time TIME, -- только время, без даты
created_at TIMESTAMP, -- дата и время без явной временной зоны
created_at_tz TIMESTAMP WITH TIME ZONE -- дата и время с учётом временной зоны
);
Пояснения к коду
SELECT 0.1::FLOAT + 0.2::FLOAT иллюстрирует фундаментальную, не специфичную для SQL проблему двоичного представления десятичных дробей с плавающей точкой (та же проблема существует в большинстве языков программирования) — для денежных расчётов, где точность критична, DECIMAL/NUMERIC хранит число в десятичном, а не двоичном представлении внутри, избегая этой неточности за счёт несколько большего объёма хранения и немного более медленных вычислений по сравнению с FLOAT. COALESCE(email, 'нет email') — стандартная, кросс-СУБД функция, возвращающая первое не-NULL значение из переданного списка аргументов, что даёт практичный способ заменить потенциальный NULL на понятное, отображаемое пользователю значение без необходимости отдельной проверки через CASE WHEN ... IS NULL.
Подводные камни
- Использование
FLOAT/REALдля хранения денежных сумм — это классическая, широко известная, но всё ещё часто допускаемая ошибка, способная привести к накоплению мелких, но реальных погрешностей при множестве вычислений (особенно при суммировании большого количества значений), что для финансовых данных совершенно неприемлемо;DECIMAL/NUMERICс явно указанной точностью — правильный выбор для любых денежных величин без исключений. - Сравнение с NULL через
=/!=вместоIS NULL/IS NOT NULL, как уже отмечено в статье 394 — это одна из самых частых концептуальных ошибок начинающих работу с SQL, и важно усвоить, что NULL — не значение в обычном смысле, с которым можно сравнивать через обычные операторы равенства, а маркер «неизвестности», требующий специального синтаксиса проверки. - Игнорирование различия между
TIMESTAMPиTIMESTAMP WITH TIME ZONEпри работе с приложением, обслуживающим пользователей из разных часовых зон — хранение времени без явной временной зоны (TIMESTAMP) оставляет интерпретацию хранимого значения неоднозначной (это локальное время сервера? UTC? время какого-то конкретного пользователя?), и для приложений с пользователями из разных зон стоит явно выбрать и придерживаться одной согласованной стратегии — обычно хранение всего в UTC черезTIMESTAMP WITH TIME ZONEи преобразование в локальное время пользователя только на уровне отображения в приложении. - Использование
CHAR(N)вместоVARCHAR(N)без понимания разницы в фактическом хранении —CHAR(N)дополняет каждое значение пробелами до точной длины N независимо от реальной длины сохраняемой строки, что может привести к неожиданным результатам при сравнении строк (значение с дополненными пробелами не равно тому же значению без них при строгом, посимвольном сравнении в некоторых СУБД) и обычно не даёт реальной выгоды по сравнению сVARCHARдля большинства практических случаев, где длина значений может варьироваться.