Простая настройка: мастер-таблица и связанная дочерняя таблица (один мастер, много дочерних). Допустим, мы хотим извлечь всех мастеров и их главные хронологические дочерние значения (обновлено, доступно и т. Д.). Запрос будет выглядеть так (например):
var masters = from m in Master
let mc = m.Childs.Max(c => c.CreatedOn)
select new { m, mc };
Потенциальная проблема возникает, если у master нет дочерних элементов, подзапрос выдаст NULL и преобразование из NULL в DateTime завершится неудачно с
InvalidOperationException: Нуль
значение не может быть назначено члену
с типом System.DateTime, который является
необнуляемый тип значения.
Решением для исключения является приведение mc к DateTime?
, но мне нужны мастера, у которых есть дети, и просто обходят немногие, у которых еще нет детей.
Решение № 1 Добавить where m.Childs.Count() > 0
.
Этот пнул меня сильно и неожиданно, сгенерированный SQL был просто ужасен (как и его план выполнения) и работал почти вдвое медленнее:
SELECT [t2].[Name] AS [MasterName], [t2].[value] AS [cm]
FROM (
SELECT [t0].[id], [t0].[Name], (
SELECT MAX([t1].[CreatedOn])
FROM [Child] AS [t1]
WHERE [t1].[masterId] = [t0].[id]
) AS [value]
FROM [Master] AS [t0]
) AS [t2]
WHERE ((
SELECT COUNT(*)
FROM [Child] AS [t3]
WHERE [t3].[masterId] = [t2].[id]
)) > @p0
Решение # 2 с where mc != null
еще хуже, оно дает более короткий скрипт, но выполняется намного дольше, чем предыдущий (занимает примерно столько же времени, что и два выше вместе взятых)
SELECT [t2].[Name] AS [MasterName], [t2].[value] AS [cm]
FROM (
SELECT [t0].[id], [t0].[Name], (
SELECT MAX([t1].[CreatedOn])
FROM [Child] AS [t1]
WHERE [t1].[masterId] = [t0].[id]
) AS [value]
FROM [Master] AS [t0]
) AS [t2]
WHERE ([t2].[value]) IS NOT NULL
В целом много тратится впустую времени SQL на устранение нескольких строк из десятков или тысяч и более. Это привело меня к Solution # 3 , получить все и устранить пустые стороны клиента, но для этого мне пришлось поцеловать IQueryable до свидания:
var masters = from m in Master
let mc = (DateTime?)m.Childs.Max(c => c.CreatedOn)
select new { m, mc };
var mastersNotNull = masters.AsEnumerable().Where(m => m.mc != null);
и это работает, однако я пытаюсь выяснить, есть ли какие-либо недостатки в этом? Будет ли это вести себя в любом случае принципиально иначе, чем при полной монти IQueryable? Я полагаю, это также означает, что я не могу (или не должен) использовать мастера как фактор в другом IQueryable? Любой вклад / наблюдение / альтернатива приветствуется.