Когда мы работаем с таблицами, содержащими миллионы и даже миллиарды записей, каждый байт на счету. В таких условиях хранение NULL-значений может становиться неожиданно дорогой операцией. Использование значения по умолчанию (DEFAULT) для замены NULL — это простая, но чрезвычайно эффективная стратегия.
В чем фундаментальная проблема NULL?
Чтобы понять преимущество DEFAULT, нужно разобраться, как PostgreSQL хранит данные и NULL-значения.
- Таблица — это набор строк (записей).
- Каждая строка имеет заголовок, который содержит системную информацию (например, о visibility многопользовательского механизма MVCC).
- Для хранения информации о NULL-значениях PostgreSQL использует битовую карту NULL (NULL bitmap). Это специальная область в заголовке строки, где каждый бит соответствует одному столбцу. Если бит установлен в 1, значение этого столбца равно NULL.
Ключевой вывод: NULL — это не «отсутствие данных». Это специальный маркер, на хранение которого тратится место (1 бит на столбец + выравнивание). Для таблицы с 10 столбцами, где половина из них NULL, эти биты складываются в гигабайты на миллионах строк.
Сравнительный эксперимент: Размеры таблиц с NULL и с DEFAULT
Давайте создадим две идентичные таблицы, имитирующие хранение данных, например, из лог-файла или показаний сенсоров, где некоторые поля часто не заполнены.
Таблица A (с NULL-значениями)
sql
CREATE TABLE sensor_data_null ( id BIGSERIAL PRIMARY KEY, sensor_id INTEGER NOT NULL, recorded_at TIMESTAMPTZ NOT NULL DEFAULT now(), temperature NUMERIC(5,2), -- Может быть NULL humidity NUMERIC(5,2), -- Может быть NULL pressure INTEGER, -- Может быть NULL status_code VARCHAR(10) -- Может быть NULL );
Таблица B (с DEFAULT-значениями)
sql
CREATE TABLE sensor_data_default ( id BIGSERIAL PRIMARY KEY, sensor_id INTEGER NOT NULL, recorded_at TIMESTAMPTZ NOT NULL DEFAULT now(), temperature NUMERIC(5,2) NOT NULL DEFAULT 0.0, humidity NUMERIC(5,2) NOT NULL DEFAULT 0.0, pressure INTEGER NOT NULL DEFAULT 0, status_code VARCHAR(10) NOT NULL DEFAULT 'unknown' );
Наполним их данными (1 млн записей):
Допустим, в 80% записей отсутствуют показания humidity и pressure, а в 50% — status_code.
sql
-- Для таблицы с NULL (вставляем фактические NULL) INSERT INTO sensor_data_null (sensor_id, temperature, humidity, pressure, status_code) SELECT (random() * 100)::int, (random() * 50)::numeric(5,2), CASE WHEN random() < 0.8 THEN NULL ELSE (random() * 100)::numeric(5,2) END, -- 80% NULL CASE WHEN random() < 0.8 THEN NULL ELSE (random() * 1000)::int END, -- 80% NULL CASE WHEN random() < 0.5 THEN NULL ELSE 'ok' END -- 50% NULL FROM generate_series(1, 1000000);
-- Для таблицы с DEFAULT (вставляем значения, но они будут заменены на DEFAULT при вставке) -- На самом деле, мы просто не будем указывать эти столбцы, чтобы сработал DEFAULT. INSERT INTO sensor_data_default (sensor_id, temperature) SELECT (random() * 100)::int, (random() * 50)::numeric(5,2) FROM generate_series(1, 1000000);
Замеряем размеры:
Воспользуемся функцией pg_relation_size.
sql
SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as total_size FROM pg_tables WHERE tablename LIKE 'sensor_data_%';
Вывод: Таблица с DEFAULT и NOT NULL будет заметно меньше, иногда на 20-30%, в зависимости от количества столбцов и процента NULL-значений. На 1 миллионе строк мы экономим десятки мегабайт. На 1 миллиарде — это уже десятки гигабайт.
Преимущества использования DEFAULT помимо экономии места
- Упрощение запросов и логики приложения.
Вам не нужно постоянно писать COALESCE(temperature, 0) или WHERE temperature IS NOT NULL. Вы всегда работаете с конкретным значением.
Снижается вероятность ошибок в прикладном коде, связанных с неправильной обработкой NULL. - Потенциальное увеличение производительности запросов.
Индексы: Индексы по столбцам с DEFAULT могут работать эффективнее. В некоторых случаях (например, индексы только по дереву B-tree) плотные данные упаковываются лучше.
Сканирование: Если СУБД нужно просканировать таблицу, работа с более плотными и предсказуемыми данными идет быстрее. Меньше данных нужно прочитать с диска.
JOIN-ы и агрегация: Операции с столбцами, где нет NULL, часто выполняются проще и быстрее для планировщика запросов. - Повышение целостности данных.
Ограничение NOT NULL в связке с DEFAULT гарантирует, что в столбце всегда будет какое-то осмысленное (или нейтральное) значение. Это делает модель данных более строгой и предсказуемой.
Когда использование DEFAULT может быть не лучшей идеей?
- Когда NULL является семантически важным. Если разница между "значение не было предоставлено" (NULL) и "значение равно нулю" (0) критична для бизнес-логики, то замена NULL на DEFAULT исказит данные.
- Когда DEFAULT-значение не является нейтральным. Если вы установите DEFAULT -1 для столбца customer_id, это может привести к ошибкам, если запрос не будет это учитывать.
- Сложные или тяжеловесные DEFAULT-выражения. DEFAULT gen_random_uuid() — это нормально, а DEFAULT some_heavy_function() может убить производительность вставки.
Заключение
Для огромных таблиц, где производительность и эффективное использование дискового пространства являются критичными, стратегическое использование DEFAULT в сочетании с ограничением NOT NULL — это лучшая практика.
Это превращает "отсутствие данных" из дорогостоящего маркера (NULL) в полезную, предсказуемую и компактную информацию. Вы получаете тройную выгоду: экономию места, упрощение кода и потенциал для более быстрого выполнения запросов. Прежде чем проектировать таблицу для хранения больших данных, всегда задавайте себе вопрос: "А может ли этот столбец иметь осмысленное значение по умолчанию вместо NULL?"