Django ORM генерирует безумные подзапросы с prefetch_related - страница загружается за 3 секунды
Я тут с ума схожу. Я уже несколько месяцев работаю над этим сайтом электронной коммерции, и вдруг страница каталога товаров загружается более чем за 3 секунды. ORM Django генерирует абсолютно безумный SQL с вложенными подзапросами повсюду вместо простых соединений.
Вот моя настройка (упрощенная):
class ProductManager(models.Manager):
def active(self):
return self.filter(is_active=True, category__is_active=True)
def with_reviews_stats(self):
return self.annotate(
avg_rating=Avg('reviews__rating'),
reviews_count=Count('reviews', distinct=True),
recent_reviews_count=Count(
'reviews',
filter=Q(reviews__created_at__gte=timezone.now() - timedelta(days=30)),
distinct=True
)
)
class Product(models.Model):
name = models.CharField(max_length=200)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
price = models.DecimalField(max_digits=10, decimal_places=2)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
objects = ProductManager()
# ... other models (Category, Review, CartItem, Wishlist)
И вот вид, который меня убивает:
def product_catalog_view(request):
products = Product.objects.active().with_reviews_stats()
# Category filtering
if category_id := request.GET.get('category'):
category = get_object_or_404(Category, id=category_id, is_active=True)
subcategories = Category.objects.filter(
Q(parent=category) | Q(parent__parent=category) | Q(id=category.id)
).values_list('id', flat=True)
products = products.filter(category_id__in=subcategories)
# Sort by popularity
products = products.annotate(
popularity_score=F('reviews_count') * F('avg_rating')
).order_by('-popularity_score', '-created_at')
# Try to optimize (spoiler: doesn't work)
products = products.select_related('category').prefetch_related(
'reviews__user',
Prefetch(
'reviews',
queryset=Review.objects.filter(is_approved=True).select_related('user'),
to_attr='approved_reviews'
)
)
# Add user-specific data (this is where it gets ugly)
if request.user.is_authenticated:
user_cart_items = CartItem.objects.filter(user=request.user).values_list('product_id', flat=True)
user_wishlist_items = Wishlist.objects.filter(user=request.user).values_list('product_id', flat=True)
products = products.annotate(
in_cart=Case(
When(id__in=user_cart_items, then=Value(True)),
default=Value(False),
output_field=BooleanField()
),
in_wishlist=Case(
When(id__in=user_wishlist_items, then=Value(True)),
default=Value(False),
output_field=BooleanField()
),
cart_quantity=Subquery(
CartItem.objects.filter(
user=request.user, product=OuterRef('pk')
).values('quantity')[:1]
)
)
paginator = Paginator(products, 20)
page = paginator.get_page(request.GET.get('page', 1))
return render(request, 'catalog.html', {'products': page})
Сгенерированный SQL абсолютно логичен - в нем есть примерно 5 различных подзапросов для корзины пользователя / списка пожеланий, вместо того чтобы просто выполнять левые объединения. Я запустил EXPLAIN ANALYZE, и PostgreSQL тратит 90% времени на эти подзапросы.
Что я пробовал до сих пор:
Добавлены индексы для всего (немного помогло) Пробовал только() и отложить() - никакой разницы
Переписал части на необработанном SQL - работает в 10 раз быстрее, но не достигает цели
Разбивка на несколько запросов - привет, проблема с N+1
Гуглил часами, ничего полезного не нашел
Самое странное, что если я удаляю пользовательские аннотации (in_cart, in_wishlist, cart_quantity), все выполняется быстро. Но мне нужны эти данные для интерфейса.
Мои вопросы:
Почему Django генерирует подзапросы вместо объединений для случаев / When?
Есть ли способ заставить Django использовать здесь объединения?
Я делаю что-то в корне неправильное с тем, как я структурирую этот запрос?
Должен ли я просто сдаться и использовать для этого необработанный SQL
Среда: Django 4.2, PostgreSQL 14 ~50 тыс. продуктов, ~200 тыс. отзывов Раньше это нормально работало с небольшими наборами данных
Я серьезно подумываю о переходе на raw SQL для этого представления, но я действительно не хочу терять все преимущества ORM. Кто-нибудь сталкивался с подобными проблемами производительности? Буду очень признателен за любые идеи!
РЕДАКТИРОВАТЬ: Только что попробовал удалить пользовательские методы диспетчера, и проблема не устранена, так что проблема определенно связана с пользовательскими аннотациями.
ПРАВКА 2: Для тех, кто спрашивает, да, у меня есть соответствующие индексы:
CREATE INDEX idx_product_category_active ON products_product(category_id, is_active);
CREATE INDEX idx_review_product_approved ON products_review(product_id, is_approved);
CREATE INDEX idx_cartitem_user_product ON products_cartitem(user_id, product_id);
-- etc.
Все еще чертовски медленно.