Add optional limit variable to groupBy queries (#15885)

Closes https://github.com/twentyhq/core-team-issues/issues/1600.

Two remarks 
- This `limit` variable does not reduce postgre's work at it still needs
to scan the whole table. It did not seem possible to me to optimize this
as we cannot foresee which dimensions will be used by the user, and an
optimization could only result from an index on the dimension(s) (e.g.:
group companies by addressCity limit 50 can be optimized if we have an
index on companies.addressCity + we had a default orderBy on
adressCity). But this will still optimize the FE which at the moment
receives all groups and truncates the result.
- I have not done the work on the FE as the addition of limit is a
breaking change, and will break until the workspaces' schema is rebuilt,
so we need to flush the cache. I think this could be acceptable as the
feature is in the lab but I preferred not doing it yet as it would have
no impact since in the BE I added a default limit to 50 groups, and I
expect more FE work will be done to allow the user to choose their own
limit
This commit is contained in:
Marie
2025-11-18 17:25:33 +01:00
committed by GitHub
parent be7c2ad40a
commit 5bb4abc23d
10 changed files with 93 additions and 5 deletions
@@ -14,6 +14,7 @@ import { type GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/gr
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { type GroupByDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-definition.types';
import { formatResultWithGroupByDimensionValues } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util';
import { getGroupLimit } from 'src/engine/api/graphql/graphql-query-runner/group-by/utils/get-group-limit.util';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
@@ -21,7 +22,6 @@ import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-met
import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
const GROUPS_LIMIT = 50;
const RECORDS_PER_GROUP_LIMIT = 10;
const RELATIONS_PER_RECORD_LIMIT = 5;
const SUB_QUERY_PREFIX = 'sub_query_';
@@ -41,6 +41,7 @@ export class GroupByWithRecordsService {
selectedFieldsResult,
queryRunnerContext,
orderByForRecords,
groupLimit,
}: {
queryBuilderWithGroupBy: WorkspaceSelectQueryBuilder<ObjectLiteral>;
queryBuilderWithFiltersAndWithoutGroupBy: WorkspaceSelectQueryBuilder<ObjectLiteral>;
@@ -48,9 +49,12 @@ export class GroupByWithRecordsService {
selectedFieldsResult: GraphqlQuerySelectedFieldsResult;
queryRunnerContext: CommonExtendedQueryRunnerContext;
orderByForRecords: ObjectRecordOrderBy;
groupLimit?: number;
}): Promise<CommonGroupByOutputItem[]> {
const effectiveGroupLimit = getGroupLimit(groupLimit);
const groupsResult = await queryBuilderWithGroupBy
.limit(GROUPS_LIMIT)
.limit(effectiveGroupLimit)
.getRawMany();
if (groupsResult.length === 0) {
@@ -0,0 +1,14 @@
import { DEFAULT_NUMBER_OF_GROUPS_LIMIT } from 'twenty-shared/constants';
export const getGroupLimit = (limit?: number): number => {
if (
typeof limit === 'number' &&
Number.isFinite(limit) &&
limit > 0 &&
Number.isInteger(limit)
) {
return limit;
}
return DEFAULT_NUMBER_OF_GROUPS_LIMIT;
};
@@ -186,6 +186,10 @@ export const getResolverArgs = (
type: UUIDScalarType,
isNullable: true,
},
limit: {
type: GraphQLInt,
isNullable: true,
},
};
default:
throw new Error(`Unknown resolver type: ${type}`);