Блог AST-SoftPro
Базы данных в проектировании систем: выбор и проектирование схем
Базы данных в проектировании систем: выбор и проектирование схем
База данных — это фундамент приложения. Ошибка в выборе или проектировании базы данных приводит к проблемам с производительностью, сложным миграциям и архитектурным ограничениям. В этой статье разберём, как выбрать базу данных под задачу и спроектировать схему.
SQL vs NoSQL: не война, а комплементарность
Выбор между SQL и NoSQL — не вопрос моды, а вопрос соответствия требованиям. Каждая модель данных решает свой класс задач.
SQL (PostgreSQL, MySQL): когда структура важна
SQL-базы данных обеспечивают ACID-транзакции, строгую схему и мощные JOIN-операции. Это делает их идеальными для задач, где целостность данных критична.
Когда SQL подходит:
-
Финансовые операции (платежи, переводы)
-
Заказы и инвентарь
-
Системы с нормализованными данными
-
Запросы с множественными JOIN
NoSQL (MongoDB, Redis, Elasticsearch): когда гибкость и скорость важны
NoSQL-базы жертвуют частью ACID-гарантий ради масштабируемости и гибкости схемы.
Когда NoSQL подходит:
-
Хранение документов с разной структурой
-
Кэширование (Redis)
-
Полнотекстовый поиск (Elasticsearch)
-
Временные ряды (InfluxDB)
-
Событийные потоки (Kafka)
PostgreSQL: универсальный выбор для Python-приложений
PostgreSQL — самая популярная база данных в Python-экосистеме. Она сочетает строгую целостность данных с богатой функциональностью.
Почему PostgreSQL:
-
JSONB. Хранение полуструктурированных данных без потери ACID.
-
Полнотекстовый поиск. Встроенный поиск на русском языке.
-
PostGIS. Пространственные запросы из коробки.
-
Система расширений. Расширения для ML (pgvector), графов (aggraph) и многого другого.
Пример: проектирование схемы интернет-магазина
from sqlalchemy import (
Column, Integer, String, Float, ForeignKey,
DateTime, func, create_engine
)
from sqlalchemy.orm import declarative_base, relationship
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
email = Column(String(255), unique=True, nullable=False)
name = Column(String(255))
created_at = Column(DateTime, server_default=func.now())
orders = relationship("Order", back_populates="user")
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True)
name = Column(String(255), nullable=False)
price = Column(Float, nullable=False)
description = Column(String(5000))
# JSONB для гибких атрибутов
attributes = Column(JSONB)
orders = relationship("OrderItem", back_populates="product")
class Order(Base):
__tablename__ = "orders"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
status = Column(String(50), default="pending")
created_at = Column(DateTime, server_default=func.now())
total = Column(Float)
user = relationship("User", back_populates="orders")
items = relationship("OrderItem", back_populates="order")
class OrderItem(Base):
__tablename__ = "order_items"
id = Column(Integer, primary_key=True)
order_id = Column(Integer, ForeignKey("orders.id"))
product_id = Column(Integer, ForeignKey("products.id"))
quantity = Column(Integer, default=1)
price = Column(Float, nullable=False)
order = relationship("Order", back_populates="items")
product = relationship("Product", back_populates="orders")
Индексация: не только PRIMARY KEY
from sqlalchemy import Index
# Индекс для быстрого поиска по статусу
Index("ix_orders_user_status", Order.user_id, Order.status)
# Полнотекстовый индекс для поиска товаров
Index("ix_products_name_search", func.to_tsvector("russian", Product.name))
# Индекс для JSONB-запросов
Index("ix_products_attributes", Product.attributes, postgresql_using="gin")
MongoDB: когда схема меняется
MongoDB — документная база данных, которая хранит данные в формате BSON (бинарный JSON). Она идеальна для задач, где структура данных меняется часто.
Когда MongoDB оправдана:
-
Контент-менеджмент. Статьи, комментарии, профили — всё с разной структурой.
-
Каталоги товаров. Разные категории товаров имеют разные атрибуты.
-
Логирование. Хранение логов с разной структурой.
-
Прототипирование. Быстрый старт без миграций схем.
Пример на Python (PyMongo):
from pymongo import MongoClient
from datetime import datetime
client = MongoClient("mongodb://localhost:27017/")
db = client["shop"]
products = db["products"]
# Вставка товара с гибкой структурой
product = {
"name": "Laptop",
"price": 99999,
"category": "electronics",
"specs": {
"cpu": "Intel i7",
"ram": 16,
"storage": "512GB SSD",
},
"tags": ["laptop", "electronics", "portable"],
"created_at": datetime.utcnow(),
}
products.insert_one(product)
# Поиск с агрегацией
results = products.aggregate([
{"$match": {"category": "electronics"}},
{"$group": {
"_id": "$category",
"avg_price": {"$avg": "$price"},
"count": {"$sum": 1},
}},
])
Ограничения MongoDB:
-
Нет JOIN. Связанные данные нужно дублировать или делать запросы.
-
Нет транзакций (или ограниченные — с 4.0 есть multi-document, но они медленнее).
-
Сложные аналитические запросы. Агрегация мощная, но не заменяет SQL для аналитики.
Elasticsearch: поиск и аналитика
Elasticsearch — это не база данных в традиционном смысле. Это поисковый движок, оптимизированный для полнотекстового поиска и аналитики.
Когда Elasticsearch нужен:
-
Полнотекстовый поиск с поддержкой русского языка
-
Лог-аналитика (ELK-стек)
-
Рекомендательные системы
-
Агрегации по большим объёмам данных
Пример поиска на Python:
from elasticsearch import Elasticsearch
es = Elasticsearch(["http://localhost:9200"])
# Создание индекса с русским анализатором
es.indices.create(index="products", body={
"settings": {
"analysis": {
"analyzer": {
"russian_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": ["lowercase", "russian"],
}
}
}
},
"mappings": {
"properties": {
"name": {"type": "text", "analyzer": "russian_analyzer"},
"description": {"type": "text", "analyzer": "russian_analyzer"},
"price": {"type": "integer"},
"category": {"type": "keyword"},
}
},
})
# Поиск
results = es.search(index="products", body={
"query": {
"multi_match": {
"query": "ноутбук",
"fields": ["name^2", "description"],
}
},
"aggs": {
"by_category": {
"terms": {"field": "category"},
},
},
})
Проектирование схем: ключевые принципы
1. Нормализация vs денормализация
Нормализация (3НФ) — разделение данных на таблицы для устранения дублирования. Подходит для OLTP-систем (онлайн-транзакции).
Денормализация — объединение данных для ускорения чтения. Подходит для OLAP-систем (аналитика) и кэширования.
| Подход | Плюсы | Минусы |
|---|---|---|
| Нормализация | Нет дублирования, целостность | Медленные JOIN, сложные запросы |
| Денормализация | Быстрое чтение, простые запросы | Дублирование, сложное обновление |
2. Partitioning (разбиение таблиц)
Для больших таблиц (миллионы строк) разбиение по диапазону или хэшу ускоряет запросы:
-- Разбиение по дате
CREATE TABLE orders (
id SERIAL,
user_id INT,
created_at TIMESTAMP,
total DECIMAL
) PARTITION BY RANGE (created_at);
CREATE TABLE orders_2024 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');
CREATE TABLE orders_2025 PARTITION OF orders
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
3. Materialized views для аналитики
Материализованные представления хранят результат сложного запроса и обновляются по расписанию:
CREATE MATERIALIZED VIEW order_stats AS
SELECT
u.name,
COUNT(o.id) as order_count,
SUM(o.total) as total_spent
FROM users u
JOIN orders o ON u.id = o.user_id
GROUP BY u.name;
-- Обновление по расписанию
REFRESH MATERIALIZED VIEW CONCURRENTLY order_stats;
Шардирование: когда одного сервера мало
Шардирование — это горизонтальное разбиение данных между несколькими серверами. Оно необходимо, когда данные не помещаются на одном сервере или нагрузка слишком велика.
Стратегии шардирования:
-
По пользователю. Все данные пользователя хранятся на одном шарде (по хэшу user_id).
-
По времени. Данные за разные периоды на разных серверах.
-
По географии. Данные пользователей из разных регионов на разных серверах.
Пример шардирования в Python:
from hashlib import md5
SHARDS = [
"postgresql://db1:5432/shop",
"postgresql://db2:5432/shop",
"postgresql://db3:5432/shop",
]
def get_shard(user_id: int) -> str:
"""Определение шарда по хэшу user_id"""
shard_index = int(md5(str(user_id).encode()).hexdigest(), 16) % len(SHARDS)
return SHARDS[shard_index]
# Запрос к правильному шарду
shard_url = get_shard(user_id=12345)
engine = create_engine(shard_url)
Частые ошибки
-
Выбор MongoDB «потому что модно». Большинство бизнес-приложений лучше работают на PostgreSQL с JSONB.
-
Отсутствие индексации. Поиск по полю без индекса сканирует всю таблицу — например, выборка заказов по user_id без индекса.
-
N+1 запрос. Загрузка заказов и для каждого — товаров внутри цикла. Используйте JOIN или eager loading.
-
Хранение файлов в базе. Загружайте файлы в объект-хранилище (S3, MinIO), а в базу — только ссылку.
-
Отсутствие миграций. Изменения схемы без миграций (Alembic, Flyway) приводят к рассинхронизации сред.
Заключение
Выбор базы данных — это компромисс между целостностью, производительностью и гибкостью. PostgreSQL остаётся лучшим выбором для большинства бизнес-приложений благодаря сочетанию SQL-строгоści с поддержкой JSONB. MongoDB оправдана для гибких схем, Elasticsearch — для поиска и аналитики.
Главное правило: проектируйте схему под текущие требования, но с учётом будущего роста. Индексация, партиционирование и шардирование — инструменты, которые помогут масштабировать базу данных.
Ключевые моменты:
-
PostgreSQL — универсальный выбор для Python-приложений (ACID + JSONB + полнотекстовый поиск).
-
MongoDB — для гибких схем и документных данных, но без JOIN и транзакций.
-
Elasticsearch — для полнотекстового поиска и аналитики, не замена основной базе.
-
Нормализация для OLTP, денормализация для OLAP — выбирайте под задачу.
-
Индексация, партиционирование и шардирование — инструменты масштабирования.