Хорошо ли разбивать таблицы в базе данных?

Ответ

Разбиение таблиц (нормализация) — это компромисс между целостностью данных и производительностью. В моей практике я применяю нормализацию осознанно, в зависимости от требований конкретного проекта.

Когда нормализация оправдана:

  1. Устранение аномалий данных — когда обновление должно затрагивать только одно место
  2. Сложные бизнес-правила — разные сущности с разными жизненными циклами
  3. Частые изменения структуры — легче менять небольшие таблицы

Пример нормализованной структуры для интернет-магазина:

-- Основные таблицы
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE addresses (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    address_line TEXT NOT NULL,
    city VARCHAR(100) NOT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE TABLE orders (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    address_id INT NOT NULL,
    status ENUM('pending', 'processing', 'shipped', 'delivered'),
    total DECIMAL(10, 2) NOT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (address_id) REFERENCES addresses(id)
);

Когда стоит денормализовать:

  • Частые аналитические запросы — денормализация ускоряет SELECT
  • Системы чтения (read-heavy) — кэширование вычисленных полей
  • Микросервисная архитектура — каждый сервис владеет своими данными

Мой подход:

  1. Начинаю с нормализованной схемы (3NF) для обеспечения целостности
  2. Измеряю производительность реальных запросов
  3. Денормализую только проблемные места, добавляя вычисляемые поля или материализованные представления
  4. Документирую причины денормализации в миграциях

Например, в высоконагруженном приложении я добавлял поле full_name в таблицу пользователей, хотя имя и фамилия уже хранились отдельно, чтобы избежать CONCAT в частых запросах.

Ответ 18+ 🔞

А, ну это же классика, ебать мои старые костыли! Разбивать таблицы или не разбивать — вот в чём вопрос, прямо как у Гамлета. Слушай сюда, я тебе сейчас на пальцах объясню, как я с этим живу.

Нормализация — это, по сути, такой компромисс, понимаешь? С одной стороны, у тебя данные целые и невредимые, а с другой — чтобы всё летало, как угорелое. Я к этому подхожу не с бухты-барахты, а с холодной башкой. Для каждого проекта своя история.

Когда эту всю нормализацию стоит городить, а?

  1. Чтобы аномалии данные не съели. Ну представь: обновляешь адрес в одном месте, а он у тебя в двадцати заказах размножился. Это же пиздец, чувак. Надо, чтобы тыкнул в одно поле — и везде подтянулось. Чистота, блядь.
  2. Когда бизнес-правила сложнее, чем инструкция к китайской дрели. Разные сущности, у каждой свой жизненный цикл. Клиенты, заказы, платежи — всё отдельно, чтоб не превратить базу в свалку.
  3. Если структура меняется чаще, чем твоё настроение. Менять маленькую табличку — раз плюнуть. А ковыряться в одной здоровенной, где всё в кучу сгружено — это ж волнение ебать, терпения ноль.

Вот смотри, как для магазина это выглядит, если делать по уму:

-- Основные таблицы
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE addresses (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    address_line TEXT NOT NULL,
    city VARCHAR(100) NOT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE TABLE orders (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    address_id INT NOT NULL,
    status ENUM('pending', 'processing', 'shipped', 'delivered'),
    total DECIMAL(10, 2) NOT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (address_id) REFERENCES addresses(id)
);

Красота, всё на своих местах. Но это, блядь, не серебряная пуля.

А когда надо на всё забить и денормализовать?

  • Когда отчёты и аналитика запрашивают каждые пять минут. JOIN'ы на овердохуища таблиц — это смерть производительности. Иногда лучше продублировать данные, но чтоб SELECT'ы летали.
  • Системы, где читают в сто раз чаще, чем пишут. Можно заранее посчитать и закэшировать прямо в поле таблицы — и всем хорошо.
  • В этих ваших модных микросервисах. Каждый сервис тащит своё одеяло и хранит данные так, как ему удобно, а не как в учебнике по SQL написано.

Как я вообще работаю? По шагам, без паники.

  1. Стартую всегда с нормальной схемы. По третьей нормальной форме, чтобы голова не болела за целостность. Это фундамент, ёпта.
  2. Запускаю всё в бой и начинаю мерить. Смотрю, где запросы начинают хрипеть и задыхаться. Без замеров — это гадание на кофейной гуще.
  3. Денормализую точечно, как хирург. Не всю базу, а только конкретные проблемные места. Добавляю вычисляемое поле, материализованное представление — что по ситуации.
  4. Обязательно пишу, зачем это сделал. В комментарии к миграции. Через полгода сам забудешь, почему тут это поле продублировано, а без записи будешь сидеть и думать: «Какого хуя?».

Вот реальный пример: было высоконагруженное приложение, где постоянно нужно было полное имя юзера. Имя и фамилия в отдельных полях — это хорошо. Но каждый раз делать CONCAT(first_name, ' ', last_name) в тысячах запросов — это жесть. Что сделал? Добавил поле full_name и стал его обновлять триггером. Денормализация? Да. Но запросы сразу ожили. Иногда надо быть не святее папы римского, а практичнее.