← Назад к вопросам

Какие знаешь сложности написания тяжеловесных запросов с помощью ORM?

2.0 Middle🔥 131 комментариев
#Базы данных (SQL)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Сложности написания тяжеловесных запросов с помощью ORM

ОRM — это отличный инструмент, но при работе с массивными запросами встают серьёзные проблемы. Расскажу про них из личного опыта.

1. N+1 проблема (классическая)

Это главный враг ORM при работе с связанными данными:

# ❌ Проблема: 1 запрос на пользователей + N запросов на посты каждого
users = db.query(User).all()  # 1 запрос

for user in users:
    print(user.posts)  # N дополнительных запросов!

# ✅ Решение: eager loading через join
users = db.query(User).outerjoin(Post).options(
    joinedload(User.posts)
).all()  # Один запрос с JOIN

Проблема в том, что ORM может автоматически загружать связи без твоего ведома, особенно если забыл про eager loading.

2. Сложность оптимизации

Geneated SQL может быть неэффективным:

# ❌ ORM генерирует неоптимальный запрос
users = db.query(User).filter(
    User.posts.any(Post.created_at > date)
).all()

# В результате: SELECT * FROM users WHERE (SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id AND ...) > 0
# Это дорого!

# ✅ Лучше: рукой написать эффективный SQL
from sqlalchemy import text
users = db.execute(text("""
    SELECT DISTINCT u.* FROM users u
    WHERE EXISTS (
        SELECT 1 FROM posts p WHERE p.user_id = u.id AND p.created_at > :date
    )
"""), {"date": date}).all()

3. Проблема с денормализацией и агрегатами

ОRM плохо работает с группировкой и агрегатами:

# ❌ Сложно сделать в ORM
result = db.query(
    User.name,
    func.count(Post.id).label('post_count'),
    func.avg(Post.likes).label('avg_likes')
).join(Post).group_by(User.id).all()

# Это работает, но может генерировать неправильный SQL для разных БД

# ✅ Проще и безопаснее — сырой SQL
result = db.execute(text("""
    SELECT u.name, COUNT(p.id) as post_count, AVG(p.likes) as avg_likes
    FROM users u
    LEFT JOIN posts p ON p.user_id = u.id
    GROUP BY u.id
""")).all()

4. Проблемы с фильтрацией и ORdering

Сложные условия часто требуют сложного кода на ORM:

# ❌ Громоздко в ORM
from sqlalchemy import and_, or_

query = db.query(Post).filter(
    and_(
        or_(
            Post.status == 'published',
            Post.status == 'draft'
        ),
        Post.created_at > date,
        or_(
            and_(Post.views > 1000, Post.likes > 100),
            and_(Post.shares > 50)
        )
    )
).order_by(Post.created_at.desc()).all()

# ✅ Проще в SQL
result = db.execute(text("""
    SELECT * FROM posts
    WHERE status IN ('published', 'draft')
    AND created_at > :date
    AND (
        (views > 1000 AND likes > 100)
        OR shares > 50
    )
    ORDER BY created_at DESC
"""), {"date": date}).all()

5. Проблемы с рекурсией и hierarchical данными

ORM очень плохо работает с рекурсивными структурами (комментарии к комментариям, категории с подкатегориями):

# ❌ ORM подход — дорого и медленно
comment = db.query(Comment).filter(Comment.id == 1).first()
replies = comment.replies  # N+1 проблема
for reply in replies:
    sub_replies = reply.replies  # Ещё N запросов!

# ✅ Рекурсивный CTE — один запрос
result = db.execute(text("""
    WITH RECURSIVE comment_tree AS (
        SELECT id, parent_id, text, 0 as depth FROM comments WHERE id = :id
        UNION ALL
        SELECT c.id, c.parent_id, c.text, ct.depth + 1
        FROM comments c
        JOIN comment_tree ct ON c.parent_id = ct.id
    )
    SELECT * FROM comment_tree
"""), {"id": 1}).all()

6. Проблемы с batch операциями

Массовые обновления/удаления в ORM могут быть очень медленными:

# ❌ Очень медленно: обновляется каждая запись отдельно
users = db.query(User).filter(User.status == 'inactive').all()
for user in users:
    user.status = 'archived'
    db.commit()  # Отдельный запрос для каждого!

# ✅ Быстро: один запрос на всё
db.query(User).filter(User.status == 'inactive').update(
    {User.status: 'archived'},
    synchronize_session=False
)
db.commit()

# ✅✅ Ещё быстрее: сырой SQL
db.execute(text("""
    UPDATE users SET status = 'archived' WHERE status = 'inactive'
"""))
db.commit()

7. Проблемы с типизацией и безопасностью

ORM может скрывать SQL injection уязвимости, но тоже требует внимания:

# ❌ Строковая конкатенация — уязвимо!
query = f"SELECT * FROM users WHERE email = '{email}'"

# ✅ ORM параметризирует
user = db.query(User).filter(User.email == email).first()

# ✅ Сырой SQL с параметрами — тоже безопасно
user = db.execute(
    text("SELECT * FROM users WHERE email = :email"),
    {"email": email}
).first()

# ❌ Даже в сыром SQL можно ошибиться
user = db.execute(text(f"SELECT * FROM users WHERE email = '{email}")).first()  # Уязвимо!

8. Проблемы с caching и invalidation

ORM session кэширует объекты, что иногда скрывает изменения:

# ❌ Проблема: session кэш не знает о внешних изменениях
user = db.query(User).filter(User.id == 1).first()

# Кто-то другой обновил пользователя в БД напрямую
# Но наша сессия всё ещё видит старые данные!
print(user.name)  # Старое значение

# ✅ Решение: явно обновить
db.refresh(user)
print(user.name)  # Новое значение

Когда ORM плох, когда хорош

ORM хорош для:

  • Простые CRUD операции
  • Запросы с 1-2 связями
  • Быстрое прототипирование

Использовать сырой SQL для:

  • Агрегаты и GROUP BY
  • Рекурсивные запросы
  • Batch операции (UPDATE/DELETE на большие объёмы)
  • Сложные JOIN'ы с дополнительной логикой
  • Когда производительность критична

Мой подход

from sqlalchemy import text
from sqlalchemy.orm import Session

class Repository:
    def __init__(self, session: Session):
        self.session = session
    
    # Простые операции — ORM
    def get_by_id(self, user_id: int):
        return self.session.query(User).filter(User.id == user_id).first()
    
    # Сложные запросы — SQL
    def get_top_authors(self, limit: int = 10):
        return self.session.execute(text("""
            SELECT u.id, u.name, COUNT(p.id) as post_count
            FROM users u
            LEFT JOIN posts p ON p.user_id = u.id
            GROUP BY u.id
            ORDER BY post_count DESC
            LIMIT :limit
        """), {"limit": limit}).fetchall()
    
    # Batch операции — SQL
    def archive_inactive_users(self, days: int = 30):
        self.session.execute(text("""
            UPDATE users SET status = 'archived'
            WHERE status = 'inactive'
            AND last_login < NOW() - INTERVAL :days DAY
        """), {"days": days})
        self.session.commit()

Вывод

ORM — инструмент, а не серебряная пуля. Лучше знать оба подхода и использовать то, что подходит:

  1. Профилируй — не гадай о производительности
  2. Эксплейны — смотри как БД выполняет запрос
  3. Комбинируй — ORM + сырой SQL в одном проекте
  4. Индексы — часто проблема не в ORM, а в БД
  5. Кэш — для тяжелых запросов используй Redis
Какие знаешь сложности написания тяжеловесных запросов с помощью ORM? | PrepBro