Tengo una tabla MySQL de registros de ~ 10M con la que interactúo usando SqlAlchemy. Descubrí que las consultas en grandes subconjuntos de esta tabla consumirán demasiada memoria a pesar de que pensé que estaba usando un generador incorporado que obtenía inteligentemente fragmentos del tamaño de un bocado del conjunto de datos:
for thing in session.query(Things):
analyze(thing)
Para evitar esto, encuentro que tengo que construir mi propio iterador que muerde en trozos:
lastThingID = None
while True:
things = query.filter(Thing.id < lastThingID).limit(querySize).all()
if not rows or len(rows) == 0:
break
for thing in things:
lastThingID = row.id
analyze(thing)
¿Es esto normal o hay algo que me falta con respecto a los generadores integrados de SA?
La respuesta a esta pregunta parece indicar que el consumo de memoria no es de esperar.
python
mysql
sqlalchemy
Paul
fuente
fuente
Respuestas:
La mayoría de las implementaciones de DBAPI almacenan en búfer las filas a medida que se obtienen; por lo general, antes de que el ORM de SQLAlchemy obtenga un resultado, todo el conjunto de resultados está en la memoria.
Pero entonces, la forma en que
Query
funciona es que carga completamente el conjunto de resultados dado de forma predeterminada antes de devolverle sus objetos. El fundamento aquí se refiere a las consultas que son más que simples declaraciones SELECT. Por ejemplo, en las uniones a otras tablas que pueden devolver la misma identidad de objeto varias veces en un conjunto de resultados (común con la carga ansiosa), el conjunto completo de filas debe estar en la memoria para que los resultados correctos se puedan devolver, de lo contrario, colecciones y tal podría estar solo parcialmente poblado.Entonces
Query
ofrece una opción para cambiar este comportamiento a través deyield_per()
. Esta llamada haráQuery
que produzca filas en lotes, donde le da el tamaño del lote. Como dicen los documentos, esto solo es apropiado si no está haciendo ningún tipo de carga ansiosa de colecciones, por lo que básicamente es si realmente sabe lo que está haciendo. Además, si el DBAPI subyacente almacena previamente las filas en el búfer, seguirá existiendo esa sobrecarga de memoria, por lo que el enfoque solo se escala ligeramente mejor que no usarlo.Casi nunca lo uso
yield_per()
; en cambio, utilizo una mejor versión del enfoque LIMIT que sugieres anteriormente usando funciones de ventana. LIMIT y OFFSET tienen un gran problema de que los valores de OFFSET muy grandes hacen que la consulta se vuelva cada vez más lenta, ya que un OFFSET de N hace que recorra N filas; es como hacer la misma consulta cincuenta veces en lugar de una, cada vez que lee un mayor y mayor número de filas. Con un enfoque de función de ventana, obtengo previamente un conjunto de valores de "ventana" que se refieren a partes de la tabla que quiero seleccionar. Luego emito declaraciones SELECT individuales que cada una extrae de una de esas ventanas a la vez.El enfoque de la función de ventana está en la wiki y lo uso con gran éxito.
También tenga en cuenta: no todas las bases de datos admiten funciones de ventana; necesita Postgresql, Oracle o SQL Server. En mi humilde opinión, usar al menos Postgresql definitivamente vale la pena: si está usando una base de datos relacional, también podría usar la mejor.
fuente
No soy un experto en bases de datos, pero cuando uso SQLAlchemy como una simple capa de abstracción de Python (es decir, no uso el objeto de consulta ORM), se me ocurrió una solución satisfactoria para consultar una tabla de 300M de filas sin aumentar el uso de memoria ...
Aquí hay un ejemplo ficticio:
from sqlalchemy import create_engine, select conn = create_engine("DB URL...").connect() q = select([huge_table]) proxy = conn.execution_options(stream_results=True).execute(q)
Luego, uso el
fetchmany()
método SQLAlchemy para iterar sobre los resultados en unwhile
bucle infinito :while 'batch not empty': # equivalent of 'while True', but clearer batch = proxy.fetchmany(100000) # 100,000 rows at a time if not batch: break for row in batch: # Do your stuff here... proxy.close()
Este método me permitió hacer todo tipo de agregación de datos sin ninguna sobrecarga de memoria peligrosa.
NOTE
elstream_results
trabaja con PostgreSQL y elpyscopg2
adaptador, pero supongo que no va a funcionar con cualquier DBAPI, ni con cualquier controlador de base de datos ...Hay un caso de uso interesante en esta publicación de blog que inspiró mi método anterior.
fuente
pymysql
), esta debería ser la respuesta aceptada en mi humilde opinión.He estado buscando un recorrido / paginación eficiente con SQLAlchemy y me gustaría actualizar esta respuesta.
Creo que puede usar la llamada de segmento para limitar adecuadamente el alcance de una consulta y podría reutilizarla de manera eficiente.
Ejemplo:
window_size = 10 # or whatever limit you like window_idx = 0 while True: start,stop = window_size*window_idx, window_size*(window_idx+1) things = query.slice(start, stop).all() if things is None: break for thing in things: analyze(thing) if len(things) < window_size: break window_idx += 1
fuente
.all()
sea necesario. Noto que la velocidad mejoró mucho después de la primera llamada..all()
la variable de cosas hay una consulta que no admite len ()En el espíritu de la respuesta de Joel, utilizo lo siguiente:
WINDOW_SIZE = 1000 def qgen(query): start = 0 while True: stop = start + WINDOW_SIZE things = query.slice(start, stop).all() if len(things) == 0: break for thing in things: yield thing start += WINDOW_SIZE
fuente
Usar LIMIT / OFFSET es malo, porque necesita encontrar todas las columnas {OFFSET} antes, por lo que cuanto más grande es OFFSET, la solicitud más larga obtiene. El uso de la consulta en ventana para mí también da malos resultados en una tabla grande con una gran cantidad de datos (espera los primeros resultados durante demasiado tiempo, eso no es bueno en mi caso para una respuesta web fragmentada).
El mejor enfoque dado aquí https://stackoverflow.com/a/27169302/450103 . En mi caso, resolví el problema simplemente usando el índice en el campo de fecha y hora y obteniendo la siguiente consulta con datetime> = previous_datetime. Estúpido, porque usé ese índice en diferentes casos antes, pero pensé que para obtener todos los datos, la consulta en ventana sería mejor. En mi caso me equivoqué.
fuente
AFAIK, la primera variante aún obtiene todas las tuplas de la tabla (con una consulta SQL) pero construye la presentación ORM para cada entidad cuando se itera. Por lo tanto, es más eficiente que crear una lista de todas las entidades antes de iterar, pero aún debe recuperar todos los datos (sin procesar) en la memoria.
Por lo tanto, usar LIMIT en tablas enormes me parece una buena idea.
fuente