2011-01-31 17 views
21

Tengo una tarea que debe ejecutarse en la mayoría de los objetos en mi base de datos cada cierto período de tiempo (una vez al día, una vez por semana, lo que sea). Básicamente esto significa que tengo una consulta que se parece a esto ejecutándose en su propio hilo.Limitar la memoria Usar en un * Large * Django QuerySet

for model_instance in SomeModel.objects.all(): 
    do_something(model_instance) 

(Tenga en cuenta que en realidad es un filtro() no todos(), pero ninguno de los de menos todavía terminan la selección de un muy grande conjunto de objetos.)

El problema que estoy Me encuentro con que, después de correr por un tiempo, mi proveedor de alojamiento elimina el hilo porque estoy usando demasiada memoria. Estoy asumiendo todo este uso de la memoria está sucediendo porque aunque el objeto QuerySet devuelto por mi consulta inicialmente tiene una huella de memoria muy pequeña, termina creciendo como el objeto QuerySet almacena en caché cada model_instance al iterar a través de ellos.

Mi pregunta es, "¿cuál es la mejor manera de iterar a través de casi SomeModel en mi base de datos de una manera eficiente de la memoria?" o tal vez mi pregunta es "¿cómo puedo 'anular la caché' de las instancias del modelo desde un django queryset?"

EDITAR: Estoy usando los resultados del conjunto de preguntas para construir una serie de objetos nuevos. Como tal, no termino de actualizar los objetos consultados.

+0

Deberá proporcionar alguna pista sobre lo que está haciendo con el conjunto de preguntas. Django tiene reglas, y varias operaciones requieren cargar todo el QuerySet en la memoria, donde otras operaciones simplemente procesan las filas de una en una. http://docs.djangoproject.com/en/1.2/topics/db/queries/#querysets-are-lazy. Proporcione alguna pista sobre cómo está utilizando sus objetos QuerySet. –

+0

Lo siento, debería especificar que estoy usando la información de los objetos QuerySet para crear objetos nuevos (de un tipo diferente). Así que nunca actualizo los objetos que estoy buscando. –

Respuesta

16

Entonces, lo que en realidad terminé haciendo es construir algo en lo que pueda 'envolver' un QuerySet. Funciona al hacer una copia profunda del QuerySet, usando la sintaxis del segmento -por ejemplo, some_queryset[15:45]- pero luego hace otro copia en profundidad del QuerySet original cuando el segmento ha sido completamente iterado. Esto significa que solo el conjunto de Objetos devuelto en 'este' segmento en particular se almacena en la memoria.

class MemorySavingQuerysetIterator(object): 

    def __init__(self,queryset,max_obj_num=1000): 
     self._base_queryset = queryset 
     self._generator = self._setup() 
     self.max_obj_num = max_obj_num 

    def _setup(self): 
     for i in xrange(0,self._base_queryset.count(),self.max_obj_num): 
      # By making a copy of of the queryset and using that to actually access 
      # the objects we ensure that there are only `max_obj_num` objects in 
      # memory at any given time 
      smaller_queryset = copy.deepcopy(self._base_queryset)[i:i+self.max_obj_num] 
      logger.debug('Grabbing next %s objects from DB' % self.max_obj_num) 
      for obj in smaller_queryset.iterator(): 
       yield obj 

    def __iter__(self): 
     return self 

    def next(self): 
     return self._generator.next() 

Así que en lugar de ...

for obj in SomeObject.objects.filter(foo='bar'): <-- Something that returns *a lot* of Objects 
    do_something(obj); 

Harías ...

for obj in MemorySavingQuerysetIterator(in SomeObject.objects.filter(foo='bar')): 
    do_something(obj); 

Tenga en cuenta que la intención de esto es Almacenar memoria en su Python intérprete. Básicamente hace esto al hacer más consultas de la base de datos. Por lo general, las personas intentan hacer exactamente lo contrario, es decir, minimizar las consultas de la base de datos tanto como sea posible sin importar el uso de la memoria. Espero que alguien lo encuentre útil sin embargo.

+1

Esto usa LIMIT y OFFSET, por lo que crece extremadamente lento a medida que aumenta la compensación ... – Stefano

3

Estoy continuar la investigación y como que se parece a lo que quiero hacer el equivalente de un SQL OFFSET y LIMIT, que según Django Doc's on Limiting Querysets significa que quiero usar la sintaxis rebanada, por ejemplo, SomeModel.objects.all()[15:25]

Así ahora estoy pensando que tal vez algo como esto es lo que estoy buscando:

# Figure out the number of objects I can safely hold in memory 
# I'll just say 100 for right now 
number_of_objects = 100 
count = SomeModel.objects.all().count(): 
for i in xrange(0,count,number_of_objects): 
    smaller_queryset = SomeModel.objects.all()[i:i+number_of_objects] 
    for model_instance in smaller_queryset: 
     do_something(model_instance) 

