Consejos de rendimiento para el acceso a la base de datos y Entity Framework

Consejos de rendimiento para el acceso a la base de datos y Entity Framework

Uno de los errores más comunes en un proyecto de desarrollo es olvidarse del rendimiento hasta que surge un problema. A menudo escuché a personas citar a Knuth diciendo que "la optimización prematura es la raíz de todos los males", insinuando que en este momento es demasiado pronto para pensar en ajustar el rendimiento.

Por supuesto, el ajuste y la mejora del rendimiento se posponen y posponen y posponen un poco más... hasta que hay una prueba de rendimiento en la preproducción y todo falla. (Eso es si tiene suerte, al menos lo ha detectado antes de que pase a producción. Muchas veces ese es el primer lugar donde se detecta el problema).

Creo en hacer que funcione primero antes de que funcione rápido, pero dentro de esa declaración, hay una implicación de que "trabajar" y "trabajar rápido" son ambos necesario. Hacer que simplemente funcione no es suficiente. Y Knuth se cita fuera de contexto:la cita completa es "Deberíamos olvidarnos de las pequeñas eficiencias , dicen alrededor del 97 % de las veces:la optimización prematura es la raíz de todos los males. ” (énfasis mío). Eso es pequeñas eficiencias , no grandes. También dice:“En las disciplinas de ingeniería establecidas, una mejora del 12 %, fácil de obtener, nunca se considera marginal y creo que el mismo punto de vista debería prevalecer en la ingeniería de software “. ¡¡12 %!!

Me gustaría compartir 3 consejos que he usado para marcar una gran diferencia en el rendimiento de una aplicación .NET que usa Entity Framework. A menudo escuché a personas criticar a Entity Framework por ser lento, pero me mantengo al margen de los interminables argumentos religiosos sin sentido sobre si lo es o no. Todo lo que puedo decir es que, según mi experiencia, el cuello de botella de rendimiento nunca ha sido culpa de Entity Framework:está en otro lugar o en la forma en que se ha utilizado Entity Framework.

Índices faltantes

Esto no tiene nada que ver con Entity Framework; es un cambio en la base de datos, no en el código .NET. Entity Framework genera SQL en segundo plano y lo envía a la base de datos para su ejecución, y no tiene idea de si este SQL realizará un escaneo de tabla completo enormemente costoso o si utilizará índices de manera inteligente para evitar tener que buscar en cada fila. en la base de datos.

Para mí, este es el primer puerto de escala cuando alguien dice que una aplicación que accede a una base de datos es lenta. SQL Server tiene algunas herramientas excelentes para ayudar con esto:puede usar SQL Profiler para registrar un archivo de seguimiento de todas las consultas SQL que llegan a una base de datos durante un período de tiempo y luego usar este archivo de seguimiento en el Asesor de ajuste del motor de base de datos para identificar qué índices que el motor cree que marcará la mayor diferencia en su aplicación.

He visto resultados sorprendentes de esta técnica:las mejoras del 97% no son infrecuentes. Nuevamente, no es realmente un consejo de Entity Framework, pero vale la pena revisarlo.

El problema de "Seleccionar N+1"

Entonces, de nuevo, no es realmente un problema de Entity Framework... sí, ¡aquí está surgiendo un pequeño tema! Esto es algo común a muchos ORM.

Básicamente, creo que el problema es un efecto secundario de la "carga diferida". Por ejemplo, supongamos que su aplicación consulta una base de datos sobre automóviles. Los autos están representados por un objeto POCO "Car", que contiene una lista de objetos secundarios del tipo POCO "Rueda".

Desde su aplicación, puede consultar por clave principal un automóvil con placa de matrícula "ABC 123", que (con suerte) arroja un objeto como resultado. Luego llama al método "Ruedas" para obtener información sobre las ruedas del automóvil.

Si su base de datos está lógicamente normalizada, probablemente haya realizado al menos dos consultas aquí:la original para obtener el automóvil y luego otra para obtener información sobre las ruedas. Si luego llama a una propiedad del objeto "Rueda" que compone la lista, probablemente hará otra consulta a la base de datos para obtener esa información.

En realidad, esta es una gran ventaja de los ORM:usted, como desarrollador, no tiene que hacer un trabajo adicional para cargar información sobre objetos secundarios, y la consulta solo ocurre cuando la aplicación solicita información sobre ese objeto. Todo se abstrae de ti y se llama carga diferida.

