diff options
| author | Shadowghost <Ghost_of_Stone@web.de> | 2026-07-23 11:52:26 +0200 |
|---|---|---|
| committer | Shadowghost <Ghost_of_Stone@web.de> | 2026-07-23 20:50:06 +0200 |
| commit | 9aa79682bab9a18f87b0c13c776a19e3eb357335 (patch) | |
| tree | e67434b9af5b5f2711517952808457ce1c00e594 /src | |
| parent | 88216e0ec4adc388a1db795f18fa8f4591ebc099 (diff) | |
Reduce correlated subqueries to improve performance
Diffstat (limited to 'src')
| -rw-r--r-- | src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs | 65 |
1 files changed, 41 insertions, 24 deletions
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs index 1af7460540..fec37ce723 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs @@ -70,15 +70,24 @@ public static class JellyfinQueryHelperExtensions bool invert = false) { var itemFilter = OneOrManyExpressionBuilder<BaseItemEntity, Guid>(referenceIds, f => f.Id); - var typeFilter = OneOrManyExpressionBuilder<ItemValue, ItemValueType>(itemValueTypes, iv => iv.Type); - - return baseQuery.Where(item => - context.ItemValues - .Where(typeFilter) - .Join(context.ItemValuesMap, e => e.ItemValueId, e => e.ItemValueId, (itemVal, map) => new { itemVal, map }) - .Any(val => - context.BaseItems.Where(itemFilter).Any(e => e.CleanName == val.itemVal.CleanValue) - && val.map.ItemId == item.Id) == EF.Constant(!invert)); + var typeFilter = OneOrManyExpressionBuilder<ItemValueMap, ItemValueType>(itemValueTypes, m => m.ItemValue.Type); + + // Flat sub-selects + Contains instead of a nested correlated .Any(...Any(...)). + var referencedCleanValues = context.BaseItems + .Where(itemFilter) + .Select(e => e.CleanName); + + var matchingItemIds = context.ItemValuesMap + .Where(typeFilter) + .Where(m => referencedCleanValues.Contains(m.ItemValue.CleanValue)) + .Select(m => m.ItemId); + + if (invert) + { + return baseQuery.Where(e => !matchingItemIds.Contains(e.Id)); + } + + return baseQuery.Where(e => matchingItemIds.Contains(e.Id)); } /// <summary> @@ -102,13 +111,21 @@ public static class JellyfinQueryHelperExtensions var itemFilter = OneOrManyExpressionBuilder<BaseItemEntity, Guid>(referenceIds, f => f.Id); - return item => - context.ItemValues - .Join(context.ItemValuesMap, e => e.ItemValueId, e => e.ItemValueId, (item, map) => new { item, map }) - .Any(val => - val.item.Type == itemValueType - && context.BaseItems.Where(itemFilter).Any(e => e.CleanName == val.item.CleanValue) - && val.map.ItemId == item.Id) == EF.Constant(!invert); + // Flat sub-selects + Contains instead of a nested correlated .Any(...Any(...)). + var referencedCleanValues = context.BaseItems + .Where(itemFilter) + .Select(e => e.CleanName); + + var matchingItemIds = context.ItemValuesMap + .Where(m => m.ItemValue.Type == itemValueType && referencedCleanValues.Contains(m.ItemValue.CleanValue)) + .Select(m => m.ItemId); + + if (invert) + { + return item => !matchingItemIds.Contains(item.Id); + } + + return item => matchingItemIds.Contains(item.Id); } /// <summary> @@ -224,14 +241,14 @@ public static class JellyfinQueryHelperExtensions var containsMethodInfo = _containsQueryCache.GetOrAdd(typeof(TProperty), static (key) => _containsMethodGenericCache.MakeGenericMethod(key)); - // Threshold picked from microbenchmarks on SQLite: inline IN(const,...) beats a - // parameterized array lookup by ~5-10% up to ~32 elements. - if (oneOf.Count <= 32) - { - return Expression.Lambda<Func<TEntity, bool>>(Expression.Call(null, containsMethodInfo, Expression.Constant(oneOf), property.Body), parameter); - } - - return Expression.Lambda<Func<TEntity, bool>>(Expression.Call(null, containsMethodInfo, Expression.Call(null, _efParameterInstruction.MakeGenericMethod(oneOf.GetType()), Expression.Constant(oneOf)), property.Body), parameter); + // Always wrap the collection in EF.Parameter so EF Core caches a single compiled plan and reuses it across calls. + return Expression.Lambda<Func<TEntity, bool>>( + Expression.Call( + null, + containsMethodInfo, + Expression.Call(null, _efParameterInstruction.MakeGenericMethod(oneOf.GetType()), Expression.Constant(oneOf)), + property.Body), + parameter); } internal static class ParameterReplacer |
