$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогSQLModel для продвинутых: как маппить связи, управлять схемами и не терять контроль над запросами

SQLModel для продвинутых: как маппить связи, управлять схемами и не терять контроль над запросами

$ sudo teach IT
·17 августа 2026 г.·11 мин·37
SQLModel для продвинутых: как маппить связи, управлять схемами и не терять контроль над запросами

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

Содержание
Маппинг отношений: что именно даёт SQLModel и где начинаются сложностиКак SQLModel конструирует модели поверх SQLAlchemyСвязи один-к-одному: осторожность с уникальностью и каскадамиОдин-ко-многим: где чаще всего ломают данныеМногие-ко-многим: явная таблица связи выигрывает у “магии”Управление ограничениями и целостностью: индексы, уникальность, проверкиИндексы и уникальные ограничения: думайте о запросах, а не о «красоте»CHECK и ограничения диапазонов: где они действительно нужныВнешние ключи и поведение удаления/обновленияСхемы и миграции: как сохранить контроль, когда модель растётКогда вам перестают подходить «автосоздание таблиц»Как SQLModel влияет на миграцииПрактический чеклист миграций при работе с SQLModelПроизводительность запросов: как не потерять контроль в ORMСамая частая проблема: N+1 и ленивые загрузкиselectinload: обычно хороший баланс для коллекцийjoinedload: когда join оправданОграничивайте выборку полей: SQLModel позволяет действовать осознанноСложные фильтры и объединение условий: не скатывайтесь в «логическую кашу»Построение запросов с учётом структуры моделиКак правильно делать фильтрацию по связямdistinct и дублирование результатовСортировки и пагинация: не забудьте про детерминированностьТонкости маппинга: типы, Optional и поведение сериализацииOptional и nullable: соответствие должно быть буквальнымdefaults и defaultfactory: различайте Python-side и DB-side поведениеСериализация и рекурсивность: аккуратно с вложенными связямиСочетание удобства схем и контроля над запросами: подход, который масштабируетсяРазделяйте: доменная модель vs «контракты» для запросовСоглашение по загрузке связейПрофилирование и объяснение планов — обязательно при сложных запросахТипичные ошибки при использовании SQLModel «на зрелости»1) Полагаться на автозагрузку отношений в циклах2) Менять модель, не обновляя миграции3) Делать связи слишком «мягкими» (nullable везде)4) Отсутствие явного контроля DTO5) Игнорировать составные индексыИтог: как выстроить зрелый подход к SQLModel

SQLModel часто воспринимают как «SQLAlchemy, но проще». Для стартовых CRUD — действительно удобно. Но когда приложение растёт: появляются сложные связи, ограничения, необходимость контролировать запросы, а также планирование миграций и производительности — внезапно выясняется, что «простота» требует дисциплины.

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


Маппинг отношений: что именно даёт SQLModel и где начинаются сложности

Как SQLModel конструирует модели поверх SQLAlchemy

SQLModel строится вокруг SQLAlchemy ORM и использует типы Python для генерации схемы. Вы объявляете поля в виде аннотаций, а SQLModel/SQLAlchemy формируют таблицы и ORM-классы. Существенная практическая разница: вы можете полагаться на «автогенерацию», но для продвинутых сценариев важно понимать, как это превращается в настоящие SQL-конструкции и какие варианты загрузки связей вы выбираете.

Связи один-к-одному: осторожность с уникальностью и каскадами

Классический случай — профиль пользователя и сам пользователь. В SQLAlchemy/SQLModel такие отношения почти всегда требуют unique на внешнем ключе (если связь реализована через таблицу профиля) и продуманной политики удаления.

Пример:

code
from typing import Optional
from sqlmodel import SQLModel, Field, Relationship


