Entender mejor los problemas de rendimiento del rendimiento de SQLalchemy

Para citar la documentación de SQLalchemy :

El método Query.yield_per () no es compatible con la mayoría de los esquemas de carga impacientes, incluida la subconsulta y la carga con colecciones.

Advertencia

Utilice 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 = ‘unido’ o ‘subconsulta’) ya que esas colecciones se borrarán para una nueva carga cuando se encuentren en un lote de resultados posterior. En el caso de carga de ‘subconsulta’, se obtiene el resultado completo de todas las filas, lo que generalmente anula el propósito de yield_per ().

También tenga en cuenta que mientras yield_per () configurará la opción de ejecución de stream_results en True, actualmente esto solo lo entiende el dialecto psycopg2, que transmitirá los resultados utilizando los cursores del lado del servidor en lugar del búfer previo a todas las filas para esta consulta. Otros DBAPI pre-almacenan en memoria todas las filas antes de que estén disponibles. El uso de memoria de las filas de la base de datos sin procesar es mucho menor que el de un objeto mapeado por ORM, pero aún debe tenerse en cuenta cuando se hace una evaluación comparativa.

Realmente tengo un problema para entender cómo yield_per() rendimiento de yield_per() y cuál es exactamente el problema al usar este método. Además, ¿cuál es la forma correcta de solucionar estos problemas y seguir utilizando esta función para recorrer una gran cantidad de filas?

Estoy interesado en toda la información constructiva que tiene, pero aquí hay algunas preguntas de sugerencias:

  • ¿Cómo puede haber múltiples instancias de la misma fila? ¿Solo a través de relaciones (si dos filas de la tabla de iteración tienen un FK a la misma fila en otra tabla)? ¿Hay algún problema si no sabe que sucede o solo lee los atributos de las relaciones?
  • lazy = ‘join’ o ‘subquery’ no son posibles, pero ¿por qué exactamente? Ambos son simplemente partes de su consulta en la que llama a yield_per() .
    • Si se borran en un lote de resultados posterior, simplemente vuelva a cargarlo. ¿Entonces, dónde está el problema? ¿O es el único problema que pierde los cambios de sus relaciones si ha realizado cambios?
    • En el caso de una carga de ‘subconsulta’, ¿por qué se recuperan todas las filas? El servidor SQL puede tener que guardar una tabla grande, pero ¿por qué no devolver simplemente el resultado en lotes uno tras otro para toda la consulta?
    • En un ejemplo en el yield_per() doc q = sess.query(Object).yield_per(100).options(lazyload('*'), joinedload(Object.some_related)) desactivan eagerload con lazyload('*') pero mantener una sola carga unida. ¿Hay alguna manera de seguir utilizando yield_per() con eagerload? ¿Cuales son las condiciones?
  • Dicen que psycopg2 es el único DBAPI que soporta resultados de flujo. Entonces, ¿ese es el único DBAPI que puede usar con yield_per() ? Según tengo entendido, yield_per usa la función cursor.fetchmany() ( ejemplo ) de DBAPI que admite muchos de ellos. Y, por lo que entiendo, cursor.fetchmany() admite la cursor.fetchmany() solo partes del resultado y no lo hace todo (si fuera a buscar todo, ¿por qué existe la función?)
  • Tengo la sensación de que yield_per() es completamente seguro (incluso con eagerload) si solo tiene acceso de lectura (por ejemplo, para estadísticas). ¿Es eso correcto?

Las dos estrategias de carga problemáticas yield_per excepciones si intenta usarlas con yield_per , por lo que no tiene que preocuparse demasiado.

Creo que el único problema con la subqueryload es que la carga por lotes de la segunda consulta aún no está implementada. Nada saldría mal semánticamente, pero si está utilizando yield_per , probablemente tenga una buena razón para no querer cargar todos los resultados a la vez. Así que SQLAlchemy se niega cortésmente a ir en contra de sus deseos.

joinedload es un poco más sutil. Solo está prohibido en el caso de una colección, donde una fila primaria puede tener varias filas asociadas. Digamos que su consulta produce resultados en bruto como este, donde A y B son claves principales de diferentes tablas:

  A | B ---+--- 1 | 1 1 | 2 1 | 3 1 | 4 2 | 5 2 | 6 

Ahora yield_per(3) estos con yield_per(3) . El problema es que SQLAlchemy solo puede limitar la cantidad que obtiene por filas , pero tiene que devolver objetos . Aquí, SQLAlchemy solo ve las primeras tres filas, por lo que crea un objeto A con clave 1 y tres hijos B : 1, 2 y 3.

Cuando carga el siguiente lote, desea crear un nuevo objeto A con la tecla 1 … ah, pero ya tiene uno de ellos, por lo que no es necesario volver a crearlo. El extra B , 4, se pierde. (Por lo tanto, no, incluso la lectura de colecciones unidas con yield_per no es segura, puede que falten fragmentos de sus datos).

Podría decir “bueno, simplemente siga leyendo filas hasta que tenga un objeto completo”, pero ¿y si ese A tiene cien hijos? ¿O un millón? SQLAlchemy no puede garantizar razonablemente que pueda hacer lo que usted pidió y producir resultados correctos, por lo que se niega a intentarlo.


Recuerde que el DBAPI está diseñado para que cualquier base de datos se pueda utilizar con la misma API, incluso si esa base de datos no admite todas las funciones de DBAPI. Tenga en cuenta que la DBAPI está diseñada alrededor de los cursores, ¡pero MySQL en realidad no tiene cursores! Los adaptadores DBAPI para MySQL tienen que falsificarlos, en su lugar.

Entonces, mientras el cursor.fetchmany(100) funcionará , puede ver en el código fuente de MySQLdb que no se extrae perezosamente del servidor; reúne todo en una gran lista, luego devuelve una porción cuando llama a fetchmany .

Lo que admite psycopg2 es la transmisión real, donde los resultados se recuerdan de forma persistente en el servidor, y el proceso de Python solo ve algunos de ellos a la vez.

Aún puedes usar yield_per con MySQLdb , o cualquier otro DBAPI; Ese es el punto central del diseño de DBAPI. Tendrá que pagar el costo de la memoria para todas las filas en bruto ocultas en el DBAPI (que son tuplas, bastante baratas), pero no tendrá que pagar todos los objetos ORM al mismo tiempo.