2011-02-24 30 views
10

Estoy realmente atrapado en este. Tengo una amplia experiencia en SQL, pero acabo de comenzar un nuevo trabajo y ellos prefieren usar LINQ para consultas simples. Así, en el espíritu de aprendizaje, he tratado de volver a escribir esta simple consulta SQL:LINQ to SQL Conversion Overflows

SELECT 
    AVG([Weight]/[Count]) AS [Average], 
    COUNT(*) AS [Count] 
FROM [dbo].[Average Weight] 
WHERE 
    [ID] = 187 

En aras de la claridad, aquí está el esquema de tabla:

CREATE TABLE [dbo].[Average Weight] 
(
    [ID] INT NOT NULL, 
    [Weight] DECIMAL(8, 4) NOT NULL, 
    [Count] INT NOT NULL, 
    [Date] DATETIME NOT NULL, 
    PRIMARY KEY([ID], [Date]) 
) 

Esto es lo que ocurrió con :

var averageWeight = Data.Context.AverageWeight 
    .Where(i => i.ID == 187) 
    .GroupBy(w => w.ID) 
    .Select(i => new { Average = i.Average(a => a.Weight/a.Count), Count = i.Count() }); 

Data.Context.AverageWeight es un objeto Linq to SQL generado por SQLMetal. Si trato de averageWeight.First() obtengo una OverflowException. Utilicé el Analizador de SQL para ver a qué se parece la consulta parametrizada generada por LINQ. Re-sangría que tiene este aspecto:

EXEC sp_executesql N' 
SELECT TOP(1) 
    [t2].[value] AS [Average], 
    [t2].[value2] AS [Count] 
FROM (
     SELECT 
      AVG([t1].[value]) AS [value], 
      COUNT(*) AS [value2] 
     FROM (
       SELECT 
        [t0].[Weight]/(CONVERT(DECIMAL(29, 4), [t0].[Count])) AS 
        [value], 
        [t0].[ID] 
       FROM [dbo].[Average Weight] AS [t0] 
      ) AS [t1] 
     WHERE 
      ([t1].[ID] = @p0) 
     GROUP BY 
      [t1].[ID] 
    ) AS [t2]', 
    N'@p0 int', 
    @p0 = 187 

anidación excesiva a un lado, sólo veo un problema: DECIMAL (29, 4). (La consulta se ejecuta y arroja el resultado esperado). Tengo entendido que cualquier elemento superior a 28 desbordará el tipo de datos decimales C#. [Count] es un INT, por lo que necesita ser CONVERTIDO, pero [Weight] es un DECIMAL (8, 4). No tengo idea de por qué LINQ usaría un tipo de datos tan grande.

¿Por qué LINQ CONVERT a un tipo de datos que causa y desborda? ¿Hay alguna forma de cambiar este comportamiento? ¿O estoy en el camino correcto?

También, Data.Context.AverageWeight fue generado por SqlMetal y verifiqué que Weight es un decimal y que Column Attribute es correcto (Decimal (8,4)).

Gracias de antemano.

Actualización: Parece que LINQ to SQL es el culpable. He cambiado de LINQ como esto:

var averageWeight = Data.Context.AverageWeight 
    .Where(i => i.ID == 187) 
    .GroupBy(w => w.ID) 
    .Select(i => new { Average = i.Average(a => a.Weight)/(decimal)i.Average(a => a.Count), Count = i.Count() }); 

Ahora el SQL generado es similar al siguiente:

SELECT TOP(1) 
    [t2].[value] AS [Average], 
    [t2].[value2] AS [Count] 
FROM (
     SELECT 
      AVG([t1].[value]) AS [value], 
      COUNT(*) AS [value2] 
     FROM (
       SELECT 
        [t0].[Weight]/(CONVERT(DECIMAL(16, 4), [t0].[Count])) AS [value], 
        [t0].[ID] 
       FROM [dbo].[Average Weight] AS [t0] 
      ) AS [t1] 
     WHERE 
      ([t1].[ID] = 187) 
     GROUP BY 
      [t1].[ID] 
    ) AS [t2] 

El resultado de esto es:

Average     Count 
0.000518750000000  16 

El enfoque anterior dio:

Average     Count 
0.000518750000000000000 16 