class User(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    email: str = Field(index=True, nullable=False)

    profile: Optional["UserProfile"] = Relationship(back_populates="user")


class UserProfile(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)

    user_id: int = Field(nullable=False, unique=True, index=True)
    bio: str = Field(default="", nullable=False)

    user: User = Relationship(back_populates="profile")

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

  1. unique=True на user_id обеспечивает корректную «один-к-одному».
  2. back_populates синхронизирует обе стороны.
  3. Поле profile сделано Optional, чтобы корректно переживать отсутствие профиля.

Типичная ошибка: оставить unique=True только на уровне логики приложения — схема не защитит от дублей.

Один-ко-многим: где чаще всего ломают данные

Например, один пользователь имеет много заказов.

code
from typing import List, Optional
from sqlmodel import SQLModel, Field, Relationship


class Customer(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str

    orders: List["Order"] = Relationship(back_populates="customer")


class Order(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    number: str = Field(index=True, nullable=False)

    customer_id: int = Field(foreign_key="customer.id", nullable=False, index=True)
    customer: Customer = Relationship(back_populates="orders")

Проблемы, которые встречаются в продакшене чаще всего:

  • Несогласованность nullable. Если customer_id не nullable, а при создании вы иногда сохраняете черновые записи без клиента — миграции и данные начнут «воевать» с моделью.
  • Слишком широкий выбор полей в __repr__/логах: при неосмотрительном обращении к отношениям можно случайно инициировать ленивую загрузку, а затем — N+1 запросы.

Многие-ко-многим: явная таблица связи выигрывает у “магии”

SQLModel работает с ORM-отношениями, но в продвинутых системах лучше использовать явную таблицу-связку, особенно если у связи есть атрибуты (роль в проекте, дата добавления, уровень доступа).

Пример: User и Project связаны через ProjectMembership.

code
from datetime import datetime
from typing import Optional
from sqlmodel import SQLModel, Field, Relationship


class User(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    email: str = Field(index=True)

    memberships: list["ProjectMembership"] = Relationship(back_populates="user")


class Project(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True, unique=True)

    memberships: list["ProjectMembership"] = Relationship(back_populates="project")


class ProjectMembership(SQLModel, table=True):
    user_id: int = Field(foreign_key="user.id", primary_key=True)
    project_id: int = Field(foreign_key="project.id", primary_key=True)

    role: str = Field(default="member", nullable=False)
    created_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)

    user: User = Relationship(back_populates="memberships")
    project: Project = Relationship(back_populates="memberships")

Почему это важно:

  • У вас появляется контроль уникальности на уровне пары (user_id, project_id) через составной primary key.
  • Можно добавлять поля в связь без переписывания схемы.
  • Запросы становятся предсказуемыми: вы явно выбираете членства и связываете с User/Project.

Типичная ошибка: пытаться сделать many-to-many без явной таблицы, а потом внезапно потребовать role и получить сложную миграцию с переделкой модели.


Управление ограничениями и целостностью: индексы, уникальность, проверки

Индексы и уникальные ограничения: думайте о запросах, а не о «красоте»

В SQLModel индексы и unique задаются прямо в Field(...).

code
from typing import Optional
from sqlmodel import SQLModel, Field


class Product(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    sku: str = Field(index=True, nullable=False, unique=True)
    title: str
    price_cents: int = Field(nullable=False)

Но важно помнить: индекс — это стоимость на запись. Если у вас высоконагруженные вставки, злоупотребление индексами может ухудшить общий throughput.

Практика:

  • индексируйте поля, по которым реально фильтруете (WHERE),
  • составные индексы планируйте под реальные паттерны (WHERE a=? AND b=?),
  • уникальность задавайте на уровне БД, если это требование предметной области.

CHECK и ограничения диапазонов: где они действительно нужны

SQLModel позволяет использовать sa_column и SQLAlchemy-инструменты для более точного управления. Но чаще всего для продвинутых ограничений вам понадобится SQLAlchemy-уровень.

Например, price_cents должен быть неотрицательным:

code
from sqlmodel import SQLModel, Field
from sqlalchemy import CheckConstraint


class Product(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    title: str
    price_cents: int = Field(nullable=False)

    __table_args__ = (
        CheckConstraint("price_cents >= 0", name="ck_product_price_non_negative"),
    )

Подводный камень: __table_args__ работает на уровне определения таблицы. Если вы используете автогенерацию схемы, убедитесь, что миграции учитывают это правильно.

Внешние ключи и поведение удаления/обновления

В реальных системах важно управлять тем, что происходит с дочерними записями при удалении родителя: CASCADE, RESTRICT, SET NULL. На практике SQLAlchemy управляет каскадами на ORM-уровне и/или на уровне БД — зависит от настроек.

В SQLModel вы часто будете добавлять параметры через SQLAlchemy-колонки, а не только foreign_key="...". Поэтому для сложных политик удаления используйте явное определение sa_column или конфигурацию отношений.

Например, если вы хотите каскад на стороне БД, потребуется более детальная настройка (в рамках SQLAlchemy). Если вы пока только делаете первичную модель — задайте поведение явно, иначе поведение станет «случайным», зависящим от ORM-настроек и миграционного сценария.


Схемы и миграции: как сохранить контроль, когда модель растёт

Когда вам перестают подходить «автосоздание таблиц»

На старте многие прогоняют create_all() и живут спокойно. Но дальше появляются:

  • необходимость версионирования схемы,
  • отдельные окружения (staging/production),
  • откат и воспроизводимость,
  • совместимость данных при изменении полей.

Здесь почти всегда переходят к миграциям через Alembic (обычно в связке со SQLAlchemy).

Как SQLModel влияет на миграции

SQLModel сам по себе не заменяет миграции. Он описывает схему, а Alembic сравнивает текущие определения с тем, что было ранее. Поэтому важно:

  1. Не меняйте поля без миграций.
    Внесение изменений только на уровне классов приведёт к несовпадению схемы и модели.
  2. Слежение за nullability и defaults.
    Изменение nullable и default_factory может потребовать аккуратной миграции с заполнением.
  3. Типы и несовпадения.
    Например, datetime без timezone может отличаться по поведению в зависимости от СУБД и настройок.

Практический чеклист миграций при работе с SQLModel

  • Всегда фиксируйте, какие поля nullable, а какие нет, и как будет выглядеть старое множество строк при применении миграции.
  • Если поле добавляется как NOT NULL, заранее продумайте:
    • значение по умолчанию на уровне схемы,
    • миграцию, которая заполнит существующие записи.
  • Если вы вводите уникальность на существующее поле — подготовьте миграцию с проверкой/очисткой данных.
  • Составные ключи и индексы проверяйте особенно тщательно: они легко ломаются из-за несовпадения имён и порядка полей.

Производительность запросов: как не потерять контроль в ORM

Самая частая проблема: N+1 и ленивые загрузки

SQLAlchemy ORM может по умолчанию загружать связанные сущности лениво (точный режим зависит от конфигураций и версий). Если вы в цикле обращаетесь к obj.related_items, ORM может выполнять запрос на каждую сущность — N+1.

Пример типичной ошибки:

code
# Псевдокод: N+1 при переборе
customers = session.exec(select(Customer)).all()
for c in customers:
    print(len(c.orders))  # может триггерить запрос на orders для каждого customer

Что делать:

  • использовать selectinload или joinedload,
  • явно формировать запрос под задачу,
  • иногда вообще уходить от ORM-объектов к «плоским» выборкам.

selectinload: обычно хороший баланс для коллекций

code
from sqlmodel import Session, select
from sqlalchemy.orm import selectinload

stmt = (
    select(Customer)
    .options(selectinload(Customer.orders))
)

customers = session.exec(stmt).all()
for c in customers:
    print(len(c.orders))  # обычно без N+1: будет один/несколько дополнительных запросов, но не на каждый customer

selectinload часто предпочтительнее joinedload, когда коллекции могут быть большими: joinedload делает один большой join и может раздувать результат.

joinedload: когда join оправдан

Если вы уверены, что связанная сущность одна (или коллекция небольшая), можно использовать joinedload:

code
from sqlalchemy.orm import joinedload
from sqlmodel import select

stmt = (
    select(User)
    .options(joinedload(User.profile))
)
users = session.exec(stmt).all()

Для one-to-one/много-к-одному join обычно полезен.

Ограничивайте выборку полей: SQLModel позволяет действовать осознанно

Часто ORM тянет больше, чем нужно. Если ваш endpoint возвращает список пользователей с парой полей — не обязательно загружать всё дерево объектов.

Рассмотрите «плоские» запросы: выбирайте столбцы, а не модели целиком. В SQLAlchemy это делается через select с конкретными колонками, а в SQLModel вы можете строить запросы на уровне SQLAlchemy.

Пример: получить список (id, email) без связанных объектов:

code
from sqlmodel import Session, select

stmt = select(User.id, User.email).where(User.email.like("%@example.com"))
rows = session.exec(stmt).all()
# rows будут содержать кортежи (id, email) либо структуры в зависимости от ORM-обвязки

Плюсы:

  • меньше данных по сети,
  • меньше нагрузки на ORM (оно меньше «сшивает» объекты).

Минусы:

  • меньше удобства, больше ответственности за формат результата.

Сложные фильтры и объединение условий: не скатывайтесь в «логическую кашу»

SQLAlchemy/SQLModel позволяют комбинировать условия. Для поддерживаемости лучше выделять выражения в переменные.

code
from sqlmodel import select

email_filter = User.email.ilike("%@example.com")
has_orders_filter = User.id.in_(
    select(Order.customer_id).where(Order.number.like("A%"))
)

stmt = select(User).where(email_filter).where(has_orders_filter)

Если условия сложные, имеет смысл:

  • документировать предположения,
  • писать тесты на SQL (или хотя бы на результаты),
  • профилировать реальный план запроса в вашей СУБД.

Построение запросов с учётом структуры модели

Как правильно делать фильтрацию по связям

Например, вы хотите выбрать проекты, в которых участвует пользователь с конкретным email.

Вместо того чтобы загружать все объекты и фильтровать в Python, стройте выражение на уровне SQL:

code
from sqlmodel import select

stmt = (
    select(Project)
    .join(ProjectMembership)
    .join(User)
    .where(User.email == "ivan@example.com")
)

projects = session.exec(stmt).all()

Это:

  • обычно быстрее,
  • снижает объём данных,
  • делает поведение повторяемым.

Подводный камень: join может дублировать строки, если вы join’ите коллекции. Тогда добавляйте distinct() или используйте корректные конструкции.

distinct и дублирование результатов

Если вы присоединяете many-to-many и выбираете родительские сущности, возможны повторения:

code
from sqlmodel import select

stmt = (
    select(Project)
    .join(ProjectMembership)
    .where(ProjectMembership.role == "admin")
    .distinct()
)

distinct() — не «лекарство от всего», а инструмент. Он влияет на план запроса и может быть дорогим на больших таблицах. Но если логика действительно требует уникальности — лучше сделать это честно на уровне SQL.

Сортировки и пагинация: не забудьте про детерминированность

Для пагинации всегда задавайте стабильный порядок. Например, сортировка только по created_at может вести к нестабильным страницам при конкурентных вставках. Добавьте вторичный ключ.

code
from sqlmodel import select, desc

stmt = (
    select(Order)
    .where(Order.customer_id == customer_id)
    .order_by(desc(Order.created_at), desc(Order.id))
    .limit(20)
    .offset(offset)
)

Тонкости маппинга: типы, Optional и поведение сериализации

Optional и nullable: соответствие должно быть буквальным

Если поле в базе nullable, в модели оно должно быть Optional[...]. Если нет — не делайте Optional только ради удобства в коде.

Это влияет и на:

  • валидацию,
  • сериализацию,
  • итоговые схемы при генерации.

defaults и default_factory: различайте Python-side и DB-side поведение

default_factory=datetime.utcnow устанавливает значение на стороне Python при создании объекта. Если вы создаёте строки не через ORM (например, через SQL или импорт), default может не сработать.

Для критичных значений (audit fields) часто лучше иметь server_default на стороне БД. В SQLModel это может потребовать SQLAlchemy-интеграции.

Сериализация и рекурсивность: аккуратно с вложенными связями

Если вы используете JSON-ответы и автоматически сериализуете модели, учтите:

  • циклы (User -> orders -> customer -> ...),
  • слишком большие вложенные структуры,
  • нежелательные поля (например, внутренние роли).

Практика: для API используйте отдельные DTO/схемы (Pydantic-модели), а ORM-модели держите в слое данных.


Сочетание удобства схем и контроля над запросами: подход, который масштабируется

Разделяйте: доменная модель vs «контракты» для запросов

На практике хорошо работает подход:

  1. SQLModel-ORM классы описывают данные и связи.
  2. Слой запросов строит выборки под конкретные use-case.
  3. Слой API сериализует DTO, которые не всегда равны ORM-модели.

Да, это больше кода. Но это снижает риск:

  • случайных N+1,
  • избыточных полей,
  • сложных JSON-структур,
  • неожиданных побочных эффектов загрузки.

Соглашение по загрузке связей

Определите правило в команде:

  • если endpoint возвращает коллекции — используйте selectinload,
  • если endpoint возвращает «один объект + один профиль» — возможен joinedload,
  • никогда не полагайтесь на ленивую загрузку в критических местах без явного измерения.

Эта дисциплина окупается быстрее любых «магических настроек».

Профилирование и объяснение планов — обязательно при сложных запросах

ORM скрывает SQL, но не скрывает производительность. В продвинутых системах вам нужны:

  • логирование запросов,
  • сравнение «что делает ORM» с ожидаемым SQL,
  • анализ планов (EXPLAIN) в вашей конкретной СУБД.

Если запрос стал медленным, сначала смотрите на:

  • правильность индексов по вашим WHERE,
  • селективность фильтров,
  • размер результатов,
  • наличие distinct/join без необходимости,
  • пагинацию (offset на больших данных часто дорог).

Типичные ошибки при использовании SQLModel «на зрелости»

1) Полагаться на автозагрузку отношений в циклах

Это приводит к N+1 и «рандомным» задержкам.

Решение: заранее определяйте options(selectinload(...)) в запросе.

2) Менять модель, не обновляя миграции

Задвоения, несоответствие nullable, сломанные уникальности.

Решение: Alembic + дисциплина версионирования схемы.

3) Делать связи слишком «мягкими» (nullable везде)

Схема перестаёт защищать данные, а ответственность уходит в код.

Решение: nullable=False там, где это предметная логика.

4) Отсутствие явного контроля DTO

Сериализация ORM может привести к циклам, утечке полей и тяжёлым JSON.

Решение: DTO для API и отдельные модели/схемы.

5) Игнорировать составные индексы

