Compartir a través de


Consultas únicas frente a Consultas divididas

Problemas de rendimiento con consultas únicas

Al trabajar con bases de datos relacionales, EF carga entidades relacionadas mediante la introducción de JOIN en una sola consulta. Aunque los JOIN son bastante estándar al usar SQL, pueden crear problemas de rendimiento significativos si se usan incorrectamente. En esta página se describen estos problemas de rendimiento y se muestra una manera alternativa de cargar entidades relacionadas que funcionan en torno a ellos.

Explosión cartesiana

Examinemos la siguiente consulta LINQ y su equivalente de SQL traducido:

var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .Include(b => b.Contributors)
    .ToList();
SELECT [b].[Id], [b].[Name], [p].[Id], [p].[BlogId], [p].[Title], [c].[Id], [c].[BlogId], [c].[FirstName], [c].[LastName]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Contributors] AS [c] ON [b].[Id] = [c].[BlogId]
ORDER BY [b].[Id], [p].[Id]

En este ejemplo, puesto que tanto Posts y Contributors son navegaciones de colección de Blog- están al mismo nivel - las bases de datos relacionales devuelven un producto cruzado: cada fila de Posts se une con cada fila de Contributors. Esto significa que si un blog determinado tiene 10 entradas y 10 colaboradores, la base de datos devuelve 100 filas para ese único blog. Este fenómeno, que a veces se denomina explosión cartesiana, puede provocar que grandes cantidades de datos se transfieran involuntariamente al cliente, especialmente cuando se agregan más JOIN del mismo nivel a la consulta; esto puede ser un problema de rendimiento importante en las aplicaciones de base de datos.

Tenga en cuenta que la explosión cartesiana no se produce cuando los dos JOIN no están en el mismo nivel:

var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .ThenInclude(p => p.Comments)
    .ToList();
SELECT [b].[Id], [b].[Name], [t].[Id], [t].[BlogId], [t].[Title], [t].[Id0], [t].[Content], [t].[PostId]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Comment] AS [c] ON [p].[Id] = [c].[PostId]
ORDER BY [b].[Id], [t].[Id]

En esta consulta, Comments es una navegación de colección dePost, a diferencia de Contributors en la consulta anterior, que era una navegación de colección de Blog. En este caso, se devuelve una sola fila para cada comentario que tiene un blog (a través de sus entradas) y no se produce un producto cruzado.

Duplicación de datos

Los JOIN pueden crear otro tipo de problema de rendimiento. Vamos a examinar la consulta siguiente, que solo carga una sola navegación de colección:

var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .ToList();
SELECT [b].[Id], [b].[Name], [b].[HugeColumn], [p].[Id], [p].[BlogId], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
ORDER BY [b].[Id]

Examinar en las columnas proyectadas, cada fila devuelta por esta consulta contiene propiedades de las tablas Blogs y Posts ; esto significa que las propiedades del blog se duplican para cada entrada que tiene el blog. Aunque esto suele ser normal y no produce ningún problema, si la Blogs tabla tiene una columna muy grande (por ejemplo, datos binarios o un texto enorme), esa columna se duplicaría y volvería a enviar al cliente varias veces. Esto puede aumentar significativamente el tráfico de red y afectar negativamente al rendimiento de la aplicación.

Si realmente no necesita la columna enorme, es fácil simplemente no consultarla:

var blogs = ctx.Blogs
    .Select(b => new
    {
        b.Id,
        b.Name,
        b.Posts
    })
    .ToList();

Mediante el uso de una proyección para elegir explícitamente qué columnas desea, puede omitir columnas grandes y mejorar el rendimiento; Tenga en cuenta que esto es una buena idea independientemente de la duplicación de datos, por lo que considere la posibilidad de hacerlo incluso cuando no cargue una navegación de recopilación. Sin embargo, dado que este proyecto el blog a un tipo anónimo, EF no realiza el seguimiento del blog y no se pueden volver a guardar los cambios en él como de costumbre.

Vale la pena señalar que, a diferencia de la explosión cartesiana, la duplicación de datos causada por JOIN no suele ser significativa, ya que el tamaño de los datos duplicados es insignificante; Esto suele ser algo que preocuparse solo si tiene columnas grandes en la tabla principal.

Consultas divididas

