diff --git a/db/docs/batched_background_migrations/backfill_packages_build_infos_project_id.yml b/db/docs/batched_background_migrations/backfill_packages_build_infos_project_id.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7812388d7c353756bf86d5fc6dfd1b1915acd5fe
--- /dev/null
+++ b/db/docs/batched_background_migrations/backfill_packages_build_infos_project_id.yml
@@ -0,0 +1,9 @@
+---
+migration_job_name: BackfillPackagesBuildInfosProjectId
+description: Backfills sharding key `packages_build_infos.project_id` from `packages_packages`.
+feature_category: package_registry
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154572
+milestone: '17.1'
+queued_migration_version: 20240529123638
+finalize_after: '2024-06-22'
+finalized_by: # version of the migration that finalized this BBM
diff --git a/db/docs/packages_build_infos.yml b/db/docs/packages_build_infos.yml
index 2ef7dd031a8d2e5794ca2db3ab1159ab267b2064..44e60582943aa5895575f53d7ed3000b81404022 100644
--- a/db/docs/packages_build_infos.yml
+++ b/db/docs/packages_build_infos.yml
@@ -19,3 +19,4 @@ desired_sharding_key:
         table: packages_packages
         sharding_key: project_id
         belongs_to: package
+desired_sharding_key_migration_job_name: BackfillPackagesBuildInfosProjectId
diff --git a/db/migrate/20240529123634_add_project_id_to_packages_build_infos.rb b/db/migrate/20240529123634_add_project_id_to_packages_build_infos.rb
new file mode 100644
index 0000000000000000000000000000000000000000..92414ddf3d73220da780784edb399f309c38b3c0
--- /dev/null
+++ b/db/migrate/20240529123634_add_project_id_to_packages_build_infos.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddProjectIdToPackagesBuildInfos < Gitlab::Database::Migration[2.2]
+  milestone '17.1'
+
+  def change
+    add_column :packages_build_infos, :project_id, :bigint
+  end
+end
diff --git a/db/post_migrate/20240529123635_index_packages_build_infos_on_project_id.rb b/db/post_migrate/20240529123635_index_packages_build_infos_on_project_id.rb
new file mode 100644
index 0000000000000000000000000000000000000000..399f4a6e4ace818c89a766aecee8e4388b08a449
--- /dev/null
+++ b/db/post_migrate/20240529123635_index_packages_build_infos_on_project_id.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class IndexPackagesBuildInfosOnProjectId < Gitlab::Database::Migration[2.2]
+  milestone '17.1'
+  disable_ddl_transaction!
+
+  INDEX_NAME = 'index_packages_build_infos_on_project_id'
+
+  def up
+    add_concurrent_index :packages_build_infos, :project_id, name: INDEX_NAME
+  end
+
+  def down
+    remove_concurrent_index_by_name :packages_build_infos, INDEX_NAME
+  end
+end
diff --git a/db/post_migrate/20240529123636_add_packages_build_infos_project_id_fk.rb b/db/post_migrate/20240529123636_add_packages_build_infos_project_id_fk.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fa6b6308ef5fb6d705d64f5eb4216c40e45d2906
--- /dev/null
+++ b/db/post_migrate/20240529123636_add_packages_build_infos_project_id_fk.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddPackagesBuildInfosProjectIdFk < Gitlab::Database::Migration[2.2]
+  milestone '17.1'
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_foreign_key :packages_build_infos, :projects, column: :project_id, on_delete: :cascade
+  end
+
+  def down
+    with_lock_retries do
+      remove_foreign_key :packages_build_infos, column: :project_id
+    end
+  end
+end
diff --git a/db/post_migrate/20240529123637_add_packages_build_infos_project_id_trigger.rb b/db/post_migrate/20240529123637_add_packages_build_infos_project_id_trigger.rb
new file mode 100644
index 0000000000000000000000000000000000000000..497b93a961a1e34e7c821f1ebdc497fb5a3ce02b
--- /dev/null
+++ b/db/post_migrate/20240529123637_add_packages_build_infos_project_id_trigger.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class AddPackagesBuildInfosProjectIdTrigger < Gitlab::Database::Migration[2.2]
+  milestone '17.1'
+
+  def up
+    install_sharding_key_assignment_trigger(
+      table: :packages_build_infos,
+      sharding_key: :project_id,
+      parent_table: :packages_packages,
+      parent_sharding_key: :project_id,
+      foreign_key: :package_id
+    )
+  end
+
+  def down
+    remove_sharding_key_assignment_trigger(
+      table: :packages_build_infos,
+      sharding_key: :project_id,
+      parent_table: :packages_packages,
+      parent_sharding_key: :project_id,
+      foreign_key: :package_id
+    )
+  end
+end
diff --git a/db/post_migrate/20240529123638_queue_backfill_packages_build_infos_project_id.rb b/db/post_migrate/20240529123638_queue_backfill_packages_build_infos_project_id.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d053291faa2246d64680766026f262d3cc55c776
--- /dev/null
+++ b/db/post_migrate/20240529123638_queue_backfill_packages_build_infos_project_id.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+class QueueBackfillPackagesBuildInfosProjectId < Gitlab::Database::Migration[2.2]
+  milestone '17.1'
+  restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
+
+  MIGRATION = "BackfillPackagesBuildInfosProjectId"
+  DELAY_INTERVAL = 2.minutes
+  BATCH_SIZE = 5000
+  SUB_BATCH_SIZE = 500
+
+  def up
+    queue_batched_background_migration(
+      MIGRATION,
+      :packages_build_infos,
+      :id,
+      :project_id,
+      :packages_packages,
+      :project_id,
+      :package_id,
+      job_interval: DELAY_INTERVAL,
+      batch_size: BATCH_SIZE,
+      sub_batch_size: SUB_BATCH_SIZE
+    )
+  end
+
+  def down
+    delete_batched_background_migration(
+      MIGRATION,
+      :packages_build_infos,
+      :id,
+      [
+        :project_id,
+        :packages_packages,
+        :project_id,
+        :package_id
+      ]
+    )
+  end
+end
diff --git a/db/schema_migrations/20240529123634 b/db/schema_migrations/20240529123634
new file mode 100644
index 0000000000000000000000000000000000000000..31c96d36c3a6fd07acb43cdf0515bcf18ef88a99
--- /dev/null
+++ b/db/schema_migrations/20240529123634
@@ -0,0 +1 @@
+f055161158f79913c5d69c534dcd39a1764e8633ff0ecd3291d505282d38a13f
\ No newline at end of file
diff --git a/db/schema_migrations/20240529123635 b/db/schema_migrations/20240529123635
new file mode 100644
index 0000000000000000000000000000000000000000..26ed4a555d65b8c14458f05b0827afe990e229a5
--- /dev/null
+++ b/db/schema_migrations/20240529123635
@@ -0,0 +1 @@
+ed979989b0f02b974cd6d32ca1f057ee005a5fce028b90de3df428a45b193a75
\ No newline at end of file
diff --git a/db/schema_migrations/20240529123636 b/db/schema_migrations/20240529123636
new file mode 100644
index 0000000000000000000000000000000000000000..b0c1a276e3e79e1381761d4ac6ff3d4c2a791fe7
--- /dev/null
+++ b/db/schema_migrations/20240529123636
@@ -0,0 +1 @@
+ed6575abb2ff1e3ac4da13c99124f14ffe8c562c56ec656bcf44d138bcdea8d2
\ No newline at end of file
diff --git a/db/schema_migrations/20240529123637 b/db/schema_migrations/20240529123637
new file mode 100644
index 0000000000000000000000000000000000000000..8d42c2e32ddb1ae2b1c93c4a7d6f81ae244ea879
--- /dev/null
+++ b/db/schema_migrations/20240529123637
@@ -0,0 +1 @@
+326c931139782680ca3a4cec3c6c032f77e9b88b54a07789ffe721959e49d127
\ No newline at end of file
diff --git a/db/schema_migrations/20240529123638 b/db/schema_migrations/20240529123638
new file mode 100644
index 0000000000000000000000000000000000000000..f1f0c551d1b8a9cf49a801b4a691899b52f01cda
--- /dev/null
+++ b/db/schema_migrations/20240529123638
@@ -0,0 +1 @@
+6ccaa902b1ec8583f23e1318d5daef527704c827bf085a3c2da664a819ec30cc
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 96db4934c8e4cd886de597c06a298cb21dcbbce9..97cfb0adc14c815a7137c14dbfac2720a36e1f51 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -961,6 +961,22 @@ RETURN NEW;
 END
 $$;
 
