aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-07-24 20:52:24 +0200
committerShadowghost <Ghost_of_Stone@web.de>2026-07-25 19:09:39 +0200
commitf2f66606d7fd92d5552f7ecd178c339b59887e1d (patch)
treedf852c436d9e3231c4022b8bc19754ffaa67aaf7 /Jellyfin.Server.Implementations
parent9aa79682bab9a18f87b0c13c776a19e3eb357335 (diff)
Fix DatePlayed sorting performance
Diffstat (limited to 'Jellyfin.Server.Implementations')
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs23
-rw-r--r--Jellyfin.Server.Implementations/Item/OrderMapper.cs27
2 files changed, 32 insertions, 18 deletions
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
index f19df6259e..de25e9b763 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
@@ -560,16 +560,19 @@ public sealed partial class BaseItemRepository
// Only in-progress siblings can eliminate a candidate: a version without progress has a NULL max LastPlayedDate,
// which is never greater and never ties. Restricting the sibling scan to the in-progress set keeps this bounded by
// the user's Continue Watching count instead of forcing a full BaseItems scan (COALESCE keys are non-indexable) per row.
- baseQuery = baseQuery.Where(e => e.Type == seriesTypeName || !context.BaseItems
- .Where(s => s.Id != e.Id
- && inProgressIds.Contains(s.Id)
- && (s.PrimaryVersionId ?? s.Id) == (e.PrimaryVersionId ?? e.Id))
- .Any(s =>
- inProgress.Where(su => su.ItemId == s.Id).Max(su => su.LastPlayedDate)
- > inProgress.Where(eu => eu.ItemId == e.Id).Max(eu => eu.LastPlayedDate)
- || (inProgress.Where(su => su.ItemId == s.Id).Max(su => su.LastPlayedDate)
- == inProgress.Where(eu => eu.ItemId == e.Id).Max(eu => eu.LastPlayedDate)
- && s.Id.CompareTo(e.Id) < 0)));
+ // Items in no version group at all have no sibling that could eliminate them, so short-circuit the scan for those.
+ baseQuery = baseQuery.Where(e => e.Type == seriesTypeName
+ || (e.PrimaryVersionId == null && !context.BaseItems.Any(a => a.PrimaryVersionId == e.Id))
+ || !context.BaseItems
+ .Where(s => s.Id != e.Id
+ && inProgressIds.Contains(s.Id)
+ && (s.PrimaryVersionId ?? s.Id) == (e.PrimaryVersionId ?? e.Id))
+ .Any(s =>
+ inProgress.Where(su => su.ItemId == s.Id).Max(su => su.LastPlayedDate)
+ > inProgress.Where(eu => eu.ItemId == e.Id).Max(eu => eu.LastPlayedDate)
+ || (inProgress.Where(su => su.ItemId == s.Id).Max(su => su.LastPlayedDate)
+ == inProgress.Where(eu => eu.ItemId == e.Id).Max(eu => eu.LastPlayedDate)
+ && s.Id.CompareTo(e.Id) < 0)));
}
else
{
diff --git a/Jellyfin.Server.Implementations/Item/OrderMapper.cs b/Jellyfin.Server.Implementations/Item/OrderMapper.cs
index aac85d0131..25ad81ec6c 100644
--- a/Jellyfin.Server.Implementations/Item/OrderMapper.cs
+++ b/Jellyfin.Server.Implementations/Item/OrderMapper.cs
@@ -29,19 +29,30 @@ public static class OrderMapper
/// <returns>Func to be executed later for sorting query.</returns>
public static Expression<Func<BaseItemEntity, object?>> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query, JellyfinDbContext jellyfinDbContext)
{
+ if (sortBy == ItemSortBy.DatePlayed)
+ {
+ // An item's played date is the newest of its own progress and that of its alternate versions,
+ // which track progress under their own ids. Matching both in one predicate ORs them together,
+ // which no index can serve: the user's whole UserData table gets scanned per sorted row.
+ // Two indexed lookups combined by MAX cost a seek each instead.
+ var userData = query.User is null
+ ? jellyfinDbContext.UserData
+ : jellyfinDbContext.UserData.Where(w => w.UserId == query.User.Id);
+
+ return e => userData
+ .Where(w => w.ItemId == e.Id)
+ .Select(w => w.LastPlayedDate)
+ .Concat(userData
+ .Where(w => w.Item!.PrimaryVersionId == e.Id)
+ .Select(w => w.LastPlayedDate))
+ .Max();
+ }
+
return (sortBy, query.User) switch
{
(ItemSortBy.AirTime, _) => e => e.SortName,
(ItemSortBy.Runtime, _) => e => e.RunTimeTicks,
(ItemSortBy.Random, _) => e => EF.Functions.Random(),
- (ItemSortBy.DatePlayed, not null) => e =>
- jellyfinDbContext.UserData
- .Where(w => w.UserId == query.User.Id && (w.ItemId == e.Id || w.Item!.PrimaryVersionId == e.Id))
- .Max(f => f.LastPlayedDate),
- (ItemSortBy.DatePlayed, null) => e =>
- jellyfinDbContext.UserData
- .Where(w => w.ItemId == e.Id || w.Item!.PrimaryVersionId == e.Id)
- .Max(f => f.LastPlayedDate),
(ItemSortBy.PlayCount, _) => e => e.UserData!.Where(f => f.UserId.Equals(query.User!.Id)).OrderBy(f => f.CustomDataKey).FirstOrDefault()!.PlayCount,
(ItemSortBy.IsFavoriteOrLiked, _) => e => e.UserData!.Where(f => f.UserId.Equals(query.User!.Id)).OrderBy(f => f.CustomDataKey).Select(f => (bool?)f.IsFavorite).FirstOrDefault() ?? false,
(ItemSortBy.IsFolder, _) => e => e.IsFolder,