Make the /Persons de-duplication use an index instead of grouping the table

This commit is contained in:
Shadowghost
2026-07-28 16:34:45 +02:00
parent d92e59aa72
commit 5f5c71ab75
3 changed files with 2070 additions and 9 deletions
@@ -33,6 +33,7 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
{
using var context = _dbProvider.CreateDbContext();
var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter);
int? distinctNameCount = null;
// Include PeopleBaseItemMap
if (!filter.ItemId.IsEmpty())
@@ -46,17 +47,17 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
{
// The Peoples table has one row per (Name, PersonType), so the same person can
// appear multiple times (e.g. as Actor and GuestStar). Collapse to one row per
// name so /Persons doesn't return the same BaseItem id repeatedly. Lowercase the
// grouping key so case-only duplicates collapse together.
var representativeIds = dbQuery
.GroupBy(e => e.Name.ToLower())
.Select(g => g.Min(e => e.Id));
dbQuery = context.Peoples.AsNoTracking()
.Where(p => representativeIds.Contains(p.Id))
.OrderBy(e => e.Name);
// name so /Persons doesn't return the same BaseItem id repeatedly, keeping the
// lowest id per lowercased name so case-only duplicates collapse together.
var candidates = dbQuery;
dbQuery = candidates
.Where(p => !candidates.Any(other => other.Name.ToLower() == p.Name.ToLower() && other.Id < p.Id))
.OrderBy(e => e.Name.ToLower());
distinctNameCount = candidates.Select(e => e.Name.ToLower()).Distinct().Count();
}
var count = dbQuery.Count();
var count = distinctNameCount ?? dbQuery.Count();
if (filter.StartIndex.HasValue && filter.StartIndex > 0)
{
dbQuery = dbQuery.Skip(filter.StartIndex.Value);
@@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class AddPeopleNameLowerIndex : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// Expression index, so it cannot be declared on the entity type. /Persons collapses the
// one-row-per-(Name, PersonType) table to one row per lowercased name; without this index
// that dedup scans and groups the whole table on every request.
migrationBuilder.Sql("CREATE INDEX IF NOT EXISTS \"IX_Peoples_NameLower\" ON \"Peoples\" (lower(\"Name\"));");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("DROP INDEX IF EXISTS \"IX_Peoples_NameLower\";");
}
}
}