Optimize Collection Grouping, NextUp and Latest queries

This commit is contained in:
Shadowghost
2026-02-07 00:56:55 +01:00
parent 8ddc35a1ce
commit 268d88a5fb
9 changed files with 365 additions and 214 deletions

View File

@@ -1693,7 +1693,7 @@ namespace MediaBrowser.Controller.Entities
return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
}
private bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
protected bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
{
var blockedTags = user.GetPreference(PreferenceKind.BlockedTags);
var allowedTags = user.GetPreference(PreferenceKind.AllowedTags);
@@ -2487,7 +2487,7 @@ namespace MediaBrowser.Controller.Entities
return path;
}
public virtual void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields)
public virtual void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields, (int Played, int Total)? precomputedCounts = null)
{
if (RunTimeTicks.HasValue)
{

View File

@@ -730,36 +730,9 @@ namespace MediaBrowser.Controller.Entities
public QueryResult<BaseItem> QueryRecursive(InternalItemsQuery query)
{
var user = query.User;
if (!query.ForceDirect && RequiresPostFiltering(query))
{
IEnumerable<BaseItem> items;
Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
var totalCount = 0;
if (query.User is null)
{
items = GetRecursiveChildren(filter);
totalCount = items.Count();
}
else
{
// Save pagination params before clearing them to prevent pagination from happening
// before sorting. PostFilterAndSort will apply pagination after sorting.
var limit = query.Limit;
var startIndex = query.StartIndex;
query.Limit = null;
query.StartIndex = null;
items = GetRecursiveChildren(user, query, out totalCount);
// Restore pagination params so PostFilterAndSort can apply them after sorting
query.Limit = limit;
query.StartIndex = startIndex;
}
return PostFilterAndSort(items, query);
query.CollapseBoxSetItems = true;
}
if (this is not UserRootFolder
@@ -1690,7 +1663,7 @@ namespace MediaBrowser.Controller.Entities
return !IsPlayed(user, userItemData);
}
public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields)
public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields, (int Played, int Total)? precomputedCounts = null)
{
if (!SupportsUserDataFromChildren)
{
@@ -1699,22 +1672,28 @@ namespace MediaBrowser.Controller.Entities
if (SupportsPlayedStatus || (itemDto is not null && fields.ContainsField(ItemFields.RecursiveItemCount)))
{
// Create a minimal query with just the user - skip ConfigureUserAccess to avoid
// expensive GetUserViews calls. Since we're counting descendants of a specific
// item (this folder) that the user already has access to, TopParentIds filtering
// is redundant. The parental rating filter is applied via query.User.
var query = new InternalItemsQuery(user);
int playedCount;
int totalCount;
if (LinkedChildren.Length > 0)
if (precomputedCounts.HasValue && LinkedChildren.Length == 0)
{
(playedCount, totalCount) = ItemRepository.GetPlayedAndTotalCountFromLinkedChildren(query, Id);
// Use batch-fetched counts (avoids N+1 queries)
(playedCount, totalCount) = precomputedCounts.Value;
}
else
{
(playedCount, totalCount) = ItemRepository.GetPlayedAndTotalCount(query, Id);
// Fall back to per-item query for LinkedChildren items (BoxSets, Playlists)
// or when no batch data is available
var query = new InternalItemsQuery(user);
if (LinkedChildren.Length > 0)
{
(playedCount, totalCount) = ItemRepository.GetPlayedAndTotalCountFromLinkedChildren(query, Id);
}
else
{
(playedCount, totalCount) = ItemRepository.GetPlayedAndTotalCount(query, Id);
}
}
if (itemDto is not null && fields.ContainsField(ItemFields.RecursiveItemCount))

View File

@@ -158,25 +158,25 @@ namespace MediaBrowser.Controller.Entities.Movies
return base.IsVisible(user, skipAllowedTagsCheck);
}
if (base.IsVisible(user, skipAllowedTagsCheck))
if (!IsVisibleViaTags(user, skipAllowedTagsCheck))
{
if (LinkedChildren.Length == 0)
{
return true;
}
var userLibraryFolderIds = GetLibraryFolderIds(user);
var libraryFolderIds = LibraryFolderIds ?? GetLibraryFolderIds();
if (libraryFolderIds.Length == 0)
{
return true;
}
return userLibraryFolderIds.Any(i => libraryFolderIds.Contains(i));
return false;
}
return false;
if (LinkedChildren.Length == 0)
{
return true;
}
var userLibraryFolderIds = GetLibraryFolderIds(user);
var libraryFolderIds = LibraryFolderIds ?? GetLibraryFolderIds();
if (libraryFolderIds.Length == 0)
{
return true;
}
return userLibraryFolderIds.Any(i => libraryFolderIds.Contains(i));
}
public override bool IsVisibleStandalone(User user)

View File

@@ -697,6 +697,15 @@ namespace MediaBrowser.Controller.Library
/// <returns>Dictionary mapping parent ID to child count.</returns>
Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId);
/// <summary>
/// Batch-fetches played and total counts for multiple folder items.
/// Avoids N+1 queries when building DTOs for lists of folder items.
/// </summary>
/// <param name="folderIds">The list of folder item IDs.</param>
/// <param name="user">The user for access filtering and played status.</param>
/// <returns>Dictionary mapping folder ID to (Played count, Total count).</returns>
Dictionary<Guid, (int Played, int Total)> GetPlayedAndTotalCountBatch(IReadOnlyList<Guid> folderIds, User user);
/// <summary>
/// Configures the query with user access settings including TopParentIds for library access.
/// Call this before passing a query to methods that need user access filtering.

View File

@@ -188,6 +188,16 @@ public interface IItemRepository
/// <returns>A tuple containing (Played count, Total count).</returns>
(int Played, int Total) GetPlayedAndTotalCountFromLinkedChildren(InternalItemsQuery filter, Guid parentId);
/// <summary>
/// Batch-fetches played and total counts for multiple folder items using the AncestorIds table.
/// This avoids N+1 queries when building DTOs for lists of folder items (Series, Seasons, etc.).
/// Applies user access filtering (parental controls, tags).
/// </summary>
/// <param name="folderIds">The list of folder item IDs to get counts for.</param>
/// <param name="user">The user for access filtering and played status.</param>
/// <returns>Dictionary mapping folder ID to (Played count, Total count).</returns>
Dictionary<Guid, (int Played, int Total)> GetPlayedAndTotalCountBatch(IReadOnlyList<Guid> folderIds, User user);
/// <summary>
/// Gets the IDs of linked children for the specified parent.
/// </summary>