diff --git a/db/post_migrate/20240619114215_index_unarchived_sbom_occurrences_for_aggregations_license.rb b/db/post_migrate/20240619114215_index_unarchived_sbom_occurrences_for_aggregations_license.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8b6e166879a6875c3a6e106869b62edc3f6f5513
--- /dev/null
+++ b/db/post_migrate/20240619114215_index_unarchived_sbom_occurrences_for_aggregations_license.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class IndexUnarchivedSbomOccurrencesForAggregationsLicense < Gitlab::Database::Migration[2.2]
+  INDEX_NAME = 'index_unarchived_occurrences_for_aggregations_license'
+
+  milestone '17.2'
+
+  disable_ddl_transaction!
+
+  # rubocop:disable Migration/PreventIndexCreation -- Once complete, this index unblocks the removal of other indexes https://gitlab.com/gitlab-org/gitlab/-/issues/442486
+  def up
+    add_concurrent_index :sbom_occurrences,
+      "traversal_ids, (licenses -> 0 ->> 'spdx_identifier'), component_id, component_version_id",
+      where: 'archived = false',
+      name: INDEX_NAME
+  end
+  # rubocop:enable Migration/PreventIndexCreation
+
+  def down
+    remove_concurrent_index_by_name :sbom_occurrences, INDEX_NAME
+  end
+end
diff --git a/db/schema_migrations/20240619114215 b/db/schema_migrations/20240619114215
new file mode 100644
index 0000000000000000000000000000000000000000..28f9b18273b312ef60a456c113a91dd073dd5738
--- /dev/null
+++ b/db/schema_migrations/20240619114215
@@ -0,0 +1 @@
+4ebdcd532d085ccd3a0bb11aa13ff0013230060b8f1ba50d1fafb8855a07b525
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index cb0402099167ea21df4f76ecb3ff7dcf0ad2f3c2..8eb09bfa5148ac6cc34b955bda3136fd48cf71d9 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -29022,6 +29022,8 @@ CREATE UNIQUE INDEX index_trending_projects_on_project_id ON trending_projects U
 
 CREATE INDEX index_unarchived_occurrences_for_aggregations_component_name ON sbom_occurrences USING btree (traversal_ids, component_name, component_id, component_version_id) WHERE (archived = false);
 
+CREATE INDEX index_unarchived_occurrences_for_aggregations_license ON sbom_occurrences USING btree (traversal_ids, (((licenses -> 0) ->> 'spdx_identifier'::text)), component_id, component_version_id) WHERE (archived = false);
+
 CREATE INDEX index_unarchived_occurrences_for_aggregations_package_manager ON sbom_occurrences USING btree (traversal_ids, package_manager, component_id, component_version_id) WHERE (archived = false);
 
 CREATE INDEX index_unarchived_occurrences_for_aggregations_severity ON sbom_occurrences USING btree (traversal_ids, highest_severity, component_id, component_version_id) WHERE (archived = false);
diff --git a/ee/app/controllers/groups/dependencies_controller.rb b/ee/app/controllers/groups/dependencies_controller.rb
index 4521cee0424a68e1a360485a59b70282afa930e6..fdde450e2b93e4bc56f5344cac45a684dec409df 100644
--- a/ee/app/controllers/groups/dependencies_controller.rb
+++ b/ee/app/controllers/groups/dependencies_controller.rb
@@ -141,6 +141,8 @@ def map_sort_by(sort_by)
         :component_name
       when 'packager'
         :package_manager
+      when 'license'
+        :licenses
       when 'severity'
         :highest_severity
       else
diff --git a/ee/app/finders/dependency_management/aggregations_finder.rb b/ee/app/finders/dependency_management/aggregations_finder.rb
index 7bd8d8642a6d63ed4f062727130fb71defbad174..b42dcebd64ca1ef574d4f43ad7a0c2bef1c64a7f 100644
--- a/ee/app/finders/dependency_management/aggregations_finder.rb
+++ b/ee/app/finders/dependency_management/aggregations_finder.rb
@@ -8,7 +8,7 @@ class AggregationsFinder
     DEFAULT_PAGE_SIZE = 20
     MAX_PAGE_SIZE = 20
     DEFAULT_SORT_COLUMNS = %i[component_id component_version_id].freeze
-    SUPPORTED_SORT_COLUMNS = %i[component_name highest_severity package_manager].freeze
+    SUPPORTED_SORT_COLUMNS = %i[component_name highest_severity package_manager licenses].freeze
 
     def initialize(namespace, params: {})
       @namespace = namespace
@@ -16,7 +16,7 @@ def initialize(namespace, params: {})
     end
 
     def execute
