Remove unnecessary materializations

This commit is contained in:
Shadowghost
2026-04-26 17:55:19 +02:00
parent f806ae4018
commit fc866a64e0
6 changed files with 13 additions and 35 deletions

View File

@@ -168,9 +168,6 @@ internal static class BaseItemMapper
dto.ImageInfos = entity.Images.Select(e => MapImageFromEntity(e, appHost)).ToArray();
}
// dto.Type = entity.Type;
// dto.Data = entity.Data;
// dto.MediaType = Enum.TryParse<MediaType>(entity.MediaType);
if (dto is IHasStartDate hasStartDate)
{
hasStartDate.StartDate = entity.StartDate.GetValueOrDefault();
@@ -354,8 +351,6 @@ internal static class BaseItemMapper
}).ToArray() ?? [];
}
// dto.Type = entity.Type;
// dto.Data = entity.Data;
entity.MediaType = dto.MediaType.ToString();
if (dto is IHasStartDate hasStartDate)
{

View File

@@ -133,8 +133,9 @@ public sealed partial class BaseItemRepository
IsSeries = filter.IsSeries
});
// Materialize the matching CleanValues early. This splits one massive expression tree
// into two simpler queries, dramatically reducing EF Core expression compilation time.
// Keep this as an IQueryable sub-select. Materializing to a list would inline one
// bound parameter per CleanValue and hit SQLite's variable cap on libraries with
// high-cardinality value types (e.g. tens of thousands of artists).
var matchingCleanValues = context.ItemValuesMap
.Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type))
.Join(
@@ -142,8 +143,7 @@ public sealed partial class BaseItemRepository
ivm => ivm.ItemId,
g => g.Id,
(ivm, g) => ivm.ItemValue.CleanValue)
.Distinct()
.ToList();
.Distinct();
var innerQuery = PrepareItemQuery(context, filter)
.Where(e => e.Type == returnType)
@@ -170,10 +170,8 @@ public sealed partial class BaseItemRepository
ExcludeItemIds = filter.ExcludeItemIds
};
// Materialize the matching IDs first. This keeps the complex nested subquery
// (inner filter + ItemValues join + search + GroupBy) as a single simple SQL statement,
// and then the entity load with Includes uses a flat WHERE Id IN (...) list.
// This avoids EF having to compile the entire nested expression tree into the final query.
// Build the master query and collapse rows that share a PresentationUniqueKey
// (e.g. alternate versions) by picking the lowest Id per group.
var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter);
var orderedMasterQuery = ApplyOrder(masterQuery, filter, context)
@@ -229,9 +227,7 @@ public sealed partial class BaseItemRepository
var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
// Materialize the matching IDs to avoid nested subquery in the counts expression tree.
var itemIds = itemCountQuery.Select(e => e.Id).ToList();
var itemIds = itemCountQuery.Select(e => e.Id);
// Rewrite query to avoid SelectMany on navigation properties (which requires SQL APPLY, not supported on SQLite)
// Instead, start from ItemValueMaps and join with BaseItems

View File

@@ -432,8 +432,8 @@ public sealed partial class BaseItemRepository
|| (e.TopParentId.HasValue && f.ItemId == e.TopParentId.Value))));
}
// Exclude alternate versions and owned non-extra items from counts.
// Alternate versions have PrimaryVersionId set (pointing to their primary).
// Exclude alternate versions (have PrimaryVersionId set) and owned non-extra items.
// Extras (trailers, etc.) have OwnerId set but also have ExtraType set — keep those.
if (!filter.IncludeOwnedItems)
{
baseQuery = baseQuery.Where(e => e.PrimaryVersionId == null && (e.OwnerId == null || e.ExtraType != null));

View File

@@ -376,14 +376,6 @@ public sealed partial class BaseItemRepository
baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person));
}
if (!string.IsNullOrWhiteSpace(filter.MinSortName))
{
// this does not makes sense.
// baseQuery = baseQuery.Where(e => e.SortName >= query.MinSortName);
// whereClauses.Add("SortName>=@MinSortName");
// statement?.TryBind("@MinSortName", query.MinSortName);
}
if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
{
baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
@@ -407,7 +399,6 @@ public sealed partial class BaseItemRepository
}
}
// These are the same, for now
var nameContains = filter.NameContains;
if (!string.IsNullOrWhiteSpace(nameContains))
{

View File

@@ -150,13 +150,9 @@ public class NextUpService : INextUpService
.Where(id => id != Guid.Empty)
.Distinct()
.ToList();
var lastWatchedEpisodes = new Dictionary<Guid, BaseItemEntity>();
if (allLastWatchedIds.Count > 0)
{
var lwQuery = context.BaseItems.AsNoTracking().Where(e => allLastWatchedIds.Contains(e.Id));
lwQuery = _queryHelpers.ApplyNavigations(lwQuery, filter);
lastWatchedEpisodes = lwQuery.ToDictionary(e => e.Id);
}
var lwQuery = context.BaseItems.AsNoTracking().Where(e => allLastWatchedIds.Contains(e.Id));
lwQuery = _queryHelpers.ApplyNavigations(lwQuery, filter);
var lastWatchedEpisodes = lwQuery.ToDictionary(e => e.Id);
Dictionary<string, List<BaseItemEntity>> specialsBySeriesKey = new();
if (includeSpecials)

View File

@@ -31,7 +31,7 @@ public static class OrderMapper
{
return (sortBy, query.User) switch
{
(ItemSortBy.AirTime, _) => e => e.SortName, // TODO
(ItemSortBy.AirTime, _) => e => e.SortName,
(ItemSortBy.Runtime, _) => e => e.RunTimeTicks,
(ItemSortBy.Random, _) => e => EF.Functions.Random(),
(ItemSortBy.DatePlayed, _) => e => e.UserData!.Where(f => f.UserId.Equals(query.User!.Id)).OrderBy(f => f.CustomDataKey).FirstOrDefault()!.LastPlayedDate,