Что такое нормализация баз данных и зачем она нужна?

Ответ

Нормализация — это процесс организации структуры реляционной базы данных для минимизации избыточности данных и устранения аномалий при операциях вставки, обновления и удаления. В контексте Node.js приложений нормализация упрощает работу с данными через ORM и снижает вероятность рассогласования.

Проблема ненормализованной структуры (таблица user_orders):

-- Антипаттерн: все данные в одной таблице
CREATE TABLE user_orders (
  order_id INT,
  user_name VARCHAR(100),
  user_email VARCHAR(100),
  user_phone VARCHAR(20),
  product_name VARCHAR(100),
  product_price DECIMAL(10,2),
  product_category VARCHAR(50),
  order_date DATE
);

-- Проблемы:
-- 1. Избыточность: данные пользователя дублируются в каждом заказе
-- 2. Аномалия обновления: изменение email требует обновления всех записей
-- 3. Аномалия удаления: удаление заказа может удалить информацию о продукте

Нормализация до 3NF:

-- 1NF: Атомарные значения, нет повторяющихся групп
CREATE TABLE users (
  user_id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  email VARCHAR(100) UNIQUE NOT NULL,
  phone VARCHAR(20)
);

CREATE TABLE products (
  product_id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  price DECIMAL(10,2) NOT NULL,
  category_id INT REFERENCES categories(category_id)
);

CREATE TABLE orders (
  order_id SERIAL PRIMARY KEY,
  user_id INT REFERENCES users(user_id),
  order_date DATE DEFAULT CURRENT_DATE,
  status VARCHAR(20) DEFAULT 'pending'
);

CREATE TABLE order_items (
  order_item_id SERIAL PRIMARY KEY,
  order_id INT REFERENCES orders(order_id),
  product_id INT REFERENCES products(product_id),
  quantity INT NOT NULL CHECK (quantity > 0),
  price_at_time DECIMAL(10,2) NOT NULL  -- Историческая цена
);

CREATE TABLE categories (
  category_id SERIAL PRIMARY KEY,
  name VARCHAR(50) UNIQUE NOT NULL
);

Реализация в Node.js с Sequelize:

// Модель User
const User = sequelize.define('User', {
  name: { type: DataTypes.STRING, allowNull: false },
  email: { type: DataTypes.STRING, unique: true, allowNull: false }
});

// Модель Product
const Product = sequelize.define('Product', {
  name: { type: DataTypes.STRING, allowNull: false },
  price: { type: DataTypes.DECIMAL(10,2), allowNull: false }
});

// Модель Order с ассоциациями
const Order = sequelize.define('Order', {
  status: { type: DataTypes.STRING, defaultValue: 'pending' }
});

// Определение связей
User.hasMany(Order, { foreignKey: 'userId' });
Order.belongsTo(User, { foreignKey: 'userId' });
Order.belongsToMany(Product, { through: 'OrderItem' });
Product.belongsToMany(Order, { through: 'OrderItem' });

// Использование нормализованной структуры
const userWithOrders = await User.findOne({
  where: { email: 'john@example.com' },
  include: [{
    model: Order,
    include: [Product]
  }]
});

Преимущества нормализации для Node.js приложений:

  1. Снижение избыточности данных:

    // Вместо хранения всей информации о пользователе в каждом заказе
    // храним только user_id и JOIN'им при необходимости
  2. Устранение аномалий:

    • Вставки: Можно добавить продукт без заказа
    • Обновления: Email пользователя обновляется в одном месте
    • Удаления: Удаление заказа не удаляет информацию о продукте
  3. Улучшение производительности запросов:

    -- Индексы работают эффективнее на нормализованных таблицах
    CREATE INDEX idx_orders_user_id ON orders(user_id);
    CREATE INDEX idx_products_category ON products(category_id);
  4. Гибкость схемы: Легче добавлять новые типы данных и отношения

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

  • Частые сложные JOIN'ы, влияющие на производительность
  • Таблицы для отчетности или аналитики (OLAP)
  • Кэширование часто запрашиваемых агрегированных данных
  • Системы с преимущественно операциями чтения

Ответ 18+ 🔞