+CREATE FUNCTION trigger_9259aae92378() RETURNS trigger
+    LANGUAGE plpgsql
+    AS $$
+BEGIN
+IF NEW."project_id" IS NULL THEN
+  SELECT "project_id"
+  INTO NEW."project_id"
+  FROM "packages_packages"
+  WHERE "packages_packages"."id" = NEW."package_id";
+END IF;
+
+RETURN NEW;
+
+END
+$$;
+
 CREATE FUNCTION trigger_94514aeadc50() RETURNS trigger
     LANGUAGE plpgsql
     AS $$
@@ -12939,7 +12955,8 @@ CREATE TABLE packages_build_infos (
     id bigint NOT NULL,
     package_id integer NOT NULL,
     pipeline_id_convert_to_bigint integer,
-    pipeline_id bigint
+    pipeline_id bigint,
+    project_id bigint
 );
 
 CREATE SEQUENCE packages_build_infos_id_seq
@@ -27093,6 +27110,8 @@ CREATE INDEX index_p_ci_runner_machine_builds_on_runner_machine_id ON ONLY p_ci_
 
 CREATE INDEX index_packages_build_infos_on_pipeline_id ON packages_build_infos USING btree (pipeline_id);
 
+CREATE INDEX index_packages_build_infos_on_project_id ON packages_build_infos USING btree (project_id);
+
 CREATE INDEX index_packages_build_infos_package_id_id ON packages_build_infos USING btree (package_id, id);
 
 CREATE INDEX index_packages_build_infos_package_id_pipeline_id_id ON packages_build_infos USING btree (package_id, pipeline_id, id);
