perf(server): improve people query performance (#32441)

perf(server): improve people query performance (#32405)

fix(server): slow people query

(cherry picked from commit 6b79a82800cb984a755b163bca0cf689b486ad9a)

Co-authored-by: Zack Pollard <zack@futo.org>
This commit is contained in:
immich-push-o-matic[bot]
2026-10-14 16:27:32 +00:00
committed by GitHub
co-authored by Zack Pollard
parent 3dcaa05cd7
commit ea1bfcf248
2 changed files with 125 additions and 80 deletions
+57 -25
View File
@@ -239,9 +239,12 @@ from
"person_group"
inner join "person" as "owned" on "owned"."personGroupId" = "person_group"."id"
and "owned"."ownerId" = $5
inner join "asset_face" on "asset_face"."personGroupId" = "person_group"."id"
and "asset_face"."deletedAt" is null
and "asset_face"."isVisible" is true
inner join (
select
"asset_face"."personGroupId",
count("asset"."id") as "faceCount"
from
"asset_face"
inner join "asset" on "asset"."id" = "asset_face"."assetId"
and "asset"."visibility" = 'timeline'
and "asset"."deletedAt" is null
@@ -261,16 +264,27 @@ from
)
)
where
"owned"."isHidden" = $8
and 1 = 1
"asset_face"."personGroupId" = any (
array(
(
select
"person"."personGroupId"
from
"person"
where
"person"."ownerId" = $8
)
)
)
and "asset_face"."deletedAt" is null
and "asset_face"."isVisible" is true
group by
"person_group"."id",
"owned"."ownerId",
"owned"."personGroupId"
having
"asset_face"."personGroupId"
) as "faces" on "faces"."personGroupId" = "person_group"."id"
where
(
"owned"."name" != $9
or count("asset"."id") >= COALESCE(
or "faces"."faceCount" >= COALESCE(
(
SELECT
value -> 'people' ->> 'minimumFaces'
@@ -283,17 +297,19 @@ having
'3'
)::int
)
and "owned"."isHidden" = $11
and 1 = 1
order by
"owned"."isHidden" asc,
"owned"."isFavorite" desc,
NULLIF("owned"."name", '') is null asc,
count("asset"."id") desc,
"faces"."faceCount" desc,
NULLIF("owned"."name", '') asc nulls last,
"owned"."createdAt"
limit
$11
offset
$12
offset
$13
-- PersonRepository.getAllWithoutFaces
select
@@ -901,9 +917,12 @@ from
"person_group"
inner join "person" as "owned" on "owned"."personGroupId" = "person_group"."id"
and "owned"."ownerId" = $2
inner join "asset_face" on "asset_face"."personGroupId" = "person_group"."id"
and "asset_face"."deletedAt" is null
and "asset_face"."isVisible" is true
inner join (
select
"asset_face"."personGroupId",
count("asset"."id") as "faceCount"
from
"asset_face"
inner join "asset" on "asset"."id" = "asset_face"."assetId"
and "asset"."visibility" = 'timeline'
and "asset"."deletedAt" is null
@@ -923,27 +942,40 @@ from
)
)
where
1 = 1
group by
"person_group"."id",
"owned"."ownerId",
"owned"."personGroupId"
having
"asset_face"."personGroupId" = any (
array(
(
"owned"."name" != $5
or count("asset"."id") >= COALESCE(
select
"person"."personGroupId"
from
"person"
where
"person"."ownerId" = $5
)
)
)
and "asset_face"."deletedAt" is null
and "asset_face"."isVisible" is true
group by
"asset_face"."personGroupId"
) as "faces" on "faces"."personGroupId" = "person_group"."id"
where
(
"owned"."name" != $6
or "faces"."faceCount" >= COALESCE(
(
SELECT
value -> 'people' ->> 'minimumFaces'
FROM
user_metadata
WHERE
"userId" = $6
"userId" = $7
AND key = 'preferences'
),
'3'
)::int
)
and 1 = 1
) as "people"
-- PersonRepository.createGroup
+26 -13
View File
@@ -224,8 +224,6 @@ const asPerson = ({ ownedPerson, otherPeople, sharedBy, sharedWith }: PersonGrou
sharedWith,
});
const faceCount = (eb: ExpressionBuilder<DB, 'asset'>) => eb.fn.count('asset.id');
const withPerson = ({ viewingUserId }: WithPersonOptions) => {
return (eb: ExpressionBuilder<DB, 'asset_face'>) =>
jsonObjectFrom(
@@ -448,25 +446,40 @@ export class PersonRepository {
.innerJoin('person as owned', (join) =>
join.onRef('owned.personGroupId', '=', 'person_group.id').on('owned.ownerId', '=', userId),
)
.innerJoin('asset_face', (join) =>
join
.onRef('asset_face.personGroupId', '=', 'person_group.id')
.on('asset_face.deletedAt', 'is', null)
.on('asset_face.isVisible', 'is', true),
)
.innerJoin(
(eb) =>
eb
.selectFrom('asset_face')
.innerJoin('asset', (join) =>
join
.onRef('asset.id', '=', 'asset_face.assetId')
.on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
.on('asset.deletedAt', 'is', null)
.on((eb) => eb.or([eb('asset.ownerId', '=', anyUuid([userId, ...partnerIds])), inSharedAlbum(eb, userId)])),
.on((eb) =>
eb.or([eb('asset.ownerId', '=', anyUuid([userId, ...partnerIds])), inSharedAlbum(eb, userId)]),
),
)
.groupBy(['person_group.id', 'owned.ownerId', 'owned.personGroupId'])
.having((eb) =>
.select('asset_face.personGroupId')
.select((eb) => eb.fn.count<number>('asset.id').as('faceCount'))
// an array instead of a join or `in`, which the planner may turn into a scan of every face in the instance
.where('asset_face.personGroupId', '=', (eb) => {
const ownedGroupIds = eb
.selectFrom('person')
.select('person.personGroupId')
.where('person.ownerId', '=', userId);
return eb.fn.any(eb.fn<string[]>('array', [ownedGroupIds]));
})
.where('asset_face.deletedAt', 'is', null)
.where('asset_face.isVisible', 'is', true)
.groupBy('asset_face.personGroupId')
.as('faces'),
(join) => join.onRef('faces.personGroupId', '=', 'person_group.id'),
)
.where((eb) =>
eb.or([
eb('owned.name', '!=', ''),
eb(
faceCount,
'faces.faceCount',
'>=',
sql<number>`COALESCE(
(SELECT value -> 'people' ->> 'minimumFaces'
@@ -509,7 +522,7 @@ export class PersonRepository {
.$if(!options.closestFaceAssetId, (qb) =>
qb
.orderBy(sql`NULLIF("owned"."name", '') is null`, 'asc')
.orderBy(faceCount, 'desc')
.orderBy('faces.faceCount', 'desc')
.orderBy(sql`NULLIF("owned"."name", '')`, (om) => om.asc().nullsLast())
.orderBy('owned.createdAt'),
)