Según mis cálculos que esto haría de manera que smaller_queryset no crecería demasiado grande.

11

No puede simplemente usar Model.objects.all(). Iterator() porque obtendrá todos los elementos en su tabla a la vez. Tampoco puede ir simplemente con el modo Model.objects.all() [offset: offset + pagesize], ya que captará sus resultados.Cualquiera de esos excederá tu límite de memoria.

He intentado mezclar ambas soluciones, y funcionó:

offset = 0 
pagesize = 1000 
count = Model.objects.all().count() 
while offset < count: 
    for m in Model.objects.all()[offset : offset + pagesize].iterator: 
     do_something with m 
    offset += pagesize 

Cambiar tamaño de página para satisfacer sus necesidades, y Opcionalmente cambiar el [offset: desplazamiento + tamaño de página] al [compensado * tamaño de página: (offset + 1) * pagesize] idioma si te queda mejor. Además, por supuesto, reemplace el modelo por su nombre de modelo real.

+2

¿Qué quiere decir con "ver sus resultados"? – Divick

+0

caché Asumiría –

3

Hay un fragmento de Django para esto:

http://djangosnippets.org/snippets/1949/

Se itera sobre un conjunto de consultas al ceder filas de "trozos" más pequeños de la queryset originales. Termina usando significativamente menos memoria mientras te permite sintonizar la velocidad. Lo uso en uno de mis proyectos.

4

Qué acerca del uso Paginator del núcleo de Django y objetos de la página documentado aquí:

https://docs.djangoproject.com/en/dev/topics/pagination/

Algo como esto:

from django.core.paginator import Paginator 
from djangoapp.models import SomeModel 

paginator = Paginator(SomeModel.objects.all(), 1000) # chunks of 1000 

for page_idx in range(1, paginator.num_pages): 
    for row in paginator.page(page_idx).object_list: 
     # here you can do what you want with the row 
    print "done processing page %s" % page_idx 
7

Muchas soluciones implementan SQL OFFSET y LIMIT a través de cortar el conjunto de consultas. Como notas stefano, con conjuntos de datos más grandes esto se vuelve muy ineficiente. La forma correcta de manejar esto es usar cursers del lado del servidor para realizar un seguimiento del OFFSET.

El soporte nativo del cursor del lado del servidor es in the works for django. Hasta que esté listo, aquí es una aplicación simple, si usted está utilizando postgres con el backend psycopg2:

def server_cursor_query(Table): 
    table_name = Table._meta.db_table 

    # There must be an existing connection before creating a server-side cursor 
    if connection.connection is None: 
     dummy_cursor = connection.cursor() # not a server-side cursor 

    # Optionally keep track of the columns so that we can return a QuerySet. However, 
    # if your table has foreign keys, you may need to rename them appropriately 
    columns = [x.name for x in Table._meta.local_fields] 

    cursor = connection.connection.cursor(name='gigantic_cursor')) # a server-side 
                    # cursor 

    with transaction.atomic(): 
     cursor.execute('SELECT {} FROM {} WHERE id={}'.format(
      ', '.join(columns), table_name, id)) 

     while True: 
      rows = cursor.fetchmany(1000) 

       if not rows: 
        break 

       for row in rows: 
        fields = dict(zip(columns, row)) 
        yield Table(**fields) 

Ver this blog post para una gran explicación de los problemas de memoria a partir de consultas de gran tamaño en Django.

+0

Big +1 para usar las herramientas correctas para el trabajo. Sería grandioso ver el apoyo en el ORM de Django de hecho. Por cierto, si no te importa hurgar un poco en el interior, puedes invocar 'sql, params = queryset.query.get_compiler (using = queryset.db) .as_sql()' para obtener la consulta SQL de un queryset . Y debe usar Table.from_db para convertirlo en una instancia real en versiones recientes de Django. – spectras

+0

@spectras, '.as_sql()' parece muy útil. Sin embargo, necesitaría obtener los nombres de los campos en el mismo orden que la instrucción '.as_sql()' 'SELECT' para crear la instancia de tabla al final. ¿Hay alguna manera de hacerlo sin analizar '.as_sql()' manualmente? – drs

+1

Bueno, sí, si no te importa hurgar, pongo esto aquí: https://gist.github.com/spectras/f22d303088e4b2c498de Si lo usas, te recomiendo que configures algunas pruebas para facilitar Actualizaciones de Django. También se podría agregar soporte para select_related(), eso sería 3 líneas, pero un enlace adicional a otro ORM interno así que ... – spectras

Cuestiones relacionadas