From 0b96a0fb77cee07abac185fb1fb704388498631b Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 12 Jan 2023 11:11:30 +0100 Subject: Optimize again comments list sql query --- server/models/shared/sort.ts | 2 +- .../comment/video-comment-list-query-builder.ts | 45 ++++++++++++++-------- server/tests/api/videos/video-comments.ts | 7 ++++ 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/server/models/shared/sort.ts b/server/models/shared/sort.ts index 77e84dcf4..a9a093099 100644 --- a/server/models/shared/sort.ts +++ b/server/models/shared/sort.ts @@ -49,7 +49,7 @@ function getCommentSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): if (field === 'totalReplies') { return [ - [ Sequelize.literal('"totalReplies"'), direction ], + [ 'totalReplies', direction ], lastSort ] } diff --git a/server/models/video/sql/comment/video-comment-list-query-builder.ts b/server/models/video/sql/comment/video-comment-list-query-builder.ts index 3960f6b13..6d752d4a4 100644 --- a/server/models/video/sql/comment/video-comment-list-query-builder.ts +++ b/server/models/video/sql/comment/video-comment-list-query-builder.ts @@ -44,6 +44,7 @@ export class VideoCommentListQueryBuilder extends AbstractRunQuery { private innerSelect = '' private innerJoins = '' + private innerLateralJoins = '' private innerWhere = '' private readonly built = { @@ -59,6 +60,10 @@ export class VideoCommentListQueryBuilder extends AbstractRunQuery { private readonly options: ListVideoCommentsOptions ) { super(sequelize) + + if (this.options.includeReplyCounters && !this.options.videoId) { + throw new Error('Cannot include reply counters without videoId') + } } async listComments () { @@ -97,6 +102,7 @@ export class VideoCommentListQueryBuilder extends AbstractRunQuery { this.innerQuery = `${this.innerSelect} ` + `FROM "videoComment" AS "VideoCommentModel" ` + `${this.innerJoins} ` + + `${this.innerLateralJoins} ` + `${this.innerWhere} ` + `${this.getOrder()} ` + `${this.getInnerLimit()}` @@ -284,11 +290,6 @@ export class VideoCommentListQueryBuilder extends AbstractRunQuery { toSelect.push(this.tableAttributes.getAvatarAttributes()) } - if (this.options.includeReplyCounters === true) { - toSelect.push(this.getTotalRepliesSelect()) - toSelect.push(this.getAuthorTotalRepliesSelect()) - } - this.select = this.buildSelect(toSelect) } @@ -307,6 +308,14 @@ export class VideoCommentListQueryBuilder extends AbstractRunQuery { ]) } + if (this.options.includeReplyCounters === true) { + this.buildTotalRepliesSelect() + this.buildAuthorTotalRepliesSelect() + + toSelect.push('"totalRepliesFromVideoAuthor"."count" AS "totalRepliesFromVideoAuthor"') + toSelect.push('"totalReplies"."count" AS "totalReplies"') + } + this.innerSelect = this.buildSelect(toSelect) } @@ -344,26 +353,32 @@ export class VideoCommentListQueryBuilder extends AbstractRunQuery { // --------------------------------------------------------------------------- - private getTotalRepliesSelect () { + private buildTotalRepliesSelect () { const blockWhereString = this.getBlockWhere('replies', 'videoChannel').join(' AND ') - return `(` + - `SELECT COUNT("replies"."id") FROM "videoComment" AS "replies" ` + - `LEFT JOIN "video" ON "video"."id" = "replies"."videoId" ` + + // Help the planner by providing videoId that should filter out many comments + this.replacements.videoId = this.options.videoId + + this.innerLateralJoins += `LEFT JOIN LATERAL (` + + `SELECT COUNT("replies"."id") AS "count" FROM "videoComment" AS "replies" ` + + `INNER JOIN "video" ON "video"."id" = "replies"."videoId" AND "replies"."videoId" = :videoId ` + `LEFT JOIN "videoChannel" ON "video"."channelId" = "videoChannel"."id" ` + `WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ` + `AND "deletedAt" IS NULL ` + `AND ${blockWhereString} ` + - `) AS "totalReplies"` + `) "totalReplies" ON TRUE ` } - private getAuthorTotalRepliesSelect () { - return `(` + - `SELECT COUNT("replies"."id") FROM "videoComment" AS "replies" ` + - `INNER JOIN "video" ON "video"."id" = "replies"."videoId" ` + + private buildAuthorTotalRepliesSelect () { + // Help the planner by providing videoId that should filter out many comments + this.replacements.videoId = this.options.videoId + + this.innerLateralJoins += `LEFT JOIN LATERAL (` + + `SELECT COUNT("replies"."id") AS "count" FROM "videoComment" AS "replies" ` + + `INNER JOIN "video" ON "video"."id" = "replies"."videoId" AND "replies"."videoId" = :videoId ` + `INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ` + `WHERE "replies"."originCommentId" = "VideoCommentModel"."id" AND "replies"."accountId" = "videoChannel"."accountId"` + - `) AS "totalRepliesFromVideoAuthor"` + `) "totalRepliesFromVideoAuthor" ON TRUE ` } private getOrder () { diff --git a/server/tests/api/videos/video-comments.ts b/server/tests/api/videos/video-comments.ts index e077cbf73..e35500b0b 100644 --- a/server/tests/api/videos/video-comments.ts +++ b/server/tests/api/videos/video-comments.ts @@ -169,6 +169,13 @@ describe('Test video comments', function () { expect(body.data[2].totalReplies).to.equal(0) }) + it('Should list the and sort them by total replies', async function () { + const body = await command.listThreads({ videoId: videoUUID, sort: 'totalReplies' }) + + expect(body.data[2].text).to.equal('my super first comment') + expect(body.data[2].totalReplies).to.equal(3) + }) + it('Should delete a reply', async function () { await command.delete({ videoId, commentId: replyToDeleteId }) -- cgit v1.2.3