Video SQL query optimization

Reduce number of rows to parse in NodeJS by generating the JSON directly in
PostgreSQL
This commit is contained in:
Chocobozzz
2026-03-24 14:04:49 +01:00
parent 05f2889381
commit 097456f166
5 changed files with 49 additions and 30 deletions
+1 -1
View File
@@ -283,7 +283,7 @@ async function prepare () {
}
servers = await Promise.all([
createSingleServer(1, config, { nodeArgs: [ '--inspect' ] }),
createSingleServer(1, config, { nodeArgs: [ '--inspect=9230' ] }),
createSingleServer(2, config),
createSingleServer(3, config)
])
@@ -45,9 +45,14 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery {
)
this.addJoin(
'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatars" ' +
'ON "VideoChannel->Actor"."id" = "VideoChannel->Actor->Avatars"."actorId" ' +
`AND "VideoChannel->Actor->Avatars"."type" = ${ActorImageType.AVATAR}`
`LEFT JOIN LATERAL (` +
`SELECT json_agg(` +
` jsonb_build_object(` +
` ` + this.tables.getAvatarAttributes().map(attr => `'${attr}', "${attr}"`).join(', ') +
` )` +
`) AS "Avatars"` +
` FROM "actorImage" WHERE "actorId" = "VideoChannel->Actor"."id" AND "type" = ${ActorImageType.AVATAR}` +
`) AS "VideoChannel->Actor->AvatarsJSON" ON TRUE`
)
this.attributes = {
@@ -55,7 +60,7 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery {
...this.buildAttributesObject('VideoChannel', this.tables.getChannelAttributes()),
...this.buildActorInclude('VideoChannel->Actor'),
...this.buildAvatarInclude('VideoChannel->Actor->Avatars'),
'"VideoChannel->Actor->AvatarsJSON"."Avatars"': '"VideoChannel.Actor.AvatarsJSON"',
...this.buildServerInclude('VideoChannel->Actor->Server')
}
}
@@ -72,9 +77,14 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery {
)
this.addJoin(
'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatars" ' +
'ON "VideoChannel->Account->Actor"."id" = "VideoChannel->Account->Actor->Avatars"."actorId" ' +
`AND "VideoChannel->Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}`
`LEFT JOIN LATERAL (` +
`SELECT json_agg(` +
` jsonb_build_object(` +
` ` + this.tables.getAvatarAttributes().map(attr => `'${attr}', "${attr}"`).join(', ') +
` )` +
`) AS "Avatars"` +
` FROM "actorImage" WHERE "actorId" = "VideoChannel->Account->Actor"."id" AND "type" = ${ActorImageType.AVATAR}` +
`) AS "VideoChannel->Account->Actor->AvatarsJSON" ON TRUE`
)
this.attributes = {
@@ -82,18 +92,26 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery {
...this.buildAttributesObject('VideoChannel->Account', this.tables.getAccountAttributes()),
...this.buildActorInclude('VideoChannel->Account->Actor'),
...this.buildAvatarInclude('VideoChannel->Account->Actor->Avatars'),
'"VideoChannel->Account->Actor->AvatarsJSON"."Avatars"': '"VideoChannel.Account.Actor.AvatarsJSON"',
...this.buildServerInclude('VideoChannel->Account->Actor->Server')
}
}
protected includeThumbnails () {
this.addJoin('LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"')
protected includeThumbnailsJSON () {
this.addJoin(
` LEFT JOIN LATERAL (` +
` SELECT json_agg(` +
` jsonb_build_object(` +
` ` + this.tables.getThumbnailAttributes().map(attr => `'${attr}', "${attr}"`).join(', ') +
` )` +
` ) AS "thumbnails" FROM "thumbnail" WHERE "videoId" = "video"."id"` +
`) AS "ThumbnailsJSON" ON TRUE`
)
this.attributes = {
...this.attributes,
...this.buildAttributesObject('Thumbnails', this.tables.getThumbnailAttributes())
'"ThumbnailsJSON"."thumbnails"': '"ThumbnailsJSON"'
}
}
@@ -268,28 +268,29 @@ export class VideoModelBuilder {
}
private addActorAvatar (row: SQLRow, actorPrefix: string, actor: ActorModel) {
const avatarPrefix = `${actorPrefix}.Avatars`
const id = row[`${avatarPrefix}.id`]
const key = `${row.id}${id}`
if (!id || this.actorImagesDone.has(key)) return
const attributes = this.grab(row, this.tables.getAvatarAttributes(), avatarPrefix)
const avatarModel = new ActorImageModel(attributes, this.buildOpts)
actor.Avatars.push(avatarModel)
const key = `${actorPrefix}${row.id}`
if (this.actorImagesDone.has(key)) return
this.actorImagesDone.add(key)
const avatars = row[`${actorPrefix}.AvatarsJSON`] as any || []
for (const avatar of avatars) {
const avatarModel = new ActorImageModel(avatar, this.buildOpts)
actor.Avatars.push(avatarModel)
}
}
private addThumbnail (row: SQLRow, videoModel: VideoModel) {
const id = row['Thumbnails.id']
if (!id || this.thumbnailsDone.has(id)) return
if (this.thumbnailsDone.has(videoModel.id)) return
const attributes = this.grab(row, this.tables.getThumbnailAttributes(), 'Thumbnails')
const thumbnailModel = new ThumbnailModel(attributes, this.buildOpts)
videoModel.Thumbnails.push(thumbnailModel)
const thumbnails = row['ThumbnailsJSON'] as any || []
this.thumbnailsDone.add(id)
for (const thumbnail of thumbnails) {
const thumbnailModel = new ThumbnailModel(thumbnail, this.buildOpts)
videoModel.Thumbnails.push(thumbnailModel)
}
this.thumbnailsDone.add(videoModel.id)
}
private addWebVideoFile (row: SQLRow, videoModel: VideoModel) {
@@ -137,7 +137,7 @@ export class VideosModelGetQuerySubBuilder extends AbstractVideoQueryBuilder {
}
if (thumbnailsInclude.has(options.type)) {
this.includeThumbnails()
this.includeThumbnailsJSON()
}
if (blacklistedInclude.has(options.type)) {
@@ -85,7 +85,7 @@ export class VideosModelListQueryBuilder extends AbstractVideoQueryBuilder {
this.includeChannels()
this.includeAccounts()
this.includeThumbnails()
this.includeThumbnailsJSON()
if (options.user) {
this.includeUserHistory(options.user.id)