c# - multiple - LINQ generando SQL con selecciones anidadas duplicadas
linq to sql c# ejemplos (2)
Soy muy nuevo en .NET Entity Framework, y creo que es increíble, pero de alguna manera estoy teniendo este extraño problema (perdón por el español, pero mi programa está en ese idioma, de todos modos no es un gran problema, solo la columna o nombres de propiedades): estoy haciendo una consulta LINQ To Entities normal para obtener una lista de UltimaConsulta, como esta:
var query = from uc in bd.UltimasConsultas
select uc;
UltimasConsultas es una vista, por cierto. Lo que pasa es que LINQ está generando este SQL para la consulta:
SELECT
[Extent1].[IdPaciente] AS [IdPaciente],
[Extent1].[Nombre] AS [Nombre],
[Extent1].[PrimerApellido] AS [PrimerApellido],
[Extent1].[SegundoApellido] AS [SegundoApellido],
[Extent1].[Fecha] AS [Fecha]
FROM (SELECT
[UltimasConsultas].[IdPaciente] AS [IdPaciente],
[UltimasConsultas].[Nombre] AS [Nombre],
[UltimasConsultas].[PrimerApellido] AS [PrimerApellido],
[UltimasConsultas].[SegundoApellido] AS [SegundoApellido],
[UltimasConsultas].[Fecha] AS [Fecha]
FROM [dbo].[UltimasConsultas] AS [UltimasConsultas]) AS [Extent1]
¿Por qué LINQ está generando un Select anidado? Pensé en los videos y ejemplos que genera selecciones de SQL normales para este tipo de consultas. ¿Tengo que configurar algo (el modelo de entidad se estaba generando desde un asistente, por lo que es la configuración predeterminada)? Gracias de antemano por sus respuestas.
Básicamente, se trata de definir en qué consiste Extent1 y qué variables se relacionarán con cada entrada. Luego se asigna la tabla de la base de datos real a Extent1 para que pueda devolver todas las entradas para esa tabla.
Esto es lo que está pidiendo su consulta. Es solo que LINQ no puede agregar un carácter comodín como lo haría si lo hubiera hecho a mano.
Para ser claros, LINQ to Entities no genera el SQL. En su lugar, genera un árbol de comandos canónicos ADO.NET, y el proveedor ADO.NET para su base de datos, probablemente SQL Server en este caso, genera el SQL.
Entonces, ¿por qué genera esta tabla derivada (creo que "tabla derivada" es el término más correcto para la característica de SQL que se usa aquí)? Debido a que el código que genera el SQL tiene que generar SQL para una amplia variedad de consultas LINQ, la mayoría de las cuales no son tan triviales como la que se muestra. Estas consultas a menudo seleccionarán datos para varios tipos (muchos de los cuales pueden ser anónimos, en lugar de tipos con nombre), y para mantener la generación de SQL relativamente sana, se agrupan en extensiones para cada tipo.
Otra pregunta: ¿Por qué debería importarte? Es fácil demostrar que el uso de la tabla derivada en esta declaración es "libre" desde el punto de vista del rendimiento.
Seleccioné una tabla al azar de una base de datos poblada y ejecuté la siguiente consulta:
SELECT [AddressId]
,[Address1]
,[Address2]
,[City]
,[State]
,[ZIP]
,[ZIPExtension]
FROM [VertexRM].[dbo].[Address]
Veamos el costo:
<StmtSimple StatementCompId="1" StatementEstRows="7900" StatementId="1" StatementOptmLevel="TRIVIAL" StatementSubTreeCost="0.123824" StatementText="/****** Script for SelectTopNRows command from SSMS ******/
SELECT [AddressId]
 ,[Address1]
 ,[Address2]
 ,[City]
 ,[State]
 ,[ZIP]
 ,[ZIPExtension]
 FROM [VertexRM].[dbo].[Address]" StatementType="SELECT">