Да ты посмотри, какая история с этими базами данных! Сидишь ты такой, пишешь своё Node.js-приложение, и тут — ёпта — возникает вопрос: как данные-то хранить? Можно, конечно, всё в одну кучу свалить, как тот самый распиздяй, который все вещи в один чемодан пихает, а потом орет, что носки найти не может.

Вот смотри, есть у нас таблица user_orders. Выглядит вроде логично: заказ, пользователь, продукт — всё вместе, красота. Ан нет, чувак! Это же пиздец какой-то, если разобраться.

CREATE TABLE user_orders (
  order_id INT,
  user_name VARCHAR(100),  -- Один и тот же Вася будет в 100 заказах
  user_email VARCHAR(100), -- И его почта тоже
  product_name VARCHAR(100), -- И название товара
  -- ... и так далее
);

Представь: у Васи сменилась почта. И теперь тебе надо бегать по всем его заказам и в каждой строчке почту менять. Это ж ебать-колотить, какой геморрой! Это и есть «аномалия обновления». А если товар удалить из заказа — так и информация о нём нахуй пропадет. Вообще доверия к такой схеме — ноль ебать.

Нормализация — это когда умные дядьки придумали, как эту хуйню по полочкам разложить. Чтобы не было дублей, чтобы всё логично связано было. Как будто ты не в одну корзину всё кидаешь, а завел себе шкаф с отделениями: носки к носкам, майки к майкам.

Сначала приводим всё к первой нормальной форме (1NF). Это значит: никаких массивов в одной ячейке, всё атомарно. Каждая ячейка — одно значение. Не «яблоки, груши», а отдельно «яблоки» и отдельно «груши».

Потом вторая форма (2NF). Чтобы всё, что не относится к первичному ключу, зависело от него целиком, а не от части. Если ключ составной.

Ну и третья (3NF) — чтобы никакие поля не зависели друг от друга транзитивно. Типа «почта» зависит от «пользователя», а не от «номера заказа».

В итоге получается не одна жирная таблица, а несколько аккуратных:

CREATE TABLE users (          -- Отдельно пользователи
  user_id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL
);

CREATE TABLE products (       -- Отдельно продукты
  product_id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL
);

CREATE TABLE orders (         -- Отдельно заказы
  order_id SERIAL PRIMARY KEY,
  user_id INT REFERENCES users(user_id) -- Ссылка на юзера
);

CREATE TABLE order_items (    -- И таблица-связка: что в каком заказе
  order_id INT REFERENCES orders(order_id),
  product_id INT REFERENCES products(product_id),
  quantity INT NOT NULL
);

А в Node.js с Sequelize это выглядит вообще красиво:

const User = sequelize.define('User', { name: DataTypes.STRING });
const Product = sequelize.define('Product', { name: DataTypes.STRING });
const Order = sequelize.define('Order', {});

// Настраиваем связи
User.hasMany(Order);
Order.belongsTo(User);
Order.belongsToMany(Product, { through: 'OrderItem' });
Product.belongsToMany(Order, { through: 'OrderItem' });

// И потом запрос такой, чтоб всё достать:
const userOrders = await User.findOne({
  where: { id: 1 },
  include: [{ model: Order, include: [Product] }]
});

В чём, блядь, профит?

  1. Дублей нет. Почта Васи хранится в одном месте. Поменял — и нахуй всем достаточно.
  2. Целостность. Удалил заказ — товары остались в каталоге. Не надо выёбываться с восстановлением.
  3. Производительность. На маленьких полях индексы строятся быстрее, JOIN'ы лучше работают.
  4. Гибкость. Захотел добавить категории товаров — создал таблицу categories и ссылку в products. И не надо переделывать всю хуйню.

Но! Не всё так однозначно, чувак. Иногда нормализация приводит к тому, что для простого отчёта тебе надо сделать 15 JOIN'ов. База начинает ебашить диск как сумасшедшая. Вот тогда иногда денормализацию применяют — сознательно дублируют данные, чтобы читалось быстрее. Например, для дашбордов или аналитики. Но это уже осознанный компромисс, а не потому что ты ленивая жопа.

Короче, нормализуй, пока не станет больно. А как станет больно — думай, где можно слегка считерить. Главное — понимать, зачем ты это делаешь, а не просто следовать правилам, как мартышлюшка.

Видео-ответы