diff --git a/core/core.services.yml b/core/core.services.yml
index 9dc264e5be..f1b2a19d66 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -524,7 +524,7 @@ services:
     class: Drupal\Core\Extension\ModuleInstaller
     tags:
       - { name: service_collector, tag: 'module_install.uninstall_validator', call: addUninstallValidator }
-    arguments: ['%app.root%', '@module_handler', '@kernel']
+    arguments: ['%app.root%', '@module_handler', '@kernel', '@database']
     lazy: true
   extension.list.module:
     class: Drupal\Core\Extension\ModuleExtensionList
diff --git a/core/includes/schema.inc b/core/includes/schema.inc
index 538f1b3251..2ef67b8f57 100644
--- a/core/includes/schema.inc
+++ b/core/includes/schema.inc
@@ -112,8 +112,15 @@ function drupal_set_installed_schema_version($module, $version) {
  *
  * @param string $module
  *   The module for which the tables will be created.
+ *
+ * @deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. No direct
+ *   replacement is provided.
+ *
+ * @see https://www.drupal.org/node/2970993
+ * @see \Drupal\Core\Extension\ModuleInstaller::installSchema()
  */
 function drupal_install_schema($module) {
+  @trigger_error('drupal_install_schema() is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. No direct replacement is provided. See https://www.drupal.org/node/2970993', E_USER_DEPRECATED);
   $schema = drupal_get_module_schema($module);
   _drupal_schema_initialize($schema, $module, FALSE);
 
@@ -127,8 +134,15 @@ function drupal_install_schema($module) {
  *
  * @param string $module
  *   The module for which the tables will be removed.
+ *
+ * @deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. No direct
+ *   replacement is provided.
+ *
+ * @see https://www.drupal.org/node/2970993
+ * @see \Drupal\Core\Extension\ModuleInstaller::uninstallSchema()
  */
 function drupal_uninstall_schema($module) {
+  @trigger_error('drupal_uninstall_schema() is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. No direct replacement is provided. See https://www.drupal.org/node/2970993', E_USER_DEPRECATED);
   $tables = drupal_get_module_schema($module);
   _drupal_schema_initialize($tables, $module, FALSE);
   $schema = \Drupal::database()->schema();
@@ -151,8 +165,16 @@ function drupal_uninstall_schema($module) {
  * @param string $table
  *   The name of the table. If not given, the module's complete schema
  *   is returned.
+ *
+ * @deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. No direct
+ *   replacement is provided. Testing classes could use
+ *   \Drupal\TestTools\Extension\SchemaInspector for introspection.
+ *
+ * @see https://www.drupal.org/node/2970993
+ * @see \Drupal\TestTools\Extension\SchemaInspector::getTablesSpecification()
  */
 function drupal_get_module_schema($module, $table = NULL) {
+  @trigger_error('drupal_get_module_schema() is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. No direct replacement is provided. Testing classes could use \Drupal\TestTools\Extension\SchemaInspector for introspection. See https://www.drupal.org/node/2970993', E_USER_DEPRECATED);
   // Load the .install file to get hook_schema.
   module_load_install($module);
   $schema = \Drupal::moduleHandler()->invoke($module, 'schema');
@@ -181,8 +203,14 @@ function drupal_get_module_schema($module, $table = NULL) {
  *   (optional) Whether to additionally remove 'description' keys of all tables
  *   and fields to improve performance of serialize() and unserialize().
  *   Defaults to TRUE.
+ *
+ * @deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. No direct
+ *   replacement is provided.
+ *
+ * @see https://www.drupal.org/node/2970993
  */
 function _drupal_schema_initialize(&$schema, $module, $remove_descriptions = TRUE) {
+  @trigger_error('_drupal_schema_initialize() is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. No direct replacement is provided. See https://www.drupal.org/node/2970993', E_USER_DEPRECATED);
   // Set the name and module key for all tables.
   foreach ($schema as $name => &$table) {
     if (empty($table['module'])) {
diff --git a/core/lib/Drupal/Core/Database/database.api.php b/core/lib/Drupal/Core/Database/database.api.php
index aaeba667ef..890f36bd9a 100644
--- a/core/lib/Drupal/Core/Database/database.api.php
+++ b/core/lib/Drupal/Core/Database/database.api.php
@@ -390,7 +390,9 @@
  * ];
  * @endcode
  *
- * @see drupal_install_schema()
+ * @see \Drupal\Core\Extension\ModuleInstaller::installSchema()
+ * @see \Drupal\Core\Extension\ModuleInstaller::uninstallSchema()
+ * @see \Drupal\TestTools\Extension\SchemaInspector::getTablesSpecification()
  *
  * @}
  */
diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
index bc2f9d0323..c5e6953332 100644
--- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -4,6 +4,8 @@
 
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Database\Schema;
 use Drupal\Core\DrupalKernelInterface;
 use Drupal\Core\Entity\EntityStorageException;
 use Drupal\Core\Entity\FieldableEntityInterface;
@@ -44,6 +46,13 @@ class ModuleInstaller implements ModuleInstallerInterface {
    */
   protected $root;
 
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
   /**
    * The uninstall validators.
    *
@@ -60,14 +69,21 @@ class ModuleInstaller implements ModuleInstallerInterface {
    *   The module handler.
    * @param \Drupal\Core\DrupalKernelInterface $kernel
    *   The drupal kernel.
+   * @param \Drupal\Core\Database\Connection $connection
+   *   The database connection.
    *
    * @see \Drupal\Core\DrupalKernel
    * @see \Drupal\Core\CoreServiceProvider
    */
-  public function __construct($root, ModuleHandlerInterface $module_handler, DrupalKernelInterface $kernel) {
+  public function __construct($root, ModuleHandlerInterface $module_handler, DrupalKernelInterface $kernel, Connection $connection = NULL) {
     $this->root = $root;
     $this->moduleHandler = $module_handler;
     $this->kernel = $kernel;
+    if (!$connection) {
+      @trigger_error('The database connection must be passed to ' . __METHOD__ . '. Creating ' . __CLASS__ . ' without it is deprecated in drupal:9.1.0 and will be required in drupal:10.0.0. See https://www.drupal.org/node/2970993', E_USER_DEPRECATED);
+      $connection = \Drupal::service('database');
+    }
+    $this->connection = $connection;
   }
 
   /**
@@ -227,7 +243,7 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
         $this->moduleHandler->invokeAll('module_preinstall', [$module]);
 
         // Now install the module's schema if necessary.
-        drupal_install_schema($module);
+        $this->installSchema($this->connection->schema(), $module);
 
         // Clear plugin manager caches.
         \Drupal::getContainer()->get('plugin.cache_clearer')->clearCachedDefinitions();
@@ -468,7 +484,7 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
       }
 
       // Remove the schema.
-      drupal_uninstall_schema($module);
+      $this->uninstallSchema($this->connection->schema(), $module);
 
       // Remove the module's entry from the config. Don't check schema when
       // uninstalling a module since we are only clearing a key.
@@ -585,6 +601,7 @@ protected function updateKernel($module_filenames) {
     // dependencies.
     $container = $this->kernel->getContainer();
     $this->moduleHandler = $container->get('module_handler');
+    $this->connection = $container->get('database');
   }
 
   /**
@@ -606,4 +623,40 @@ public function validateUninstall(array $module_list) {
     return $reasons;
   }
 
+  /**
+   * Creates all tables defined in a module's hook_schema().
+   *
+   * @param \Drupal\Core\Database\Schema $schema
+   *   The database Schema object to create tables.
+   * @param string $module
+   *   The module for which the tables will be created.
+   *
+   * @internal
+   */
+  protected function installSchema(Schema $schema, $module) {
+    $tables = $this->moduleHandler->invoke($module, 'schema') ?? [];
+    foreach ($tables as $name => $table) {
+      $schema->createTable($name, $table);
+    }
+  }
+
+  /**
+   * Removes all tables defined in a module's hook_schema().
+   *
+   * @param \Drupal\Core\Database\Schema $schema
+   *   The database Schema object to drop tables.
+   * @param string $module
+   *   The module for which the tables will be removed.
+   *
+   * @internal
+   */
+  protected function uninstallSchema(Schema $schema, $module) {
+    $tables = $this->moduleHandler->invoke($module, 'schema') ?? [];
+    foreach (array_keys($tables) as $table) {
+      if ($schema->tableExists($table)) {
+        $schema->dropTable($table);
+      }
+    }
+  }
+
 }
diff --git a/core/modules/system/tests/modules/module_test/module_test.install b/core/modules/system/tests/modules/module_test/module_test.install
index 9d80d89605..25cf025aaa 100644
--- a/core/modules/system/tests/modules/module_test/module_test.install
+++ b/core/modules/system/tests/modules/module_test/module_test.install
@@ -30,7 +30,7 @@ function module_test_schema() {
  * Implements hook_install().
  */
 function module_test_install() {
-  $schema = drupal_get_module_schema('module_test', 'module_test');
+  $schema = module_test_schema()['module_test'];
   Database::getConnection()->insert('module_test')
     ->fields([
       'data' => $schema['fields']['data']['type'],
diff --git a/core/modules/system/tests/src/Functional/Module/ModuleTestBase.php b/core/modules/system/tests/src/Functional/Module/ModuleTestBase.php
index 63e4388cdc..fd79e9dab7 100644
--- a/core/modules/system/tests/src/Functional/Module/ModuleTestBase.php
+++ b/core/modules/system/tests/src/Functional/Module/ModuleTestBase.php
@@ -8,6 +8,7 @@
 use Drupal\Core\Config\FileStorage;
 use Drupal\Core\Logger\RfcLogLevel;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\TestTools\Extension\SchemaInspector;
 
 /**
  * Helper class for module test cases.
@@ -59,7 +60,7 @@ public function assertTableCount($base_table, $count = TRUE) {
    *   The name of the module.
    */
   public function assertModuleTablesExist($module) {
-    $tables = array_keys(drupal_get_module_schema($module));
+    $tables = array_keys(SchemaInspector::getTablesSpecification(\Drupal::moduleHandler(), $module));
     $tables_exist = TRUE;
     $schema = Database::getConnection()->schema();
     foreach ($tables as $table) {
@@ -77,7 +78,7 @@ public function assertModuleTablesExist($module) {
    *   The name of the module.
    */
   public function assertModuleTablesDoNotExist($module) {
-    $tables = array_keys(drupal_get_module_schema($module));
+    $tables = array_keys(SchemaInspector::getTablesSpecification(\Drupal::moduleHandler(), $module));
     $tables_exist = FALSE;
     $schema = Database::getConnection()->schema();
     foreach ($tables as $table) {
diff --git a/core/tests/Drupal/KernelTests/Core/Database/InsertDefaultsTest.php b/core/tests/Drupal/KernelTests/Core/Database/InsertDefaultsTest.php
index 9050cbe256..687d9328e9 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/InsertDefaultsTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/InsertDefaultsTest.php
@@ -13,15 +13,14 @@ class InsertDefaultsTest extends DatabaseTestBase {
 
   /**
    * Tests that we can run a query that uses default values for everything.
+   *
+   * @see \database_test_schema()
    */
   public function testDefaultInsert() {
     $query = $this->connection->insert('test')->useDefaults(['job']);
     $id = $query->execute();
-
-    $schema = drupal_get_module_schema('database_test', 'test');
-
     $job = $this->connection->query('SELECT [job] FROM {test} WHERE [id] = :id', [':id' => $id])->fetchField();
-    $this->assertEqual($schema['fields']['job']['default'], $job, 'Default field value is set.');
+    $this->assertSame('Undefined', $job, 'Default field value is set.');
   }
 
   /**
@@ -45,17 +44,16 @@ public function testDefaultEmptyInsert() {
 
   /**
    * Tests that we can insert fields with values and defaults in the same query.
+   *
+   * @see \database_test_schema()
    */
   public function testDefaultInsertWithFields() {
     $query = $this->connection->insert('test')
       ->fields(['name' => 'Bob'])
       ->useDefaults(['job']);
     $id = $query->execute();
-
-    $schema = drupal_get_module_schema('database_test', 'test');
-
     $job = $this->connection->query('SELECT [job] FROM {test} WHERE [id] = :id', [':id' => $id])->fetchField();
-    $this->assertEqual($schema['fields']['job']['default'], $job, 'Default field value is set.');
+    $this->assertSame('Undefined', $job, 'Default field value is set.');
   }
 
 }
diff --git a/core/tests/Drupal/KernelTests/Core/Extension/SchemaDeprecationTest.php b/core/tests/Drupal/KernelTests/Core/Extension/SchemaDeprecationTest.php
new file mode 100644
index 0000000000..b656435048
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Extension/SchemaDeprecationTest.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Extension;
+
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests deprecated schema.inc functions.
+ *
+ * @group legacy
+ * @group extension
+ */
+class SchemaDeprecationTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['system', 'dblog'];
+
+  /**
+   * @expectedDeprecation drupal_install_schema() is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. No direct replacement is provided. See https://www.drupal.org/node/2970993
+   * @expectedDeprecation drupal_uninstall_schema() is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. No direct replacement is provided. See https://www.drupal.org/node/2970993
+   * @expectedDeprecation drupal_get_module_schema() is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. No direct replacement is provided. Testing classes could use \Drupal\TestTools\Extension\SchemaInspector for introspection. See https://www.drupal.org/node/2970993
+   */
+  public function testDeprecatedInstallSchema() {
+    drupal_install_schema('dblog');
+    $table = 'watchdog';
+    $this->assertArrayHasKey($table, drupal_get_module_schema('dblog'));
+    $this->assertTrue(\Drupal::database()->schema()->tableExists($table));
+    drupal_uninstall_schema('dblog');
+  }
+
+  /**
+   * @expectedDeprecation _drupal_schema_initialize() is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. No direct replacement is provided. See https://www.drupal.org/node/2970993
+   */
+  public function testDeprecatedSchemaInitialize() {
+    $module = 'dblog';
+    $table = 'watchdog';
+    $schema = [$table => []];
+    _drupal_schema_initialize($schema, $module, FALSE);
+    $this->assertEquals($module, $schema[$table]['module']);
+    $this->assertEquals($table, $schema[$table]['name']);
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php
index e5afa6bba9..c6a3569ae8 100644
--- a/core/tests/Drupal/KernelTests/KernelTestBase.php
+++ b/core/tests/Drupal/KernelTests/KernelTestBase.php
@@ -22,6 +22,7 @@
 use Drupal\Tests\TestRequirementsTrait;
 use Drupal\Tests\Traits\PhpUnitWarnings;
 use Drupal\TestTools\Comparator\MarkupInterfaceComparator;
+use Drupal\TestTools\Extension\SchemaInspector;
 use PHPUnit\Framework\Exception;
 use PHPUnit\Framework\TestCase;
 use Symfony\Component\DependencyInjection\Reference;
@@ -710,14 +711,20 @@ protected function installConfig($modules) {
    *   If $module is not enabled or the table schema cannot be found.
    */
   protected function installSchema($module, $tables) {
-    // drupal_get_module_schema() is technically able to install a schema
-    // of a non-enabled module, but its ability to load the module's .install
-    // file depends on many other factors. To prevent differences in test
-    // behavior and non-reproducible test failures, we only allow the schema of
-    // explicitly loaded/enabled modules to be installed.
-    if (!$this->container->get('module_handler')->moduleExists($module)) {
+    /** @var \Drupal\Core\Extension\ModuleHandlerInterface $module_handler */
+    $module_handler = $this->container->get('module_handler');
+    // Database connection schema is technically able to create database tables
+    // using any valid specification, for example of a non-enabled module. But
+    // ability to load the module's .install file depends on many other factors.
+    // To prevent differences in test behavior and non-reproducible test
+    // failures, we only allow the schema of explicitly loaded/enabled modules
+    // to be installed.
+    if (!$module_handler->moduleExists($module)) {
       throw new \LogicException("$module module is not enabled.");
     }
+    $specification = SchemaInspector::getTablesSpecification($module_handler, $module);
+    /** @var \Drupal\Core\Database\Schema $schema */
+    $schema = $this->container->get('database')->schema();
     $tables = (array) $tables;
     foreach ($tables as $table) {
       // The tables key_value and key_value_expire are lazy loaded and therefore
@@ -727,11 +734,10 @@ protected function installSchema($module, $tables) {
         @trigger_error('Installing the tables key_value and key_value_expire with the method KernelTestBase::installSchema() is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. The tables are now lazy loaded and therefore will be installed automatically when used. See https://www.drupal.org/node/3143286', E_USER_DEPRECATED);
         continue;
       }
-      $schema = drupal_get_module_schema($module, $table);
-      if (empty($schema)) {
+      if (empty($specification[$table])) {
         throw new \LogicException("$module module does not define a schema for table '$table'.");
       }
-      $this->container->get('database')->schema()->createTable($table, $schema);
+      $schema->createTable($table, $specification[$table]);
     }
   }
 
diff --git a/core/tests/Drupal/TestTools/Extension/SchemaInspector.php b/core/tests/Drupal/TestTools/Extension/SchemaInspector.php
new file mode 100644
index 0000000000..29584ecc1e
--- /dev/null
+++ b/core/tests/Drupal/TestTools/Extension/SchemaInspector.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\TestTools\Extension;
+
+use Drupal\Core\Extension\ModuleHandlerInterface;
+
+/**
+ * Provides methods to access modules' schema.
+ */
+class SchemaInspector {
+
+  /**
+   * Returns the module's schema specification.
+   *
+   * This function can be used to retrieve a schema specification provided by
+   * hook_schema(), so it allows you to derive your tables from existing
+   * specifications.
+   *
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $handler
+   *   The module handler to use for calling schema hook.
+   * @param string $module
+   *   The module to which the table belongs.
+   *
+   * @return array
+   *   An array of schema definition provided by hook_schema().
+   *
+   * @see \hook_schema()
+   */
+  public static function getTablesSpecification(ModuleHandlerInterface $handler, $module) {
+    if ($handler->loadInclude($module, 'install')) {
+      return $handler->invoke($module, 'schema') ?? [];
+    }
+    return [];
+  }
+
+}
