sql - queryset - orm django queries
¿Por qué iterar a través de un gran Django QuerySet consume grandes cantidades de memoria? (9)
Aquí una solución que incluye len y count:
class GeneratorWithLen(object):
"""
Generator that includes len and count for given queryset
"""
def __init__(self, generator, length):
self.generator = generator
self.length = length
def __len__(self):
return self.length
def __iter__(self):
return self.generator
def __getitem__(self, item):
return self.generator.__getitem__(item)
def next(self):
return next(self.generator)
def count(self):
return self.__len__()
def batch(queryset, batch_size=1024):
"""
returns a generator that does not cache results on the QuerySet
Aimed to use with expected HUGE/ENORMOUS data sets, no caching, no memory used more than batch_size
:param batch_size: Size for the maximum chunk of data in memory
:return: generator
"""
total = queryset.count()
def batch_qs(_qs, _batch_size=batch_size):
"""
Returns a (start, end, total, queryset) tuple for each batch in the given
queryset.
"""
for start in range(0, total, _batch_size):
end = min(start + _batch_size, total)
yield (start, end, total, _qs[start:end])
def generate_items():
queryset.order_by() # Clearing... ordering by id if PK autoincremental
for start, end, total, qs in batch_qs(queryset):
for item in qs:
yield item
return GeneratorWithLen(generate_items(), total)
Uso:
events = batch(Event.objects.all())
len(events) == events.count()
for event in events:
# Do something with the Event
La tabla en cuestión contiene aproximadamente diez millones de filas.
for event in Event.objects.all():
print event
Esto hace que el uso de la memoria aumente de manera constante a 4 GB aproximadamente, momento en el que las filas se imprimen rápidamente. El largo retraso antes de la impresión de la primera fila me sorprendió, esperaba que se imprimiera casi al instante.
También probé Event.objects.iterator()
que se comportó de la misma manera.
No entiendo qué está cargando Django en la memoria o por qué está haciendo esto. Esperaba que Django repitiera los resultados en el nivel de la base de datos, lo que significaría que los resultados se imprimirían aproximadamente a una tasa constante (en lugar de todos a la vez después de una larga espera).
¿Qué he entendido mal?
(No sé si es relevante, pero estoy usando PostgreSQL).
Django no tiene una buena solución para recuperar elementos grandes de la base de datos.
import gc
# Get the events in reverse order
eids = Event.objects.order_by("-id").values_list("id", flat=True)
for index, eid in enumerate(eids):
event = Event.object.get(id=eid)
# do necessary work with event
if index % 100 == 0:
gc.collect()
print("completed 100 items")
values_list se puede utilizar para buscar todos los identificadores en las bases de datos y luego buscar cada objeto por separado. Durante un tiempo, los objetos grandes se crearán en la memoria y no se recolectarán como basura hasta que se salga el ciclo. El código anterior hace la recolección manual de basura después de consumir cada centésimo elemento.
El comportamiento predeterminado de Django es almacenar en caché todo el resultado del QuerySet cuando evalúa la consulta. Puede usar el método de iterador de QuerySet para evitar este almacenamiento en caché:
for event in Event.objects.all().iterator():
print event
https://docs.djangoproject.com/en/dev/ref/models/querysets/#iterator
El método iterator () evalúa el queryset y luego lee los resultados directamente sin hacer caching en el nivel QuerySet. Este método da como resultado un mejor rendimiento y una reducción significativa de la memoria al iterar sobre una gran cantidad de objetos a los que solo debe acceder una vez. Tenga en cuenta que el almacenamiento en caché aún se realiza en el nivel de la base de datos.
Usar iterator () reduce el uso de memoria para mí, pero aún es más alto de lo que esperaba. Usar el enfoque de paginador sugerido por mpaf usa mucha menos memoria, pero es 2-3 veces más lento para mi caso de prueba.
from django.core.paginator import Paginator
def chunked_iterator(queryset, chunk_size=10000):
paginator = Paginator(queryset, chunk_size)
for page in range(1, paginator.num_pages + 1):
for obj in paginator.page(page).object_list:
yield obj
for event in chunked_iterator(Event.objects.all()):
print event
Esto es de los documentos: http://docs.djangoproject.com/en/dev/ref/models/querysets/
Ninguna actividad de base de datos ocurre realmente hasta que haga algo para evaluar el conjunto de preguntas.
Por lo tanto, cuando se ejecuta el print event
la consulta se activa (que es una exploración de tabla completa de acuerdo con su comando) y carga los resultados. Estás pidiendo todos los objetos y no hay forma de obtener el primer objeto sin obtenerlos todos.
Pero si haces algo como:
Event.objects.all()[300:900]
http://docs.djangoproject.com/en/dev/topics/db/queries/#limiting-querysets
Luego agregará compensaciones y límites al sql internamente.
Nate C estaba cerca, pero no del todo.
De los documentos :
Puede evaluar un QuerySet de las siguientes maneras:
Iteración. Un QuerySet es iterable y ejecuta su consulta de base de datos la primera vez que itera sobre él. Por ejemplo, esto imprimirá el título de todas las entradas en la base de datos:
for e in Entry.objects.all(): print e.headline
Entonces, se recuperan tus diez millones de filas, todas a la vez, cuando ingresas por primera vez a ese ciclo y obtienes la forma iterativa del conjunto de preguntas. La espera que experimenta es que Django carga las filas de la base de datos y crea objetos para cada una, antes de devolver algo sobre lo que realmente puede iterar. Entonces tienes todo en la memoria y los resultados se derraman.
Desde mi lectura de los documentos, iterator()
no hace más que pasar por alto los mecanismos de caché interno de QuerySet. Creo que podría tener sentido hacerlo uno por uno, pero eso requeriría diez millones de visitas individuales en su base de datos. Quizás no sea tan deseable.
Iterando sobre grandes conjuntos de datos de manera eficiente es algo que todavía no hemos acertado, pero hay algunos fragmentos que pueden ser útiles para sus propósitos:
Para grandes cantidades de registros, un cursor de base de datos funciona aún mejor. Necesitas SQL sin formato en Django, el cursor Django es algo diferente a un cursur SQL.
El método LIMIT - OFFSET sugerido por Nate C podría ser lo suficientemente bueno para su situación. Para grandes cantidades de datos, es más lento que un cursor porque tiene que ejecutar la misma consulta una y otra vez y tiene que saltar sobre más y más resultados.
Porque de esa forma los objetos de un conjunto de preguntas completo se cargan en la memoria de una sola vez. Debe dividir su conjunto de preguntas en bits más pequeños y digeribles. El patrón para hacer esto se llama alimentación con cuchara. Aquí hay una breve implementación.
def spoonfeed(qs, func, chunk=1000, start=0):
'''''' Chunk up a large queryset and run func on each item.
Works with automatic primary key fields.
chunk -- how many objects to take on at once
start -- PK to start from
>>> spoonfeed(Spam.objects.all(), nom_nom)
''''''
while start < qs.order_by(''pk'').last().pk:
for o in qs.filter(pk__gt=start, pk__lte=start+chunk):
yeild func(o)
start += chunk
Para usar esto, escribe una función que realice operaciones en tu objeto:
def set_population_density(town):
town.population_density = calculate_population_density(...)
town.save()
y que ejecutar esa función en su queryset:
spoonfeed(Town.objects.all(), set_population_density)
Esto se puede mejorar con multiprocesamiento para ejecutar func
en múltiples objetos en paralelo.
Puede que no sea la más rápida o la más eficiente, pero como solución ya hecha, ¿por qué no utilizar los objetos Paginator y Página de django core documentados aquí?
https://docs.djangoproject.com/en/dev/topics/pagination/
Algo como esto:
from django.core.paginator import Paginator
from djangoapp.models import model
paginator = Paginator(model.objects.all(), 1000) # chunks of 1000, you can
# change this to desired chunk size
for page in range(1, paginator.num_pages + 1):
for row in paginator.page(page).object_list:
# here you can do whatever you want with the row
print "done processing page %s" % page
Usualmente utilizo consultas crudas de MySQL en lugar de Django ORM para este tipo de tarea.
MySQL admite el modo de transmisión, por lo que podemos recorrer todos los registros de forma segura y rápida sin un error de falta de memoria.
import MySQLdb
db_config = {} # config your db here
connection = MySQLdb.connect(
host=db_config[''HOST''], user=db_config[''USER''],
port=int(db_config[''PORT'']), passwd=db_config[''PASSWORD''], db=db_config[''NAME''])
cursor = MySQLdb.cursors.SSCursor(connection) # SSCursor for streaming mode
cursor.execute("SELECT * FROM event")
while True:
record = cursor.fetchone()
if record is None:
break
# Do something with record here
cursor.close()
connection.close()
Árbitro: