diff --git a/app/models/user.rb b/app/models/user.rb
index e49a39079ef3a0b00d8e310e68723f0888ac858c..fa58455ad358ceb57b5973cc00fa5024525d7b34 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -391,7 +391,7 @@ def blocked?
 
     # rubocop: disable CodeReuse/ServiceClass
     # Ideally we should not call a service object here but user.block
-    # is also bcalled by Users::MigrateToGhostUserService which references
+    # is also called by Users::MigrateToGhostUserService which references
     # this state transition object in order to do a rollback.
     # For this reason the tradeoff is to disable this cop.
     after_transition any => :blocked do |user|
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index 4ec875098fa0210b22d2175e291e0c83eac808d2..46eec08212587b9ba56c91b1e8f207cf607a0d62 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -54,7 +54,7 @@ def execute(user, options = {})
 
       yield(user) if block_given?
 
-      MigrateToGhostUserService.new(user).execute unless options[:hard_delete]
+      MigrateToGhostUserService.new(user).execute(hard_delete: options[:hard_delete])
 
       response = Snippets::BulkDestroyService.new(current_user, user.snippets).execute(options)
       raise DestroyError, response.message if response.error?
diff --git a/app/services/users/migrate_to_ghost_user_service.rb b/app/services/users/migrate_to_ghost_user_service.rb
index 515d782141647f243c27c2292d07107a932c8309..575614e8743ca62a538c991121177aadfa47b0e1 100644
--- a/app/services/users/migrate_to_ghost_user_service.rb
+++ b/app/services/users/migrate_to_ghost_user_service.rb
@@ -10,14 +10,21 @@ module Users
   class MigrateToGhostUserService
     extend ActiveSupport::Concern
 
-    attr_reader :ghost_user, :user
+    attr_reader :ghost_user, :user, :hard_delete
 
     def initialize(user)
       @user = user
       @ghost_user = User.ghost
     end
 
-    def execute
+    # If an admin attempts to hard delete a user, in some cases associated
+    # records may have a NOT NULL constraint on the user ID that prevent that record
+    # from being destroyed. In such situations we must assign the record to the ghost user.
+    # Passing in `hard_delete: true` will ensure these records get assigned to
+    # the ghost user before the user is destroyed. Other associated records will be destroyed.
+    # letting the other associated records be destroyed.
+    def execute(hard_delete: false)
+      @hard_delete = hard_delete
       transition = user.block_transition
 
       # Block the user before moving records to prevent a data race.
@@ -46,6 +53,8 @@ def execute
     private
 
     def migrate_records
+      return if hard_delete
+
       migrate_issues
       migrate_merge_requests
       migrate_notes
diff --git a/ee/app/models/resource_iteration_event.rb b/ee/app/models/resource_iteration_event.rb
index 14c36d3029c6319f357a11169c3a92f1c7417f65..69ca9269c8e3ec6b3f9908e38ea13f2927a78677 100644
--- a/ee/app/models/resource_iteration_event.rb
+++ b/ee/app/models/resource_iteration_event.rb
@@ -1,7 +1,10 @@
 # frozen_string_literal: true
 
 class ResourceIterationEvent < ResourceTimeboxEvent
+  include EachBatch
+
   belongs_to :iteration
 
   scope :with_api_entity_associations, -> { preload(:iteration, :user) }
+  scope :by_user, -> (user) { where(user_id: user ) }
 end
diff --git a/ee/app/services/ee/users/migrate_to_ghost_user_service.rb b/ee/app/services/ee/users/migrate_to_ghost_user_service.rb
index ee5172298258f543caf12d98ea1b8a9d79b38d53..02d0209a634281c576f09386eda9d9ba38fbbe30 100644
--- a/ee/app/services/ee/users/migrate_to_ghost_user_service.rb
+++ b/ee/app/services/ee/users/migrate_to_ghost_user_service.rb
@@ -3,9 +3,16 @@
 module EE
   module Users
     module MigrateToGhostUserService
+      BATCH_SIZE = 1000
+
       private
 
       def migrate_records
+        # these should always be ghosted
+        migrate_resource_iteration_events
+
+        return super if hard_delete
+
         migrate_epics
         migrate_requirements_management_requirements
         migrate_vulnerabilities_feedback
@@ -27,6 +34,12 @@ def migrate_vulnerabilities_feedback
         user.vulnerability_feedback.update_all(author_id: ghost_user.id)
         user.commented_vulnerability_feedback.update_all(comment_author_id: ghost_user.id)
       end
+
+      def migrate_resource_iteration_events
+        ResourceIterationEvent.by_user(user).each_batch(of: BATCH_SIZE) do |batch|
+          batch.update_all(user_id: ghost_user.id)
+        end
+      end
     end
   end
 end
diff --git a/ee/spec/services/ee/users/destroy_service_spec.rb b/ee/spec/services/ee/users/destroy_service_spec.rb
index cdbad571b051984f28b2b047365c5bdf831efe68..c767c83671e8ca5c5ee2edd84778f565c2d05b9d 100644
--- a/ee/spec/services/ee/users/destroy_service_spec.rb
+++ b/ee/spec/services/ee/users/destroy_service_spec.rb
@@ -42,6 +42,20 @@
         end
       end
 
+      context 'migrating associated records' do
+        context 'when hard_delete option is given' do
+          let!(:resource_iteration_event) { create(:resource_iteration_event, user: user) }
+
+          it 'will ghost certain records' do
+            expect_any_instance_of(Users::MigrateToGhostUserService).to receive(:execute).once.and_call_original
+
+            service.execute(user, hard_delete: true)
+
+            expect(resource_iteration_event.reload.user).to be_ghost
+          end
+        end
+      end
+
       context 'when user has oncall rotations' do
         let(:schedule) { create(:incident_management_oncall_schedule, project: project) }
         let(:rotation) { create(:incident_management_oncall_rotation, schedule: schedule) }
