diff --git a/ee/app/graphql/resolvers/ci/runner_usage_by_project_resolver.rb b/ee/app/graphql/resolvers/ci/runner_usage_by_project_resolver.rb
index 138b06db8ff3bef2f125d20ff59949c9848b49df..c1ff1fac19c8318d6603e133e42c8004fc5ab3f6 100644
--- a/ee/app/graphql/resolvers/ci/runner_usage_by_project_resolver.rb
+++ b/ee/app/graphql/resolvers/ci/runner_usage_by_project_resolver.rb
@@ -44,11 +44,12 @@ def resolve(from_date: nil, to_date: nil, runner_type: nil, projects_limit: nil)
             "'to_date' must be greater than 'from_date' and be within 1 year"
         end
 
-        result = ::Ci::Runners::GetUsageByProjectService.new(current_user,
+        result = ::Ci::Runners::GetUsageByProjectService.new(
+          current_user,
           runner_type: runner_type,
           from_date: from_date,
           to_date: to_date,
-          max_project_count: [MAX_PROJECTS_LIMIT, projects_limit || DEFAULT_PROJECTS_LIMIT].min
+          max_item_count: [MAX_PROJECTS_LIMIT, projects_limit || DEFAULT_PROJECTS_LIMIT].min
         ).execute
 
         raise Gitlab::Graphql::Errors::ArgumentError, result.message if result.error?
@@ -61,7 +62,7 @@ def resolve(from_date: nil, to_date: nil, runner_type: nil, projects_limit: nil)
       def prepare_result(payload)
         payload.map do |project_usage|
           {
-            project_id: project_usage['grouped_project_id'],
+            project_id: project_usage['project_id_bucket'],
             ci_minutes_used: project_usage['total_duration_in_mins'],
             ci_build_count: project_usage['count_builds']
           }
diff --git a/ee/app/graphql/resolvers/ci/runner_usage_resolver.rb b/ee/app/graphql/resolvers/ci/runner_usage_resolver.rb
index 9f44b56c069c7f7a1b810e22d7f3f5f1e5c4c90c..83c4867be27004a33a7ee71d3adc45f9259a112c 100644
--- a/ee/app/graphql/resolvers/ci/runner_usage_resolver.rb
+++ b/ee/app/graphql/resolvers/ci/runner_usage_resolver.rb
@@ -49,7 +49,7 @@ def resolve(from_date: nil, to_date: nil, runner_type: nil, runners_limit: nil)
           runner_type: runner_type,
           from_date: from_date,
           to_date: to_date,
-          max_runners_count: [MAX_RUNNERS_LIMIT, runners_limit || DEFAULT_RUNNERS_LIMIT].min
+          max_item_count: [MAX_RUNNERS_LIMIT, runners_limit || DEFAULT_RUNNERS_LIMIT].min
         ).execute
 
         raise_resource_not_available_error!(result.message) if result.error?
@@ -62,7 +62,7 @@ def resolve(from_date: nil, to_date: nil, runner_type: nil, runners_limit: nil)
       def prepare_result(payload)
         payload.map do |runner_usage|
           {
-            runner_id: runner_usage['runner_id'],
+            runner_id: runner_usage['runner_id_bucket'],
             ci_minutes_used: runner_usage['total_duration_in_mins'],
             ci_build_count: runner_usage['count_builds']
           }
diff --git a/ee/app/services/ci/runners/generate_usage_csv_service.rb b/ee/app/services/ci/runners/generate_usage_csv_service.rb
index 79f82b382c4a4e05b97195d0f0c120a6352efb4e..04eeee4b86ff954852aeecf614c9dd0981950315 100644
--- a/ee/app/services/ci/runners/generate_usage_csv_service.rb
+++ b/ee/app/services/ci/runners/generate_usage_csv_service.rb
@@ -27,8 +27,8 @@ def initialize(current_user, runner_type:, from_date:, to_date:, max_project_cou
 
       def execute
         result = ::Ci::Runners::GetUsageByProjectService.new(current_user, runner_type: runner_type,
-          from_date: from_date, to_date: to_date, max_project_count: max_project_count,
-          group_by_columns: [:status, :runner_type]).execute
+          from_date: from_date, to_date: to_date, max_item_count: max_project_count,
+          additional_group_by_columns: %i[status runner_type]).execute
 
         return result unless result.success?
 