<StatementSetOptions ANSI_NULLS="false" ANSI_PADDING="false" ANSI_WARNINGS="false" ARITHABORT="true" CONCAT_NULL_YIELDS_NULL="false" NUMERIC_ROUNDABORT="false" QUOTED_IDENTIFIER="false" />
<QueryPlan CachedPlanSize="9" CompileTime="0" CompileCPU="0" CompileMemory="64">
<RelOp AvgRowSize="246" EstimateCPU="0.008847" EstimateIO="0.114977" EstimateRebinds="0" EstimateRewinds="0" EstimateRows="7900" LogicalOp="Clustered Index Scan" NodeId="0" Parallel="false" PhysicalOp="Clustered Index Scan" EstimatedTotalSubtreeCost="0.123824">
Ahora comparemos eso con la consulta con la tabla derivada:
SELECT
[Extent1].[AddressId]
,[Extent1].[Address1]
,[Extent1].[Address2]
,[Extent1].[City]
,[Extent1].[State]
,[Extent1].[ZIP]
,[Extent1].[ZIPExtension]
FROM (SELECT [AddressId]
,[Address1]
,[Address2]
,[City]
,[State]
,[ZIP]
,[ZIPExtension]
FROM[VertexRM].[dbo].[Address]) AS [Extent1]
Y el costo:
<StmtSimple StatementCompId="1" StatementEstRows="7900" StatementId="1" StatementOptmLevel="TRIVIAL" StatementSubTreeCost="0.123824" StatementText="/****** Script for SelectTopNRows command from SSMS ******/
SELECT 
 [Extent1].[AddressId]
 ,[Extent1].[Address1]
 ,[Extent1].[Address2]
 ,[Extent1].[City]
 ,[Extent1].[State]
 ,[Extent1].[ZIP]
 ,[Extent1].[ZIPExtension]
 FROM (SELECT [AddressId]
 ,[Address1]
 ,[Address2]
 ,[City]
 ,[State]
 ,[ZIP]
 ,[ZIPExtension]
 FROM[VertexRM].[dbo].[Address]) AS [Extent1]" StatementType="SELECT">
<StatementSetOptions ANSI_NULLS="false" ANSI_PADDING="false" ANSI_WARNINGS="false" ARITHABORT="true" CONCAT_NULL_YIELDS_NULL="false" NUMERIC_ROUNDABORT="false" QUOTED_IDENTIFIER="false" />
<QueryPlan CachedPlanSize="9" CompileTime="0" CompileCPU="0" CompileMemory="64">
<RelOp AvgRowSize="246" EstimateCPU="0.008847" EstimateIO="0.114977" EstimateRebinds="0" EstimateRewinds="0" EstimateRows="7900" LogicalOp="Clustered Index Scan" NodeId="0" Parallel="false" PhysicalOp="Clustered Index Scan" EstimatedTotalSubtreeCost="0.123824">
En ambos casos, SQL Server simplemente escanea el índice agrupado. No en vano, el costo es casi el mismo.
Echemos un vistazo a una consulta un poco más complicada. Abrí LINQPad e ingresé la siguiente consulta en la misma tabla, más una tabla relacionada:
from a in Addresses
select new
{
Id = a.Id,
Address1 = a.Address1,
Address2 = a.Address2,
City = a.City,
State = a.State,
ZIP = a.ZIP,
ZIPExtension = a.ZIPExtension,
PersonCount = a.EntityAddresses.Count()
}
Esto genera el siguiente SQL:
SELECT
1 AS [C1],
[Project1].[AddressId] AS [AddressId],
[Project1].[Address1] AS [Address1],
[Project1].[Address2] AS [Address2],
[Project1].[City] AS [City],
[Project1].[State] AS [State],
[Project1].[ZIP] AS [ZIP],
[Project1].[ZIPExtension] AS [ZIPExtension],
[Project1].[C1] AS [C2]
FROM ( SELECT
[Extent1].[AddressId] AS [AddressId],
[Extent1].[Address1] AS [Address1],
[Extent1].[Address2] AS [Address2],
[Extent1].[City] AS [City],
[Extent1].[State] AS [State],
[Extent1].[ZIP] AS [ZIP],
[Extent1].[ZIPExtension] AS [ZIPExtension],
(SELECT
COUNT(cast(1 as bit)) AS [A1]
FROM [dbo].[EntityAddress] AS [Extent2]
WHERE [Extent1].[AddressId] = [Extent2].[AddressId]) AS [C1]
FROM [dbo].[Address] AS [Extent1]
) AS [Project1]
Analizando esto, podemos ver que Project1
es la proyección sobre el tipo anónimo. Extent1
es la tabla / entidad de Address
. Y Extent2
es la tabla para la asociación. Ahora no hay una tabla derivada para la Address
, pero hay una para la proyección.
No sé si alguna vez ha escrito un sistema de generación de SQL, pero no es fácil. Creo que el problema general de probar que una consulta de LINQ a entidades y una consulta de SQL son equivalentes es NP-duro, aunque algunos casos específicos son obviamente mucho más fáciles. SQL está intencionalmente incompleto, porque sus diseñadores querían que todas las consultas de SQL se ejecutaran en tiempo limitado. LINQ, no es así.
En resumen, este es un problema muy difícil de resolver, y la combinación del Entity Framework y sus proveedores a veces sacrifica cierta legibilidad en favor de la coherencia en una amplia gama de consultas. Pero no debería ser un problema de rendimiento.