SQL

БЛОК 1. Основы реляционных БД — 5. Типы данных

📚 24 вопросовПройти тест →
Лекция

БЛОК 1. Основы реляционных БД — 5. Типы данных

SQL

5. Типы данных

🧭 Введение: почему типы данных влияют на деньги, скорость и баги

На старте проекта типы часто выбирают «чтобы просто работало»: TEXT почти для всего, FLOAT для цен, TIMESTAMP без обсуждения часовых поясов.
Проблемы приходят позже: неверные суммы в отчётах, странные даты после релиза в другой стране, медленные фильтры и сложные миграции.
Тип данных задаёт границы значения, точность вычислений, поведение сортировки, размер хранения и удобство индексации.
Поэтому типы данных нужно проектировать как часть архитектуры, а не как второстепенную деталь.
💡 Совет: Выбирайте тип по бизнес-смыслу поля и по сценарию чтения, а не по принципу «этот тип я чаще вижу в примерах».
Вывод: Неправильный тип сегодня становится дорогой миграцией и источником скрытых ошибок завтра.

⚠️ Проблема -> решение

Если типы выбраны неосознанно, в БД попадают неточные цены, «плавающие» даты и неочевидные ограничения длины.
Код может временно маскировать проблему, но при росте данных и нагрузки ошибки становятся системными.
Решение — на этапе схемы определить тип каждого поля по четырём вопросам:
что хранится, какая точность нужна, как это фильтруется/сортируется и как долго живут данные.
🟢 Если совсем просто: Тип должен отражать смысл поля, а не удобство быстрого прототипа.
🎯 Как понять, что этап прошёл успешно: Для каждого ключевого поля команда может объяснить, почему выбран именно этот тип и какие риски он снимает.

🛠️ Чем помогает и как работает

Грамотные типы данных повышают предсказуемость системы: БД хранит корректные значения и быстрее выполняет типовые запросы.
Это напрямую снижает риск логических багов в 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 в боевых системах чаще безопаснее 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-семантики там, где нужен глобальный момент.
🟢 Если совсем просто: Для событий в реальном мире чаще нужен 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.
  • Для ключевых идентификаторов заранее выбирайте стратегию BIGINT vs UUID.
  • Для текстов задавайте ограничения длины только по доменной необходимости.
  • JSONB применяйте как дополнение, а не как замену реляционной модели.
  • Договоритесь в команде: отсутствие значения — это NULL, а не пустая строка ''.
  • Любой выбор типа документируйте рядом с моделью и проверяйте на типовых запросах.
Вывод: Сильная схема начинается с осознанных типов и завершается эксплуатационным здравым смыслом.

🧾 Заключение

Типы данных определяют не только то, что можно записать, но и то, как система будет себя вести через год на реальной нагрузке.
Если выбирать типы по смыслу поля, точности и сценариям чтения, база становится надёжной опорой для продукта.
💡 Совет: Перед созданием новой таблицы проходите мини-чеклист: точность чисел, семантика времени, ограничения текста, роль JSON и стратегия ID.
Вывод: Тема «Типы данных» — это фундамент качества данных, который напрямую влияет на стабильность релизов и доверие к аналитике.
🎯

Проверьте знания

Закрепите материал — пройдите тест по теме «БЛОК 1. Основы реляционных БД — 5. Типы данных»

Пройти тест →