Подписывайтесь:

Блог AST-SoftPro

Базы данных в проектировании систем: выбор и проектирование схем

05.07.2026 13 мин чтения
Базы данных в проектировании систем: выбор и проектирование схем

Базы данных в проектировании систем: выбор и проектирование схем

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

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)

Частые ошибки

  1. Выбор MongoDB «потому что модно». Большинство бизнес-приложений лучше работают на PostgreSQL с JSONB.

  2. Отсутствие индексации. Поиск по полю без индекса сканирует всю таблицу — например, выборка заказов по user_id без индекса.

  3. N+1 запрос. Загрузка заказов и для каждого — товаров внутри цикла. Используйте JOIN или eager loading.

  4. Хранение файлов в базе. Загружайте файлы в объект-хранилище (S3, MinIO), а в базу — только ссылку.

  5. Отсутствие миграций. Изменения схемы без миграций (Alembic, Flyway) приводят к рассинхронизации сред.

Заключение

Выбор базы данных — это компромисс между целостностью, производительностью и гибкостью. PostgreSQL остаётся лучшим выбором для большинства бизнес-приложений благодаря сочетанию SQL-строгоści с поддержкой JSONB. MongoDB оправдана для гибких схем, Elasticsearch — для поиска и аналитики.

Главное правило: проектируйте схему под текущие требования, но с учётом будущего роста. Индексация, партиционирование и шардирование — инструменты, которые помогут масштабировать базу данных.

Ключевые моменты:

  1. PostgreSQL — универсальный выбор для Python-приложений (ACID + JSONB + полнотекстовый поиск).

  2. MongoDB — для гибких схем и документных данных, но без JOIN и транзакций.

  3. Elasticsearch — для полнотекстового поиска и аналитики, не замена основной базе.

  4. Нормализация для OLTP, денормализация для OLAP — выбирайте под задачу.

  5. Индексация, партиционирование и шардирование — инструменты масштабирования.

AI-Помощник