Часто индекса «на одно поле» недостаточно под ваш реальный паттерн фильтрации.

Решение: индексируйте комбинации под WHERE a=? AND b=?.


Итог: как выстроить зрелый подход к SQLModel

SQLModel отлично помогает стартовать и поддерживать согласованность между типами Python и схемой. Но на продвинутых этапах выигрывает не «простота», а управляемость: явные связи и ограничения, дисциплина миграций, контроль загрузки и построения запросов под конкретные use-case.

Если хотите углубиться именно в практики построения моделей и запросов (и разложить по полкам типовые ловушки), полезно пройти системный разбор темы — например, курс SQLModel. Но даже без него логика, которую важно усвоить: ORM — это не магия, а генератор SQL. Ваш контроль над схемой и запросами начинается там, где вы начинаете смотреть не только на Python-классы, но и на то, какой SQL они производят и как он выполняется на уровне СУБД.

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

SQLModel

Курс по теме

SQLModel

С нуля до уверенной работы с базами данных на Python. Подходит для начинающих и тех, кто уже пишет на Python. Разберитесь с SQLModel раз и навсегда — и перестаньте бояться SQL и таблиц.

Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу

Приложения для iPhone и Apple Watch на SwiftUI

Разработка приложений для iPhone и Apple Watch на SwiftUI: навигация, SwiftData, виджеты, часы, выпуск. Нужен Mac с Xcode 27, сами устройства не нужны.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу

