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 не спасёт
Лучшие практики по схеме:
- Нормализация как база
- Цель нормализации – минимизировать дубли и аномалии обновления: чтобы изменение одного факта не требовало править 10 таблиц. Обычно хватает 3НФ (в боевых системах чаще всего доходят до 3NF и останавливаются).
- Пример: вместо хранения имени клиента в каждой строке таблицы
orders, создаёшь таблицуcustomers, а вordersсохраняешьcustomer_id(FOREIGN KEY).
- Не перегибай с нормализацией
- Слишком агрессивная нормализация = куча JOIN’ов, тяжёлые SELECT’ы.
- В read-heavy системах допустима частичная денормализация: хранить предрасчитанную сумму заказа (
total_amount), количество позиций, агрегаты. Это ускоряет чтение ценой небольшого дублирования.
- Первичные и внешние ключи
- Почти всегда используй суррогатный ключ (INT/BIGINT, SERIAL/IDENTITY) как PK, а не «натуральный» (email, номер паспорта и т.п.).
- Внешние ключи должны быть явными, с ON DELETE/UPDATE поведением, которое тебе реально нужно (CASCADE / RESTRICT / SET NULL).
- Типы данных
- Не складывай всё в VARCHAR / TEXT.
- Используй:
BOOLEANдля флагов,NUMERIC/DECIMALдля денег,TIMESTAMP WITH TIME ZONEдля времени,JSONBв Postgres только там, где реально нужна гибкость, а не «чтобы было».
- Правильный тип = меньше места, лучше индексы, меньше багов при сравнениях.
- Индексы
- Индексируй:
- PK (обычно индекс создаётся автоматически),
- колонки, по которым часто фильтруешь (
WHERE), - колонки, по которым JOIN’ишь.
- Не индексируй всё подряд: каждый индекс замедляет INSERT/UPDATE/DELETE и занимает место.
- Если запрос часто сортирует по колонке, имеет смысл индекс по ней (или составной индекс по
(filter_column, order_column)).
3. Паттерны и анти-паттерны SQL-запросов
Лучшие практики:
- Читаемость важнее «умности»
Плохо:
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 *, а чёткий список колонок – это и быстрее, и безопаснее (меньше данных, меньше ломается при изменении схемы).
- Избегай N+1 запросов
Классика: ты берёшь список заказов, а для каждого делаешь отдельный запрос за клиентом. Это особенно больно с ORM.
- SQL-решение: объединить в один запрос с JOIN или сделать
IN (list_of_ids)/WHERE id IN (...). - В ORM-мире: использовать механизмы «eager loading» (например,
selectinload/joinedloadв SQLAlchemy,select_related/prefetch_relatedв Django ORM).
- Корректное использование LIMIT/OFFSET и пагинации
LIMIT+OFFSETок для небольших данных, но приOFFSET 100000БД всё равно бежит по куче строк.- На больших таблицах лучше пагинация по курсору:
WHERE id > last_seen_id ORDER BY id LIMIT 50.
- Агрегации и группировки
- Считай агрегаты в БД (
COUNT,SUM,AVG,GROUP BY), а не в приложении. - Структурируй запрос так, чтобы сначала сузить набор данных, потом агрегировать.
4. Транзакции, блокировки и изоляция
Лучшие практики:
- Всегда думай о границах транзакции: «что должно быть либо выполнено целиком, либо не выполнено вообще».
- Не держи транзакции открытыми дольше, чем нужно. Не делай внутри транзакции:
- долгих вычислений,
- запросов к другим медленным сервисам,
- ожиданий пользователя.
- Уровень изоляции:
- по умолчанию чаще всего READ COMMITTED достаточно;
- для сложных финансовых операций – рассмотрить REPEATABLE READ / SERIALIZABLE, но помнить о возможных блокировках и ретраях.
- Используй
FOR UPDATEаккуратно: это блокирует строки.
В ORM-мире эти вещи обычно прячутся в абстракции session / transaction / atomic. Важно не забывать:
- явный контекст:
- SQLAlchemy:
with session.begin(): ... - Django:
@transaction.atomic()
- SQLAlchemy:
5. Оптимизация: индексы, EXPLAIN и профилирование
- Всегда смотри
EXPLAIN/EXPLAIN ANALYZE
- Для сложных запросов запускай
EXPLAINи смотри план:- какие индексы используются / не используются,
- нет ли
Seq Scanна гигантских таблицах там, где ожидаешь Index Scan.
- Статьи по оптимизации постоянно подчёркивают: «без понимания плана запроса, оптимизировать – лотерея».
- Логика: сначала правильно спроектировать, потом оптимизировать запросы
- Хорошая схема (нормализация + ключи + индексы) даёт намного больше, чем попытки микро-оптимизации.
- Уже после этого можно:
- переписать подзапрос в JOIN,
- менять порядок фильтров,
- использовать материализованные представления для тяжёлых отчётов.
- Кэширование
- На уровне БД:
- кэш планов запросов,
- иногда 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:
- Логика не должна жить в моделях
Анти-паттерн: напихать кучу бизнес-логики в ORM-модель (огромные методы save() / clean() / __init__ и т.д.).
Лучше:
- модели описывают структуру данных и простые инварианты;
- бизнес-логика живёт в сервисах/доменных классах, которые используют модели и репозитории (или QuerySet/Session).
- Явные границы транзакций и сессии
- Не держи одну глобальную Session на всё приложение.
- Используй паттерн Unit of Work:
- запросы агрегируются в рамках юнита (сессии/транзакции);
- по завершению – commit/rollback.
- Виден SQL – меньше сюрпризов
- Включай лог SQL на dev-среде (например,
echo=Trueв SQLAlchemy илиdjango.db.backendslogger в DEBUG). - Слушай N+1: если видишь сотни одинаковых запросов – добавь
select_related/prefetch_relatedилиjoinedload.
- Не пытайся делать «всё» ORM’ом
- Если запрос получается монструозным в ORM-DSL, а на чистом SQL он понятен в две строки – часто выгоднее написать raw SQL и завернуть его в репозиторий/фасад.
- SQLAlchemy прямо рекомендует гибридный подход: ORM для обычных CRUD, Core/SQL – для сложных запросовMedium+1.
- Миграции
- Используй встроенные/стандартные средства:
- Django:
makemigrations/migrate; - SQLAlchemy: Alembic;
- другие ORM часто имеют свои миграторы.
- Django:
- Не меняй схемы «вручную» на проде – только через миграции, которые можно повторить, откатить и воспроизвести в других окружениях.
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, с комментарием, где что удобнее.



