Трудно дать вам хороший совет, потому что сгенерированный SQL-запрос выглядит хорошо для этой модели, и в настоящее время на оптимизаторы SQL-запросов (CBO) не должно влиять то, как вы пишете запрос, как старые RBO (CBO означает Cost Оптимизатор на основе, RBO - Оптимизатор на основе правил). Они должны иметь возможность превратить EXISTS
или IN
в JOIN
(создать тот же план выполнения, что и JOIN
). Единственное различие между текущим SQL и оригиналом состоит в одном дополнительном соединении, которое при поиске кластерного индекса PK не должно существенно влиять на производительность.
Но поскольку вы говорите, что, по-видимому, что-то неизвестное заставляет ОСО выбирать плохой план. А поскольку план зависит от данных, которых у меня нет, все, что я могу сделать, - это предложить два альтернативных функционально эквивалентных запроса.
Во-первых, ваш текущий (медленный) запрос выглядит так:
var input = new { DefendantCode = "Abc", Skip = 4, Take = 2 };
var defendantCodePattern = "%" + input.DefendantCode + "%";
var query = db.Set<Record>()
.Where(r => r.IsActive)
.Where(r => EF.Functions.Like(r.Employer.DefendantCode, defendantCodePattern)
|| EF.Functions.Like(r.Contractor.DefendantCode, defendantCodePattern)
|| r.RecordProducts.Any(rp => EF.Functions.Like(rp.ProductDefendant.Defendant.DefendantCode, defendantCodePattern))
)
.Select(r => new
{
ID = r.RecordID,
StartDate = r.StartDate,
EndDate = r.EndDate,
WitnessName = r.Witness.FullName,
SourceCode = r.Source != null ? r.Source.SourceCode : "zzzzz",
JobsiteName = r.Jobsite != null ? r.Jobsite.JobsiteName : "zzzzz",
ShipName = r.Ship != null ? r.Ship.ShipName : "zzzzz",
EmployeeCode = r.Employer != null ? r.Employer.DefendantCode : "zzzzz",
})
//.Distinct()
.OrderBy(t => t.SourceCode)
.Skip(input.Skip).Take(input.Take);
Некоторые вещи, которые стоит упомянуть. Во-первых, запрос использует проекцию (Select
), поэтому Include
/ ThenInclude
не нужны (потому что они игнорируются ). Во-вторых, общий шаблон поиска создается и сохраняется вне запроса, в результате чего вместо параметра 3. в качестве параметра sing используется третий. В-третьих, Distinct
не требуется для этого запроса, поэтому я удалил его.
Теперь потенциальные попытки улучшить скорость выполнения сгенерированного SQL-запроса.
(1) Если связанная таблица Defendant
невелика, вы можете предварительно выбрать DefendantID
s, соответствующие поисковому фильтру, а затем использовать Contains
(в переводе на SQL IN
) для фильтрации. Это поможет устранить некоторые из соединений. например,
var defendantIds = db.Set<Defendant>()
.Where(d => EF.Functions.Like(d.DefendantCode, defendantCodePattern))
.Select(d => d.DefendantID)
.ToList();
и затем (второй Where
):
.Where(r => defendantIds.Contains(r.Employer.DefendantID)
|| defendantIds.Contains(r.Contractor.DefendantID)
|| r.RecordProducts.Any(rp => defendantIds.Contains(rp.ProductDefendant.Defendant.DefendantID))
)
(2) Следующий трюк заменит EXISTS
на LEFT JOIN
. Заменить второе Where
на:
.SelectMany(r => r.RecordProducts.DefaultIfEmpty(), (r, rp) => new { r, rp })
.Where(x => EF.Functions.Like(x.r.Employer.DefendantCode, defendantCodePattern)
|| EF.Functions.Like(x.r.Contractor.DefendantCode, defendantCodePattern)
|| EF.Functions.Like(x.rp.ProductDefendant.Defendant.DefendantCode, defendantCodePattern)
)
.Select(x => x.r)
и раскомментируйте .Distinct()
(здесь это необходимо, потому что LEFT JOIN
(из SelectMany
) умножает исходные записи). Сгенерированный SQL в этом случае выглядит так:
SELECT [t].[ID], [t].[StartDate], [t].[EndDate], [t].[WitnessName], [t].[SourceCode], [t].[JobsiteName], [t].[ShipName], [t].[EmployeeCode]
FROM (
SELECT DISTINCT [r].[RecordID] AS [ID], [r].[StartDate], [r].[EndDate], [r.Witness].[FullName] AS [WitnessName], CASE
WHEN [r].[SourceID] IS NOT NULL
THEN [r.Source].[SourceCode] ELSE N'zzzzz'
END AS [SourceCode], CASE
WHEN [r].[JobsiteID] IS NOT NULL
THEN [r.Jobsite].[JobsiteName] ELSE N'zzzzz'
END AS [JobsiteName], CASE
WHEN [r].[ShipID] IS NOT NULL
THEN [r.Ship].[ShipName] ELSE N'zzzzz'
END AS [ShipName], CASE
WHEN [r].[EmployerID] IS NOT NULL
THEN [r.Employer].[DefendantCode] ELSE N'zzzzz'
END AS [EmployeeCode]
FROM [Records] AS [r]
LEFT JOIN [Ships] AS [r.Ship] ON [r].[ShipID] = [r.Ship].[ShipID]
LEFT JOIN [Jobsites] AS [r.Jobsite] ON [r].[JobsiteID] = [r.Jobsite].[JobsiteID]
LEFT JOIN [Sources] AS [r.Source] ON [r].[SourceID] = [r.Source].[SourceID]
LEFT JOIN [Witnesses] AS [r.Witness] ON [r].[WitnessID] = [r.Witness].[WitnessID]
LEFT JOIN [Defendants] AS [r.Contractor] ON [r].[ContractorID] = [r.Contractor].[DefendantID]
LEFT JOIN [Defendants] AS [r.Employer] ON [r].[EmployerID] = [r.Employer].[DefendantID]
LEFT JOIN [Records_Products] AS [r.RecordProducts] ON [r].[RecordID] = [r.RecordProducts].[RecordID]
LEFT JOIN [Product_Defendant] AS [r.RecordProducts.ProductDefendant] ON [r.RecordProducts].[DefendantProductID] = [r.RecordProducts.ProductDefendant].[DefendantProductID]
LEFT JOIN [Defendants] AS [r.RecordProducts.ProductDefendant.Defendant] ON [r.RecordProducts.ProductDefendant].[DefendantID] = [r.RecordProducts.ProductDefendant.Defendant].[DefendantID]
WHERE ([r].[IsActive] = 1) AND (([r.Employer].[DefendantCode] LIKE @__defendantCodePattern_1 OR [r.Contractor].[DefendantCode] LIKE @__defendantCodePattern_1) OR [r.RecordProducts.ProductDefendant.Defendant].[DefendantCode] LIKE @__defendantCodePattern_1)
) AS [t]
ORDER BY [t].[SourceCode]
OFFSET @__p_2 ROWS FETCH NEXT @__p_3 ROWS ONLY
Как я сказал в самом начале, обычно это не должно влиять на план CBO. Но я определенно вижу примерный план выполнения, отличный от исходного, поэтому стоит попробовать (хотя запрос LINQ выглядит некрасиво).