Modelado para el rendimiento

En muchos casos, la forma en que el modelo puede tener un impacto profundo en el rendimiento de la aplicación; aunque un modelo normalizado y "correcto" correctamente es normalmente un buen punto de partida, en las aplicaciones del mundo real, algunos compromisos pragmáticos pueden hacer mucho para lograr un buen rendimiento. Dado que es bastante difícil cambiar el modelo una vez que una aplicación se ejecuta en producción, merece la pena tener en cuenta el rendimiento al crear el modelo inicial.

Desnormalización y almacenamiento en caché

La desnormalización es la práctica de agregar datos redundantes al esquema, normalmente para eliminar combinaciones al consultar. Por ejemplo, para un modelo con blogs y publicaciones, donde cada entrada tiene una clasificación, es posible que tenga que mostrar con frecuencia la clasificación media del blog. El enfoque sencillo para esto agruparía las publicaciones por su blog y calcularía el promedio como parte de la consulta; pero esto requiere una combinación costosa entre las dos tablas. La desnormalización agregaría el promedio calculado de todas las publicaciones a una nueva columna del blog, de modo que sea accesible inmediatamente, sin unir ni calcular.

El anterior se puede ver como una forma de almacenamiento en caché: la información agregada de las entradas se almacena en caché en su blog; y, al igual que con cualquier almacenamiento en caché, el problema es cómo mantener el valor almacenado en caché al día con los datos que almacena en caché. En muchos casos, es correcto que los datos almacenados en caché se retarden un poco. Por ejemplo, en el ejemplo anterior, suele ser razonable que la clasificación media del blog no esté completamente actualizada en ningún momento dado. Si ese es el caso, puede volver a calcularlo cada vez y luego. De lo contrario, se debe configurar un sistema más elaborado para mantener actualizados los valores almacenados en caché.

A continuación se detallan algunas técnicas para la desnormalización y el almacenamiento en caché en EF Core y se apuntan a las secciones pertinentes de la documentación.

Columnas calculadas almacenadas

Si los datos que se van a almacenar en caché son un producto de otras columnas de la misma tabla, una columna calculada almacenada puede ser una solución perfecta. Por ejemplo, una Customer puede tener columnas FirstName y LastName, pero es posible que necesitemos buscar por el nombre completo del cliente. La base de datos mantiene automáticamente una columna calculada almacenada (que vuelve a calcularla cada vez que cambia la fila) e incluso puede definir un índice sobre ella para acelerar las consultas.

Actualización de columnas de caché cuando cambian las entradas

Si la columna almacenada en caché debe hacer referencia a entradas desde fuera de la fila de la tabla, no puede usar columnas calculadas. Sin embargo, todavía es posible recalcular la columna cada vez que cambia su entrada; por ejemplo, puede recalcular la clasificación media del blog cada vez que se cambia, se agrega o quita una entrada. Asegúrese de identificar las condiciones exactas cuando se necesite la actualización; de lo contrario, el valor almacenado en caché dejará de estar sincronizado.

Una manera de hacerlo es realizar la actualización usted mismo a través de la API de EF Core normal. SaveChangesEventos o interceptores se pueden usar para comprobar automáticamente si se actualizan las publicaciones y para realizar el recálculo de esa manera. Tenga en cuenta que esto suele implicar recorridos de ida y vuelta de base de datos adicionales, ya que se deben enviar comandos adicionales.

Para aplicaciones más sensibles al rendimiento, los desencadenadores de base de datos se pueden definir para realizar automáticamente la actualización en la base de datos. Esto guarda los recorridos de ida y vuelta de la base de datos adicionales, se produce automáticamente dentro de la misma transacción que la actualización principal y puede ser más fácil de configurar. EF no proporciona ninguna API específica para crear o mantener desencadenadores, pero es perfectamente adecuado crear una migración vacía y agregar la definición del desencadenador a través de SQL sin procesar.

Vistas indexadas o materializadas

Las vistas materializadas (o indexadas) son similares a las vistas normales, salvo que sus datos se almacenan en el disco ("materializado"), en lugar de calcularse cada vez que se consulta la vista. Estas vistas son conceptualmente similares a las columnas calculadas almacenadas, ya que almacenan en caché los resultados de cálculos potencialmente costosos; sin embargo, almacenan en caché el conjunto de resultados de una consulta completa en lugar de una sola columna. Las vistas materializadas se pueden consultar al igual que cualquier tabla normal y, puesto que se almacenan en caché en el disco, estas consultas se ejecutan de forma muy rápida y económica sin necesidad de realizar constantemente los cálculos costosos de la consulta que define la vista.