@@ -39,7 +39,7 @@ def execute
 
         # rubocop: disable CodeReuse/ActiveRecord -- This is an enumerable
         # rubocop: disable Database/AvoidUsingPluckWithoutLimit -- This is an enumerable
-        export_status[:projects_written] = rows.pluck('grouped_project_id').compact.sort.uniq.count
+        export_status[:projects_written] = rows.pluck('project_id_bucket').compact.sort.uniq.count
         # rubocop: enable Database/AvoidUsingPluckWithoutLimit
         # rubocop: enable CodeReuse/ActiveRecord
         export_status[:projects_expected] =
@@ -58,7 +58,7 @@ def execute
 
       def header_to_value_hash
         {
-          'Project ID' => 'grouped_project_id',
+          'Project ID' => 'project_id_bucket',
           'Project path' => 'project_path',
           'Status' => 'status',
           'Runner type' => 'runner_type',
@@ -70,7 +70,7 @@ def header_to_value_hash
 
       def transform_rows(result)
         # rubocop: disable CodeReuse/ActiveRecord -- This is a ClickHouse query
-        ids = result.pluck('grouped_project_id') # rubocop: disable Database/AvoidUsingPluckWithoutLimit -- The limit is already implemented in the ClickHouse query
+        ids = result.pluck('project_id_bucket') # rubocop: disable Database/AvoidUsingPluckWithoutLimit -- The limit is already implemented in the ClickHouse query
         # rubocop: enable CodeReuse/ActiveRecord
         return result if ids.empty?
 
@@ -80,7 +80,7 @@ def transform_rows(result)
         runner_types_by_value = Ci::Runner.runner_types.to_h.invert
         # Annotate rows with project paths, human-readable durations, etc.
         result.each do |row|
-          row['project_path'] = projects[row['grouped_project_id']&.to_i]
+          row['project_path'] = projects[row['project_id_bucket']&.to_i]
           row['runner_type'] = runner_types_by_value[row['runner_type']&.to_i]
           row['total_duration_human_readable'] =
             ActiveSupport::Duration.build(row['total_duration_in_mins'] * 60).inspect
diff --git a/ee/app/services/ci/runners/get_usage_by_project_service.rb b/ee/app/services/ci/runners/get_usage_by_project_service.rb
index 0ca651113e6699972c32f65a75ad29690635d814..044cdde30a902a29ab49b097eb3e94b7cdfafd43 100644
--- a/ee/app/services/ci/runners/get_usage_by_project_service.rb
+++ b/ee/app/services/ci/runners/get_usage_by_project_service.rb
@@ -2,98 +2,19 @@
 
 module Ci
   module Runners
-    class GetUsageByProjectService
-      include Gitlab::Utils::StrongMemoize
-      def initialize(current_user, runner_type:, from_date:, to_date:, max_project_count:, group_by_columns: [])
-        @current_user = current_user
-        @runner_type = Ci::Runner.runner_types[runner_type]
-        @from_date = from_date
-        @to_date = to_date
-        @max_project_count = max_project_count
-        @group_by_columns = group_by_columns
-      end
-
-      def execute
-        unless ::Gitlab::ClickHouse.configured?
-          return ServiceResponse.error(message: 'ClickHouse database is not configured',
-            reason: :db_not_configured)
-        end
-
-        unless Ability.allowed?(@current_user, :read_runner_usage)
-          return ServiceResponse.error(message: 'Insufficient permissions',
-            reason: :insufficient_permissions)
-        end
-
-        data = ClickHouse::Client.select(clickhouse_query, :main)
-        ServiceResponse.success(payload: data)
-      end
+    class GetUsageByProjectService < GetUsageServiceBase
+      extend ::Gitlab::Utils::Override
 
       private
 
-      attr_reader :runner_type, :from_date, :to_date, :group_by_columns, :max_project_count
-
-      def clickhouse_query
-        grouping_columns = ['grouped_project_id', *group_by_columns].join(', ')
-        raw_query = <<~SQL.squish
-          WITH top_projects AS
-            (
-              SELECT project_id
-              FROM ci_used_minutes_mv
-              WHERE #{where_conditions}
-              GROUP BY project_id
-              ORDER BY sumSimpleState(total_duration) DESC
-              LIMIT {max_project_count: UInt64}
-            )
-          SELECT IF(project_id IN top_projects, project_id, NULL) AS grouped_project_id, #{select_list}
-          FROM ci_used_minutes_mv
-          WHERE #{where_conditions}
-          GROUP BY #{grouping_columns}
-          ORDER BY (grouped_project_id IS NULL), #{order_list}
-        SQL
-
-        ClickHouse::Client::Query.new(raw_query: raw_query, placeholders: placeholders)
-      end
-
-      def placeholders
-        placeholders = {
-          runner_type: runner_type,
-          from_date: format_date(from_date),
-          to_date: format_date(to_date + 1), # Include jobs until the end of the day
-          max_project_count: max_project_count
-        }
-
-        placeholders.compact
-      end
-
-      def select_list
-        [
-          *group_by_columns,
-          'countMerge(count_builds) AS count_builds',
-          'toUInt64(sumSimpleState(total_duration) / 60000) AS total_duration_in_mins'
-        ].join(', ')
-      end
-      strong_memoize_attr :select_list
-
-      def order_list
-        [
-          'total_duration_in_mins DESC',
-          'grouped_project_id ASC',
-          *group_by_columns.map { |column| "#{column} ASC" }
-        ].join(', ')
-      end
-      strong_memoize_attr :order_list
-
-      def where_conditions
-        <<~SQL
-          #{'runner_type = {runner_type: UInt8} AND' if runner_type}
-          finished_at_bucket >= {from_date: DateTime('UTC', 6)} AND
-          finished_at_bucket < {to_date: DateTime('UTC', 6)}
-        SQL
+      override :table_name
+      def table_name
+        'ci_used_minutes'
       end
-      strong_memoize_attr :where_conditions
 
-      def format_date(date)
-        date.strftime('%Y-%m-%d %H:%M:%S')
+      override :bucket_column
+      def bucket_column
+        'project_id'
       end
     end
   end
diff --git a/ee/app/services/ci/runners/get_usage_service.rb b/ee/app/services/ci/runners/get_usage_service.rb
index c1af8dc2c56d6c3053de3e934618bd99ba9f0867..4916acd4e3888f724ad4beea25c093aec244d3a6 100644
--- a/ee/app/services/ci/runners/get_usage_service.rb
+++ b/ee/app/services/ci/runners/get_usage_service.rb
@@ -2,82 +2,19 @@
 
 module Ci
   module Runners
-    class GetUsageService
-      include Gitlab::Utils::StrongMemoize
-      def initialize(current_user, runner_type:, from_date:, to_date:, max_runners_count:)
-        @current_user = current_user
-        @runner_type = Ci::Runner.runner_types[runner_type]
-        @from_date = from_date
-        @to_date = to_date
-        @max_runners_count = max_runners_count
-      end
-
-      def execute
-        unless ::Gitlab::ClickHouse.configured?
-          return ServiceResponse.error(message: 'ClickHouse database is not configured',
-            reason: :db_not_configured)
-        end
-
-        unless Ability.allowed?(@current_user, :read_runner_usage)
-          return ServiceResponse.error(message: 'Insufficient permissions',
-            reason: :insufficient_permissions)
-        end
-
-        data = ClickHouse::Client.select(clickhouse_query, :main)
-        ServiceResponse.success(payload: data)
-      end
+    class GetUsageService < GetUsageServiceBase
+      extend ::Gitlab::Utils::Override
 
       private
 
-      attr_reader :runner_type, :from_date, :to_date, :max_runners_count
-
-      def clickhouse_query
-        raw_query = <<~SQL.squish
-          WITH top_runners AS
-            (
-              SELECT runner_id
-              FROM ci_used_minutes_by_runner_daily
-              WHERE #{where_conditions}
-              GROUP BY runner_id
-              ORDER BY sumSimpleState(total_duration) DESC
-              LIMIT {max_runners_count: UInt64}
-            )
-          SELECT
-            IF(ci_used_minutes_by_runner_daily.runner_id IN top_runners, ci_used_minutes_by_runner_daily.runner_id, NULL)
-              AS runner_id,
-            countMerge(count_builds) AS count_builds,
-            toUInt64(sumSimpleState(total_duration) / 60000) AS total_duration_in_mins
-          FROM ci_used_minutes_by_runner_daily
-          WHERE #{where_conditions}
-          GROUP BY runner_id
-          ORDER BY (runner_id IS NULL), total_duration_in_mins DESC, runner_id ASC
-        SQL
-
-        ClickHouse::Client::Query.new(raw_query: raw_query, placeholders: placeholders)
-      end
-
-      def placeholders
-        placeholders = {
-          runner_type: runner_type,
-          from_date: format_date(from_date),
-          to_date: format_date(to_date + 1), # Include jobs until the end of the day
-          max_runners_count: max_runners_count
-        }
-
-        placeholders.compact
-      end
-
-      def where_conditions
-        <<~SQL
-          #{'runner_type = {runner_type: UInt8} AND' if runner_type}
-          finished_at_bucket >= {from_date: DateTime('UTC', 6)} AND
-          finished_at_bucket < {to_date: DateTime('UTC', 6)}
-        SQL
+      override :table_name
+      def table_name
+        'ci_used_minutes_by_runner_daily'
       end
-      strong_memoize_attr :where_conditions
 
-      def format_date(date)
-        date.strftime('%Y-%m-%d %H:%M:%S')
+      override :bucket_column
+      def bucket_column
+        'runner_id'
       end
     end
   end
diff --git a/ee/app/services/ci/runners/get_usage_service_base.rb b/ee/app/services/ci/runners/get_usage_service_base.rb
new file mode 100644
index 0000000000000000000000000000000000000000..29182e52e7152f46d0e9ee237964e7806760e158
--- /dev/null
+++ b/ee/app/services/ci/runners/get_usage_service_base.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+module Ci
+  module Runners
+    class GetUsageServiceBase
+      include Gitlab::Utils::StrongMemoize
+
+      def initialize(current_user, runner_type:, from_date:, to_date:, max_item_count:, additional_group_by_columns: [])
+        @current_user = current_user
+        @runner_type = Ci::Runner.runner_types[runner_type]
+        @from_date = from_date
+        @to_date = to_date
+        @max_item_count = max_item_count
+        @additional_group_by_columns = additional_group_by_columns
+      end
+
+      def execute
+        unless ::Gitlab::ClickHouse.configured?
+          return ServiceResponse.error(message: 'ClickHouse database is not configured',
+            reason: :db_not_configured)
+        end
+
+        unless Ability.allowed?(@current_user, :read_runner_usage)
+          return ServiceResponse.error(message: 'Insufficient permissions',
+            reason: :insufficient_permissions)
+        end
+
+        data = ClickHouse::Client.select(clickhouse_query, :main)
+        ServiceResponse.success(payload: data)
+      end
+
+      private
+
+      attr_reader :runner_type, :from_date, :to_date, :max_item_count, :additional_group_by_columns
+
+      def clickhouse_query
+        grouping_columns = ["#{bucket_column}_bucket", *additional_group_by_columns].join(', ')
+        raw_query = <<~SQL.squish
+          WITH top_buckets AS
+            (
+              SELECT #{bucket_column} AS #{bucket_column}_bucket
+              FROM #{table_name}
+              WHERE #{where_conditions}
+              GROUP BY #{bucket_column}
+              ORDER BY sumSimpleState(total_duration) DESC
+              LIMIT {max_item_count: UInt64}
+            )
+          SELECT
+            IF(#{table_name}.#{bucket_column} IN top_buckets, #{table_name}.#{bucket_column}, NULL)
+              AS #{bucket_column}_bucket,
+            #{select_list}
+          FROM #{table_name}
+          WHERE #{where_conditions}
+          GROUP BY #{grouping_columns}
+          ORDER BY #{order_list}
+        SQL
+
+        ClickHouse::Client::Query.new(raw_query: raw_query, placeholders: placeholders)
+      end
+
+      def table_name
+        raise NotImplementedError
+      end
+
+      def bucket_column
+        raise NotImplementedError
+      end
+
+      def select_list
+        [
+          *additional_group_by_columns,
+          'countMerge(count_builds) AS count_builds',
+          'toUInt64(sumSimpleState(total_duration) / 60000) AS total_duration_in_mins'
+        ].join(', ')
+      end
+      strong_memoize_attr :select_list
+
+      def order_list
+        [
+          "(#{bucket_column}_bucket IS NULL)",
+          'total_duration_in_mins DESC',
+          "#{bucket_column}_bucket ASC"
+        ].join(', ')
+      end
+      strong_memoize_attr :order_list
+
+      def where_conditions
+        <<~SQL
+          #{'runner_type = {runner_type: UInt8} AND' if runner_type}
+          finished_at_bucket >= {from_date: DateTime('UTC', 6)} AND
+          finished_at_bucket < {to_date: DateTime('UTC', 6)}
+        SQL
+      end
+      strong_memoize_attr :where_conditions
+
+      def placeholders
+        {
+          runner_type: runner_type,
+          from_date: format_date(from_date),
+          to_date: format_date(to_date + 1), # Include jobs until the end of the day
+          max_item_count: max_item_count
+        }.compact
+      end
+
+      def format_date(date)
+        date.strftime('%Y-%m-%d %H:%M:%S')
+      end
+    end
+  end
+end
diff --git a/ee/spec/services/ci/runners/get_usage_by_project_service_spec.rb b/ee/spec/services/ci/runners/get_usage_by_project_service_spec.rb
index 964f516804534b476a862bc6af406ff84f2ee4f0..ba3fe6e15662713ccd0aef1e629b05b5b4ec28bf 100644
--- a/ee/spec/services/ci/runners/get_usage_by_project_service_spec.rb
+++ b/ee/spec/services/ci/runners/get_usage_by_project_service_spec.rb
@@ -29,11 +29,11 @@
   let(:runner_type) { nil }
   let(:from_date) { Date.new(2023, 12, 1) }
   let(:to_date) { Date.new(2023, 12, 31) }
-  let(:max_project_count) { 50 }
-  let(:group_by_columns) { [] }
+  let(:max_item_count) { 50 }
+  let(:additional_group_by_columns) { [] }
   let(:service) do
     described_class.new(user, runner_type: runner_type, from_date: from_date, to_date: to_date,
-      group_by_columns: group_by_columns, max_project_count: max_project_count)
+      additional_group_by_columns: additional_group_by_columns, max_item_count: max_item_count)
   end
 
   let(:result) { service.execute }
@@ -74,51 +74,49 @@
   end
 
   it 'exports usage data' do
-    is_expected.to eq(
-      [{ "grouped_project_id" => builds.last.project.id, "count_builds" => 4, "total_duration_in_mins" => 140 },
-        { "grouped_project_id" => builds[3].project.id, "count_builds" => 1, "total_duration_in_mins" => 17 },
-        { "grouped_project_id" => builds[2].project.id, "count_builds" => 1, "total_duration_in_mins" => 16 },
-        { "grouped_project_id" => builds[1].project.id, "count_builds" => 1, "total_duration_in_mins" => 15 },
-        { "grouped_project_id" => builds[0].project.id, "count_builds" => 1, "total_duration_in_mins" => 14 }]
-    )
+    is_expected.to eq([
+      { 'project_id_bucket' => builds.last.project.id, 'count_builds' => 4, 'total_duration_in_mins' => 140 },
+      { 'project_id_bucket' => builds[3].project.id, 'count_builds' => 1, 'total_duration_in_mins' => 17 },
+      { 'project_id_bucket' => builds[2].project.id, 'count_builds' => 1, 'total_duration_in_mins' => 16 },
+      { 'project_id_bucket' => builds[1].project.id, 'count_builds' => 1, 'total_duration_in_mins' => 15 },
+      { 'project_id_bucket' => builds[0].project.id, 'count_builds' => 1, 'total_duration_in_mins' => 14 }
+    ])
   end
 
-  context 'when group_by_columns specified' do
-    let(:group_by_columns) { [:status, :runner_type] }
+  context 'when additional_group_by_columns specified' do
+    let(:additional_group_by_columns) { [:status, :runner_type] }
 
     it 'exports usage data grouped by status and runner_type' do
-      is_expected.to eq(
-        [
-          { "grouped_project_id" => builds.last.project.id, "status" => "failed", "runner_type" => 2,
-            "count_builds" => 1, "total_duration_in_mins" => 120 },
-          { "grouped_project_id" => builds[3].project.id, "status" => "skipped", "runner_type" => 1,
-            "count_builds" => 1, "total_duration_in_mins" => 17 },
-          { "grouped_project_id" => builds[2].project.id, "status" => "canceled", "runner_type" => 1,
-            "count_builds" => 1, "total_duration_in_mins" => 16 },
-          { "grouped_project_id" => builds[1].project.id, "status" => "failed", "runner_type" => 1, "count_builds" => 1,
-            "total_duration_in_mins" => 15 },
-          { "grouped_project_id" => builds[0].project.id, "status" => "success", "runner_type" => 1,
-            "count_builds" => 1, "total_duration_in_mins" => 14 },
-          { "grouped_project_id" => builds.last.project.id, "status" => "failed", "runner_type" => 1,
-            "count_builds" => 1, "total_duration_in_mins" => 10 },
-          { "grouped_project_id" => builds.last.project.id, "status" => "success", "runner_type" => 1,
-            "count_builds" => 1, "total_duration_in_mins" => 7 },
-          { "grouped_project_id" => builds.last.project.id, "status" => "canceled", "runner_type" => 2,
-            "count_builds" => 1, "total_duration_in_mins" => 3 }
-        ]
-      )
+      is_expected.to eq([
+        { 'project_id_bucket' => builds.last.project.id, 'status' => 'failed', 'runner_type' => 2,
+          'count_builds' => 1, 'total_duration_in_mins' => 120 },
+        { 'project_id_bucket' => builds[3].project.id, 'status' => 'skipped', 'runner_type' => 1,
+          'count_builds' => 1, 'total_duration_in_mins' => 17 },
+        { 'project_id_bucket' => builds[2].project.id, 'status' => 'canceled', 'runner_type' => 1,
+          'count_builds' => 1, 'total_duration_in_mins' => 16 },
+        { 'project_id_bucket' => builds[1].project.id, 'status' => 'failed', 'runner_type' => 1, 'count_builds' => 1,
+          'total_duration_in_mins' => 15 },
+        { 'project_id_bucket' => builds[0].project.id, 'status' => 'success', 'runner_type' => 1,
+          'count_builds' => 1, 'total_duration_in_mins' => 14 },
+        { 'project_id_bucket' => builds.last.project.id, 'status' => 'failed', 'runner_type' => 1,
+          'count_builds' => 1, 'total_duration_in_mins' => 10 },
+        { 'project_id_bucket' => builds.last.project.id, 'status' => 'success', 'runner_type' => 1,
+          'count_builds' => 1, 'total_duration_in_mins' => 7 },
+        { 'project_id_bucket' => builds.last.project.id, 'status' => 'canceled', 'runner_type' => 2,
+          'count_builds' => 1, 'total_duration_in_mins' => 3 }
+      ])
     end
   end
 
-  context "when max_project_count doesn't fit all projects" do
-    let(:max_project_count) { 2 }
+  context 'when the number of projects exceeds max_item_count' do
+    let(:max_item_count) { 2 }
 
     it 'exports usage data for the 2 top projects plus aggregate for other projects' do
-      is_expected.to eq(
-        [{ "grouped_project_id" => builds.last.project.id, "count_builds" => 4, "total_duration_in_mins" => 140 },
-          { "grouped_project_id" => builds[3].project.id, "count_builds" => 1, "total_duration_in_mins" => 17 },
-          { "grouped_project_id" => nil, "count_builds" => 3, "total_duration_in_mins" => 45 }]
-      )
+      is_expected.to eq([
+        { 'project_id_bucket' => builds.last.project.id, 'count_builds' => 4, 'total_duration_in_mins' => 140 },
+        { 'project_id_bucket' => builds[3].project.id, 'count_builds' => 1, 'total_duration_in_mins' => 17 },
+        { 'project_id_bucket' => nil, 'count_builds' => 3, 'total_duration_in_mins' => 45 }
+      ])
     end
   end
 
@@ -126,8 +124,8 @@
     let(:runner_type) { :group_type }
 
     it 'exports usage data for runners of specified type' do
-      is_expected.to eq(
-        [{ "grouped_project_id" => builds.last.project.id, "count_builds" => 2, "total_duration_in_mins" => 123 }]
+      is_expected.to contain_exactly(
+        { 'project_id_bucket' => builds.last.project.id, 'count_builds' => 2, 'total_duration_in_mins' => 123 }
       )
     end
   end
@@ -154,7 +152,9 @@
     let(:builds) { [build_before, build_in_range, build_overflowing_the_range, build_after] }
 
     it 'only exports usage data for builds created in the date range' do
-      is_expected.to eq([{ "grouped_project_id" => project.id, "count_builds" => 2, "total_duration_in_mins" => 172 }])
+      is_expected.to contain_exactly(
+        { 'project_id_bucket' => project.id, 'count_builds' => 2, 'total_duration_in_mins' => 172 }
+      )
     end
   end
 
diff --git a/ee/spec/services/ci/runners/get_usage_service_spec.rb b/ee/spec/services/ci/runners/get_usage_service_spec.rb
index 81baac61e5a728172e713884d71f70ea845d1ad6..c02e0d9894046feb0313080b9e424ea6b749bd29 100644
--- a/ee/spec/services/ci/runners/get_usage_service_spec.rb
+++ b/ee/spec/services/ci/runners/get_usage_service_spec.rb
@@ -2,8 +2,7 @@
 
 require 'spec_helper'
 
-RSpec.describe Ci::Runners::GetUsageService, :click_house, :enable_admin_mode,
-  feature_category: :fleet_visibility do
+RSpec.describe Ci::Runners::GetUsageService, :click_house, :enable_admin_mode, feature_category: :fleet_visibility do
   let_it_be(:project) { create(:project) }
   let_it_be(:instance_runners) { create_list(:ci_runner, 3, :instance, :with_runner_manager) }
   let_it_be(:group) { create(:group) }
@@ -25,10 +24,10 @@
   let(:runner_type) { nil }
   let(:from_date) { Date.new(2023, 12, 1) }
   let(:to_date) { Date.new(2023, 12, 31) }
-  let(:max_runners_count) { 50 }
+  let(:max_item_count) { 50 }
   let(:service) do
     described_class.new(user, runner_type: runner_type, from_date: from_date, to_date: to_date,
-      max_runners_count: max_runners_count)
+      max_item_count: max_item_count)
   end
 
   let(:result) { service.execute }
@@ -65,22 +64,22 @@
 
   it 'exports usage data by runner' do
     is_expected.to eq(
-      [{ "runner_id" => group_runner.id, "count_builds" => 5, "total_duration_in_mins" => 500 }] +
-        instance_runners.each_with_index.map do |runner, index|
-          { "runner_id" => runner.id, "count_builds" => index + 1, "total_duration_in_mins" => 10 * (index + 1) }
-        end.reverse
+      [{ 'runner_id_bucket' => group_runner.id, 'count_builds' => 5, 'total_duration_in_mins' => 500 }] +
+      instance_runners.each_with_index.map do |runner, index|
+        { 'runner_id_bucket' => runner.id, 'count_builds' => index + 1, 'total_duration_in_mins' => 10 * (index + 1) }
+      end.reverse
     )
   end
 
-  context "when max_runners_count doesn't fit all projects" do
-    let(:max_runners_count) { 2 }
+  context 'when the number of runners exceeds max_item_count' do
+    let(:max_item_count) { 2 }
 
-    it 'exports usage data for the 2 top projects plus aggregate for other projects' do
-      is_expected.to eq(
-        [{ "runner_id" => group_runner.id, "count_builds" => 5, "total_duration_in_mins" => 500 },
-          { "runner_id" => instance_runners.last.id, "count_builds" => 3, "total_duration_in_mins" => 30 },
-          { "runner_id" => nil, "count_builds" => 3, "total_duration_in_mins" => 30 }]
-      )
+    it 'exports usage data for the 2 top runners plus aggregate for other projects' do
+      is_expected.to eq([
+        { 'runner_id_bucket' => group_runner.id, 'count_builds' => 5, 'total_duration_in_mins' => 500 },
+        { 'runner_id_bucket' => instance_runners.last.id, 'count_builds' => 3, 'total_duration_in_mins' => 30 },
+        { 'runner_id_bucket' => nil, 'count_builds' => 3, 'total_duration_in_mins' => 30 }
+      ])
     end
   end
 
@@ -89,7 +88,7 @@
 
     it 'exports usage data for runners of specified type' do
       is_expected.to eq(
-        [{ "runner_id" => group_runner.id, "count_builds" => 5, "total_duration_in_mins" => 500 }]
+        [{ 'runner_id_bucket' => group_runner.id, 'count_builds' => 5, 'total_duration_in_mins' => 500 }]
       )
     end
   end
@@ -116,7 +115,9 @@
     let(:builds) { [build_before, build_in_range, build_overflowing_the_range, build_after] }
 
     it 'only exports usage data for builds created in the date range' do
-      is_expected.to eq([{ "runner_id" => group_runner.id, "count_builds" => 2, "total_duration_in_mins" => 172 }])
+      is_expected.to contain_exactly(
+        { 'runner_id_bucket' => group_runner.id, 'count_builds' => 2, 'total_duration_in_mins' => 172 }
+      )
     end
   end
 
diff --git a/gems/click_house-client/lib/click_house/client/query.rb b/gems/click_house-client/lib/click_house/client/query.rb
index 41435d239cfe64d4c8dfca950b4ab4d82503a9ae..65582031a4cd84d40ec87889710cbd4f01857f7a 100644
--- a/gems/click_house-client/lib/click_house/client/query.rb
+++ b/gems/click_house-client/lib/click_house/client/query.rb
@@ -3,9 +3,9 @@
 module ClickHouse
   module Client
     class Query < QueryLike
-      SUBQUERY_PLACEHOLDER_REGEX = /{\w+:Subquery}/ # exmaple: {var:Subquery}, special "internal" type for subqueries
-      PLACEHOLDER_REGEX = /{\w+:\w+}/ # exmaple: {var:UInt8}
-      PLACEHOLDER_NAME_REGEX = /{(\w+):/ # exmaple: {var:UInt8} => var
+      SUBQUERY_PLACEHOLDER_REGEX = /{\w+:Subquery}/ # example: {var:Subquery}, special "internal" type for subqueries
+      PLACEHOLDER_REGEX = /{\w+:\w+}/ # example: {var:UInt8}
+      PLACEHOLDER_NAME_REGEX = /{(\w+):/ # example: {var:UInt8} => var
 
       def initialize(raw_query:, placeholders: {})
         raise QueryError, 'Empty query string given' if raw_query.blank?