diff --git a/changelogs/unreleased/ab-partition-management.yml b/changelogs/unreleased/ab-partition-management.yml
new file mode 100644
index 0000000000000000000000000000000000000000..35d9ce9ed0ad2fc4f5504dfcc0b39a39a2f2a058
--- /dev/null
+++ b/changelogs/unreleased/ab-partition-management.yml
@@ -0,0 +1,5 @@
+---
+title: Create time-space partitions in separate schema
+merge_request: 34504
+author:
+type: other
diff --git a/config/initializers/active_record_schema_ignore_tables.rb b/config/initializers/active_record_schema_ignore_tables.rb
index 661135f8ade36a26861cd244efc18e5f624dabd3..559455cbf46720753c448d00f50a21a8a561124a 100644
--- a/config/initializers/active_record_schema_ignore_tables.rb
+++ b/config/initializers/active_record_schema_ignore_tables.rb
@@ -1,2 +1,5 @@
 # Ignore table used temporarily in background migration
 ActiveRecord::SchemaDumper.ignore_tables = ["untracked_files_for_uploads"]
+
+# Ignore dynamically managed partitions in static application schema
+ActiveRecord::SchemaDumper.ignore_tables += ["partitions_dynamic.*"]
diff --git a/db/migrate/20200615101135_create_dynamic_partitions_schema.rb b/db/migrate/20200615101135_create_dynamic_partitions_schema.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4e6636a216460d7bee607dbb0e434b82fd379845
--- /dev/null
+++ b/db/migrate/20200615101135_create_dynamic_partitions_schema.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class CreateDynamicPartitionsSchema < ActiveRecord::Migration[6.0]
+  include Gitlab::Database::SchemaHelpers
+
+  DOWNTIME = false
+
+  def up
+    execute 'CREATE SCHEMA partitions_dynamic'
+
+    create_comment(:schema, :partitions_dynamic, <<~EOS.strip)
+      Schema to hold partitions managed dynamically from the application, e.g. for time space partitioning.
+    EOS
+  end
+
+  def down
+    execute 'DROP SCHEMA partitions_dynamic'
+  end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 7cd30c15c332aae2d95344f5b1c87e92fd99134a..0f2ff89c7036685204ca1bebf0a43bfa20f6b764 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -1,5 +1,9 @@
 SET search_path=public;
 