@@ -30401,6 +30420,8 @@ CREATE TRIGGER trigger_8e66b994e8f0 BEFORE INSERT OR UPDATE ON audit_events_stre
 
 CREATE TRIGGER trigger_8fbb044c64ad BEFORE INSERT OR UPDATE ON design_management_designs FOR EACH ROW EXECUTE FUNCTION trigger_8fbb044c64ad();
 
+CREATE TRIGGER trigger_9259aae92378 BEFORE INSERT OR UPDATE ON packages_build_infos FOR EACH ROW EXECUTE FUNCTION trigger_9259aae92378();
+
 CREATE TRIGGER trigger_94514aeadc50 BEFORE INSERT OR UPDATE ON deployment_approvals FOR EACH ROW EXECUTE FUNCTION trigger_94514aeadc50();
 
 CREATE TRIGGER trigger_96a76ee9f147 BEFORE INSERT OR UPDATE ON design_management_versions FOR EACH ROW EXECUTE FUNCTION trigger_96a76ee9f147();
@@ -31388,6 +31409,9 @@ ALTER TABLE ONLY remote_development_namespace_cluster_agent_mappings
 ALTER TABLE ONLY zoekt_indices
     ADD CONSTRAINT fk_bf205d4773 FOREIGN KEY (zoekt_enabled_namespace_id) REFERENCES zoekt_enabled_namespaces(id) ON DELETE SET NULL;
 
+ALTER TABLE ONLY packages_build_infos
+    ADD CONSTRAINT fk_c0bc6b19ff FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+
 ALTER TABLE ONLY design_management_versions
     ADD CONSTRAINT fk_c1440b4896 FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL;
 
diff --git a/lib/gitlab/background_migration/backfill_packages_build_infos_project_id.rb b/lib/gitlab/background_migration/backfill_packages_build_infos_project_id.rb
new file mode 100644
index 0000000000000000000000000000000000000000..92ed7c94890dc26153ed13b60c2a2223ad5e8456
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_packages_build_infos_project_id.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module BackgroundMigration
+    class BackfillPackagesBuildInfosProjectId < BackfillDesiredShardingKeyJob
+      operation_name :backfill_packages_build_infos_project_id
+      feature_category :package_registry
+    end
+  end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_packages_build_infos_project_id_spec.rb b/spec/lib/gitlab/background_migration/backfill_packages_build_infos_project_id_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0d2ad26999e3589422611f9a5869a72c652d05f7
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_packages_build_infos_project_id_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillPackagesBuildInfosProjectId,
+  feature_category: :package_registry,
+  schema: 20240529123634 do
+  include_examples 'desired sharding key backfill job' do
+    let(:batch_table) { :packages_build_infos }
+    let(:backfill_column) { :project_id }
+    let(:backfill_via_table) { :packages_packages }
+    let(:backfill_via_column) { :project_id }
+    let(:backfill_via_foreign_key) { :package_id }
+  end
+end
diff --git a/spec/migrations/20240529123638_queue_backfill_packages_build_infos_project_id_spec.rb b/spec/migrations/20240529123638_queue_backfill_packages_build_infos_project_id_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..00ff2e4b150cf8930129cc0f9f2465095231e442
--- /dev/null
+++ b/spec/migrations/20240529123638_queue_backfill_packages_build_infos_project_id_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueBackfillPackagesBuildInfosProjectId, feature_category: :package_registry do
+  let!(:batched_migration) { described_class::MIGRATION }
+
+  it 'schedules a new batched migration' do
+    reversible_migration do |migration|
+      migration.before -> {
+        expect(batched_migration).not_to have_scheduled_batched_migration
+      }
+
+      migration.after -> {
+        expect(batched_migration).to have_scheduled_batched_migration(
+          table_name: :packages_build_infos,
+          column_name: :id,
+          interval: described_class::DELAY_INTERVAL,
+          batch_size: described_class::BATCH_SIZE,
+          sub_batch_size: described_class::SUB_BATCH_SIZE,
+          gitlab_schema: :gitlab_main_cell,
+          job_arguments: [
+            :project_id,
+            :packages_packages,
+            :project_id,
+            :package_id
+          ]
+        )
+      }
+    end
+  end
+end