SQLAlchemy: ¿Escanear enormes tablas usando ORM?

Actualmente estoy jugando con SQLAlchemy un poco, lo que es realmente muy bueno.

Para las pruebas, creé una tabla enorme que contenía mis archivos de imágenes, indexadas por hashes SHA1 (para eliminar duplicados :-)). Lo que fue impresionantemente rápido …

Por diversión hice el equivalente a un select * sobre la base de datos SQLite resultante:

 session = Session() for p in session.query(Picture): print(p) 

Esperaba ver los hashes desplazándose, pero en su lugar solo seguía escaneando el disco. Al mismo tiempo, el uso de la memoria se disparó, alcanzando 1 GB después de unos segundos. Esto parece provenir de la función de mapa de identidad de SQLAlchemy, que pensé que solo mantenía referencias débiles.

¿Alguien me puede explicar esto? ¡¿Pensé que cada imagen p se recolectaría después de escribir el hash ?!

Está bien, acabo de encontrar una manera de hacerlo yo mismo. Cambiando el código a

 session = Session() for p in session.query(Picture).yield_per(5): print(p) 

carga solo 5 imágenes a la vez. Parece que la consulta cargará todas las filas a la vez de forma predeterminada. Sin embargo, todavía no entiendo el descargo de responsabilidad en ese método. Cita de los documentos de SQLAlchemy

ADVERTENCIA: use este método con precaución; Si la misma instancia está presente en más de un lote de filas, los cambios de los usuarios finales a los atributos se sobrescribirán. En particular, generalmente es imposible usar esta configuración con colecciones cargadas con entusiasmo (es decir, cualquier flojo = Falso) ya que esas colecciones se borrarán para una nueva carga cuando se encuentren en un lote de resultados posterior.

Entonces, si usar yield_per es realmente la forma correcta ™ de analizar grandes cantidades de datos SQL mientras se usa el ORM, ¿cuándo es seguro usarlo?

Esto es lo que normalmente hago para esta situación:

 def page_query(q): offset = 0 while True: r = False for elem in q.limit(1000).offset(offset): r = True yield elem offset += 1000 if not r: break for item in page_query(Session.query(Picture)): print item 

Esto evita los diversos búferes que también hacen DBAPIs (como psycopg2 y MySQLdb). Aún debe usarse de manera adecuada si su consulta tiene JOIN explícitos, aunque las colecciones cargadas con entusiasmo están garantizadas para cargarse completamente ya que se aplican a una subconsulta que tiene el LÍMITE / DESPLAZAMIENTO real suministrado.

Me he dado cuenta de que Postgresql tarda casi tanto tiempo en devolver las últimas 100 filas de un gran conjunto de resultados, como lo hace en devolver el resultado completo (menos la sobrecarga real de obtención de filas), ya que OFFSET simplemente hace un análisis de todo el asunto.

Puede aplazar la imagen para recuperar solo en el acceso. Puede hacerlo sobre una base de consulta por consulta. me gusta

 session = Session() for p in session.query(Picture).options(sqlalchemy.orm.defer("picture")): print(p) 

o puedes hacerlo en el mapeador

 mapper(Picture, pictures, properties={ 'picture': deferred(pictures.c.picture) }) 

Cómo lo haces está en la documentación aquí.

Si lo hace de cualquier manera, se asegurará de que la imagen solo se cargue cuando acceda al atributo.