La compatibilidad específica con vistas materializadas varía en todas las bases de datos. En algunas bases de datos (por ejemplo, PostgreSQL), las vistas materializadas deben actualizarse manualmente para que sus valores se sincronicen con sus tablas subyacentes. Normalmente esto se hace a través de un temporizador, en los casos en los que se acepta algún intervalo de datos, o a través de una llamada de desencadenador o procedimiento almacenado en condiciones específicas. Las vistas indexadas de SQL Server, por otro lado, se actualizan automáticamente a medida que se modifican sus tablas subyacentes; esto garantiza que la vista siempre muestre los datos más recientes a costa de actualizaciones más lentas. Además, las vistas de índice de SQL Server tienen varias restricciones sobre lo que admiten; consulte la documentación para obtener más información.

EF no proporciona actualmente ninguna API específica para crear o mantener vistas, materializadas o indexadas o de otro modo; pero es perfectamente adecuado crear una migración vacía y agregar la definición de vista a través de SQL sin procesar.

Asignación de la herencia

Se recomienda leer la página dedicada en la herencia antes de continuar con esta sección.

EF Core admite actualmente tres técnicas para asignar un modelo de herencia a una base de datos relacional:

  • Tabla por jerarquía (TPH), en la que se asigna una jerarquía completa de clases de .NET a una sola tabla de base de datos.
  • Tabla por tipo (TPT), en la que cada tipo de la jerarquía de .NET se asigna a una tabla diferente de la base de datos.
  • Tipo de tabla por concreto (TPC), en el que cada tipo concreto de la jerarquía de .NET se asigna a una tabla diferente de la base de datos, donde cada tabla contiene columnas para todas las propiedades del tipo correspondiente.

La elección de la técnica de asignación de herencia puede tener un impacto considerable en el rendimiento de la aplicación: se recomienda medir cuidadosamente antes de confirmar una elección.

Intuitivamente, TPT podría parecerse a la técnica "más limpia"; una tabla independiente para cada tipo de .NET hace que el esquema de base de datos sea similar a la jerarquía de tipos de .NET. Además, dado que TPH debe representar toda la jerarquía en una sola tabla, las filas tienen todas las columnas independientemente del tipo que se mantenga realmente en la fila y las columnas no relacionadas siempre estén vacías y no se usen. Aparte de parecer una técnica de asignación "no limpia", muchos creen que estas columnas vacías ocupan un espacio considerable en la base de datos y también pueden afectar al rendimiento.

Sugerencia

Si el sistema de base de datos lo admite (por ejemplo, SQL Server), considere la posibilidad de usar "columnas dispersas" para las columnas TPH que rara vez se rellenarán.

Sin embargo, la medición muestra que TPT es en la mayoría de los casos la técnica de asignación inferior desde el punto de vista del rendimiento. Donde todos los datos de TPH proceden de una sola tabla, las consultas de TPT deben combinar varias tablas y las combinaciones son una de las principales fuentes de problemas de rendimiento en las bases de datos relacionales. Por lo general, las bases de datos tienden a tratar bien con columnas vacías y las características, como las columnas dispersas de SQL Server, pueden reducir aún más esta sobrecarga.

TPC tiene características de rendimiento similares a TPH, pero es ligeramente más lenta al seleccionar entidades de todos los tipos, ya que esto implica varias tablas. Sin embargo, TPC realmente destaca al consultar entidades de un solo tipo hoja: la consulta solo usa una sola tabla y no necesita ningún filtrado.

Para obtener un ejemplo concreto, vea este punto de referencia que configura un modelo simple con una jerarquía de 7 tipos. Se inicializarán 5000 filas para cada tipo (total de 35 000 filas) y el banco de pruebas simplemente carga todas las filas de la base de datos:

Método Media Error StdDev Gen 0 Gen 1 Asignado
TPH 149,0 ms 3,38 ms 9,80 ms 4000.0000 1000.0000 40 MB
TPT 312,9 ms 6,17 ms 10,81 ms 9000.0000 3000.0000 75 MB
TPC 158,2 ms 3,24 ms 8,88 ms 5000.0000 2000.0000 46 MB

Como se puede ver, TPH y TPC son considerablemente más eficientes que TPT para este escenario. Tenga en cuenta que los resultados reales dependen siempre de la consulta específica que se ejecuta y del número de tablas de la jerarquía, por lo que otras consultas pueden mostrar un intervalo de rendimiento diferente. Se recomienda usar este código de prueba comparativa como plantilla para probar otras consultas.