Enhance query performance by applying AsSplitQuery to multiple handlers and loaders for Channels and ScheduleTemplates, improving efficiency in data retrieval and reducing unnecessary data duplication in queries.
ci / build-backend (push) Successful in 1m26s
ci / build-frontend (push) Successful in 49s
ci / tests (push) Successful in 4m18s
ci / sonar (push) Failing after 2m31s

This commit is contained in:
Leonid Pershin
2026-07-26 21:50:01 +03:00
parent b52080e026
commit c4c93cff02
14 changed files with 25 additions and 0 deletions
@@ -16,6 +16,7 @@ public sealed class AddBumperTextVariantCommandHandler(IAppDbContext dbContext)
var channel = await dbContext
.Channels.Include(c => c.BumperTemplates)
.ThenInclude(t => t.Variants)
.AsSplitQuery()
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null)
return Result.Failure<Guid>(ChannelErrors.NotFound);
@@ -42,6 +42,7 @@ public sealed class BumperSpecLoader(
.Channels.AsNoTracking()
.Include(c => c.BumperTemplates)
.ThenInclude(t => t.Variants)
.AsSplitQuery()
.FirstOrDefaultAsync(c => c.Id == cache.ChannelId, cancellationToken);
var template = channel?.BumperTemplates.FirstOrDefault(t => t.Id == cache.TemplateId);
var variant = template?.Variants.FirstOrDefault(v => v.Id == cache.VariantId);
@@ -16,6 +16,7 @@ public sealed class RemoveBumperTextVariantCommandHandler(IAppDbContext dbContex
var channel = await dbContext
.Channels.Include(c => c.BumperTemplates)
.ThenInclude(t => t.Variants)
.AsSplitQuery()
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null)
return Result.Failure(ChannelErrors.NotFound);
@@ -33,6 +33,7 @@ public sealed class RenderBumperPreviewCommandHandler(
.Channels.AsNoTracking()
.Include(c => c.BumperTemplates)
.ThenInclude(t => t.Variants)
.AsSplitQuery()
.FirstOrDefaultAsync(c => c.Id == query.ChannelId, cancellationToken);
if (channel is null)
return Result.Failure(ChannelErrors.NotFound);
@@ -16,6 +16,7 @@ public sealed class UpdateBumperTextVariantCommandHandler(IAppDbContext dbContex
var channel = await dbContext
.Channels.Include(c => c.BumperTemplates)
.ThenInclude(t => t.Variants)
.AsSplitQuery()
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null)
return Result.Failure(ChannelErrors.NotFound);
@@ -19,6 +19,7 @@ public sealed class GetChannelQueryHandler(IAppDbContext dbContext)
.Channels.AsNoTracking()
.Include(c => c.BumperTemplates)
.ThenInclude(t => t.Variants)
.AsSplitQuery()
.FirstOrDefaultAsync(c => c.Id == query.Id, cancellationToken);
if (channel is null)
return Result.Failure<ChannelDto>(ChannelErrors.NotFound);
@@ -18,6 +18,8 @@ public sealed class GetShowQueryHandler(IAppDbContext dbContext)
.Shows.AsNoTracking()
.Include(s => s.Episodes)
.Include(s => s.Genres)
// Сиблинги: серии × жанры перемножились бы в одном запросе (см. ListShowsQueryHandler).
.AsSplitQuery()
.FirstOrDefaultAsync(s => s.Id == query.Id, cancellationToken);
if (show is null)
return Result.Failure<ShowDto>(ShowErrors.NotFound);
@@ -17,6 +17,9 @@ public sealed class ListShowsQueryHandler(IAppDbContext dbContext)
.Shows.AsNoTracking()
.Include(s => s.Episodes)
.Include(s => s.Genres)
// Серии и жанры — сиблинги: одним запросом это честное перемножение (сериал на 200 серий
// с тремя жанрами даёт 600 строк вместо 203). Разделяем.
.AsSplitQuery()
.Where(s =>
query.Interstitials
? s.Kind == ShowKind.Interstitial
@@ -60,6 +60,10 @@ public sealed class GridScheduleGenerator(
var channel = await dbContext
.Channels.Include(c => c.BumperTemplates)
.ThenInclude(t => t.Variants)
// Вложенные коллекции тянем отдельными запросами: иначе колонки родителя (у шаблона —
// jsonb с правилами, у слоя — jsonb применимости) приезжают по копии на каждую строку
// листа. Тик планировщика повторяет эти два запроса на каждый канал.
.AsSplitQuery()
.FirstOrDefaultAsync(c => c.Id == channelId, cancellationToken);
if (channel is null || !channel.IsEnabled || channel.TemplateId is null)
return new GenerationReport(0, [], ChannelSkipped: true);
@@ -70,6 +74,7 @@ public sealed class GridScheduleGenerator(
var template = await dbContext
.ScheduleTemplates.Include(t => t.Layers)
.ThenInclude(l => l.Slots)
.AsSplitQuery()
.FirstOrDefaultAsync(t => t.Id == channel.TemplateId, cancellationToken);
if (template is null)
return new GenerationReport(0, [], ChannelSkipped: true);
@@ -174,6 +179,7 @@ public sealed class GridScheduleGenerator(
.Channels.AsNoTracking()
.Include(c => c.BumperTemplates)
.ThenInclude(t => t.Variants)
.AsSplitQuery()
.FirstOrDefaultAsync(c => c.Id == channelId, cancellationToken);
if (channel is null || channel.TemplateId is null)
return null;
@@ -182,6 +188,7 @@ public sealed class GridScheduleGenerator(
.ScheduleTemplates.AsNoTracking()
.Include(t => t.Layers)
.ThenInclude(l => l.Slots)
.AsSplitQuery()
.FirstOrDefaultAsync(t => t.Id == channel.TemplateId, cancellationToken);
if (template is null)
return null;
@@ -25,6 +25,7 @@ public sealed class CopyTemplateCommandHandler(IAppDbContext dbContext)
.ScheduleTemplates.AsNoTracking()
.Include(t => t.Layers)
.ThenInclude(l => l.Slots)
.AsSplitQuery()
.FirstOrDefaultAsync(t => t.ChannelId == command.SourceChannelId, cancellationToken);
if (source is null)
return Result.Failure<CopyTemplateResultDto>(ChannelErrors.TemplateNotFound);
@@ -24,6 +24,7 @@ public sealed class GetChannelTemplateQueryHandler(IAppDbContext dbContext)
.ScheduleTemplates.AsNoTracking()
.Include(t => t.Layers)
.ThenInclude(l => l.Slots)
.AsSplitQuery()
.FirstOrDefaultAsync(t => t.ChannelId == channel.Id, cancellationToken);
if (template is null)
return Result.Failure<ScheduleTemplateDto>(ChannelErrors.TemplateNotFound);
@@ -109,6 +109,7 @@ internal static class LayerLoader
dbContext
.ScheduleTemplates.Include(t => t.Layers)
.ThenInclude(l => l.Slots)
.AsSplitQuery()
.FirstOrDefaultAsync(t => t.Id == templateId, cancellationToken);
public static Task<ScheduleTemplate?> ByLayerAsync(
@@ -119,5 +120,6 @@ internal static class LayerLoader
dbContext
.ScheduleTemplates.Include(t => t.Layers)
.ThenInclude(l => l.Slots)
.AsSplitQuery()
.FirstOrDefaultAsync(t => t.Layers.Any(l => l.Id == layerId), cancellationToken);
}
@@ -89,6 +89,7 @@ public sealed class SlotWriter(IAppDbContext dbContext)
dbContext
.ScheduleTemplates.Include(t => t.Layers)
.ThenInclude(l => l.Slots)
.AsSplitQuery()
.FirstOrDefaultAsync(t => t.Layers.Any(l => l.Id == layerId), cancellationToken);
/// <summary>Загружает шаблон по слоту.</summary>
@@ -99,6 +100,7 @@ public sealed class SlotWriter(IAppDbContext dbContext)
dbContext
.ScheduleTemplates.Include(t => t.Layers)
.ThenInclude(l => l.Slots)
.AsSplitQuery()
.FirstOrDefaultAsync(
t => t.Layers.Any(l => l.Slots.Any(s => s.Id == slotId)),
cancellationToken
@@ -28,6 +28,7 @@ public sealed class ValidateTemplateQueryHandler(IAppDbContext dbContext)
.ScheduleTemplates.AsNoTracking()
.Include(t => t.Layers)
.ThenInclude(l => l.Slots)
.AsSplitQuery()
.FirstOrDefaultAsync(t => t.ChannelId == channel.Id, cancellationToken);
if (template is null)
return Result.Failure<IReadOnlyList<TemplateIssueDto>>(ChannelErrors.TemplateNotFound);