Подтвердить что ты не робот

Предотвратите запуск django admin SELECT COUNT (*) в форме списка

Каждый раз, когда я использую Admin для перечисления записей модели, Admin подсчитывает строки в таблице. Хуже того, похоже, что это происходит даже тогда, когда вы фильтруете свой запрос.

Например, если я хочу показать только модели с идентификатором 123, 456, 789, я могу сделать:

/admin/myapp/mymodel/?id__in=123,456,789

Но запущенные запросы (среди прочего):

SELECT COUNT(*) FROM `myapp_mymodel` WHERE `myapp_mymodel`.`id` IN (123, 456, 789) # okay
SELECT COUNT(*) FROM `myapp_mymodel` # why???

Что убивает mysql + innodb. Кажется, что проблема частично подтверждена в этом билете, но моя проблема кажется более конкретной, поскольку она подсчитывает все строки, даже если она не предполагается.

Есть ли способ отключить подсчет глобальных строк?

Примечание. Я использую django 1.2.7.

4b9b3361

Ответ 1

Хорошо, думаю, я нашел решение. Как предложил Питер, наилучшим подходом является работа над свойством count, и это можно сделать, переопределив его с помощью настраиваемого набора запросов (как показано в этот пост), который специализируется на подсчете с приблизительным эквивалентом:

from django.db import connections, models
from django.db.models.query import QuerySet

class ApproxCountQuerySet(QuerySet):
    """Counting all rows is very expensive on large Innodb tables. This
    is a replacement for QuerySet that returns an approximation if count()
    is called with no additional constraints. In all other cases it should
    behave exactly as QuerySet.

    Only works with MySQL. Behaves normally for all other engines.
    """

    def count(self):
        # Code from django/db/models/query.py

        if self._result_cache is not None and not self._iter:
            return len(self._result_cache)

        is_mysql = 'mysql' in connections[self.db].client.executable_name.lower()

        query = self.query
        if (is_mysql and not query.where and
                query.high_mark is None and
                query.low_mark == 0 and
                not query.select and
                not query.group_by and
                not query.having and
                not query.distinct):
            # If query has no constraints, we would be simply doing
            # "SELECT COUNT(*) FROM foo". Monkey patch so the we
            # get an approximation instead.
            cursor = connections[self.db].cursor()
            cursor.execute("SHOW TABLE STATUS LIKE %s",
                    (self.model._meta.db_table,))
            return cursor.fetchall()[0][4]
        else:
            return self.query.get_count(using=self.db)

Затем в admin:

class MyAdmin(admin.ModelAdmin):

    def queryset(self, request):
        qs = super(MyAdmin, self).queryset(request)
        return qs._clone(klass=ApproxCountQuerySet)

Приблизительная функция может испортить вещи на странице номер 100000, но она достаточно хороша для моего случая.

Ответ 3

Я нашел ответ Nova очень полезным, но я использую postgres. Я немного изменил его, чтобы работать для postgres с некоторыми небольшими изменениями для обработки пространств имен таблиц и немного отличающейся логики "обнаружить postgres".

Здесь версия pg.

class ApproxCountPgQuerySet(models.query.QuerySet):
  """approximate unconstrained count(*) with reltuples from pg_class"""

  def count(self):
      if self._result_cache is not None and not self._iter:
          return len(self._result_cache)

      if hasattr(connections[self.db].client.connection, 'pg_version'):
          query = self.query
          if (not query.where and query.high_mark is None and query.low_mark == 0 and
              not query.select and not query.group_by and not query.having and not query.distinct):
              # If query has no constraints, we would be simply doing
              # "SELECT COUNT(*) FROM foo". Monkey patch so the we get an approximation instead.
              parts = [p.strip('"') for p in self.model._meta.db_table.split('.')]
              cursor = connections[self.db].cursor()
              if len(parts) == 1:
                  cursor.execute("select reltuples::bigint FROM pg_class WHERE relname = %s", parts)
              else:
                  cursor.execute("select reltuples::bigint FROM pg_class c JOIN pg_namespace n on (c.relnamespace = n.oid) WHERE n.nspname = %s AND c.relname = %s", parts)
          return cursor.fetchall()[0][0]
      return self.query.get_count(using=self.db)

Ответ 4

Решение Nova (ApproxCountQuerySet) отлично работает, однако в новых версиях метода запросов Django был заменен get_queryset, поэтому теперь он должен быть:

class MyAdmin(admin.ModelAdmin):

    def get_queryset(self, request):
        qs = super(MyAdmin, self).get_queryset(request)
        return qs._clone(klass=ApproxCountQuerySet)

Ответ 5

Если это серьезная проблема, вам, возможно, придется принять Drastic Actions ™.

Глядя на код для установки 1.3.1, я вижу, что код администратора использует paginator, возвращаемый get_paginator(). Класс paginator по умолчанию находится в django/core/paginator.py. У этого класса есть частное значение _count, которое установлено в Paginator._get_count() (строка 120 в моей копии). Это, в свою очередь, используется для установки свойства класса Paginator под названием count. Я думаю, что _get_count() - ваша цель. Теперь этап установлен.

У вас есть несколько вариантов:

  • Непосредственно модифицируйте источник. Я не рекомендую это, но, поскольку вы, похоже, застряли в 1.2.7, вы можете обнаружить, что это наиболее целесообразно. Не забудьте зафиксировать это изменение! Будущие сопровождающие (в том числе, возможно, сами) будут благодарны вам за головы.

  • Monkeypatch класса. Это лучше, чем прямое изменение, потому что: а) если вам не нравится это изменение, вы просто закомментируете monkeypatch, и б) он с большей вероятностью будет работать с будущими версиями Django. У меня есть monkeypatch, возвращающийся на 4 года, потому что они все еще не исправили ошибку в коде шаблона _resolve_lookup(), который не распознает callables на верхнем уровне оценки, только на более низких уровнях. Хотя патч (который обертывает метод класса) был написан против 0.97-pre, он все еще работает на 1.3.1.

Я не тратил время на то, чтобы выяснить, какие изменения вам придется внести для вашей проблемы, но это может быть связано с добавлением члена _approx_count к соответствующим классам class META, а затем тестирования, чтобы узнать, что attr существует. Если это так и есть None, вы делаете sql.count() и устанавливаете его. Вам также может понадобиться reset, если вы находитесь на (или рядом) последней странице списка. Свяжитесь со мной, если вам потребуется дополнительная помощь по этому поводу; моя электронная почта находится в моем профиле.