No hay nada malo o malo con la carga diferida. Como cualquier herramienta, tiene un lugar y hay oportunidades para hacer un mal uso de ella. Donde más lo he visto mal usado es en el escenario donde un desarrollador:

  • devuelve un objeto de una llamada de Entity Framework;
  • cierra la sesión (es decir, la conexión a la base de datos);
  • busca en el objeto principal un objeto secundario y obtiene una excepción que dice que la sesión está cerrada;

El desarrollador entonces hace una de dos cosas:

  • El desarrollador traslada toda la lógica al método en el que la sesión está abierta porque la carga diferida soluciona todos sus problemas. Esto conduce a un gran lío de código. En algún momento, siempre, este código se copia y pega, generalmente en un bucle, lo que genera montones y montones de consultas a la base de datos. Debido a que SQL Server es brillante, probablemente haya realizado todas estas consultas en unos pocos segundos y nadie realmente se dé cuenta hasta que se implemente en producción y cientos de usuarios intenten hacer todo esto a la vez y el sitio colapsará. (Ok, esto es demasiado dramático:sus eventos de prueba de rendimiento captarán esto. Porque, por supuesto, está haciendo pruebas de rendimiento antes de pasar a producción, ¿no es así? ¿No es así? ?)
  • El mejor desarrollador se da cuenta de que mover todo el código a un solo método es una mala idea, y aunque la carga diferida le permite hacer esto, es un mal uso de la técnica. Leen algunos blogs, descubren esta cosa llamada carga ansiosa y escriben código como este:
var car = (from c in context.Cars.Include("Wheel")
            where c.RegistrationPlate == "ABC 123"
            select c).FirstOrDefault<Car>();

Entity Framework es lo suficientemente inteligente como para reconocer lo que está pasando aquí:en lugar de hacer una consulta tonta en la tabla Car, se une a la tabla Wheel y envía una consulta para obtener todo lo que necesita para Car and the Wheels.

Así que esto es bueno, pero en mi carrera, casi todas las aplicaciones tienen una relación mucho más compleja entre el objeto y las entidades de la base de datos que un simple padre e hijo. Esto conduce a cadenas de consultas mucho más complejas.

Una técnica que he usado con éxito es crear una vista de base de datos que incluye todo lo necesario para el método comercial de la aplicación. Me gusta usar vistas porque me da un control mucho más granular sobre exactamente cuáles son las uniones entre tablas y también qué campos se devuelven de la base de datos. También simplifica el código de Entity Framework. Pero la mayor ventaja es que la vista se convierte en una interfaz, en realidad un contrato, entre la base de datos y el código. Entonces, si tiene un experto en bases de datos que le dice "Mire, sus problemas de rendimiento se deben a cómo está diseñada su base de datos; puedo solucionar esto, pero si lo hago, probablemente dañará su aplicación ", podrá responder "Bueno, consultamos la base de datos a través de una vista, por lo que siempre que pueda crear una vista que tenga las mismas columnas y salida, puede cambiar la base de datos sin afectar nosotros.

Por supuesto, si está usando una vista de base de datos, eso significa que no podrá actualizar objetos usando Entity Framework porque una vista es de solo lectura... lo que anula el propósito de usar un ORM. Sin embargo, si tiene a alguien que exige una solución para un sitio lento, es mucho menos intrusivo crear e indexar una vista que rediseñar la aplicación.

Nota:no defiendo esto como una bala mágica, es solo una técnica que a veces tiene su lugar.

Como Sin Seguimiento

Esta es una configuración de Entity Framework. Si está utilizando vistas, o sabe que su llamada a Entity Framework no necesitará actualizar la base de datos, puede obtener un aumento de rendimiento adicional utilizando la palabra clave AsNoTracking.

var cars = context.Cars.AsNoTracking().Where(c => c.Color == "Red");

Esto le dará un impulso de rendimiento si está devolviendo grandes volúmenes de datos, pero menos para volúmenes más pequeños. Su kilometraje puede variar, pero recuerde que debe asegurarse de no actualizar el contexto para usar esto.

Resumen

  • Ignore la sabiduría de las publicaciones de los grupos de noticias que dicen "Entity Framework es lento, no puede hacer nada";
  • En su lugar, ejecute SQL Server Profiler en la base de datos y pase el archivo de seguimiento resultante a través del Asesor de ajuste del motor de base de datos de SQL Server para encontrar índices que mejorarán las consultas más lentas;
  • Analice el código para identificar el problema "Seleccione N+1"; casi siempre hay uno de estos en alguna parte del código. Si desea encontrarlo, desactive la carga diferida y ejecute sus pruebas.
  • Si está devolviendo grandes volúmenes de datos a una lista de solo lectura, vea si puede usar AsNoTracking para obtener un poco más de rendimiento de su aplicación.