5. Типы данных
🧭 Введение: почему типы данных влияют на деньги, скорость и баги
На старте проекта типы часто выбирают «чтобы просто работало»:
Проблемы приходят позже: неверные суммы в отчётах, странные даты после релиза в другой стране, медленные фильтры и сложные миграции.
TEXT почти для всего, FLOAT для цен, TIMESTAMP без обсуждения часовых поясов.Проблемы приходят позже: неверные суммы в отчётах, странные даты после релиза в другой стране, медленные фильтры и сложные миграции.
Тип данных задаёт границы значения, точность вычислений, поведение сортировки, размер хранения и удобство индексации.
Поэтому типы данных нужно проектировать как часть архитектуры, а не как второстепенную деталь.
Поэтому типы данных нужно проектировать как часть архитектуры, а не как второстепенную деталь.
💡 Совет:
Выбирайте тип по бизнес-смыслу поля и по сценарию чтения, а не по принципу «этот тип я чаще вижу в примерах».
✅ Вывод:
Неправильный тип сегодня становится дорогой миграцией и источником скрытых ошибок завтра.
⚠️ Проблема -> решение
Если типы выбраны неосознанно, в БД попадают неточные цены, «плавающие» даты и неочевидные ограничения длины.
Код может временно маскировать проблему, но при росте данных и нагрузки ошибки становятся системными.
Код может временно маскировать проблему, но при росте данных и нагрузки ошибки становятся системными.
Решение — на этапе схемы определить тип каждого поля по четырём вопросам:
что хранится, какая точность нужна, как это фильтруется/сортируется и как долго живут данные.
что хранится, какая точность нужна, как это фильтруется/сортируется и как долго живут данные.
🟢 Если совсем просто:
Тип должен отражать смысл поля, а не удобство быстрого прототипа.
🎯 Как понять, что этап прошёл успешно:
Для каждого ключевого поля команда может объяснить, почему выбран именно этот тип и какие риски он снимает.
🛠️ Чем помогает и как работает
Грамотные типы данных повышают предсказуемость системы: БД хранит корректные значения и быстрее выполняет типовые запросы.
Это напрямую снижает риск логических багов в API и отчётах.
Это напрямую снижает риск логических багов в API и отчётах.
🟢 Если совсем просто:
Типы данных — это базовые «рельсы», по которым движутся все записи.
🎯 Как понять, что этап прошёл успешно:
Цены считаются без расхождений, даты интерпретируются одинаково, а текстовые поля не теряют смысл.
Чем помогает:
- защищает точность финансовых и количественных данных;
- делает поведение даты/времени предсказуемым;
- уменьшает размер и стоимость хранения;
- улучшает качество фильтрации, сортировки и индексации.
Как это работает:
- Шаг 1: определяем бизнес-смысл поля.
- Шаг 2: выбираем класс типа (
число,текст,дата/время,булево,структурированный). - Шаг 3: фиксируем точность/длину (
NUMERIC(p,s),VARCHAR(n)и т.д.). - Шаг 4: задаём обязательность и ограничения (
NOT NULL,CHECK,DEFAULT). - Шаг 5: проверяем влияние на индексы и типовые фильтры.
- Шаг 6: документируем решение, чтобы команда не «переизобретала» тип в каждом сервисе.
✅ Вывод:
Типы данных — это часть контракта модели, а не косметика схемы.
📚 Ключевые термины (простыми словами)
Перед практикой полезно синхронизировать словарь, чтобы одинаково понимать решения по типам.
🟢 Если совсем просто:
Это базовые слова, которые постоянно звучат в ревью схемы.
🎯 Как понять, что этап прошёл успешно:
Вы можете на своём проекте объяснить, где и зачем используется каждый термин.
- Integer types (целые типы) — хранят целые числа (
SMALLINT,INT,BIGINT). - Numeric/Decimal (точные десятичные) — точные значения с фиксированной точкой (
NUMERIC/DECIMAL). - Floating-point (числа с плавающей точкой) — приблизительные значения (
REAL,DOUBLE PRECISION). - Text types (текстовые типы) — строковые значения (
TEXT,VARCHAR,CHAR). - Timestamp (дата-время без TZ-контекста) — хранит дату и время без нормализации часового пояса.
- Timestamptz (дата-время с TZ-семантикой) — хранит момент времени с корректной интерпретацией по часовым поясам.
- UUID — глобально уникальный идентификатор.
- JSONB — структурированные JSON-данные с возможностью индексации и поиска.
✅ Вывод:
Общий словарь по типам данных уменьшает число споров «по привычке» и ускоряет архитектурные решения.
🔢 1. Целые типы и идентификаторы: SMALLINT / INT / BIGINT / SERIAL
Выбор целочисленного типа кажется простым, но именно здесь часто закладывают будущие ограничения роста.
Например, если диапазон быстро исчерпывается, схема требует болезненной миграции PK и всех внешних ключей.
Например, если диапазон быстро исчерпывается, схема требует болезненной миграции PK и всех внешних ключей.
🟢 Если совсем просто:
Выбирайте размер целого типа под реальный срок жизни данных, а не под стартовый объём.
🎯 Как понять, что этап прошёл успешно:
Вы знаете прогноз диапазона значения и можете объяснить, почему тип не исчерпается раньше плана.
Назначение:
Хранить целые значения с нужным диапазоном и стабильной производительностью.
Простыми словами:
Целый тип — это «контейнер» фиксированного размера для целого числа.
Для новичка:
Для PK в боевых системах чаще безопаснее
BIGINT/BIGSERIAL, чем INT при долгом горизонте роста.
В PostgreSQL SERIAL/BIGSERIAL — исторический сахар над sequence; в новых схемах часто используют GENERATED ... AS IDENTITY.Аналогия:
Это как выбрать размер склада заранее: маленький дешевле сейчас, но может быстро закончиться.
Пример:
CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, age SMALLINT CHECK (age >= 0), login_attempts INT NOT NULL DEFAULT 0);🔎 Как это происходит на практике:
- Контекст: проект рассчитан на многолетний рост и интеграции.
- Действия: PK делают
BIGSERIAL, а ограниченные по смыслу счётчики —SMALLINT/INT. - Результат: схема масштабируется без ранней миграции ключей.
Характеристики:
SMALLINT— компактный, но маленький диапазон;INT— стандартный баланс;BIGINT— больший объём хранения, но высокий запас по диапазону.IDENTITYв PostgreSQL — современный и более явный способ автогенерации, чем legacy-SERIAL.
Когда использовать:
SMALLINT для действительно маленьких диапазонов, INT для обычных счётчиков, BIGINT для долгоживущих PK и больших потоков данных.✅ Вывод:
Тип целого поля — это решение о горизонте роста, а не только о текущем объёме.
💰 2. NUMERIC vs FLOAT: точные деньги и приблизительные вычисления
Одна из самых дорогих ошибок новичков — хранить деньги в
Проблема не всегда заметна сразу, но в суммах и отчётах накапливаются расхождения.
FLOAT/REAL.Проблема не всегда заметна сразу, но в суммах и отчётах накапливаются расхождения.
🟢 Если совсем просто:
Деньги и точные значения —
NUMERIC, научные приближённые вычисления — FLOAT.🎯 Как понять, что этап прошёл успешно:
Суммы по заказам и платежам совпадают с ожидаемыми значениями без «копеечных» артефактов.
Назначение:
Выбрать тип числа по требованию к точности.
Простыми словами:
NUMERIC хранит точно, FLOAT хранит приблизительно.Для новичка:
Для цен, балансов, комиссий и налогов используйте
NUMERIC(p,s); FLOAT для финансов почти всегда ошибка.Аналогия:
NUMERIC — как точные весы в аптеке, FLOAT — как быстрый бытовой датчик с допустимой погрешностью.Пример:
CREATE TABLE invoices ( id BIGSERIAL PRIMARY KEY, amount NUMERIC(12,2) NOT NULL CHECK (amount >= 0), tax_rate NUMERIC(5,4) NOT NULL CHECK (tax_rate >= 0), sensor_value DOUBLE PRECISION);🔎 Как это происходит на практике:
- Контекст: есть финансовые расчёты и телеметрия в одной системе.
- Действия: деньги и ставки переводят в
NUMERIC, телеметрию оставляют вDOUBLE PRECISION. - Результат: финансовые итоги точные, а расчёты датчиков остаются производительными.
Характеристики:
NUMERICточный, но тяжелее по вычислениям;FLOAT/DOUBLEбыстрее в части операций, но даёт погрешности;- для денег
NUMERIC— безопасный базовый выбор.
Когда использовать:
NUMERIC для финансов и точных коэффициентов, FLOAT/DOUBLE — для научных/инженерных данных, где допускается приблизительность.✅ Вывод:
Если поле участвует в деньгах, точность важнее микровыгоды по скорости.
🔤 3. Текстовые типы: TEXT, VARCHAR(n), CHAR(n)
Текстовые типы часто выбирают хаотично: где-то
На практике важно понимать, где нужна жёсткая длина, а где достаточно мягкого ограничения через
TEXT, где-то VARCHAR(255) «по привычке».На практике важно понимать, где нужна жёсткая длина, а где достаточно мягкого ограничения через
CHECK.🟢 Если совсем просто:
TEXT и VARCHAR выбираются по бизнес-ограничениям, а не по мифу «VARCHAR всегда быстрее».
В PostgreSQL TEXT и VARCHAR работают одинаково по хранению и производительности; ключевая разница — в ограничении длины.🎯 Как понять, что этап прошёл успешно:
Для строковых полей есть осознанные ограничения длины и понятные правила валидации.
Назначение:
Хранить строки с правильной семантикой длины и формата.
Простыми словами:
Текстовый тип должен отражать реальную природу поля: свободный текст или код с фиксированным форматом.
Для новичка:
TEXT отлично подходит для описаний и комментариев, а длину лучше ограничивать там, где это бизнес-требование.Аналогия:
TEXT — как блокнот без жёстной сетки, VARCHAR(n) — как форма с ограничением по символам.Пример:
CREATE TABLE products ( id BIGSERIAL PRIMARY KEY, title VARCHAR(200) NOT NULL, description TEXT, sku VARCHAR(32) NOT NULL UNIQUE CHECK (sku ~ '^[A-Z0-9_-]+$'));🔎 Как это происходит на практике:
- Контекст: команда хранит и SEO-тексты, и короткие коды.
- Действия: для длинных описаний используют
TEXT, для кодов —VARCHAR+CHECK. - Результат: данные валидны по формату и не ломают интеграции.
Характеристики:
TEXTудобен для свободного текста;VARCHAR(n)фиксирует бизнес-лимит длины;CHAR(n)уместен редко, в основном для строго фиксированных кодов.
Когда использовать:
TEXT для неограниченных описаний, VARCHAR(n) для полей с чётким лимитом, CHAR(n) для редких фиксированных идентификаторов.✅ Вывод:
Ограничение длины должно идти от домена, а не от шаблона «везде 255».
🕒 4. DATE/TIME/TIMESTAMP/TIMESTAMPTZ: время без сюрпризов
Дата и время часто ломают отчёты и уведомления сильнее, чем кажется.
Типичный корень проблемы: хранение «локального времени» без TZ-семантики там, где нужен глобальный момент.
Типичный корень проблемы: хранение «локального времени» без TZ-семантики там, где нужен глобальный момент.
🟢 Если совсем просто:
Для событий в реальном мире чаще нужен
TIMESTAMPTZ, а не TIMESTAMP.🎯 Как понять, что этап прошёл успешно:
Одно и то же событие показывается корректно для пользователей из разных часовых поясов.
Назначение:
Хранить календарные и временные значения без неоднозначности интерпретации.
Простыми словами:
TIMESTAMP — дата-время «как записано», TIMESTAMPTZ — конкретный момент времени.
В PostgreSQL TIMESTAMPTZ хранит момент времени; часовой пояс влияет на ввод/вывод и задаётся timezone сессии, а не хранится отдельным полем.Для новичка:
Для
created_at, paid_at, event_at в большинстве случаев безопаснее TIMESTAMPTZ.Аналогия:
TIMESTAMP — как запись «встреча в 10:00» без указания города, TIMESTAMPTZ — как точка на глобальной временной шкале.Пример:
CREATE TABLE events ( id BIGSERIAL PRIMARY KEY, event_name TEXT NOT NULL, event_at TIMESTAMPTZ NOT NULL, local_timezone TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW());🔎 Как это происходит на практике:
- Контекст: пользователи системы живут в разных часовых поясах.
- Действия: события сохраняют в
TIMESTAMPTZ, а отображают в локальной TZ пользователя. - Результат: уведомления и отчёты совпадают по времени для всех регионов.
Характеристики:
DATE— только календарная дата;TIME— только время суток;TIMESTAMP— дата-время без TZ-контекста;TIMESTAMPTZ— дата-время с корректной интерпретацией момента.
Когда использовать:
TIMESTAMPTZ для аудита и событий, DATE для дня рождения/дедлайна без времени, TIME для расписания в рамках суток.✅ Вывод:
Неправильный тип времени превращается в системный источник «неуловимых» багов.
🧩 5. BOOLEAN, UUID, JSONB: где сильная сторона, а где ловушка
Современные схемы часто используют
Эти типы мощные, но их нужно применять по назначению, иначе модель теряет прозрачность.
BOOLEAN, UUID и JSONB.Эти типы мощные, но их нужно применять по назначению, иначе модель теряет прозрачность.
🟢 Если совсем просто:
UUID — для распределённых идентификаторов, BOOLEAN — для бинарных флагов, JSONB — для гибких вспомогательных данных.🎯 Как понять, что этап прошёл успешно:
JSON не подменяет реляционные связи, а UUID/BOOLEAN используются там, где они действительно оправданы.
Назначение:
Закрыть специальные потребности модели без деградации структуры.
Простыми словами:
Эти типы полезны, если не пытаться ими заменить нормальные таблицы и связи.
Для новичка:
JSON и массивы допустимы для метаданных и payload, но ключевые связи модели лучше хранить обычными колонками + FK.
UUID удобен как публичный идентификатор и межсервисный ключ, но обычно тяжелее по индексам, чем
BIGINT.
Вставки по случайным UUID (например, v4) могут быть менее дружелюбны к B-tree, чем монотонные ключи.
Частая практика: внутренний BIGINT для связей + внешний public_id UUID для API.Аналогия:
JSONB — как «карман» для дополнительных атрибутов, но не замена основному шкафу документов.
Пример:
CREATE TABLE audit_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), actor_id BIGINT, is_success BOOLEAN NOT NULL, event_type TEXT NOT NULL, payload JSONB NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW());🔎 Как это происходит на практике:
- Контекст: логирование действий пользователей и интеграций.
- Действия: основную структуру хранят в колонках, переменные детали — в
payload JSONB. - Результат: аналитика остаётся стабильной, а гибкость для новых полей сохраняется.
Характеристики:
BOOLEANкомпактен и однозначен для true/false;UUIDудобен в распределённых системах и внешних интеграциях;JSONBудобен для полуструктурированных данных, но требует дисциплины в модели.- если по полю внутри
JSONBчасто фильтруют, это поле обычно лучше вынести в отдельную колонку; - если фильтрация по
JSONBвсё же нужна, заранее продумывают путь запроса и индекс (частоGIN).
Когда использовать:
BOOLEAN для бинарных признаков, UUID для глобальных ключей/публичных ID, JSONB для метаданных и редко меняющихся дополнительных атрибутов.✅ Вывод:
Гибкие типы усиливают модель только тогда, когда не размывают её реляционное ядро.
⚖️ Сравнение популярных типов
Ниже краткая таблица для быстрого выбора типа под задачу.
🟢 Если совсем просто:
Сначала определите точность и семантику времени, потом выбирайте остальное.
🎯 Как понять, что этап прошёл успешно:
Для каждого поля в схеме вы можете выбрать строку из таблицы как обоснование.
| Тип | Что хранит лучше всего | Типичный риск неправильного выбора |
|---|---|---|
BIGINT / BIGSERIAL | долгоживущие ключи и большие счётчики | ранний переполненный диапазон |
NUMERIC(p,s) | деньги и точные коэффициенты | ошибка сумм при замене на FLOAT |
DOUBLE PRECISION | приближённые вычисления и телеметрия | недопустимая погрешность в финансах |
TEXT | свободный длинный текст | отсутствие бизнес-валидации формата |
VARCHAR(n) | строки с понятным лимитом | искусственные отказы при неверном n |
TIMESTAMPTZ | глобальные моменты времени | путаница при замене на TIMESTAMP |
JSONB | метаданные и гибкие payload | размывание модели и сложные запросы |
✅ Вывод:
У каждого типа есть «родная» зона применения и типичный анти-паттерн.
📌 Must-know факты
Это минимальный набор знаний для проектирования типов в рабочей схеме.
🟢 Если совсем просто:
Запомните эти пункты как чек-лист перед ревью таблицы.
🎯 Как понять, что этап прошёл успешно:
Вы можете пройти по каждой колонке и проверить её на эти правила.
- Для денег и точных значений используйте
NUMERIC, а неFLOAT. - Для событий и аудита в большинстве случаев выбирайте
TIMESTAMPTZ. - Ограничения длины строки должны исходить из бизнес-требований, а не из «магических 255».
JSONBхорош для метаданных, но не заменяет FK и нормализованные связи.- Размер целочисленного типа нужно выбирать с горизонтом роста, а не только под текущий объём.
- Для критичных полей тип нужно дополнять
NOT NULL,CHECK,DEFAULT. - Типы должны проектироваться под сценарии чтения: фильтры, сортировку, индексацию.
✅ Вывод:
Хороший выбор типа данных — это всегда сочетание доменной логики и эксплуатационных требований.
🧨 Частые мифы
Здесь — типичные ложные установки, из-за которых потом возникают болезненные миграции.
🟢 Если совсем просто:
Мифы про типы часто выглядят как «ускорение старта», но создают технический долг.
🎯 Как понять, что этап прошёл успешно:
Вы можете аргументированно объяснить, почему каждый миф опасен.
❌ Миф:
Для денег
FLOAT достаточно, раз визуально «почти совпадает».✅ Как правильно:
Для финансовых значений используйте
NUMERIC(p,s).📎 Почему это важно:
Даже маленькая погрешность становится заметной в суммах, налогах и отчётности.
❌ Миф:
VARCHAR(255) — универсальный тип для любого текста.✅ Как правильно:
Выбирайте
TEXT или VARCHAR(n) по реальному ограничению домена.📎 Почему это важно:
Случайные лимиты ломают импорт и интеграции без реальной бизнес-причины.
❌ Миф:
TIMESTAMP и TIMESTAMPTZ взаимозаменяемы.✅ Как правильно:
Для глобальных событий и аудита обычно нужен
TIMESTAMPTZ.📎 Почему это важно:
Иначе время по-разному интерпретируется в разных регионах и средах.
❌ Миф:
Можно хранить всё в
JSONB, а нормализацию сделать потом.✅ Как правильно:
Ключевые связи модели держите реляционно,
JSONB используйте для вспомогательной гибкости.📎 Почему это важно:
Иначе усложняются JOIN, контроль целостности и аналитические запросы.
❓ Часто спрашивают на собеседованиях
Это базовые вопросы по теме типов, которые регулярно встречаются в SQL-интервью.
🟢 Если совсем просто:
Проверяют, понимаете ли вы последствия выбора типа, а не только синтаксис.
🎯 Как понять, что этап прошёл успешно:
Вы отвечаете с практическими примерами таблиц и полей.
❓ Вопрос: Какой тип лучше для точных денежных значений?
✅ Ответ:
NUMERIC/DECIMAL, потому что они хранят точные десятичные значения без ошибок округления FLOAT.❓ Вопрос: Чем
TIMESTAMP отличается от TIMESTAMPTZ?
✅ Ответ: TIMESTAMP хранит дату-время без TZ-контекста, а TIMESTAMPTZ — корректно интерпретируемый момент времени.❓ Вопрос: Когда оправдан
VARCHAR(n), а когда лучше TEXT?
✅ Ответ: VARCHAR(n) — когда бизнес задаёт чёткий лимит длины, TEXT — для свободного текста без фиксированного потолка.❓ Вопрос: Можно ли использовать
FLOAT для цен «ради скорости»?
✅ Ответ: Для цен это плохая практика: скорость не компенсирует риск финансовых расхождений, нужен NUMERIC.❓ Вопрос: Когда стоит выбирать
UUID вместо BIGINT?
✅ Ответ: Когда нужен глобально уникальный идентификатор в распределённых сервисах и внешних интеграциях.❓ Вопрос: Допустим ли
JSONB в реляционной модели?
✅ Ответ: Да, для метаданных и гибких атрибутов; но ключевые связи лучше хранить обычными колонками и FK.❓ Вопрос: Зачем обсуждать тип на этапе проектирования, если можно поменять миграцией?
✅ Ответ: Изменение типа в проде может быть дорогим и рискованным, особенно для больших таблиц и ключевых полей.
🚫 Типичные ошибки
Ниже ошибки, которые чаще всего приводят к переработке схемы и багам в проде.
🟢 Если совсем просто:
Каждая ошибка здесь обычно «работает на старте», но дорого исправляется позже.
🎯 Как понять, что этап прошёл успешно:
В вашей схеме нет этих анти-паттернов или есть осознанные исключения с документированием.
Ошибка 1: хранить деньги в FLOAT
❌ Неправильно:
price FLOAT NOT NULL.✅ Правильно:
price NUMERIC(12,2) NOT NULL CHECK (price >= 0).Почему:
FLOAT даёт погрешности в расчётах и некорректные итоговые суммы.Ошибка 2: хранить глобальные события в TIMESTAMP
❌ Неправильно:
created_at TIMESTAMP NOT NULL.✅ Правильно:
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW().Почему:
Без TZ-семантики время по-разному трактуется в разных окружениях.
Ошибка 3: ограничивать все строки VARCHAR(255) без причины
❌ Неправильно:
Ставить одинаковый лимит длины для всех полей «по шаблону».
✅ Правильно:
Задавать длину только там, где это бизнес-правило; иначе использовать
TEXT.Почему:
Случайные лимиты приводят к неочевидным отказам записи.
Ошибка 4: хранить ключевые связи в JSONB
❌ Неправильно:
Класть
user_id, order_id и статусы жизненного цикла в JSON-поля.✅ Правильно:
Выносить ключевые атрибуты в отдельные колонки с FK/индексами.
Почему:
Реляционная модель теряет прозрачность, усложняются запросы и контроль целостности.
Ошибка 5: выбирать диапазон integer только по текущему объёму
❌ Неправильно:
Ставить
INT для долгоживущего PK без прогноза роста.✅ Правильно:
Оценивать горизонт нагрузки и выбирать тип с запасом (
BIGINT при необходимости).Почему:
Миграция PK и связанных FK на больших таблицах очень дорогая.
🏁 Best Practices
Эти практики помогают держать схему стабильной и предсказуемой в долгую.
🟢 Если совсем просто:
Лучше потратить 20 минут на выбор типа сейчас, чем недели на миграцию потом.
🎯 Как понять, что этап прошёл успешно:
Типовые ревью по типам проходят по чек-листу, без «магических» решений.
- Для финансов и тарифов всегда фиксируйте
NUMERIC(p,s). - Для событий, логов и аудита используйте
TIMESTAMPTZ. - Для ключевых идентификаторов заранее выбирайте стратегию
BIGINTvsUUID. - Для текстов задавайте ограничения длины только по доменной необходимости.
JSONBприменяйте как дополнение, а не как замену реляционной модели.- Договоритесь в команде: отсутствие значения — это
NULL, а не пустая строка''. - Любой выбор типа документируйте рядом с моделью и проверяйте на типовых запросах.
✅ Вывод:
Сильная схема начинается с осознанных типов и завершается эксплуатационным здравым смыслом.
🧾 Заключение
Типы данных определяют не только то, что можно записать, но и то, как система будет себя вести через год на реальной нагрузке.
Если выбирать типы по смыслу поля, точности и сценариям чтения, база становится надёжной опорой для продукта.
Если выбирать типы по смыслу поля, точности и сценариям чтения, база становится надёжной опорой для продукта.
💡 Совет:
Перед созданием новой таблицы проходите мини-чеклист: точность чисел, семантика времени, ограничения текста, роль JSON и стратегия ID.
✅ Вывод:
Тема «Типы данных» — это фундамент качества данных, который напрямую влияет на стабильность релизов и доверие к аналитике.