-      group_columns = distinct_columns.map { |column| "outer_occurrences.#{column}" }
+      group_columns = distinct_columns.map { |column| column_expression(column, 'outer_occurrences') }
 
       Sbom::Occurrence.with(namespaces_cte.to_arel)
         .select(
@@ -38,10 +38,6 @@ def execute
 
     attr_reader :namespace, :params
 
-    def distinct_columns
-      orderings.keys
-    end
-
     def keyset_order(column_expression_evaluator:, order_expression_evaluator:)
       order_definitions = orderings.map do |column, direction|
         column_expression = column_expression_evaluator.call(column)
@@ -61,8 +57,8 @@ def keyset_order(column_expression_evaluator:, order_expression_evaluator:)
 
     def outer_order
       keyset_order(
-        column_expression_evaluator: ->(column) { Sbom::Occurrence.arel_table.alias('outer_occurrences')[column] },
-        order_expression_evaluator: ->(column) { Arel.sql("MIN(outer_occurrences.#{column})") }
+        column_expression_evaluator: ->(column) { column_expression(column, 'outer_occurrences') },
+        order_expression_evaluator: ->(column) { sql_min(column, 'outer_occurrences') }
       )
     end
 
@@ -74,12 +70,13 @@ def inner_occurrences
       Sbom::Occurrence.where('sbom_occurrences.traversal_ids = namespaces.traversal_ids::bigint[]')
         .unarchived
         .order(inner_order)
-        .select_distinct(on: distinct_columns)
+        .select(distinct(on: distinct_columns))
         .keyset_paginate(cursor: cursor, per_page: page_size)
     end
 
     def inner_order
-      evaluator = ->(column) { Sbom::Occurrence.arel_table[column] }
+      evaluator = ->(column) { column_expression(column) }
+
       keyset_order(
         column_expression_evaluator: evaluator,
         order_expression_evaluator: evaluator
@@ -88,10 +85,11 @@ def inner_order
 
     def outer_occurrences
       order = orderings.map do |column, direction|
-        "inner_occurrences.#{column} #{direction.to_s.upcase}"
+        column_expression = column_expression(column, 'inner_occurrences')
+        direction == :desc ? column_expression.desc : column_expression.asc
       end
 
-      Sbom::Occurrence.select_distinct(on: distinct_columns, table_name: '"inner_occurrences"')
+      Sbom::Occurrence.select(distinct(on: distinct_columns, table_name: 'inner_occurrences'))
       .from("namespaces, LATERAL (#{inner_occurrences.to_sql}) inner_occurrences")
       .order(*order)
       .limit(page_size + 1)
@@ -114,6 +112,34 @@ def cursor
       params[:cursor]
     end
 
+    def distinct_columns
+      orderings.keys
+    end
+
+    def column_expression(column, table_name = 'sbom_occurrences')
+      if column == :licenses
+        Sbom::Occurrence.connection.quote_table_name(table_name)
+          .then { |table_name| Arel.sql("(#{table_name}.\"licenses\" -> 0 ->> 'spdx_identifier')::text") }
+      else
+        Sbom::Occurrence.arel_table.alias(table_name)[column]
+      end
+    end
+
+    def distinct(on:, table_name: 'sbom_occurrences')
+      select_values = Sbom::Occurrence.column_names.map do |column|
+        Sbom::Occurrence.connection.quote_table_name("#{table_name}.#{column}")
+      end
+      distinct_values = on.map { |column| column_expression(column, table_name) }
+
+      distinct_sql = Arel::Nodes::DistinctOn.new(distinct_values).to_sql
+
+      "#{distinct_sql} #{select_values.join(', ')}"
+    end
+
+    def sql_min(column, table_name = 'sbom_occurrences')
+      Arel::Nodes::NamedFunction.new('MIN', [column_expression(column, table_name)])
+    end
+
     def orderings
       default_orderings = DEFAULT_SORT_COLUMNS.index_with { sort_direction }
 
diff --git a/ee/app/models/sbom/occurrence.rb b/ee/app/models/sbom/occurrence.rb
index 6a1f700985c65fc3dd34637ae34f37b524575d64..73b338910cd963f195d108517ba5a22ddabfa287 100644
--- a/ee/app/models/sbom/occurrence.rb
+++ b/ee/app/models/sbom/occurrence.rb
@@ -146,16 +146,6 @@ class Occurrence < ApplicationRecord
         .where(project_authorizations: { user_id: user.id })
     end
 
-    def self.select_distinct(on:, table_name: quoted_table_name)
-      quote = ->(column) { "#{table_name}.#{connection.quote_column_name(column)}" }
-
-      distinct_values = on.map { |column| quote.call(column) }
-
-      select_values = column_names.map { |column| quote.call(column) }
-
-      select("DISTINCT ON (#{distinct_values.join(', ')}) #{select_values.join(', ')}")
-    end
-
     def location
       {
         blob_path: input_file_blob_path,
diff --git a/ee/spec/finders/dependency_management/aggregations_finder_spec.rb b/ee/spec/finders/dependency_management/aggregations_finder_spec.rb
index 493eb704e4306d319b7b56a1a2192852c867e1ac..541d72071351aaaabdbb397eb470c62e37dd3019 100644
--- a/ee/spec/finders/dependency_management/aggregations_finder_spec.rb
+++ b/ee/spec/finders/dependency_management/aggregations_finder_spec.rb
@@ -69,18 +69,19 @@
 
     describe 'sorting' do
       let_it_be(:project) { target_projects.first }
-      let_it_be(:occurrence_1) { occurrence(packager: :npm, name: 'c', severity: 'low') }
-      let_it_be(:occurrence_2) { occurrence(packager: :bundler, name: 'b', severity: 'medium') }
-      let_it_be(:occurrence_3) { occurrence(packager: :nuget, name: 'a', severity: 'high') }
-      let_it_be(:occurrence_4) { occurrence(packager: :yarn, name: 'd', severity: 'critical') }
+      let_it_be(:occurrence_1) { occurrence(traits: [:npm], name: 'c', severity: 'low') }
+      let_it_be(:occurrence_2) { occurrence(traits: [:mit, :apache_2, :bundler], name: 'b', severity: 'medium') }
+      let_it_be(:occurrence_3) { occurrence(traits: [:mpl_2, :nuget], name: 'a', severity: 'high') }
+      let_it_be(:occurrence_4) { occurrence(traits: [:apache_2, :yarn], name: 'd', severity: 'critical') }
 
       before_all do
         Sbom::Occurrence.id_in(target_occurrences.map(&:id)).delete_all
       end
 
-      def occurrence(packager:, name:, severity:)
-        component = create(:sbom_component, name: name)
-        create(:sbom_occurrence, packager, component: component, highest_severity: severity, project: project)
+      def occurrence(name:, severity:, traits: [])
+        params = { component: create(:sbom_component, name: name), highest_severity: severity, project: project }
+
+        create(:sbom_occurrence, *traits, **params)
       end
 
       shared_examples 'can sort in both asc and desc order' do |sort_by|
@@ -127,6 +128,18 @@ def occurrence(packager:, name:, severity:)
         end
       end
 
+      context 'when sorting by license id' do
+        it_behaves_like 'can sort in both asc and desc order', :licenses do
+          let_it_be(:blank_license_array) { occurrence_1 }
+          let_it_be(:mit_apache) { occurrence_2 }
+          let_it_be(:mpl) { occurrence_3 }
+          let_it_be(:apache) { occurrence_4 }
+
+          let(:expected_asc) { [apache, mit_apache, mpl, blank_license_array] }
+          let(:expected_desc) { [blank_license_array, mpl, mit_apache, apache] }
+        end
+      end
+
       context 'when sorting by package manager' do
         it_behaves_like 'can sort in both asc and desc order', :package_manager do
           let_it_be(:npm) { occurrence_1 }
diff --git a/ee/spec/requests/groups/dependencies_controller_spec.rb b/ee/spec/requests/groups/dependencies_controller_spec.rb
index 16078ae49dbf30b3eef6ac63b1c72b75c6fe2988..2abede4a9a1d3737cc320816f37cbbd78d0317ee 100644
--- a/ee/spec/requests/groups/dependencies_controller_spec.rb
+++ b/ee/spec/requests/groups/dependencies_controller_spec.rb
@@ -254,6 +254,28 @@
               expect(recording).not_to exceed_all_query_limit(1).for_model(::Sbom::Source)
             end
 
+            context 'when sorted by license in ascending order' do
+              let(:params) { { group_id: group.to_param, sort_by: 'license', sort: 'asc' } }
+
+              it 'returns sorted list' do
+                subject
+
+                expect(json_response['dependencies'].first['licenses'].first['spdx_identifier']).to eq('Apache-2.0')
+                expect(json_response['dependencies'].last['licenses'].first['spdx_identifier']).to eq('MIT')
+              end
+            end
+
+            context 'when sorted by license in descending order' do
+              let(:params) { { group_id: group.to_param, sort_by: 'license', sort: 'desc' } }
+
+              it 'returns sorted list' do
+                subject
+
+                expect(json_response['dependencies'].first['licenses'].first['spdx_identifier']).to eq('MIT')
+                expect(json_response['dependencies'].last['licenses'].first['spdx_identifier']).to eq('Apache-2.0')
+              end
+            end
+
             context 'when sorted by name in ascending order' do
               let(:params) { { group_id: group.to_param, sort_by: 'name', sort: 'asc' } }