feat(dal): UnitFilterService оптимизация запросов.

This commit is contained in:
Mikhail Kuznetsov
2026-03-17 13:20:29 +10:00
parent 3f013e2bfc
commit 3704a131b4
2 changed files with 40 additions and 50 deletions

View File

@@ -7,7 +7,6 @@ using PARR.DAL.Contracts;
using PARR.DAL.DomainServices.UnitFilterService.Models; using PARR.DAL.DomainServices.UnitFilterService.Models;
using PARR.DAL.Models.Job; using PARR.DAL.Models.Job;
using PARR.DAL.Models.Unit; using PARR.DAL.Models.Unit;
using PARR.DAL.Services.Implementations.Unit;
using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.Services.Interfaces.Job;
using PARR.DAL.Services.Interfaces.Unit; using PARR.DAL.Services.Interfaces.Unit;
using System.Diagnostics; using System.Diagnostics;
@@ -340,12 +339,12 @@ internal class UnitFilterService : IUnitFilterService
{ {
if (!relationshipFilters.Any() || !unitIds.Any()) if (!relationshipFilters.Any() || !unitIds.Any())
{ {
logger.LogDebug("ApplyRelationshipFiltersOnDbAsync: вход {UnitCount} юнитов, фильтров: 0 → возврат без изменений", logger.LogDebug("ProcessRelationshipFiltersAsync: вход {UnitCount} юнитов, фильтров: 0 → возврат без изменений",
unitIds.Count); unitIds.Count);
return unitIds.Select(id => new FilteredUnitContext { UnitId = id }).ToList(); return unitIds.Select(id => new FilteredUnitContext { UnitId = id }).ToList();
} }
logger.LogDebug("ApplyRelationshipFiltersOnDbAsync: вход {UnitCount} юнитов, фильтров: {FilterCount}", logger.LogDebug("ProcessRelationshipFiltersAsync: вход {UnitCount} юнитов, фильтров: {FilterCount}",
unitIds.Count, relationshipFilters.Count()); unitIds.Count, relationshipFilters.Count());
var resultContexts = unitIds.ToDictionary(id => id, id => new FilteredUnitContext { UnitId = id }); var resultContexts = unitIds.ToDictionary(id => id, id => new FilteredUnitContext { UnitId = id });
@@ -384,7 +383,7 @@ internal class UnitFilterService : IUnitFilterService
: resultContexts.Values.ToList(); : resultContexts.Values.ToList();
var result = unitsWithRelationships; var result = unitsWithRelationships;
logger.LogDebug("ApplyRelationshipFiltersOnDbAsync: выход {ContextCount} контекстов (из {InitialCount})", logger.LogDebug("ProcessRelationshipFiltersAsync: выход {ContextCount} контекстов (из {InitialCount})",
result.Count, resultContexts.Count); result.Count, resultContexts.Count);
#if DEBUG #if DEBUG
@@ -393,8 +392,7 @@ internal class UnitFilterService : IUnitFilterService
.Concat(result.SelectMany(c => c.ValidParentIds)) .Concat(result.SelectMany(c => c.ValidParentIds))
.Concat(result.SelectMany(c => c.ValidChildIds)) .Concat(result.SelectMany(c => c.ValidChildIds))
.Distinct() .Distinct()
.Take(100) .ToList(); // ← Убрали .Take(100)
.ToList();
// Пакетная загрузка всех юнитов // Пакетная загрузка всех юнитов
var unitsMap = await unitService.Get().AsNoTracking() var unitsMap = await unitService.Get().AsNoTracking()
@@ -406,17 +404,18 @@ internal class UnitFilterService : IUnitFilterService
var childFieldIds = relationshipFilters.Where(rf => !rf.IsParent).Select(rf => rf.FieldId).Distinct().ToList(); var childFieldIds = relationshipFilters.Where(rf => !rf.IsParent).Select(rf => rf.FieldId).Distinct().ToList();
var allFieldIds = parentFieldIds.Concat(childFieldIds).Distinct().ToList(); var allFieldIds = parentFieldIds.Concat(childFieldIds).Distinct().ToList();
// ПАКЕТНАЯ загрузка значений для ВСЕХ юнитов сразу (1 запрос вместо N+1) // ПАКЕТНАЯ загрузка значений для ВСЕХ юнитов сразу
var allValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allUnitIds, allFieldIds); var allValues = await unitInValueService.GetByUnitIdsAndFieldIdsAsync(allUnitIds, allFieldIds);
var valuesByUnit = allValues.GroupBy(v => v.UnitId) var valuesByUnit = allValues.GroupBy(v => v.UnitId)
.ToDictionary(g => g.Key, g => g.ToList()); .ToDictionary(g => g.Key, g => g.ToList());
// ПАКЕТНАЯ загрузка имён полей (1 запрос вместо N+1) // ПАКЕТНАЯ загрузка имён полей
var fieldsMap = await unitFieldService.Get() var fieldsMap = await unitFieldService.Get()
.AsNoTracking() .AsNoTracking()
.Where(f => allFieldIds.Contains(f.Id)) .Where(f => allFieldIds.Contains(f.Id))
.ToDictionaryAsync(f => f.Id, f => f.AihitName, cancellationToken); .ToDictionaryAsync(f => f.Id, f => f.AihitName, cancellationToken);
// Логируем только первые 10 юнитов (чтобы не засорять лог)
foreach (var context in result.Take(10)) foreach (var context in result.Take(10))
{ {
var u = unitsMap.GetValueOrDefault(context.UnitId); var u = unitsMap.GetValueOrDefault(context.UnitId);
@@ -431,17 +430,16 @@ internal class UnitFilterService : IUnitFilterService
{ {
logger.LogDebug("\tФильтрам соответствуют {ParentsCount} родителей:", context.ValidParentIds.Count); logger.LogDebug("\tФильтрам соответствуют {ParentsCount} родителей:", context.ValidParentIds.Count);
foreach (var parentId in context.ValidParentIds) // Логируем только первые 5 родителей (чтобы не засорять лог)
foreach (var parentId in context.ValidParentIds.Take(5))
{ {
var p = unitsMap.GetValueOrDefault(parentId); var p = unitsMap.GetValueOrDefault(parentId);
logger.LogDebug("\t\t{ParentName} (Id={ParentId})", p?.Name, parentId); logger.LogDebug("\t\t{ParentName} (Id={ParentId})", p?.Name ?? "null", parentId);
// Берём значения из кэша, а не делаем запрос
if (parentFieldIds.Any() && valuesByUnit.TryGetValue(parentId, out var parentValues)) if (parentFieldIds.Any() && valuesByUnit.TryGetValue(parentId, out var parentValues))
{ {
foreach (var pv in parentValues.Where(v => parentFieldIds.Contains(v.FieldId))) foreach (var pv in parentValues.Where(v => parentFieldIds.Contains(v.FieldId)))
{ {
// Используем предварительно загруженный словарь полей
var fieldName = fieldsMap.GetValueOrDefault(pv.FieldId) ?? $"FieldId={pv.FieldId}"; var fieldName = fieldsMap.GetValueOrDefault(pv.FieldId) ?? $"FieldId={pv.FieldId}";
logger.LogDebug("\t\t {FieldName} = {FieldValue}", logger.LogDebug("\t\t {FieldName} = {FieldValue}",
fieldName, fieldName,
@@ -449,6 +447,11 @@ internal class UnitFilterService : IUnitFilterService
} }
} }
} }
if (context.ValidParentIds.Count > 5)
{
logger.LogDebug("\t\t... и ещё {Count} родителей", context.ValidParentIds.Count - 5);
}
} }
else if (parentRelFilters.Any()) else if (parentRelFilters.Any())
{ {
@@ -460,17 +463,15 @@ internal class UnitFilterService : IUnitFilterService
{ {
logger.LogDebug("\tФильтрам соответствуют {ChildrenCount} детей:", context.ValidChildIds.Count); logger.LogDebug("\tФильтрам соответствуют {ChildrenCount} детей:", context.ValidChildIds.Count);
foreach (var childId in context.ValidChildIds) foreach (var childId in context.ValidChildIds.Take(5))
{ {
var c = unitsMap.GetValueOrDefault(childId); var c = unitsMap.GetValueOrDefault(childId);
logger.LogDebug("\t\t{ChildName} (Id={ChildId})", c?.Name, childId); logger.LogDebug("\t\t{ChildName} (Id={ChildId})", c?.Name ?? "null", childId);
// Берём значения из кэша, а не делаем запрос
if (childFieldIds.Any() && valuesByUnit.TryGetValue(childId, out var childValues)) if (childFieldIds.Any() && valuesByUnit.TryGetValue(childId, out var childValues))
{ {
foreach (var cv in childValues.Where(v => childFieldIds.Contains(v.FieldId))) foreach (var cv in childValues.Where(v => childFieldIds.Contains(v.FieldId)))
{ {
// Используем предварительно загруженный словарь полей
var fieldName = fieldsMap.GetValueOrDefault(cv.FieldId) ?? $"FieldId={cv.FieldId}"; var fieldName = fieldsMap.GetValueOrDefault(cv.FieldId) ?? $"FieldId={cv.FieldId}";
logger.LogDebug("\t\t {FieldName} = {FieldValue}", logger.LogDebug("\t\t {FieldName} = {FieldValue}",
fieldName, fieldName,
@@ -478,6 +479,11 @@ internal class UnitFilterService : IUnitFilterService
} }
} }
} }
if (context.ValidChildIds.Count > 5)
{
logger.LogDebug("\t\t... и ещё {Count} детей", context.ValidChildIds.Count - 5);
}
} }
else if (childRelFilters.Any()) else if (childRelFilters.Any())
{ {
@@ -761,7 +767,6 @@ internal class UnitFilterService : IUnitFilterService
var directionName = isParentDirection ? "Родительские" : "Дочерние"; var directionName = isParentDirection ? "Родительские" : "Дочерние";
logger.LogDebug(" {Direction} фильтры ({Count}):", directionName, relFilters.Count); logger.LogDebug(" {Direction} фильтры ({Count}):", directionName, relFilters.Count);
// Получаем все связи в нужном направлении
var allLinks = await unitInUnitService.Get() var allLinks = await unitInUnitService.Get()
.AsNoTracking() .AsNoTracking()
.Where(link => isParentDirection .Where(link => isParentDirection
@@ -780,11 +785,9 @@ internal class UnitFilterService : IUnitFilterService
isParentDirection ? "родителей" : "детей", isParentDirection ? "родителей" : "детей",
allLinks.Count); allLinks.Count);
// Группируем связи по исходному юниту для быстрого доступа
var linksBySource = allLinks.GroupBy(l => l.SourceId) var linksBySource = allLinks.GroupBy(l => l.SourceId)
.ToDictionary(g => g.Key, g => g.Select(l => l.TargetId).ToList()); .ToDictionary(g => g.Key, g => g.Select(l => l.TargetId).ToList());
// Находим связи, которые проходят ВСЕ фильтры одновременно
var targetFilterPassCount = new Dictionary<Guid, int>(); var targetFilterPassCount = new Dictionary<Guid, int>();
foreach (var relFilter in relFilters) foreach (var relFilter in relFilters)
@@ -793,7 +796,6 @@ internal class UnitFilterService : IUnitFilterService
if (string.IsNullOrWhiteSpace(valueMask)) if (string.IsNullOrWhiteSpace(valueMask))
continue; continue;
// Обработка маски LIKE
bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%"); bool isStartsWith = valueMask.EndsWith("%") && !valueMask.EndsWith("%%");
bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%"); bool isEndsWith = valueMask.StartsWith("%") && !valueMask.StartsWith("%%");
string dbValueMask; string dbValueMask;
@@ -811,27 +813,27 @@ internal class UnitFilterService : IUnitFilterService
isParentDirection ? "Parent" : "Child", isParentDirection ? "Parent" : "Child",
fieldName, dbValueMask, relFilter.IsInverse, relFilter.IsFullMatch); fieldName, dbValueMask, relFilter.IsInverse, relFilter.IsFullMatch);
// Находим целевые юниты, которые соответствуют ТЕКУЩЕМУ фильтру var matchingTargetIds = await unitInValueService.Get()
var matchingTargetIds = await unitService.Get()
.AsNoTracking() .AsNoTracking()
.Where(u => allTargetIds.Contains(u.Id)) .Where(uiv => uiv.FieldId == relFilter.FieldId)
.Where(u => u.UnitValues.Any(v => .Where(uiv => EF.Functions.ILike(uiv.Value.Value, dbValueMask))
v.FieldId == relFilter.FieldId && .Select(uiv => uiv.UnitId)
EF.Functions.ILike(v.Value.Value, dbValueMask))) .Distinct()
.Select(u => u.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var filteredMatchingTargetIds = matchingTargetIds
.Intersect(allTargetIds)
.ToList();
logger.LogDebug(" Найдено {MatchCount} {TargetType} по маске", logger.LogDebug(" Найдено {MatchCount} {TargetType} по маске",
matchingTargetIds.Count, filteredMatchingTargetIds.Count,
isParentDirection ? "родителей" : "детей"); isParentDirection ? "родителей" : "детей");
// Считаем, сколько фильтров прошёл каждый TargetId
if (relFilter.IsInverse) if (relFilter.IsInverse)
{ {
// Для IsInverse: "проходит" фильтр тот, кто НЕ соответствует маске
foreach (var targetId in allTargetIds) foreach (var targetId in allTargetIds)
{ {
if (!matchingTargetIds.Contains(targetId)) if (!filteredMatchingTargetIds.Contains(targetId))
{ {
if (!targetFilterPassCount.ContainsKey(targetId)) if (!targetFilterPassCount.ContainsKey(targetId))
targetFilterPassCount[targetId] = 0; targetFilterPassCount[targetId] = 0;
@@ -840,13 +842,12 @@ internal class UnitFilterService : IUnitFilterService
} }
logger.LogDebug(" Для IsInverse: {NonMatchCount} {TargetType} НЕ соответствуют маске", logger.LogDebug(" Для IsInverse: {NonMatchCount} {TargetType} НЕ соответствуют маске",
allTargetIds.Count - matchingTargetIds.Count, allTargetIds.Count - filteredMatchingTargetIds.Count,
isParentDirection ? "родителей" : "детей"); isParentDirection ? "родителей" : "детей");
} }
else else
{ {
// Для обычного фильтра: "проходит" тот, кто соответствует маске foreach (var targetId in filteredMatchingTargetIds)
foreach (var targetId in matchingTargetIds)
{ {
if (!targetFilterPassCount.ContainsKey(targetId)) if (!targetFilterPassCount.ContainsKey(targetId))
targetFilterPassCount[targetId] = 0; targetFilterPassCount[targetId] = 0;
@@ -855,7 +856,6 @@ internal class UnitFilterService : IUnitFilterService
} }
} }
// Оставляем только те связи, которые прошли ВСЕ фильтры
int requiredFiltersCount = relFilters.Count(r => !string.IsNullOrWhiteSpace(r.ValueMask?.Trim())); int requiredFiltersCount = relFilters.Count(r => !string.IsNullOrWhiteSpace(r.ValueMask?.Trim()));
var validTargetIds = targetFilterPassCount var validTargetIds = targetFilterPassCount
.Where(kvp => kvp.Value == requiredFiltersCount) .Where(kvp => kvp.Value == requiredFiltersCount)
@@ -867,10 +867,8 @@ internal class UnitFilterService : IUnitFilterService
isParentDirection ? "родителей" : "детей", isParentDirection ? "родителей" : "детей",
requiredFiltersCount); requiredFiltersCount);
// Список юнитов, которые не прошли фильтры (для удаления)
var unitsToRemove = new HashSet<Guid>(); var unitsToRemove = new HashSet<Guid>();
// Применяем к каждому юниту
foreach (var context in resultContexts.Values) foreach (var context in resultContexts.Values)
{ {
if (unitsToRemove.Contains(context.UnitId)) if (unitsToRemove.Contains(context.UnitId))
@@ -879,11 +877,9 @@ internal class UnitFilterService : IUnitFilterService
var unitTargets = linksBySource.GetValueOrDefault(context.UnitId, new List<Guid>()); var unitTargets = linksBySource.GetValueOrDefault(context.UnitId, new List<Guid>());
bool hasTargets = unitTargets.Any(); bool hasTargets = unitTargets.Any();
// Находим валидные связи (прошли ВСЕ фильтры)
var validTargets = unitTargets.Intersect(validTargetIds).ToList(); var validTargets = unitTargets.Intersect(validTargetIds).ToList();
bool hasValidTargets = validTargets.Any(); bool hasValidTargets = validTargets.Any();
// Логика с IsFullMatch
bool isFullMatch = relFilters.All(rf => rf.IsFullMatch); bool isFullMatch = relFilters.All(rf => rf.IsFullMatch);
bool isInverse = relFilters.Any(rf => rf.IsInverse); bool isInverse = relFilters.Any(rf => rf.IsInverse);
@@ -891,26 +887,21 @@ internal class UnitFilterService : IUnitFilterService
if (!hasTargets) if (!hasTargets)
{ {
// Нет связей вообще passesFilter = isInverse;
passesFilter = isInverse; // IsInverse=true → проходит, иначе нет
} }
else if (isFullMatch) else if (isFullMatch)
{ {
// ВСЕ связи должны быть валидными
passesFilter = validTargets.Count == unitTargets.Count; passesFilter = validTargets.Count == unitTargets.Count;
} }
else else
{ {
// ХОТЯ БЫ ОДНА связь должна быть валидной
passesFilter = hasValidTargets; passesFilter = hasValidTargets;
} }
if (passesFilter) if (passesFilter)
{ {
// Сохраняем связи
if (isFullMatch) if (isFullMatch)
{ {
// Для IsFullMatch=true: сохраняем ВСЕ связи (они все валидные)
if (isParentDirection) if (isParentDirection)
context.ValidParentIds.UnionWith(unitTargets); context.ValidParentIds.UnionWith(unitTargets);
else else
@@ -918,7 +909,6 @@ internal class UnitFilterService : IUnitFilterService
} }
else else
{ {
// Для IsFullMatch=false: сохраняем ТОЛЬКО валидные связи
if (isParentDirection) if (isParentDirection)
context.ValidParentIds.UnionWith(validTargets); context.ValidParentIds.UnionWith(validTargets);
else else
@@ -927,12 +917,10 @@ internal class UnitFilterService : IUnitFilterService
} }
else else
{ {
// Юнит не прошёл фильтр — помечаем на удаление
unitsToRemove.Add(context.UnitId); unitsToRemove.Add(context.UnitId);
} }
} }
// Удаляем юниты, которые не прошли фильтры
foreach (var unitId in unitsToRemove) foreach (var unitId in unitsToRemove)
{ {
resultContexts.Remove(unitId); resultContexts.Remove(unitId);

View File

@@ -151,7 +151,10 @@ namespace PARR.DAL
services.AddTransient<IUnitFilterService, UnitFilterService>(); services.AddTransient<IUnitFilterService, UnitFilterService>();
services.AddTransient<IMatchingStatusService, MatchingStatusService>(); services.AddTransient<IMatchingStatusService, MatchingStatusService>();
services.AddSingleton(new UnitFilterServiceOptions { LoadBatchSize = 50 }); services.Configure<UnitFilterServiceOptions>(options =>
{
options.LoadBatchSize = 50;
});
#endregion #endregion
@@ -195,6 +198,5 @@ namespace PARR.DAL
return new ScheduleResponseAreaTimeOffsetService(dbContext, settingsFromDb, logger); return new ScheduleResponseAreaTimeOffsetService(dbContext, settingsFromDb, logger);
}); });
} }
} }
} }