How to optimize an Object Relational Mapper (ORM)?

By now, only Spanish version is available. Let me know if you are interested in the English one and I will make an effort to translate | Original version from the Deloitte Spain site.

Como usted ya puede suponer si leyó los artículos anteriores de esta serie, la principal desventaja del uso de ORMs es que uno tiende a desentenderse y, en consecuencia, desconocer la magia generada por el ORM para consultar y persistir datos en el RDBMS, dando por hecho que éste no sólo será capaz de cumplir con las acciones requeridas sino de hacerlo de forma eficiente. Esto es un grave error en aplicaciones complejas en las que el rendimiento es un factor clave. Sólo supervisando y entendiendo bien esta magia que hay por debajo de los ORMs nos permitirá optimizar el rendimiento de nuestras aplicaciones.

Para cualquier ORM, la generación eficiente de código a ejecutar sobre el RDBMS es un importante desafío. Esta problemática es conocida como “impedance mismatch”. Suponiendo que el ORM ha sido diseñado de forma eficaz, resulta que sólo mediante el uso óptimo de los objetos mapeados, el ORM será capaz de generar código SQL eficiente y sólo mediante código SQL optimizado nuestra aplicación será eficiente en términos de rendimiento e interacción con la base de datos. En resumen, el rendimiento o performance de nuestra aplicación no solo depende del ORM, sino del uso que hacemos de él.

Son problemas muy comunes en equipos de trabajo con poca experiencia en el uso de ORMs y sin ningún mecanismo de supervisión y control, el abuso en el número de conexiones establecidas con el RDMBS, el excesivo volumen de datos intercambiados o la generación de SQL ineficiente. En la parte final de este artículo describiré brevemente el típico Problema del N+1 para establecer una comparativa numérica, muy simple, entre un uso adecuado e inadecuado del ORM.

Mi recomendación en aplicaciones complejas de alto rendimiento es que se debe supervisar el código SQL generado, identificar si dicho código es óptimo y en caso contrario optimizar el código que interactúa con el ORM para contribuir o propiciar a que éste genere SQL óptimo. Aplicado al caso particular de Entity Framework con uso de LINQ, la optimización del código SQL generado implicaría la optimización del código LINQ aplicado sobre las entidades correspondientes.

Deloitte
 

Para supervisar el código SQL generado por el ORM recomendaría el uso de herramientas como SQL Profiler (en caso de que el RDBMS subyacente sea SQL Server) o similares. Otras herramientas como LINQPad nos permitirán visualizar el código SQL generado por sentencias LINQ aplicadas sobre modelos de Entity Framework.

En este punto ya sabríamos como supervisar el SQL generado por el ORM, pero ¿sabemos si dicho SQL es óptimo? Evidentemente, si uno desconoce este lenguaje nunca podrá identificar si dicho código es eficiente o no, lo cual me lleva a concluir que, sin un conocimiento adecuado de SQL en este tipo de aplicaciones complejas y alto rendimiento, será muy complicado cumplir con las exigencias.

Suponiendo pues que tenemos dicho conocimiento del lenguaje SQL para avanzar en nuestras tareas de optimización, debemos asegurar que el código LINQ aplicado al ORM genera el mínimo número de conexiones necesarias a la base de datos, que dicho código es óptimo en términos de uso de índices, queries usando el mínimo número de lecturas y/o escrituras restringiendo el número de filas y columnas utilizadas o que en caso de utilizar transacciones el nivel de aislamiento es adecuado para evitar bloqueos innecesarios al mismo tiempo que se garantiza la correcta consulta y persistencia de datos. Tanto la monitorización como la depuración de queries merecería uno o varios artículos adicionales, pero al menos hacer hincapié en que un buen expertise en estas tareas será tremendamente beneficioso para potenciar el rendimiento.

Caso Práctico: Problema del “N + 1”

El problema del “N+1” es un caso típico de uso ineficiente de ORMs. En resumen, consiste en la generación de “N+1” conexiones sobre el RDBMS, con su correspondiente query, debido a un uso inadecuado del lenguaje LINQ sobre el ORM.

Nótese que el caso concreto descrito utiliza SQL Server 2016 como RDBMS, en particular con la base de datos de ejemplo AdventureWorks2016 que puedes descargar aquí, Entity Framework 6.0 como ORM y LINQ como lenguaje utilizado para interactuar con el ORM. Las consultas LINQ han sido ejecutadas usando la herramienta LINQPad.

El código LINQ implementado tan sólo recorre en bucle una tabla denominada “SalesOrderHeader”, con datos principales o de cabecera acerca de órdenes de venta, y para cada orden de venta otro bucle para recorrer las líneas de detalle de la tabla relacionada 1 a N, “SalesOrderDetail”.

Deloitte

En el primer caso, debido al uso de “lazy loading” para cargar las entidades hijas relacionadas, el ORM generará 1 + N conexiones para listar los resultados de un único registro de la tabla padre junto con “N” registros de la tabla hija. Si el número de registros “N” en la tabla hija es grande se generará un número muy elevado de conexiones impactando de forma muy negativa en el rendimiento de la aplicación.           

En el segundo caso, la instrucción “include” permite el uso de “eager loading” generándose una única conexión y consulta en el RDBMS para devolver los resultados. Aplicado al caso concreto anterior supone mejorar el tiempo de ejecución del bucle de 30 a 15 segundos.

Por último, se muestra un ejemplo de consulta LINQ eficiente que además únicamente devuelve los campos de las tablas que realmente harían falta en la aplicación. Esto sería importante para optar a generar SQL con consultas covered o al menos minimizar el número de lecturas. Conseguimos mejorar de 15 a 4 segundos.

Add comment