query practices optimizar lento framework consulta best performance linq entity-framework linq-to-entities

performance - practices - entity framework precompiled query



¿Cómo evitar la compilación del plan de consultas al utilizar IEnumerable.Contains en las consultas de Entity Framework LINQ? (1)

Tengo la siguiente consulta LINQ ejecutada utilizando Entity Framework (v6.1.1):

private IList<Customer> GetFullCustomers(IEnumerable<int> customersIds) { IQueryable<Customer> fullCustomerQuery = GetFullQuery(); return fullCustomerQuery.Where(c => customersIds.Contains(c.Id)).ToList(); }

Esta consulta se traduce en SQL bastante agradable:

SELECT [Extent1].[Id] AS [Id], [Extent1].[FirstName] AS [FirstName] -- ... FROM [dbo].[Customer] AS [Extent1] WHERE [Extent1].[Id] IN (1, 2, 3, 5)

Sin embargo, obtengo un impacto de rendimiento muy significativo en una fase de compilación de consultas. Vocación:

ELinqQueryState.GetExecutionPlan(MergeOption? forMergeOption)

Toma ~ 50% del tiempo de cada solicitud . Al profundizar, resultó que la consulta se vuelve a compilar cada vez que paso diferentes ID de clientes . De acuerdo con el artículo de MSDN , este es un comportamiento esperado porque IEnumerable que se usa en una consulta se considera volátil y es parte de SQL que se almacena en caché. Es por eso que SQL es diferente para cada combinación diferente de ID de clientes y siempre tiene un hash diferente que se usa para obtener la consulta compilada de la memoria caché.

Ahora la pregunta es: ¿Cómo puedo evitar esta re-compilación mientras sigo consultando con múltiples ID de clientes ?


Esta es una gran pregunta. Primero que nada, aquí hay un par de soluciones que vienen a la mente (todas requieren cambios en la consulta):

Primera solución

Este puede ser un poco obvio y, lamentablemente, no es de aplicación general: si la selección de elementos que necesitaría pasar a Enumerable.Contains ya existe en una tabla en la base de datos, puede escribir una consulta que llame a Enumerable.Contains en la entidad correspondiente establecer en el predicado en lugar de traer los elementos en la memoria primero. Un Enumerable.Contains llamada sobre datos en la base de datos debería resultar en algún tipo de consulta basada en JOIN que se pueda almacenar en caché. Por ejemplo, suponiendo que no haya propiedades de navegación entre los Clientes y los Clientes Seleccionados, debería poder escribir la consulta de esta manera:

var q = db.Customers.Where(c => db.SelectedCustomers.Select(s => s.Id).Contains(c.Id));

La sintaxis de la consulta con Any es un poco más simple en este caso:

var q = db.Customers.Where(c => db.SelectedCustomers.Any(s => s.Id == c.Id));

Si aún no tiene los datos de selección necesarios almacenados en la base de datos, es probable que no desee la sobrecarga de tener que almacenarlos, por lo que debe considerar la siguiente solución.

Segunda solución

Si sabe de antemano que tendrá un número máximo de elementos relativamente manejable en la lista, puede reemplazar Enumerable.Contains con un árbol de comparaciones de igualdad OR-ed, por ejemplo:

var list = new [] {1,2,3}; var q = db.Customers.Where(c => list[0] == c.Id || list[1] == c.Id || list[2] == c.Id );

Esto debería producir una consulta parametrizada que puede ser almacenada en caché. Si la lista varía en tamaño de una consulta a otra, esto debería producir una entrada de caché diferente para cada tamaño de lista. Alternativamente, podría usar una lista con un tamaño fijo y pasar algún valor centinela que sabe que nunca coincidirá con el argumento del valor, por ejemplo, 0, -1. Para producir dicha expresión de predicado programáticamente en tiempo de ejecución basado en una lista, es posible que desee considerar el uso de algo como PredicateBuilder .

Arreglos potenciales y sus desafíos

Por un lado, los cambios necesarios para admitir el almacenamiento en caché de este tipo de consulta mediante CompiledQuery explícitamente serían bastante complejos en la versión actual de EF. La razón clave es que los elementos en el IEnumerable<T> pasado al método Enumerable.Contains tendrían que traducirse en una parte estructural de la consulta para la traducción particular que producimos, por ejemplo:

var list = new [] {1,2,3}; var q = db.Customers.Where(c => list.Contains(c.Id)).ToList();

La enumerable "lista" parece una variable simple en C # / LINQ pero necesita ser traducida a una consulta como esta (simplificada para mayor claridad):

SELECT * FROM Customers WHERE Id IN(1,2,3)

Si la lista cambia a nuevo [] {5,4,3,2,1}, ¡y tendríamos que generar la consulta SQL nuevamente!

SELECT * FROM Customers WHERE Id IN(5,4,3,2,1)

Como solución potencial, hemos hablado de dejar abiertas las consultas SQL generadas con algún tipo de lugar especial, por ejemplo, almacenar en la caché de consultas que solo dice

SELECT * FROM Customers WHERE Id IN(<place holder>)

En el momento de la ejecución, podríamos seleccionar este SQL del caché y terminar la generación de SQL con los valores reales. Otra opción sería aprovechar un Parámetro de valor de tabla para la lista si la base de datos de destino puede admitirlo. La primera opción probablemente funcionará bien solo con valores constantes, esta última requiere una base de datos que admita una función especial. Ambos son muy complejos de implementar en EF.

Consultas auto compiladas

Por otro lado, para consultas compiladas automáticas (en lugar de CompiledQuery explícita), el problema se vuelve un tanto artificial: en este caso, calculamos la clave de caché de consultas después de la traducción LINQ inicial, por lo tanto, cualquier IEnumerable<T> aprobado ya debería haberse ampliado. en nodos DbExpression: un árbol de comparaciones de igualdad OR-ed en EF5, y generalmente un solo nodo DbInExpression en EF6. Dado que el árbol de consultas ya contiene una expresión distinta para cada combinación distinta de elementos en el argumento fuente de Enumerable.Contains (y, por lo tanto, para cada consulta SQL de salida distinta), es posible almacenar las consultas en caché.

Sin embargo, incluso en EF6 estas consultas no se almacenan en caché, incluso en el caso de consultas compiladas automáticamente. La razón clave es que esperamos que la variabilidad de los elementos en una lista sea alta (esto tiene que ver con el tamaño variable de la lista, pero también se ve agravada por el hecho de que normalmente no parametrizamos los valores que aparecen como constantes a la consulta, por lo que una lista de constantes se convertirá en literales constantes en SQL), por lo que con suficientes llamadas a una consulta con Enumerable.Contains . Enumerable.Contains que podría producir una considerable contaminación de caché.

También hemos considerado soluciones alternativas a esto , pero todavía no hemos implementado ninguna. Entonces, mi conclusión es que estaría mejor con la segunda solución en la mayoría de los casos si, como dije, sabe que la cantidad de elementos en la lista seguirá siendo pequeña y manejable (de lo contrario, tendrá problemas de rendimiento ).

¡Espero que esto ayude!