+CREATE SCHEMA partitions_dynamic;
+
+COMMENT ON SCHEMA partitions_dynamic IS 'Schema to hold partitions managed dynamically from the application, e.g. for time space partitioning.';
+
 CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public;
 
 CREATE TABLE public.abuse_reports (
@@ -13995,6 +13999,7 @@ COPY "schema_migrations" (version) FROM STDIN;
 20200609142508
 20200609212701
 20200615083635
+20200615101135
 20200615121217
 20200615123055
 20200615232735
diff --git a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
index f77fbe98df16747c3b99b79c6452075e575d6e38..d57ad03bd0507d2b69de346c886b0b7f1823cb6e 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
@@ -8,6 +8,7 @@ module TableManagementHelpers
 
         WHITELISTED_TABLES = %w[audit_events].freeze
         ERROR_SCOPE = 'table partitioning'
+        DYNAMIC_PARTITIONS_SCHEMA = 'partitions_dynamic'
 
         # Creates a partitioned copy of an existing table, using a RANGE partitioning strategy on a timestamp column.
         # One partition is created per month between the given `min_date` and `max_date`.
@@ -125,7 +126,7 @@ def create_daterange_partitions(table_name, column_name, min_date, max_date)
           min_date = min_date.beginning_of_month.to_date
           max_date = max_date.next_month.beginning_of_month.to_date
 
-          create_range_partition_safely("#{table_name}_000000", table_name, 'MINVALUE', to_sql_date_literal(min_date))
+          create_range_partition_safely("#{table_name}_000000", table_name, 'MINVALUE', to_sql_date_literal(min_date), schema: DYNAMIC_PARTITIONS_SCHEMA)
 
           while min_date < max_date
             partition_name = "#{table_name}_#{min_date.strftime('%Y%m')}"
@@ -133,7 +134,7 @@ def create_daterange_partitions(table_name, column_name, min_date, max_date)
             lower_bound = to_sql_date_literal(min_date)
             upper_bound = to_sql_date_literal(next_date)
 
-            create_range_partition_safely(partition_name, table_name, lower_bound, upper_bound)
+            create_range_partition_safely(partition_name, table_name, lower_bound, upper_bound, schema: DYNAMIC_PARTITIONS_SCHEMA)
             min_date = next_date
           end
         end
@@ -142,8 +143,8 @@ def to_sql_date_literal(date)
           connection.quote(date.strftime('%Y-%m-%d'))
         end
 
-        def create_range_partition_safely(partition_name, table_name, lower_bound, upper_bound)
-          if table_exists?(partition_name)
+        def create_range_partition_safely(partition_name, table_name, lower_bound, upper_bound, schema:)
+          if table_exists?("#{schema}.#{partition_name}")
             # rubocop:disable Gitlab/RailsLogger
             Rails.logger.warn "Partition not created because it already exists" \
               " (this may be due to an aborted migration or similar): partition_name: #{partition_name}"
@@ -151,7 +152,7 @@ def create_range_partition_safely(partition_name, table_name, lower_bound, upper
             return
           end
 
-          create_range_partition(partition_name, table_name, lower_bound, upper_bound)
+          create_range_partition(partition_name, table_name, lower_bound, upper_bound, schema: schema)
         end
 
         def create_sync_trigger(source_table, target_table, unique_key)
diff --git a/lib/gitlab/database/schema_helpers.rb b/lib/gitlab/database/schema_helpers.rb
index 8e544307d81934b844e50d5c209a6e90678ca757..fee52024a97aacded3008feb0f9a6113a41f8350 100644
--- a/lib/gitlab/database/schema_helpers.rb
+++ b/lib/gitlab/database/schema_helpers.rb
@@ -69,9 +69,11 @@ def assert_not_in_transaction_block(scope:)
 
       private
 
-      def create_range_partition(partition_name, table_name, lower_bound, upper_bound)
+      def create_range_partition(partition_name, table_name, lower_bound, upper_bound, schema:)
+        raise ArgumentError, 'explicit schema is required but currently missing' unless schema
+
         execute(<<~SQL)
-          CREATE TABLE #{partition_name} PARTITION OF #{table_name}
+          CREATE TABLE #{schema}.#{partition_name} PARTITION OF #{table_name}
           FOR VALUES FROM (#{lower_bound}) TO (#{upper_bound})
         SQL
       end
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
index 586b57d2002198160652b7fb40573713c12f407f..60fd039da332d3934b3d35076875f854105b92a6 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
@@ -241,7 +241,7 @@
 
   describe '#drop_partitioned_table_for' do
     let(:expected_tables) do
-      %w[000000 201912 202001 202002].map { |suffix| "#{partitioned_table}_#{suffix}" }.unshift(partitioned_table)
+      %w[000000 201912 202001 202002].map { |suffix| "partitions_dynamic.#{partitioned_table}_#{suffix}" }.unshift(partitioned_table)
     end
 
     context 'when the table is not whitelisted' do
diff --git a/spec/support/helpers/partitioning_helpers.rb b/spec/support/helpers/partitioning_helpers.rb
index 98a13915d7698ec39f0ce222e9e56c524f90e955..7aedc6953aa214eba2bacb75a2c8896ffd245a46 100644
--- a/spec/support/helpers/partitioning_helpers.rb
+++ b/spec/support/helpers/partitioning_helpers.rb
@@ -8,8 +8,8 @@ def expect_table_partitioned_by(table, columns, part_type: :range)
     expect(columns_with_part_type).to match_array(actual_columns)
   end
 
-  def expect_range_partition_of(partition_name, table_name, min_value, max_value)
-    definition = find_partition_definition(partition_name)
+  def expect_range_partition_of(partition_name, table_name, min_value, max_value, schema: 'partitions_dynamic')
+    definition = find_partition_definition(partition_name, schema: schema)
 
     expect(definition).not_to be_nil
     expect(definition['base_table']).to eq(table_name.to_s)
@@ -40,7 +40,7 @@ def find_partitioned_columns(table)
     SQL
   end
 
-  def find_partition_definition(partition)
+  def find_partition_definition(partition, schema: 'partitions_dynamic')
     connection.select_one(<<~SQL)
       select
         parent_class.relname as base_table,
@@ -48,7 +48,10 @@ def find_partition_definition(partition)
       from pg_class
       inner join pg_inherits i on pg_class.oid = inhrelid
       inner join pg_class parent_class on parent_class.oid = inhparent
-      where pg_class.relname = '#{partition}' and pg_class.relispartition;
+      inner join pg_namespace ON pg_namespace.oid = pg_class.relnamespace
+      where pg_namespace.nspname = '#{schema}'
+        and pg_class.relname = '#{partition}'
+        and pg_class.relispartition
     SQL
   end
 end