#c# #sql-server #entity-framework #linq
#c# #sql-сервер #entity-framework #linq
Вопрос:
Я застрял с проблемой производительности с EF.
using (var context = new CustomDbContext())
{
var result = context.
TransactionLines
.Where(x =>
x.Transaction.TransactionTypeId == 1433 amp;amp;
(x.Transaction.Eob.EobBatchId == null || x.Transaction.Eob.EobBatch.Status == EobBatchStatusEnum.Completed)
)
.GroupBy(x => x.VisitLine.ProcedureId)
.Select(x => new
{
Id = x.Key,
PaidAmount = x.Sum(t => t.PaidAmount),
Code = context.Procedures.Where(h => h.Id == x.Key).Select(h => h.Code).FirstOrDefault()
}).ToArray();
}
EF генерирует следующий sql:
SELECT
1 AS [C1],
[Project6].[ProcedureId] AS [ProcedureId],
[Project6].[C2] AS [C2],
[Project6].[C1] AS [C3]
FROM ( SELECT
[Project5].[ProcedureId] AS [ProcedureId],
[Project5].[C1] AS [C1],
(SELECT
SUM([Extent7].[PaidAmount]) AS [A1]
FROM [dbo].[TransactionLines] AS [Extent7]
INNER JOIN [dbo].[Transactions] AS [Extent8] ON [Extent7].[TransactionId] = [Extent8].[Id]
LEFT OUTER JOIN [dbo].[Eobs] AS [Extent9] ON [Extent8].[EobId] = [Extent9].[Id]
LEFT OUTER JOIN [dbo].[EobBatches] AS [Extent10] ON [Extent9].[EobBatchId] = [Extent10].[Id]
LEFT OUTER JOIN [dbo].[VisitLines] AS [Extent11] ON [Extent7].[VisitLineId] = [Extent11].[Id]
WHERE (([Extent9].[EobBatchId] IS NULL) OR (1 = [Extent10].[Status])) AND ([Extent8].[TransactionTypeId] = 1433) AND (([Project5].[ProcedureId] = [Extent11].[ProcedureId]) OR (([Project5].[ProcedureId] IS NULL) AND ([Extent11].[ProcedureId] IS NULL)))) AS [C2]
FROM ( SELECT
[Project4].[ProcedureId] AS [ProcedureId],
[Project4].[C1] AS [C1]
FROM ( SELECT
[Project2].[ProcedureId] AS [ProcedureId],
(SELECT TOP (1)
[Extent6].[Code] AS [Code]
FROM [dbo].[Procedures] AS [Extent6]
WHERE [Extent6].[Id] = [Project2].[ProcedureId]) AS [C1]
FROM ( SELECT
[Distinct1].[ProcedureId] AS [ProcedureId]
FROM ( SELECT DISTINCT
[Extent5].[ProcedureId] AS [ProcedureId]
FROM [dbo].[TransactionLines] AS [Extent1]
INNER JOIN [dbo].[Transactions] AS [Extent2] ON [Extent1].[TransactionId] = [Extent2].[Id]
LEFT OUTER JOIN [dbo].[Eobs] AS [Extent3] ON [Extent2].[EobId] = [Extent3].[Id]
LEFT OUTER JOIN [dbo].[EobBatches] AS [Extent4] ON [Extent3].[EobBatchId] = [Extent4].[Id]
LEFT OUTER JOIN [dbo].[VisitLines] AS [Extent5] ON [Extent1].[VisitLineId] = [Extent5].[Id]
WHERE (([Extent3].[EobBatchId] IS NULL) OR (1 = [Extent4].[Status])) AND ([Extent2].[TransactionTypeId] = 1433)
) AS [Distinct1]
) AS [Project2]
) AS [Project4]
) AS [Project5]
) AS [Project6]
Продолжительность запроса составляет ~ 3 секунды.
Если написать sql-запрос напрямую, используя Group, то длительность запроса составляет 1,5 секунды, и он использует вдвое меньше ресурсов процессора.
SELECT sq.ProcedureId, SUM(sq.PaidAmount), (SELECT TOP(1) Procedures.Code From Procedures Where Procedures.Id = sq.ProcedureId) as Code
FROM(
SELECT [Extent5].[ProcedureId] AS [ProcedureId],[Extent1].PaidAmount as [PaidAmount]
FROM [dbo].[TransactionLines] AS [Extent1]
INNER JOIN [dbo].[Transactions] AS [Extent2] ON [Extent1].[TransactionId] = [Extent2].[Id]
LEFT OUTER JOIN [dbo].[Eobs] AS [Extent3] ON [Extent2].[EobId] = [Extent3].[Id]
LEFT OUTER JOIN [dbo].[EobBatches] AS [Extent4] ON [Extent3].[EobBatchId] = [Extent4].[Id]
LEFT OUTER JOIN [dbo].[VisitLines] AS [Extent5] ON [Extent1].[VisitLineId] = [Extent5].[Id]
WHERE (([Extent3].[EobBatchId] IS NULL) OR (1 = [Extent4].[Status])) AND ([Extent2].[TransactionTypeId] = 1433)
) sq
GROUP BY sq.ProcedureId
Я написал разные linq, но все еще не могу заставить EF генерировать GroupBy вместо подзапросов.
В идеале я бы не хотел использовать функции или написанный вручную sql, потому что у меня много условий при построении логики linq.
Можно ли заставить EF генерировать SQL точно так, как он был написан в linq?
Комментарии:
1. Сначала попробуйте переписать ваш запрос LINQ, используя явные соединения.
Ответ №1:
Попробуйте избежать
context.Procedures.Where(h => h.Id == x.Key).Select(h => h.Code).FirstOrDefault()
включив Code
в GroupBy
предложение — я знаю, что это кажется избыточным, но известно, что у EF возникают проблемы с переводом операций группировки, которые включают что-то другое, кроме использования средств доступа к ключам и агрегатов:
//...
.GroupBy(x => new { Id = x.VisitLine.ProcedureId, x.VisitLine.Procedure.Code })
.Select(x => new
{
Id = x.Key.Id,
PaidAmount = x.Sum(t => t.PaidAmount),
Code = x.Key.Code
}).ToArray();
Обновление: приведенное выше генерирует следующий SQL в моей тестовой среде (последняя версия EF6.1.3):
SELECT
1 AS [C1],
[GroupBy1].[K1] AS [ProcedureId],
[GroupBy1].[A1] AS [C2],
[GroupBy1].[K2] AS [Code]
FROM ( SELECT
[Extent5].[ProcedureId] AS [K1],
[Extent6].[Code] AS [K2],
SUM([Filter1].[PaidAmount]) AS [A1]
FROM (SELECT [Extent1].[VisitLineId] AS [VisitLineId], [Extent1].[PaidAmount] AS [PaidAmount]
FROM [dbo].[TransactionLine] AS [Extent1]
INNER JOIN [dbo].[Transaction] AS [Extent2] ON [Extent1].[TransactionId] = [Extent2].[Id]
LEFT OUTER JOIN [dbo].[Eob] AS [Extent3] ON [Extent2].[EobId] = [Extent3].[Id]
LEFT OUTER JOIN [dbo].[EobBatch] AS [Extent4] ON [Extent3].[EobBatchId] = [Extent4].[Id]
WHERE (1433 = [Extent2].[TransactionTypeId]) AND ([Extent3].[EobBatchId] IS NULL OR [Extent4].[Status] = 1) ) AS [Filter1]
LEFT OUTER JOIN [dbo].[VisitLine] AS [Extent5] ON [Filter1].[VisitLineId] = [Extent5].[Id]
LEFT OUTER JOIN [dbo].[Procedure] AS [Extent6] ON [Extent5].[ProcedureId] = [Extent6].[Id]
GROUP BY [Extent5].[ProcedureId], [Extent6].[Code]
) AS [GroupBy1]
что намного лучше, как я и ожидал.
Обновление 2: EF — странный зверь. Использование двойной проекции дает желаемый результат:
//...
.GroupBy(x => x.VisitLine.ProcedureId)
.Select(x => new
{
Id = x.Key,
PaidAmount = x.Sum(t => t.PaidAmount),
})
.Select(x => new
{
x.Id,
x.PaidAmount,
Code = context.Procedures.Where(h => h.Id == x.Id).Select(h => h.Code).FirstOrDefault()
}).ToArray();
что приводит к следующему:
SELECT
1 AS [C1],
[Project2].[ProcedureId] AS [ProcedureId],
[Project2].[C1] AS [C2],
[Project2].[C2] AS [C3]
FROM ( SELECT
[GroupBy1].[A1] AS [C1],
[GroupBy1].[K1] AS [ProcedureId],
(SELECT TOP (1)
[Extent6].[Code] AS [Code]
FROM [dbo].[Procedure] AS [Extent6]
WHERE [Extent6].[Id] = [GroupBy1].[K1]) AS [C2]
FROM ( SELECT
[Extent5].[ProcedureId] AS [K1],
SUM([Filter1].[PaidAmount]) AS [A1]
FROM (SELECT [Extent1].[VisitLineId] AS [VisitLineId], [Extent1].[PaidAmount] AS [PaidAmount]
FROM [dbo].[TransactionLine] AS [Extent1]
INNER JOIN [dbo].[Transaction] AS [Extent2] ON [Extent1].[TransactionId] = [Extent2].[Id]
LEFT OUTER JOIN [dbo].[Eob] AS [Extent3] ON [Extent2].[EobId] = [Extent3].[Id]
LEFT OUTER JOIN [dbo].[EobBatch] AS [Extent4] ON [Extent3].[EobBatchId] = [Extent4].[Id]
WHERE (1433 = [Extent2].[TransactionTypeId]) AND ([Extent3].[EobBatchId] IS NULL OR [Extent4].[Status] = 1) ) AS [Filter1]
LEFT OUTER JOIN [dbo].[VisitLine] AS [Extent5] ON [Filter1].[VisitLineId] = [Extent5].[Id]
GROUP BY [Extent5].[ProcedureId]
) AS [GroupBy1]
) AS [Project2]
PS Если это неясно, ответ на ваш конкретный вопрос
Можно ли заставить EF генерировать SQL точно так, как он был написан в linq?
нет. Напротив, вы должны написать запрос LINQ определенным образом, чтобы получить желаемый (или более близкий) SQL-запрос.
Комментарии:
1. Добавление второго столбца в group by не заставляет EF генерировать GroupBy, оно добавляет дополнительный столбец в подзапросы и добавляет к ним дополнительные фильтры, которые значительно снижают производительность при текущем запросе. В моем случае использование подзапроса для кода процедуры намного лучше
2. И проблема заключается не в получении кода из процедуры, а в EF, которые генерируют несколько подзапросов с одинаковыми фильтрами, что намного менее эффективно, чем использование подхода GroupBy.
3. Я не говорю, что подзапрос неэффективен — я пытаюсь использовать конструкцию LINQ, которая не заставит EF генерировать ненужные подзапросы. Вы уверены, что предложенная модификация не приведет к гораздо лучшему SQL? Я не могу протестировать, потому что у меня нет классов модели.
4. Да, я знаю это, потому что моя первая реализация linq была написана с использованием этих 2 столбцов в groupBy. SQL-запрос тот же.
5. Спасибо. Двойной выбор дает желаемый результат. Странное поведение. Я думаю, будет полезно изучить исходный код транслятора, который преобразует выражения DB в SQL