У меня есть вопрос об оптимизации / производительности вокруг разбиения на страницы в Django для наборов запросов, которые используют select_related и annotate ro, чтобы уменьшить количество запросов.
В моем реальном случае гораздо больше моделей, но ради Простота. Я показываю упрощенную версию здесь.
class Product(models.Model):
name = models.CharField(max_length=100)
type = models.CharField(max_lenth=25)
owner = models.ForeignKey(Owner)
class Feature(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
class Author(models.Model):
name = models.CharField(max_length=100)
class Owner(models.Model):
name = models.CharField(max_length=100)
Если я выведу список или продукты только с названием продукта, этого будет достаточно:
products = Products.objects.all()
И я могу разбить на страницы Django встроенная нумерация страниц:
paginator = Paginator(products, 25)
page = paginator.get_page(0) # to render the first page.
Это работает, как ожидается, и генерирует 2 SQL запросов. Один для подсчета всех записей, другой для получения первой страницы с использованием LIMIT и OFFSET.
Запрос на подсчет выглядит следующим образом:
SELECT COUNT(*)
FROM PRODUCT
Все хорошо. Теперь, когда я добавляю несколько предложений select_related и annotate, запрос на подсчет становится (очень) неэффективным.
products = Products.objects.all().select_related("owner") \
.annotate(feature_count=Count('feature__name', filter=Q(feature__author__name='opper')))
Это, очевидно, приведет к созданию некоторых соединений LEFT OUTER и операторов count / case, что снова приведет к 2 запросам
Запрос 1:
SELECT COUNT(*)
FROM (
SELECT product.name AS Col1,
COUNT(CASE WHEN author.name = 'opper' THEN feature.name ELSE NULL END) AS feature_count,
FROM product
LEFT OUTER JOIN feature
ON (product.name = feature.product)
LEFT OUTER JOIN author
ON (feature.author = author.name)
LEFT OUTER JOIN owner
ON (product.owner = owner.name)
GROUP BY product.name
ORDER BY NULL
) subquery
Запрос 2:
SELECT product.*,
COUNT(CASE WHEN author.name = 'opper' THEN feature.name ELSE NULL END) AS feature_count,
FROM product
LEFT OUTER JOIN feature
ON (product.name = feature.product)
LEFT OUTER JOIN author
ON (feature.author = author.name)
LEFT OUTER JOIN owner
ON (product.owner = owner.name)
LIMIT 25 OFFSET 0
Запрос 2 в порядке, поскольку он позволяет получить все связанные данные и счет в одном запросе и избежать N + 1 проблема с запросом при циклическом просмотре всех продуктов в представлении / рендеринге.
Однако Query 1 не так хорош, как теперь объединяет все таблицы перед подсчетом, что медленнее.
Насколько я вижу, первый запрос может быть оптимизирован так, чтобы он был просто:
SELECT COUNT(*)
FROM PRODUCT
Это приведет к тому же значению счетчика, поскольку все соединения являются ЛЕВЫМИ ВНЕШНИМИ СОЕДИНЕНИЯМИ по дизайну.
Есть ли способ избежать этих ненужных объединений? Я взломал свой проект, чтобы избежать их, но мне нужен более структурированный подход. Может быть, создать реализацию Paginator, которая использует для запроса подсчета сокращенный запрос.
Есть мысли о том, имеет ли это смысл?
Может ли это быть результатом объединения и аннотирования в подсчете? запрос маленький? Здесь я провел очень ограниченный тест, но даже с 150 продуктами запрос на подсчет уже в 6 раз медленнее.