SQL большой гайд. Как правильно выбрать ORM

Эта статья — не про «как написать SELECT», а про настоящую инженерную работу: принципы нормализации, дизайн схем, практики оптимизации SQL, работа с транзакциями, и главное — как выбрать и использовать ORM так, чтобы он помогал, а не мешал.

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

Готовы? Тогда начинаем строить архитектуру, которую не стыдно масштабировать.

1. Базовая идея: SQL и ORM не вместо, а вместе

SQL – язык для работы с реляционными БД (PostgreSQL, MySQL, SQLite, MSSQL и т.д.). Хорошая архитектура вокруг БД начинается с:

  • правильного дизайна схемы (нормализация, ключи, индексы);
  • аккуратного написания запросов (читаемость, предсказуемая производительность);
  • понимания транзакций и блокировок;
  • осознанного выбора: где ORM, а где «голый» SQL.

ORM (Object-Relational Mapper) добавляет сверху слой: ты работаешь с объектами, а не пишешь SQL руками. ORM автоматизирует маппинг объектов в строки таблиц, даёт кросс-СУБД абстракцию и делает тестирование проще (можно подменить реальную БД на SQLite или фейковый слой). Но за удобство платишь контролем и иногда – производительностью.

T.me/sqlhub – sql лучший обучающий телеграм канал. С нуля до профи на приктике.


2. Дизайн схемы: фундамент, без которого никакой ORM не спасёт

Лучшие практики по схеме:

  1. Нормализация как база
  • Цель нормализации – минимизировать дубли и аномалии обновления: чтобы изменение одного факта не требовало править 10 таблиц. Обычно хватает 3НФ (в боевых системах чаще всего доходят до 3NF и останавливаются).
  • Пример: вместо хранения имени клиента в каждой строке таблицы orders, создаёшь таблицу customers, а в orders сохраняешь customer_id (FOREIGN KEY).
  1. Не перегибай с нормализацией
  • Слишком агрессивная нормализация = куча JOIN’ов, тяжёлые SELECT’ы.
  • В read-heavy системах допустима частичная денормализация: хранить предрасчитанную сумму заказа (total_amount), количество позиций, агрегаты. Это ускоряет чтение ценой небольшого дублирования.
  1. Первичные и внешние ключи
  • Почти всегда используй суррогатный ключ (INT/BIGINT, SERIAL/IDENTITY) как PK, а не «натуральный» (email, номер паспорта и т.п.).
  • Внешние ключи должны быть явными, с ON DELETE/UPDATE поведением, которое тебе реально нужно (CASCADE / RESTRICT / SET NULL).
  1. Типы данных
  • Не складывай всё в VARCHAR / TEXT.
  • Используй:
    • BOOLEAN для флагов,
    • NUMERIC/DECIMAL для денег,
    • TIMESTAMP WITH TIME ZONE для времени,
    • JSONB в Postgres только там, где реально нужна гибкость, а не «чтобы было».
  • Правильный тип = меньше места, лучше индексы, меньше багов при сравнениях.
  1. Индексы
  • Индексируй:
    • PK (обычно индекс создаётся автоматически),
    • колонки, по которым часто фильтруешь (WHERE),
    • колонки, по которым JOIN’ишь.
  • Не индексируй всё подряд: каждый индекс замедляет INSERT/UPDATE/DELETE и занимает место.
  • Если запрос часто сортирует по колонке, имеет смысл индекс по ней (или составной индекс по (filter_column, order_column)).

3. Паттерны и анти-паттерны SQL-запросов

Лучшие практики:

  1. Читаемость важнее «умности»

Плохо:

SELECT * FROM orders o, customers c
WHERE o.customer_id = c.id AND amount > 100 AND status = 'PAID';

Лучше:

SELECT
    o.id,
    o.amount,
    o.status,
    c.name AS customer_name
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id
WHERE o.amount > 100
  AND o.status = 'PAID';
  • Явный JOIN вместо «старого» синтаксиса через запятую.
  • Не SELECT *, а чёткий список колонок – это и быстрее, и безопаснее (меньше данных, меньше ломается при изменении схемы).
  1. Избегай N+1 запросов

Классика: ты берёшь список заказов, а для каждого делаешь отдельный запрос за клиентом. Это особенно больно с ORM.

  • SQL-решение: объединить в один запрос с JOIN или сделать IN (list_of_ids) / WHERE id IN (...).
  • В ORM-мире: использовать механизмы «eager loading» (например, selectinload/joinedload в SQLAlchemy, select_related/prefetch_related в Django ORM).
  1. Корректное использование LIMIT/OFFSET и пагинации
  • LIMIT + OFFSET ок для небольших данных, но при OFFSET 100000 БД всё равно бежит по куче строк.
  • На больших таблицах лучше пагинация по курсору: WHERE id > last_seen_id ORDER BY id LIMIT 50.
  1. Агрегации и группировки
  • Считай агрегаты в БД (COUNT, SUM, AVG, GROUP BY), а не в приложении.
  • Структурируй запрос так, чтобы сначала сузить набор данных, потом агрегировать.

