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

Встречал ли проблемы с ORM

1.0 Junior🔥 121 комментариев
#Базы данных (SQL)

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

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

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

Встречал ли проблемы с ORM

Да, много. ORM - это мощный инструмент но у него есть недостатки.

Типичные проблемы с ORM

1. N+1 запросы

Классическая проблема:

# ПЛОХО
posts = Post.objects.all()
for post in posts:  # 1000 постов
    print(post.author.name)  # Каждый раз новый запрос! 1001 запрос!

# ХОРОШО
posts = Post.objects.select_related('author')
for post in posts:
    print(post.author.name)  # 1 запрос

Как я это находил:

  • Django Debug Toolbar
  • Логирование SQL запросов
  • Профилирование

2. Сложные JOIN'ы которые ORM не может выразить

# Нужна рекурсивная CTE для иерархии категорий
# ORM не может это выразить

# Пришлось использовать raw SQL:
with connection.cursor() as cursor:
    cursor.execute('''
        WITH RECURSIVE cat_tree AS (
            SELECT id, name, parent_id FROM categories WHERE parent_id IS NULL
            UNION ALL
            SELECT c.id, c.name, c.parent_id FROM categories c
            INNER JOIN cat_tree ct ON c.parent_id = ct.id
        )
        SELECT * FROM cat_tree
    ''')

3. Performance проблемы с аннотирование

# ПЛОХО - медленно
users = User.objects.annotate(
    post_count=Count('posts'),
    comment_count=Count('comments'),
    like_count=Count('likes'),
    follower_count=Count('followers')
)

# Результат: Cartesian product! Дублирование рядов.

# ХОРОШО - отдельные запросы или кэш
post_counts = User.objects.values('id').annotate(
    post_count=Count('posts')
)

4. Неправильный ORDER BY с GROUP BY

# ПЛОХО
posts = Post.objects.values('author').annotate(
    count=Count('id')
).order_by('-created_at')  # Ошибка! created_at не в GROUP BY

# ХОРОШО
posts = Post.objects.values('author').annotate(
    count=Count('id'),
    latest=Max('created_at')
).order_by('-latest')

5. Memory утечки с большими querysets

# ПЛОХО - загруженно всё в памяти
all_users = User.objects.all()  # 1 млн пользователей в памяти!

# ХОРОШО - iterator() для streaming
for user in User.objects.all().iterator(chunk_size=1000):
    process(user)

# Или используй bulk_create с batch_size

Серьёзные проблемы которые я встретил

Проблема 1: Race conditions с update

# Два процесса одновременно
Process 1: account.balance -= 100  # 1000 -> 900
Process 2: account.balance += 50   # 1000 -> 1050

# Результат: 1050 вместо 950! Потеря денег!

# Решение: SELECT FOR UPDATE
from django.db import transaction
with transaction.atomic():
    account = Account.objects.select_for_update().get(id=1)
    account.balance -= 100
    account.save()

Проблема 2: Bulk операции не триггерят сигналы

# ПЛОХО
User.objects.filter(is_active=False).delete()
# post_delete сигнал НЕ вызывается!

# ХОРОШО
for user in User.objects.filter(is_active=False):
    user.delete()  # post_delete вызовется

# Или используй bulk_delete из пакетов

Проблема 3: Cascade delete может быть очень медленно

# ПЛОХО
# Пользователь с 10000 постов, каждый пост с 1000 комментов
user.delete()  # 10 млн DELETE операций!

# Решение: удалить в БД напрямую без каскада или batches
Comment.objects.filter(post__author=user).delete()
Post.objects.filter(author=user).delete()
user.delete()

Когда я выбрал Raw SQL

# 1. Рекурсивные структуры (CTE)
# 2. Window функции (PARTITION BY, ROW_NUMBER)
# 3. Очень сложные аналитические запросы
# 4. Performance критичный код
# 5. Специфичные для БД функции

Как я дебажу ORM проблемы

from django.db import connection
from django.test.utils import override_settings

# 1. Смотрю сгенерированный SQL
print(User.objects.filter(name='John').query)

# 2. Считаю количество запросов
@override_settings(DEBUG=True)
def test():
    users = User.objects.all()
    for user in users:
        user.posts.all()  # N+1!
    print(len(connection.queries))

# 3. Профилирую
import time
start = time.time()
users = User.objects.select_related('profile').all()
print(f"Time: {time.time() - start}")

# 4. Использую django-debug-toolbar
# Показывает все запросы в browser

Решения которые я применял

1. Кэширование результатов

from django.core.cache import cache

# Кэшируем expensive запрос
def get_user_stats(user_id):
    cache_key = f'user_stats:{user_id}'
    stats = cache.get(cache_key)
    if stats is None:
        stats = User.objects.filter(id=user_id).annotate(
            post_count=Count('posts')
        ).first()
        cache.set(cache_key, stats, 3600)
    return stats

2. Денормализация

# Вместо аннотирование каждый раз
# Кэшируем в отдельный field

class User(models.Model):
    post_count = models.IntegerField(default=0)
    
    @classmethod
    def update_stats(cls, user_id):
        user = cls.objects.get(id=user_id)
        user.post_count = user.posts.count()
        user.save(update_fields=['post_count'])

3. Batch операции

# Вместо сотни UPDATE запросов
updates = []
for user in users:
    user.last_login = now()
    updates.append(user)
User.objects.bulk_update(updates, ['last_login'], batch_size=1000)

4. Raw SQL для critical path

# High performance критичный код
query = '''
SELECT u.id, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.id
ORDER BY post_count DESC
LIMIT 100
'''
# Это быстрее чем ORM

Выводы

  • ORM хороший для 90% случаев
  • Остальные 10% требуют знания SQL
  • N+1 - самая частая проблема
  • Всегда профилируй перед оптимизацией
  • Sometimes Raw SQL проще и быстрее
  • Важно знать когда использовать каждый подход