Ya no es un desbordamiento, pero la consulta es menos eficiente. No sé por qué LINQ to SQL CONVERTIRÁ en una precisión tan alta. No de las otras variables son tan precisas. Y hasta donde puedo decir, no hay nada que pueda hacer en LINQ para forzar el tipo de datos.

¿Alguna idea?

+0

El 'CONVERTIR (DECIMAL (...))' se está aplicando a * Count *, no a 'Weight'. –

+0

¿Es esto Linq para SQL o Entity Framework? –

+0

¿Por qué GroupBy en el LINQ pero no en el SQL original? –

Respuesta

2

No soy un experto, pero mirando las tablas de asignación de tipo SQL-CLR (por ejemplo http://msdn.microsoft.com/en-us/library/bb386947.aspx) se puede ver que Los valores decimales de SQL se convierten al tipo CLR System.Decimal y los valores CLR System.Decimal se convierten al tipo SQL DECIMAL(29,4).

Así que en tu ejemplo, a.Weight como un decimal SQL se convierte en un CLR System.Decimal. Por tanto, la división de a.Weight por a.Count se trata como una división System.Decimal y el operando derecho (a.Count) deben ser convertidos a un CLR System.Decimal. LINQ luego se traduce este tipo de conversión de nuevo a SQL que se traduce en Conde de ser convertido en un DECIMAL(29,4).

Desafortunadamente,

a.Weight/(double) a.Count 

no funcionará porque el operando de la derecha se debe convertir a System.Decimal, pero un doble no se puede convertir automáticamente como una lata int. Sin embargo,

(double) a.Weight/a.Count 

funcionará porque la división ahora se trata como una división de dobles, no System.Decimals, por lo que el SQL resultante se parece a esto:

SELECT (CONVERT(Float,[t0].[Weight]))/(CONVERT(Float,[t0].[Count])) AS [value] 
... 

Lo que realmente queremos es que LINQ para tratar a.Count como si ya fuera un decimal, no un int. Puede hacerlo cambiando la propiedad Tipo de recuento en su archivo DBML (see here). Cuando hice esto, la consulta LINQ:

var averageweight = context.AverageWeights 
      .Where(i => i.ID == 187) 
      .GroupBy(w => w.ID) 
      .Select(i => new {Average = i.Average(a => a.Weight/a.Count), Count = i.Count()}); 

resultados en el SQL:

SELECT AVG([t0].[Weight]/[t0].[Count]) AS [Average], COUNT(*) AS [Count] 
FROM [dbo].[AverageWeight] AS [t0] 
WHERE [t0].[ID] = @p0 
GROUP BY [t0].[ID] 

que es el resultado deseado. Sin embargo, cambiar el tipo de propiedad Count en el archivo DBML puede tener otros efectos secundarios no deseados.

Por cierto, el SQL generado a partir de su consulta Linq actualizada parece estar equivocado. El Linq claramente requiere que el promedio de todos los pesos se divida por el promedio de todos los recuentos, pero esto no es lo que hace el SQL. Cuando escribo la misma consulta LINQ, el SQL que recibo es:

SELECT [t1].[value]/(CONVERT(Decimal(29,4),[t1].[value2])) AS [Average], [t1].[value3] AS [Count] 
FROM (
    SELECT AVG([t0].[Weight]) AS [value], AVG([t0].[Count]) AS [value2], COUNT(*) AS [value3] 
    FROM [dbo].[Average Weight] AS [t0] 
    WHERE [t0].[ID] = @p0 
    GROUP BY [t0].[ID] 
    ) AS [t1] 

Tenga en cuenta que hay dos llamadas a AVG en lugar de sólo uno. También tenga en cuenta que la conversión a Decimal(29,4) todavía está presente ya que Linq todavía está haciendo una división System.Decimal.

0

no he probado pero se puede tratar de echar a.Count:

... a.Weight/(double) a.Count ... 
+0

Esto probablemente funcionaría, pero también tendría que calcular el peso al doble para que esto funcione y luego mi tipo de devolución sería un doble. Realmente no considero que sea una solución para convertir todo a tipos de datos de coma flotante. Linq debería ser capaz de usar los números de punto fijo correctamente. De todos modos, puedo pensar en varias formas de solucionar este problema. Lo que realmente espero es que alguien entienda a Linq lo suficientemente bien como para explicarme por qué sucede esto ... – Ben

+0

Además, (y lamentablemente) convertirlo a decimal (el C#) no cambia la forma en que LINQ genera el SQL. – Ben