Para solucionar los problemas de rendimiento descritos anteriormente, EF le permite especificar que una consulta LINQ determinada debe dividirse en varias consultas SQL. En lugar de instrucciones JOIN, las consultas divididas generan una consulta SQL adicional por cada navegación de colección incluida:

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
        .AsSplitQuery()
        .ToList();
}

Generará la consulta SQL siguiente:

SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url]
FROM [Blogs] AS [b]
ORDER BY [b].[BlogId]

SELECT [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title], [b].[BlogId]
FROM [Blogs] AS [b]
INNER JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]
ORDER BY [b].[BlogId]

Advertencia

Al usar consultas divididas con Skip/Take, preste especial atención a hacer que el pedido de consulta sea totalmente único; no hacerlo podría provocar que se devuelvan datos incorrectos. Por ejemplo, si los resultados se ordenan solo por fecha, pero puede haber varios resultados con la misma fecha, cada una de las consultas divididas podría obtener resultados diferentes de la base de datos. La ordenación por fecha e id., o cualquier otra propiedad única o combinación de propiedades, hace que la ordenación sea totalmente única y evita este problema. Tenga en cuenta que las bases de datos relacionales no aplican ninguna ordenación de forma predeterminada, incluso en la clave principal.

Nota:

Las entidades relacionadas uno a uno se cargan siempre mediante instrucciones JOIN en la misma consulta, ya que esto no afecta al rendimiento.

Habilitación de consultas divididas globalmente

También puede configurar consultas divididas como el valor predeterminado para el contexto de la aplicación:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(
            @"Server=(localdb)\mssqllocaldb;Database=EFQuerying;Trusted_Connection=True;ConnectRetryCount=0",
            o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
}

Cuando las consultas divididas están configuradas como valor predeterminado, todavía es posible configurar consultas específicas para que se ejecuten como consultas únicas:

using (var context = new SplitQueriesBloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
        .AsSingleQuery()
        .ToList();
}

EF Core usa el modo de consulta única de forma predeterminada si no existe ninguna configuración. Dado que puede producir problemas de rendimiento, EF Core genera una advertencia cuando se cumplen las condiciones siguientes:

  • EF Core detecta que la consulta carga varias colecciones.
  • El usuario no ha configurado el modo de división de consultas globalmente.
  • El usuario no ha usado el operador AsSingleQuery/AsSplitQuery en la consulta.

Para desactivar la advertencia, configure el modo de división de consultas globalmente o en el nivel de consulta en un valor adecuado.

Características de las consultas divididas

Si bien las consultas divididas evitan las incidencias de rendimiento asociadas a las instrucciones JOIN y la explosión cartesiana, también existen algunas desventajas:

  • Aunque la mayoría de las bases de datos garantizan la coherencia de los datos en las consultas únicas, no sucede lo mismo en el caso de varias consultas. Si la base de datos se actualiza de forma simultánea al ejecutar las consultas, es posible que los datos resultantes no sean coherentes. Esto se puede mitigar mediante el encapsulado de las consultas en una transacción serializable o de instantáneas, aunque esto puede generar incidencias propias de rendimiento. Para obtener más información, vea la documentación de la base de datos.
  • Cada consulta implica actualmente un ciclo adicional de ida y vuelta de red a la base de datos. Varios ciclos de ida y vuelta de red pueden degradar el rendimiento, sobre todo si la latencia en la base de datos es alta (por ejemplo, servicios en la nube).
  • Aunque algunas bases de datos permiten el consumo de los resultados de varias consultas al mismo tiempo (SQL Server con MARS, SQLite), la mayoría de ellas solo permiten sola consulta activa en un momento dado. Por lo tanto, todos los resultados de las consultas anteriores deben almacenarse en búfer en la memoria de la aplicación antes de ejecutar consultas posteriores, lo que llevará a un aumento en los requisitos de memoria.
  • Al incluir navegaciones de referencia, así como navegaciones de recopilación, cada una de las consultas divididas incluirá combinaciones con las navegaciones de referencia. Como consecuencia, se puede degradar el rendimiento, especialmente si hay muchas navegaciones de referencia. Vote por #29182 si desea que se corrija algo.

Por desgracia, no hay una estrategia para cargar entidades relacionadas que se ajuste a todos los escenarios. Tenga en cuenta las ventajas y desventajas de las consultas únicas y divididas, para seleccionar la que mejor se ajuste a sus necesidades.