diff --git a/ee/spec/services/ee/users/migrate_to_ghost_user_service_spec.rb b/ee/spec/services/ee/users/migrate_to_ghost_user_service_spec.rb
index 79c25dcb03c412108f03db2013d404abcba01175..c3fb1f8d95bd2ca28e5bf0c1a6b36369dcc3617d 100644
--- a/ee/spec/services/ee/users/migrate_to_ghost_user_service_spec.rb
+++ b/ee/spec/services/ee/users/migrate_to_ghost_user_service_spec.rb
@@ -3,10 +3,11 @@
 require 'spec_helper'
 
 RSpec.describe Users::MigrateToGhostUserService do
-  context 'epics' do
-    let!(:user)   { create(:user) }
-    let(:service) { described_class.new(user) }
+  let!(:user)   { create(:user) }
+  let(:service) { described_class.new(user) }
+  let(:always_ghost) { false }
 
+  context 'epics' do
     context 'deleted user is present as both author and edited_user' do
       include_examples "migrating a deleted user's associated records to the ghost user", Epic, [:author, :last_edited_by] do
         let(:created_record) do
@@ -23,29 +24,28 @@
   end
 
   context 'vulnerability_feedback author' do
-    let!(:user)   { create(:user) }
-    let(:service) { described_class.new(user) }
-
     include_examples "migrating a deleted user's associated records to the ghost user", Vulnerabilities::Feedback, [:author] do
       let(:created_record) { create(:vulnerability_feedback, author: user) }
     end
   end
 
   context 'vulnerability_feedback comment author' do
-    let!(:user)   { create(:user) }
-    let(:service) { described_class.new(user) }
-
     include_examples "migrating a deleted user's associated records to the ghost user", Vulnerabilities::Feedback, [:comment_author] do
       let(:created_record) { create(:vulnerability_feedback, comment_author: user) }
     end
   end
 
   context 'requirements' do
-    let!(:user)   { create(:user) }
-    let(:service) { described_class.new(user) }
-
     include_examples "migrating a deleted user's associated records to the ghost user", RequirementsManagement::Requirement, [:author] do
       let(:created_record) { create(:requirement, author: user) }
     end
   end
+
+  context 'resource_iteration_events' do
+    let(:always_ghost) { true }
+
+    include_examples "migrating a deleted user's associated records to the ghost user", ResourceIterationEvent, [:user] do
+      let(:created_record) { create(:resource_iteration_event, issue: create(:issue), user: user, iteration: create(:iteration)) }
+    end
+  end
 end
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 76b84e3b4aba48b88934398d7cb3a40bf680eedf..602db66dba1708f9090fbcb30e75aee75a46d7a8 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -215,8 +215,8 @@
       end
     end
 
-    context "migrating associated records" do
-      let!(:issue)     { create(:issue, author: user) }
+    context 'migrating associated records' do
+      let!(:issue) { create(:issue, author: user) }
 
       it 'delegates to the `MigrateToGhostUser` service to move associated records to the ghost user' do
         expect_any_instance_of(Users::MigrateToGhostUserService).to receive(:execute).once.and_call_original
@@ -226,12 +226,14 @@
         expect(issue.reload.author).to be_ghost
       end
 
-      it 'does not run `MigrateToGhostUser` if hard_delete option is given' do
-        expect_any_instance_of(Users::MigrateToGhostUserService).not_to receive(:execute)
+      context 'when hard_delete option is given' do
+        it 'will not ghost certain records' do
+          expect_any_instance_of(Users::MigrateToGhostUserService).to receive(:execute).once.and_call_original
 
-        service.execute(user, hard_delete: true)
+          service.execute(user, hard_delete: true)
 
-        expect(Issue.exists?(issue.id)).to be_falsy
+          expect(Issue.exists?(issue.id)).to be_falsy
+        end
       end
     end
 
diff --git a/spec/services/users/migrate_to_ghost_user_service_spec.rb b/spec/services/users/migrate_to_ghost_user_service_spec.rb
index c36889f20ec4dfa97de8fedec21c63ab13c14192..073ebaae5b07345a8504e7bd6963b69992a93222 100644
--- a/spec/services/users/migrate_to_ghost_user_service_spec.rb
+++ b/spec/services/users/migrate_to_ghost_user_service_spec.rb
@@ -3,9 +3,10 @@
 require 'spec_helper'
 
 RSpec.describe Users::MigrateToGhostUserService do
-  let!(:user)      { create(:user) }
-  let!(:project)   { create(:project, :repository) }
-  let(:service)    { described_class.new(user) }
+  let!(:user)        { create(:user) }
+  let!(:project)     { create(:project, :repository) }
+  let(:service)      { described_class.new(user) }
+  let(:always_ghost) { false }
 
   context "migrating a user's associated records to the ghost user" do
     context 'issues' do
diff --git a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
index 2fbc01a9195b1c5e2780a487bd7a3f18752ec7aa..1e291a90163d2910012ded0df8d193e6caf9fdae 100644
--- a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
+++ b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
@@ -42,6 +42,18 @@
       end
     end
 
+    it 'will only migrate specific records during a hard_delete' do
+      service.execute(hard_delete: true)
+
+      migrated_record = record_class.find_by_id(record.id)
+
+      check_user = always_ghost ? User.ghost : user
+
+      migrated_fields.each do |field|
+        expect(migrated_record.public_send(field)).to eq(check_user)
+      end
+    end
+
     context "race conditions" do
       context "when #{record_class_name} migration fails and is rolled back" do
         before do