query - Evita que django admin ejecute SELECT COUNT(*) en el formulario de lista
django rawqueryset (6)
Cada vez que uso Admin para listar las entradas de un modelo, el Administrador cuenta las filas en la tabla. Peor aún, parece que lo está haciendo incluso cuando está filtrando su consulta.
Por ejemplo, si quiero mostrar solo los modelos cuya identificación es 123, 456, 789, puedo hacerlo:
/admin/myapp/mymodel/?id__in=123,456,789
Pero las consultas corridas (entre otras) son:
SELECT COUNT(*) FROM `myapp_mymodel` WHERE `myapp_mymodel`.`id` IN (123, 456, 789) # okay
SELECT COUNT(*) FROM `myapp_mymodel` # why???
Lo que está matando a mysql + innodb. Parece que el problema está parcialmente reconocido en este ticket , pero mi problema parece más específico, ya que cuenta todas las filas, incluso si no se supone que lo haga.
¿Hay una manera de deshabilitar ese recuento de filas globales?
Nota: Estoy usando django 1.2.7.
Django 1.8 le permite deshabilitar esto configurando show_full_result_count = False
.
Encontré la respuesta de Nova muy útil, pero uso postgres. Lo modifiqué ligeramente para que funcione para postgres con algunas alteraciones leves para manejar los espacios de nombres de tablas y una lógica de "detectar postgres" ligeramente diferente.
Aquí está la versión 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)
Es posible cambiar el paginador predeterminado usado por la clase admin. Aquí hay uno que almacena en caché el resultado por un corto período de tiempo: https://gist.github.com/e4c5/6852723
Está bien, creo que encontré una solución. Como sugirió Peter, el mejor enfoque es trabajar en la propiedad de count
y se puede hacer reemplazándola con un conjunto de consultas personalizadas (como se ve en esta publicación ) que especializa el conteo 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 desordenar las cosas en la página número 100000, pero es lo suficientemente bueno para mi caso.
La solución de Nova (ApproxCountQuerySet) funciona muy bien, sin embargo, en las versiones más recientes del método queryset de Django se reemplazó con get_queryset, por lo que ahora debería ser:
class MyAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super(MyAdmin, self).get_queryset(request)
return qs._clone(klass=ApproxCountQuerySet)
Si este es un problema grave, es posible que deba tomar Drastic Actions ™.
Mirando el código para una instalación 1.3.1, veo que el código de administración está usando el paginador devuelto por get_paginator()
. La clase de paginador predeterminada 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.
Tienes unas cuantas opciones:
Modificar directamente la fuente. No lo recomiendo, pero como parece que estás atascado en 1.2.7, puedes encontrar que es lo más conveniente. ¡Recuerde documentar este cambio! Los futuros mantenedores (incluido posiblemente usted mismo) le agradecerán por el aviso.
Monkeypatch la clase. Esto es mejor que la modificación directa porque a) si no te gusta el cambio, solo comentas el monkeypatch, yb) es más probable que funcione con futuras versiones de Django. Tengo un Monkeypatch que se remonta a más de 4 años porque aún no han corregido un error en la variable de plantilla
_resolve_lookup()
código que no reconoce callables en el nivel superior de evaluación, solo en niveles inferiores. Aunque el parche (que envuelve el método de una clase) fue escrito contra 0.97-pre, todavía funciona en 1.3.1.
No pasé el tiempo para averiguar exactamente qué cambios tendrías que hacer para tu problema, pero podría estar en la línea de agregar un miembro _approx_count
a la class META
apropiada de class META
y luego probar si ese attr existe. Si lo hace y es None
entonces debes hacer sql.count()
y configurarlo. Es posible que también deba restablecerlo si se encuentra en (o cerca) la última página de la lista. Ponte en contacto conmigo si necesitas un poco más de ayuda en esto; Mi correo electrónico está en mi perfil.