← Назад к вопросам
Приведи примеры спроектированной базы данных с денормализацией
1.0 Junior🔥 141 комментариев
#Хранилища данных
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Примеры денормализованных баз данных
Денормализация — это умышленное нарушение нормальных форм БД для повышения производительности. Приведу реальные примеры из своего опыта.
Пример 1: Интернет-магазин с анализом продаж
Нормализованная схема (плохо для аналитики):
-- Множество таблиц, много JOIN'ов
CREATE TABLE orders (
id UUID PRIMARY KEY,
customer_id UUID REFERENCES customers(id),
created_at TIMESTAMP
);
CREATE TABLE order_items (
id UUID PRIMARY KEY,
order_id UUID REFERENCES orders(id),
product_id UUID REFERENCES products(id),
quantity INT,
price_per_unit DECIMAL
);
CREATE TABLE products (
id UUID PRIMARY KEY,
name VARCHAR,
category_id UUID REFERENCES categories(id),
supplier_id UUID REFERENCES suppliers(id)
);
CREATE TABLE categories (id UUID PRIMARY KEY, name VARCHAR);
CREATE TABLE customers (id UUID PRIMARY KEY, city VARCHAR, country VARCHAR);
-- Запрос с 5 JOIN'ами (медленно!)
SELECT
o.created_at,
c.city,
cat.name as category,
p.name,
oi.quantity,
oi.price_per_unit
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
JOIN categories cat ON p.category_id = cat.id
JOIN customers c ON o.customer_id = c.id;
Денормализованная схема (хорошо для аналитики):
-- Одна таблица фактов с pre-aggregated данными
CREATE TABLE fct_order_items (
order_item_id UUID PRIMARY KEY,
order_id UUID,
order_date DATE,
order_timestamp TIMESTAMP,
-- Денормализованные данные из customer
customer_id UUID,
customer_city VARCHAR,
customer_country VARCHAR,
-- Денормализованные данные из product
product_id UUID,
product_name VARCHAR,
product_category VARCHAR,
supplier_id UUID,
-- Меры
quantity INT,
unit_price DECIMAL,
total_amount DECIMAL,
-- Для быстрого фильтра
created_date DATE,
CONSTRAINT fk_order FOREIGN KEY (order_id) REFERENCES orders(id)
);
-- Создаём индексы по часто используемым полям
CREATE INDEX idx_fct_order_date ON fct_order_items(order_date);
CREATE INDEX idx_fct_category ON fct_order_items(product_category);
CREATE INDEX idx_fct_country ON fct_order_items(customer_country);
-- Теперь запрос без JOIN'ов
SELECT
order_date,
customer_city,
product_category,
product_name,
SUM(quantity) as total_qty,
AVG(unit_price) as avg_price,
SUM(total_amount) as revenue
FROM fct_order_items
WHERE order_date >= '2024-01-01'
GROUP BY 1, 2, 3, 4;
Плюсы денормализации:
- Запросы выполняются в 10-100x быстрее (no joins)
- Проще писать аналитические запросы
- Лучше масштабируемость для больших объёмов
Минусы:
- Дублирование данных (customer_city может измениться)
- Обновление сложнее (нужно синхронизировать)
- Необходимо управлять целостностью данных
Пример 2: Real-time аналитика в SaaS
Вызов: Dashboards должны показывать метрики в реальном времени (5+ млн событий в день).
Решение с денормализацией:
-- Исходные таблицы (нормализованные)
CREATE TABLE events (
event_id UUID PRIMARY KEY,
event_type VARCHAR, -- 'page_view', 'click', 'signup'
user_id UUID,
session_id UUID,
timestamp TIMESTAMP,
metadata JSONB
);
CREATE TABLE users (
user_id UUID PRIMARY KEY,
plan VARCHAR, -- 'free', 'pro', 'enterprise'
company_id UUID,
created_at TIMESTAMP
);
-- Денормализованная таблица для быстрых запросов
CREATE TABLE events_denormalized (
event_id UUID PRIMARY KEY,
event_type VARCHAR,
-- Денормализованные пользовательские данные
user_id UUID,
user_plan VARCHAR,
company_id UUID,
user_created_date DATE,
session_id UUID,
timestamp TIMESTAMP,
event_date DATE,
event_hour DATE, -- для GROUP BY по часам
-- Денормализованные метаданные (частые поля)
page_url VARCHAR,
referrer VARCHAR,
device_type VARCHAR,
country VARCHAR,
metadata JSONB
);
-- Мат. представление для ещё большей скорости
CREATE MATERIALIZED VIEW mv_events_hourly AS
SELECT
event_date,
event_hour,
event_type,
user_plan,
country,
COUNT(*) as event_count,
COUNT(DISTINCT user_id) as unique_users,
COUNT(DISTINCT session_id) as unique_sessions
FROM events_denormalized
GROUP BY 1, 2, 3, 4, 5;
-- Refresh каждый час
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_events_hourly;
-- Супер быстрый запрос для dashboards
SELECT
event_type,
user_plan,
SUM(event_count) as total_events,
SUM(unique_users) as active_users
FROM mv_events_hourly
WHERE event_hour BETWEEN now() - interval '24 hours' AND now()
GROUP BY 1, 2
ORDER BY total_events DESC;
Пример 3: Data warehouse для больших объёмов
Архитектура:
OLTP (нормализованные)
↓
ELT pipeline
↓
Data warehouse (денормализованные)
↓
Agg tables / Materialized Views
↓
BI tools / Reports
Денормализованная схема в хранилище:
-- Таблица фактов: широкая, одна большая таблица
CREATE TABLE warehouse.fct_sales (
-- IDs
sale_id BIGINT,
order_id BIGINT,
customer_id BIGINT,
product_id BIGINT,
store_id BIGINT,
salesperson_id BIGINT,
-- Денормализованные атрибуты
sale_date DATE,
sale_year INT,
sale_month INT,
sale_quarter INT,
sale_week INT,
customer_segment VARCHAR, -- Денормализовано
customer_region VARCHAR, -- Денормализовано
product_category VARCHAR, -- Денормализовано
product_subcategory VARCHAR, -- Денормализовано
product_brand VARCHAR, -- Денормализовано
store_name VARCHAR, -- Денормализовано
store_country VARCHAR, -- Денормализовано
-- Меры
quantity INT,
unit_price DECIMAL(10, 2),
discount_percent DECIMAL(5, 2),
tax_amount DECIMAL(10, 2),
sale_amount DECIMAL(12, 2),
cost_amount DECIMAL(12, 2),
profit_amount DECIMAL(12, 2)
);
-- Практически no joins нужны
SELECT
sale_year,
sale_month,
customer_segment,
product_category,
COUNT(*) as transaction_count,
SUM(quantity) as total_units,
SUM(sale_amount) as revenue,
SUM(profit_amount) as profit,
SUM(profit_amount) / SUM(sale_amount) as profit_margin
FROM warehouse.fct_sales
WHERE sale_year >= 2024
GROUP BY 1, 2, 3, 4
HAVING COUNT(*) > 100
ORDER BY revenue DESC;
Пример 4: Логирование и мониторинг
-- Нормализованное логирование (проблема: много JOIN'ов)
CREATE TABLE logs (id, service_id, level, message, created_at);
CREATE TABLE services (id, name, version);
CREATE TABLE environments (id, name);
-- Денормализованное логирование (решение)
CREATE TABLE logs_denormalized (
log_id UUID PRIMARY KEY,
timestamp TIMESTAMP,
log_date DATE,
log_hour DATE,
service_name VARCHAR, -- вместо service_id
service_version VARCHAR, -- вместо join
environment VARCHAR, -- вместо join
log_level VARCHAR, -- ERROR, WARN, INFO
message TEXT,
-- Быстрые индексы
error_code VARCHAR,
stack_trace TEXT,
user_id UUID,
request_id UUID,
response_time_ms INT,
INDEX idx_timestamp (timestamp),
INDEX idx_service_level (service_name, log_level),
INDEX idx_date_hour (log_date, log_hour)
);
-- Поиск ошибок (быстро!)
SELECT
service_name,
COUNT(*) as error_count,
AVG(response_time_ms) as avg_response_time
FROM logs_denormalized
WHERE log_date >= CURRENT_DATE - INTERVAL '7 days'
AND log_level = 'ERROR'
GROUP BY service_name
ORDER BY error_count DESC;
Когда использовать денормализацию
✅ Используй, если:
- Нужна высокая скорость чтения (analytics, reports)
- Данные часто читаются, редко пишутся
- Объём данных большой (миллиарды строк)
- Нормализованные запросы содержат много JOIN'ов
- Одно значение, часто используемое в фильтре (category, country)
❌ Избегай, если:
- Много писем и обновлений (нормализация лучше)
- Мало данных (не нужна оптимизация)
- Значение часто изменяется (несинхронизированность)
- OLTP система (используй нормализацию)
Best practices
- Начни с нормализацией, потом денормализуй при необходимости
- Используй ETL для управления денормализованными таблицами
- Мониторь данные на актуальность и целостность
- Документируй какие данные денормализованы и почему
- Кэшируй мат. представления, если обновление дорого
Заключение
Денормализация — мощный инструмент для performance, но с ответственностью. Используй с умом в read-heavy сценариях (analytics, reporting), но будь осторожен с write-heavy системами.