Django запрос количества страниц разбит на медленный - PullRequest
0 голосов
/ 02 мая 2020

У меня есть вопрос об оптимизации / производительности вокруг разбиения на страницы в 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 раз медленнее.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...