2012-05-03 15 views
18

Cada vez que utilizo Admin para listar las entradas de un modelo, el administrador cuenta las filas en la tabla. Peor aún, parece estar haciéndolo incluso cuando está filtrando su consulta.Evite que django admin ejecute SELECT COUNT (*) en el formulario de lista

Por ejemplo, si quiero mostrar sólo los modelos cuyo ID es 123, 456, 789 que puedo hacer:

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

Pero las consultas corrieron (entre otros) son:

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

Que está matando a mysql + innodb. Parece que el problema es parcialmente reconocido in this ticket, pero mi problema parece más específico ya que cuenta todas las filas, incluso si no se supone que lo haga.

¿Hay alguna forma de deshabilitar el recuento global de filas?

Nota: Estoy usando django 1.2.7.

+1

Cuando filtra se muestra, por ejemplo, "21 resultados (3011 en total)", por lo que se requiere el 'conteo (*)' para mostrar el total. Para desactivarlo, creo que vas a tener que hackear mucho la aplicación de administración django. – Alasdair

+0

Ahora veo, gracias. Supongo que tendremos que esperar a que llegue ese boleto, entonces. – Nova

+0

'Nota: Estoy usando django 1.2.7. - considere actualizar. –

Respuesta

18

De acuerdo, creo que encontré una solución.Como Peter sugirió, el mejor enfoque es trabajar en la propiedad count y se puede hacer por la sustituya por conjunto consulta personalizada (como se ve en this post) que se especializa el recuento con un equivalente aproximado:

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) 

Luego, en el administrador:

class MyAdmin(admin.ModelAdmin): 

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

La función aproximada podría estropear las cosas en la página número 100000, pero es lo suficientemente buena para mi caso.

+2

Gracias. Con un pequeño ajuste, esto también funcionó en PostgreSQL, aunque tuve que usar la consulta 'SELECT reltuples :: int FROM pg_class WHERE oid = '% s' :: regclass;' – Cerin

+4

Ya no funciona en 1.9 – LS55321

+0

@ LS55321 Any idea de cómo podemos usar ApproxCountQuerySet? – Jickson

3

Si se trata de un problema grave, es posible que deba tomar Drastic Actions ™.

Al observar el código de una instalación 1.3.1, veo que el código de administrador está utilizando el paginador devuelto por . La clase predeterminada de paginador parece estar en django/core/paginator.py. Esa clase tiene un valor privado llamado _count que se establece en Paginator._get_count() (línea 120 en mi copia). Esto a su vez se usa para establecer una propiedad de la clase Paginator llamada count. Creo que _get_count() es tu objetivo. Ahora el escenario está listo.

usted tiene un par de opciones:

  1. modificar directamente la fuente. Hago no recomiendo esto, pero dado que pareces estar estancado en 1.2.7, puede que encuentres que es el más conveniente. ¡Recuerde documentar este cambio! Los futuros mantenedores (incluso posiblemente usted) le agradecerán por el aviso.

  2. Monkeypatch the class. Esto es mejor que la modificación directa porque a) si no le gusta el cambio, solo comenta el monopatch, yb) es más probable que funcione con versiones futuras de Django. Tengo un monopatch que se remonta a más de 4 años porque todavía no se ha corregido un error en el código de variable de plantilla _resolve_lookup() que no reconoce los callables en el nivel superior de evaluación, solo en los niveles inferiores. Aunque el parche (que envuelve el método de una clase) se escribió contra 0.97-pre, todavía funciona en 1.3.1.

no pasé el tiempo para averiguar exactamente qué cambios habría que hacer para su problema, pero podría ser a lo largo de las líneas de la adición de un miembro de _approx_count a clases apropiadas class META y luego las pruebas para ver si ese attr existe. Si lo hace y es None, entonces haga el sql.count() y configúrelo. Es posible que también deba restablecerlo si está en (o cerca de) la última página de la lista. Contáctame si necesitas un poco más de ayuda en esto; Mi correo electrónico está en mi perfil.

7

Encontré la respuesta de Nova muy útil, pero uso postgres. Lo modifiqué un poco para que funcione en postgres con algunas ligeras modificaciones para manejar los espacios de nombres de tabla, y una lógica ligeramente diferente de "detectar postgres".

Aquí está la versión de 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) 
3

La solución de la Nova (ApproxCountQuerySet) funciona muy bien, sin embargo, en las versiones más recientes del método queryset Django obtuve reemplazado con get_queryset, por lo que ahora debe ser:

class MyAdmin(admin.ModelAdmin): 

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