4. Транзакции, блокировки и изоляция

Лучшие практики:

  • Всегда думай о границах транзакции: «что должно быть либо выполнено целиком, либо не выполнено вообще».
  • Не держи транзакции открытыми дольше, чем нужно. Не делай внутри транзакции:
    • долгих вычислений,
    • запросов к другим медленным сервисам,
    • ожиданий пользователя.
  • Уровень изоляции:
    • по умолчанию чаще всего READ COMMITTED достаточно;
    • для сложных финансовых операций – рассмотрить REPEATABLE READ / SERIALIZABLE, но помнить о возможных блокировках и ретраях.
  • Используй FOR UPDATE аккуратно: это блокирует строки.

В ORM-мире эти вещи обычно прячутся в абстракции session / transaction / atomic. Важно не забывать:

  • явный контекст:
    • SQLAlchemy: with session.begin(): ...
    • Django: @transaction.atomic()

5. Оптимизация: индексы, EXPLAIN и профилирование

  1. Всегда смотри EXPLAIN / EXPLAIN ANALYZE
  • Для сложных запросов запускай EXPLAIN и смотри план:
    • какие индексы используются / не используются,
    • нет ли Seq Scan на гигантских таблицах там, где ожидаешь Index Scan.
  • Статьи по оптимизации постоянно подчёркивают: «без понимания плана запроса, оптимизировать – лотерея».
  1. Логика: сначала правильно спроектировать, потом оптимизировать запросы
  • Хорошая схема (нормализация + ключи + индексы) даёт намного больше, чем попытки микро-оптимизации.
  • Уже после этого можно:
    • переписать подзапрос в JOIN,
    • менять порядок фильтров,
    • использовать материализованные представления для тяжёлых отчётов.
  1. Кэширование
  • На уровне БД:
    • кэш планов запросов,
    • иногда materialized view.
  • На уровне приложения:
    • кэшировать результаты тяжёлых запросов (Redis, Memcached),
    • но не забывать про инвалидацию.

6. Когда нужен ORM, а когда нет

Плюсы ORM:

  • кросс-СУБД абстракция (один и тот же код может работать с SQLite в тестах и PostgreSQL в проде);
  • уменьшает бойлерплейт: CRUD-операции, миграции, модели;
  • быстрее разработка в веб-приложениях, особенно при стандартных CRUD-схемах;
  • .

Минусы ORM:

  • магия: сложно увидеть, какой реальный SQL генерируется, пока не включишь лог;
  • риск N+1 и неэффективных запросов, если не думать;
  • тяжело выразить очень сложные или специфичные запросы;
  • иногда падаешь на «стеклянный потолок» производительности и всё равно пишешь raw SQL.

Типичный вывод индустрии: гибридный подход – лучшее из двух миров. Простой CRUD через ORM, тяжёлые отчёты/аналитику/узкие места – через «ручной» SQL или views/stored proceduresMedium+1.


7. Как выбирать ORM (на примере Python)

Фокус: Python-стек (SQLAlchemy, Django ORM, асинхронные ORM).

7.1. Ключевые критерии выбора

Смотри на:

  • Тип проекта:
    • монолитное Django-приложение;
    • чистый FastAPI/Flask backend;
    • микросервисы;
    • data-pipeline с SQL.
  • Контроль над SQL:
    • нужен ли очень точный контроль плана запросов и тонких фич СУБД;
    • важна ли portability (смена БД).
  • Async / sync:
    • нужен ли natively async ORM (Tortoise ORM, SQLModel+SQLAlchemy Async, GINO и др.).
  • Экосистема и документация:
    • насколько живой проект, есть ли хорошая документация и комьюнити;
    • насколько легко найти примеры и ответы на StackOverflow.
  • Подход к маппингу:
    • Active Record (модель = объект, сам себя сохраняет);
    • Data Mapper (отдельная модель и отдельный слой работы с БД).

7.2. Django ORM vs SQLAlchemy (Python классика)

Общая картина по современным обзорам:

  • Django ORM:
    • идеален, если ты уже выбрал Django;
    • проще порог входа, меньше кода, всё «из коробки»: миграции, админка, связи, QuerySet API;
    • реализует Active Record: модель знает, как себя сохранить/обновить;
    • отлично для быстрых MVP и типичных CRUD-схемGeeksforGeeks+2DEV Community+2.
  • SQLAlchemy:
    • фреймворк-агностик (можно использовать с Flask, FastAPI, чистым скриптом – чем угодно);
    • Data Mapper: модели чисто описывают данные, а сессия/мэппер управляют их жизненным циклом; это даёт очень гибкий контроль;
    • мощный ORM + крутой Core (low-level SQL-конструктор) – можно безболезненно падать на «почти чистый SQL»;
    • подходит для сложных, нетривиальных схем, кастомных SQL, нескольких БД и т.п.ebs-integrator.com+2docs.sqlalchemy.org+2.