Приложения для macOS на SwiftUI

Разработка приложений для Mac на SwiftUI: окна и меню, Liquid Glass, SwiftData, сеть, выпуск. Нужен Mac с macOS 27 и Xcode 27.

Перейти к курсу

Другие статьи

SQLModel vs SQLAlchemy: что выбрать для Python-проекта с FastAPI
sqlmodel

SQLModel vs SQLAlchemy: что выбрать для Python-проекта с FastAPI

Сравниваем два подхода к работе с базами данных в Python-экосистеме: где SQLModel упрощает жизнь, а где мощность SQLAlchemy незаменима. С примерами моделей, запросов и миграций.

18 июля 2026 г.
600
Переход с raw SQL на SQLModel: где выигрываете, а где придётся думать
переход

Переход с raw SQL на SQLModel: где выигрываете, а где придётся думать

Сравним подходы на практике: модели, связи, ограничения, миграции и работа с сложными запросами. Дадим правила, когда SQLModel — лучший выбор, а когда лучше писать SQL осознанно.

25 июля 2026 г.
500
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1231
Что такое переменные, типы и условия в коде: объясню без матана на примерах
такое

Что такое переменные, типы и условия в коде: объясню без матана на примерах

Разберём ключевые понятия для старта — переменные, типы данных, сравнения и условные операторы — простыми словами и на бытовых примерах. В конце соберём мини-скрипт, который принимает решение по введённым данным.

