Переполнение преобразования LINQ в SQL
Я действительно застрял на этом. У меня обширный опыт в SQL, но я только что начал новую работу, и они предпочитают использовать LINQ для простых запросов. Поэтому в духе обучения я попытался переписать этот простой SQL-запрос:
SELECT
AVG([Weight] / [Count]) AS [Average],
COUNT(*) AS [Count]
FROM [dbo].[Average Weight]
WHERE
[ID] = 187
для ясности, Вот таблица-схема:
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])
)
вот что я придумал:
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() });
данные.Контекст.AverageWeight-это объект Linq to SQL, созданный SQLMetal. Если я попытаюсь averageWeight.First()
Я получаю исключение OverflowException. Я использовал профилировщик SQL, чтобы увидеть, как выглядит параметризованный запрос, сгенерированный LINQ. Повторно отступ, который выглядит так:
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
чрезмерное вложение в сторону, я вижу только одну проблему: DECIMAL (29, 4). (Запрос выполняется и дает ожидаемый результат.) Насколько я понимаю, все, что выше 28, будет переполнять десятичный тип данных C#. [Count] является INT, поэтому его нужно преобразовать, но [Weight] является десятичным(8, 4). Я понятия не имею, почему LINQ будет использовать такой большой тип данных.
почему LINQ преобразуется в тип данных, который вызывает и переполнение? Есть ли способ изменить такое поведение? Или я на верном пути?
Кроме Того, Данных.Контекст.AverageWeight был создан с помощью sqlmetal и я проверил вес-десятичное и атрибут столбец правильное (десятичной(8,4)).
спасибо заранее.
обновление: Таким образом, похоже, что LINQ to SQL может быть виновником. Я изменил свой LINQ, как это:
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() });
теперь сгенерированный SQL выглядит так:
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]
в результате:
Average Count
0.000518750000000 16
предыдущий подход дал:
Average Count
0.000518750000000000000 16
переполнения больше нет, но запрос менее эффективен. Я не знаю, почему LINQ to SQL преобразуется в такую высокую точность. Другие переменные не столь точны. И насколько я могу судить, я ничего не могу сделать в LINQ, чтобы заставить данные тип.
какие идеи?
2 ответов
я не эксперт, но глядя на таблицы сопоставления типов SQL-CLR (например,http://msdn.microsoft.com/en-us/library/bb386947.aspx) вы можете видеть, что десятичные значения SQL преобразуются в CLR System.Decimal
тип и CLR System.Decimal
значения преобразуются в SQL DECIMAL(29,4)
тип.
Итак, в вашем примере,a.Weight
как SQL decimal преобразуется в CLR System.Decimal.
отдела a.Weight
by a.Count
поэтому рассматривается как System.Decimal
деление и правый операнд (a.Count
) должен быть преобразован в CLR System.Decimal.
Linq затем переводит это преобразование типа обратно в SQL, в результате чего Count преобразуется в DECIMAL(29,4).
к сожалению,
a.Weight / (double) a.Count
не будет работать, поскольку правый операнд должен быть преобразован в System.Decimal
но двойной не может быть автоматически преобразован, как int может. Однако,
(double) a.Weight / a.Count
будет работать, потому что разделение теперь рассматривается как разделение двойников, а не System.Decimals,
таким образом, результирующий SQL выглядит так:
SELECT (CONVERT(Float,[t0].[Weight])) / (CONVERT(Float,[t0].[Count])) AS [value]
...
то, что вы действительно хотите, чтобы Linq лечить a.Count
как будто это уже десятичное, а не int. Это можно сделать, изменив тип свойства Count в файле DBML (посмотреть здесь). Когда я это сделал, запрос 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()});
результаты в 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]
что является желаемым результатом. Однако, изменение типа свойства count в DBML-файл может иметь и другие побочные эффекты.
кстати, SQL, сгенерированный из вашего обновленного запроса Linq, кажется неправильным. Linq явно требует, чтобы среднее значение всех Весов делилось на среднее значение всех счетчиков, но это не то, что делает SQL. Когда я пишу тот же запрос Linq, SQL, который я получаю:
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]
обратите внимание, что есть два вызова AVG
, а не только один. Также обратите внимание, что преобразование Decimal(29,4)
все еще присутствует, так как Linq все еще делает System.Decimal
деление.