diff --git a/changelogs/unreleased/202423-remove-records-without-group-from-webhooks.yml b/changelogs/unreleased/202423-remove-records-without-group-from-webhooks.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dff08ca6494a71a631f049ad86617d1fdd17019d
--- /dev/null
+++ b/changelogs/unreleased/202423-remove-records-without-group-from-webhooks.yml
@@ -0,0 +1,5 @@
+---
+title: Remove records without group from webhooks table
+merge_request: 57863
+author:
+type: other
diff --git a/db/post_migrate/20210330091751_remove_records_without_group_from_webhooks_table.rb b/db/post_migrate/20210330091751_remove_records_without_group_from_webhooks_table.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c384aa25ac4259f673019ee4db771fd1e37a7170
--- /dev/null
+++ b/db/post_migrate/20210330091751_remove_records_without_group_from_webhooks_table.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class RemoveRecordsWithoutGroupFromWebhooksTable < ActiveRecord::Migration[6.0]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  class WebHook < ActiveRecord::Base
+    include EachBatch
+
+    self.table_name = 'web_hooks'
+  end
+
+  class Group < ActiveRecord::Base
+    self.inheritance_column = :_type_disabled
+    self.table_name = 'namespaces'
+  end
+
+  def up
+    subquery = Group.select(1).where(Group.arel_table[:id].eq(WebHook.arel_table[:group_id]))
+
+    WebHook.each_batch(of: 500, column: :id) do |relation|
+      relation.where(type: 'GroupHook').where.not('EXISTS (?)', subquery).delete_all
+    end
+  end
+
+  def down
+    # no-op
+  end
+end
diff --git a/db/schema_migrations/20210330091751 b/db/schema_migrations/20210330091751
new file mode 100644
index 0000000000000000000000000000000000000000..0536252e980107bca6aace53111644162ef59598
--- /dev/null
+++ b/db/schema_migrations/20210330091751
@@ -0,0 +1 @@
+3a195b9671846409cf6665b13caad9713541d9cdd95c9f246c22b7db225ab02c
\ No newline at end of file
diff --git a/spec/migrations/remove_records_without_group_from_webhooks_table_spec.rb b/spec/migrations/remove_records_without_group_from_webhooks_table_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a28ca12a10da3df3ac4582c50494d63b1fb275c4
--- /dev/null
+++ b/spec/migrations/remove_records_without_group_from_webhooks_table_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+require Rails.root.join('db', 'migrate', '20210325092215_add_not_valid_foreign_key_to_group_hooks.rb')
+
+RSpec.describe RemoveRecordsWithoutGroupFromWebhooksTable, schema: 20210330091751 do
+  let(:web_hooks) { table(:web_hooks) }
+  let(:groups) { table(:namespaces) }
+
+  before do
+    group = groups.create!(name: 'gitlab', path: 'gitlab-org')
+    web_hooks.create!(group_id: group.id, type: 'GroupHook')
+    web_hooks.create!(group_id: nil)
+
+    AddNotValidForeignKeyToGroupHooks.new.down
+    web_hooks.create!(group_id: non_existing_record_id, type: 'GroupHook')
+    AddNotValidForeignKeyToGroupHooks.new.up
+  end
+
+  it 'removes group hooks where the referenced group does not exist', :aggregate_failures do
+    expect { RemoveRecordsWithoutGroupFromWebhooksTable.new.up }.to change { web_hooks.count }.by(-1)
+    expect(web_hooks.where.not(group_id: groups.select(:id)).count).to eq(0)
+    expect(web_hooks.where.not(group_id: nil).count).to eq(1)
+  end
+end