30 сентября 2026 г.
10
FastAPI и фоновые задачи: когда лучше использовать BackgroundTasks, а когда очередь
fastapi

FastAPI и фоновые задачи: когда лучше использовать BackgroundTasks, а когда очередь

Поймём различия между синхронной обработкой, BackgroundTasks и внешними очередями. Разберём idempotency, ретраи и мониторинг фоновых процессов.

22 июля 2026 г.
830
Нужно ли мне знать математику, чтобы начать программировать?
знать

Нужно ли мне знать математику, чтобы начать программировать?

Разберём, где математика реально нужна (и где нет) для новичка: основы Python/веб/автоматизация/аналитика. В конце составим понятный маршрут обучения без лишней теории и подскажем, что повторить, если вы чувствуете пробелы.

28 сентября 2026 г.
50

Комментарии

Пока нет комментариев

Содержание

Маппинг отношений: что именно даёт SQLModel и где начинаются сложностиКак SQLModel конструирует модели поверх SQLAlchemyСвязи один-к-одному: осторожность с уникальностью и каскадамиОдин-ко-многим: где чаще всего ломают данныеМногие-ко-многим: явная таблица связи выигрывает у “магии”Управление ограничениями и целостностью: индексы, уникальность, проверкиИндексы и уникальные ограничения: думайте о запросах, а не о «красоте»CHECK и ограничения диапазонов: где они действительно нужныВнешние ключи и поведение удаления/обновленияСхемы и миграции: как сохранить контроль, когда модель растётКогда вам перестают подходить «автосоздание таблиц»Как SQLModel влияет на миграцииПрактический чеклист миграций при работе с SQLModelПроизводительность запросов: как не потерять контроль в ORMСамая частая проблема: N+1 и ленивые загрузкиselectinload: обычно хороший баланс для коллекцийjoinedload: когда join оправданОграничивайте выборку полей: SQLModel позволяет действовать осознанноСложные фильтры и объединение условий: не скатывайтесь в «логическую кашу»Построение запросов с учётом структуры моделиКак правильно делать фильтрацию по связямdistinct и дублирование результатовСортировки и пагинация: не забудьте про детерминированностьТонкости маппинга: типы, Optional и поведение сериализацииOptional и nullable: соответствие должно быть буквальнымdefaults и defaultfactory: различайте Python-side и DB-side поведениеСериализация и рекурсивность: аккуратно с вложенными связямиСочетание удобства схем и контроля над запросами: подход, который масштабируетсяРазделяйте: доменная модель vs «контракты» для запросовСоглашение по загрузке связейПрофилирование и объяснение планов — обязательно при сложных запросахТипичные ошибки при использовании SQLModel «на зрелости»1) Полагаться на автозагрузку отношений в циклах2) Менять модель, не обновляя миграции3) Делать связи слишком «мягкими» (nullable везде)4) Отсутствие явного контроля DTO5) Игнорировать составные индексыИтог: как выстроить зрелый подход к SQLModel