Сводка мнений: Django ORM — быстрый старт и удобство в рамках Django; SQLAlchemy — максимальная гибкость и контроль ценой более сложного входаPLANEKS+1.

7.3. Другие популярные Python ORM

  • SQLModel (поверх SQLAlchemy, от автора FastAPI):
    • Pydantic-модели + SQLAlchemy, приятная типизация;
    • удобно для FastAPI и сервисов, где хочется и ORM, и чётких схем.
  • Tortoise ORM, GINO и др.:
    • асинхронные ORM, интегрируются с async фреймворками;
    • удобны, если архитектура heavily async (много одновременно открытых коннекций, веб-сокеты, и т.д.)Nucamp.
  • Peewee, Pony ORM:
    • более лёгкие и простые, подходят для маленьких проектов и хобби.

7.4. Практическая матрица выбора

Условно:

  • Django монолит, классический сайт / админка / REST:
    • бери Django ORM (нет смысла тащить SQLAlchemy отдельно).
  • FastAPI / Flask, сложные запросы, возможно несколько БД:
    • SQLAlchemy (Core + ORM) или SQLModel.
  • Очень простое приложение, одна маленькая БД:
    • можно рассмотреть Peewee / Pony / даже чистый SQL.
  • Высоконагруженный сервис с критичными запросами:
    • SQLAlchemy Core + немного ORM; сложные места явно на raw SQL / views.
  • Жёсткие требования к async:
    • Tortoise ORM / SQLAlchemy Async / GINO.

8. Как писать код с ORM так, чтобы он не превратился в ад

Независимо от выбора ORM:

  1. Логика не должна жить в моделях

Анти-паттерн: напихать кучу бизнес-логики в ORM-модель (огромные методы save() / clean() / __init__ и т.д.).
Лучше:

  • модели описывают структуру данных и простые инварианты;
  • бизнес-логика живёт в сервисах/доменных классах, которые используют модели и репозитории (или QuerySet/Session).
  1. Явные границы транзакций и сессии
  • Не держи одну глобальную Session на всё приложение.
  • Используй паттерн Unit of Work:
    • запросы агрегируются в рамках юнита (сессии/транзакции);
    • по завершению – commit/rollback.
  1. Виден SQL – меньше сюрпризов
  • Включай лог SQL на dev-среде (например, echo=True в SQLAlchemy или django.db.backends logger в DEBUG).
  • Слушай N+1: если видишь сотни одинаковых запросов – добавь select_related/prefetch_related или joinedload.
  1. Не пытайся делать «всё» ORM’ом
  • Если запрос получается монструозным в ORM-DSL, а на чистом SQL он понятен в две строки – часто выгоднее написать raw SQL и завернуть его в репозиторий/фасад.
  • SQLAlchemy прямо рекомендует гибридный подход: ORM для обычных CRUD, Core/SQL – для сложных запросовMedium+1.
  1. Миграции
  • Используй встроенные/стандартные средства:
    • Django: makemigrations / migrate;
    • SQLAlchemy: Alembic;
    • другие ORM часто имеют свои миграторы.
  • Не меняй схемы «вручную» на проде – только через миграции, которые можно повторить, откатить и воспроизвести в других окружениях.

9. Резюме в формате «если кратко»

  • Сначала делай нормальную схему: нормализация до ~3NF, адекватные типы, ключи, индексы.
  • Пиши читаемый SQL: явно JOIN’ы, без SELECT *, с адекватной пагинацией и агрегациями.
  • Контролируй транзакции и EXPLAIN: смотри планы, следи за блокировками и временем выполнения.
  • ORM бери не вместо SQL, а как слой удобства:
    • Django ORM для быстрого Django-монолита,
    • SQLAlchemy/SQLModel для гибких Python-сервисов,
    • async ORM, если архитектура сильно асинхронная.
  • Критичные места не бойся писать на «чистом» SQL.
  • Строй вокруг всего этого нормальный инженерный процесс: миграции, тесты, линтеры и статический анализ.

Если хочешь, дальше могу сделать отдельный блок:

  • пример схемы (PostgreSQL) + SQL best practices по ней;
  • затем тот же домен реализовать через SQLAlchemy ORM и через Django ORM, с комментарием, где что удобнее.
+1
0
+1
0
+1
0
+1
0
+1
0

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *