diff --git a/core/lib/Drupal/Core/Database/SupportsTemporaryTablesInterface.php b/core/lib/Drupal/Core/Database/SupportsTemporaryTablesInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..2e6bd0fb06c5e2131bd0fd7bc6cb0dbd6a8584b2
--- /dev/null
+++ b/core/lib/Drupal/Core/Database/SupportsTemporaryTablesInterface.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\Core\Database;
+
+/**
+ * Adds support for temporary tables.
+ *
+ * @ingroup database
+ */
+interface SupportsTemporaryTablesInterface {
+
+  /**
+   * Runs a SELECT query and stores its results in a temporary table.
+   *
+   * Use this as a substitute for ->query() when the results need to stored
+   * in a temporary table. Temporary tables exist for the duration of the page
+   * request. User-supplied arguments to the query should be passed in as
+   * separate parameters so that they can be properly escaped to avoid SQL
+   * injection attacks.
+   *
+   * Note that if you need to know how many results were returned, you should do
+   * a SELECT COUNT(*) on the temporary table afterwards.
+   *
+   * @param string $query
+   *   A string containing a normal SELECT SQL query.
+   * @param array $args
+   *   (optional) An array of values to substitute into the query at placeholder
+   *   markers.
+   * @param array $options
+   *   (optional) An associative array of options to control how the query is
+   *   run. See the documentation for DatabaseConnection::defaultOptions() for
+   *   details.
+   *
+   * @return string
+   *   The name of the temporary table.
+   */
+  public function queryTemporary($query, array $args = [], array $options = []);
+
+}
diff --git a/core/misc/cspell/dictionary.txt b/core/misc/cspell/dictionary.txt
index 9921ab84d833b9f8065f748aaff7a54eb7465825..a5c4d82f34289c9e994c121f62fb5b0646bc5285 100644
--- a/core/misc/cspell/dictionary.txt
+++ b/core/misc/cspell/dictionary.txt
@@ -1024,6 +1024,7 @@ renderered
 renormalize
 reparenting
 reparsed
+relpersistence
 replyto
 resave
 resaved
@@ -1042,6 +1043,7 @@ revisionid
 revisioning
 revlog
 revpub
+relname
 ribisi
 ritchie
 rolename
diff --git a/core/modules/mysql/src/Driver/Database/mysql/Connection.php b/core/modules/mysql/src/Driver/Database/mysql/Connection.php
index 910387eb5f6c4d27182f8447c8dccbe4a617f8c0..3e8437e8f9fa6a29dac2f28e29445d60fdff2178 100644
--- a/core/modules/mysql/src/Driver/Database/mysql/Connection.php
+++ b/core/modules/mysql/src/Driver/Database/mysql/Connection.php
@@ -9,6 +9,7 @@
 use Drupal\Core\Database\DatabaseNotFoundException;
 use Drupal\Core\Database\DatabaseException;
 use Drupal\Core\Database\Connection as DatabaseConnection;
+use Drupal\Core\Database\SupportsTemporaryTablesInterface;
 use Drupal\Core\Database\TransactionNoActiveException;
 
 /**
@@ -19,7 +20,7 @@
 /**
  * MySQL implementation of \Drupal\Core\Database\Connection.
  */
-class Connection extends DatabaseConnection {
+class Connection extends DatabaseConnection implements SupportsTemporaryTablesInterface {
 
   /**
    * Error code for "Unknown database" error.
@@ -222,6 +223,15 @@ public function queryRange($query, $from, $count, array $args = [], array $optio
     return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function queryTemporary($query, array $args = [], array $options = []) {
+    $tablename = 'db_temporary_' . uniqid();
+    $this->query('CREATE TEMPORARY TABLE {' . $tablename . '} Engine=MEMORY ' . $query, $args, $options);
+    return $tablename;
+  }
+
   public function driver() {
     return 'mysql';
   }
diff --git a/core/modules/mysql/tests/src/Kernel/mysql/TemporaryQueryTest.php b/core/modules/mysql/tests/src/Kernel/mysql/TemporaryQueryTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..484ccc75227e98e72d0fda04c68afd0e42eef008
--- /dev/null
+++ b/core/modules/mysql/tests/src/Kernel/mysql/TemporaryQueryTest.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\Tests\mysql\Kernel\mysql;
+
+use Drupal\KernelTests\Core\Database\TemporaryQueryTestBase;
+
+/**
+ * Tests the temporary query functionality.
+ *
+ * @group Database
+ */
+class TemporaryQueryTest extends TemporaryQueryTestBase {
+
+  /**
+   * Confirms that temporary tables work.
+   */
+  public function testTemporaryQuery() {
+    parent::testTemporaryQuery();
+
+    $connection = $this->getConnection();
+
+    $table_name_test = $connection->queryTemporary('SELECT [name] FROM {test}', []);
+
+    // Assert that the table is indeed a temporary one.
+    $temporary_table_info = $connection->query("SHOW CREATE TABLE {" . $table_name_test . "}")->fetchAssoc();
+    $this->stringContains($temporary_table_info["Create Table"], "CREATE TEMPORARY TABLE");
+
+    // Assert that both have the same field names.
+    $normal_table_fields = $connection->query("SELECT * FROM {test}")->fetch();
+    $temp_table_name = $connection->queryTemporary('SELECT * FROM {test}');
+    $temp_table_fields = $connection->query("SELECT * FROM {" . $temp_table_name . "}")->fetch();
+
+    $normal_table_fields = array_keys(get_object_vars($normal_table_fields));
+    $temp_table_fields = array_keys(get_object_vars($temp_table_fields));
+
+    $this->assertEmpty(array_diff($normal_table_fields, $temp_table_fields));
+  }
+
+}
diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php b/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php
index 67ed8016316eaa9fd891b3606b258a2e516d3fc6..58d500e361d3257aaf74fc8670cf0c4fa7972ec9 100644
--- a/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php
+++ b/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php
@@ -8,6 +8,7 @@
 use Drupal\Core\Database\DatabaseNotFoundException;
 use Drupal\Core\Database\StatementInterface;
 use Drupal\Core\Database\StatementWrapper;
+use Drupal\Core\Database\SupportsTemporaryTablesInterface;
 
 // cSpell:ignore ilike nextval
 
@@ -19,7 +20,7 @@
 /**
  * PostgreSQL implementation of \Drupal\Core\Database\Connection.
  */
-class Connection extends DatabaseConnection {
+class Connection extends DatabaseConnection implements SupportsTemporaryTablesInterface {
 
   /**
    * The name by which to obtain a lock for retrieve the next insert id.
@@ -209,6 +210,15 @@ public function queryRange($query, $from, $count, array $args = [], array $optio
     return $this->query($query . ' LIMIT ' . (int) $count . ' OFFSET ' . (int) $from, $args, $options);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function queryTemporary($query, array $args = [], array $options = []) {
+    $tablename = 'db_temporary_' . uniqid();
+    $this->query('CREATE TEMPORARY TABLE {' . $tablename . '} AS ' . $query, $args, $options);
+    return $tablename;
+  }
+
   public function driver() {
     return 'pgsql';
   }
diff --git a/core/modules/pgsql/tests/src/Kernel/pgsql/TemporaryQueryTest.php b/core/modules/pgsql/tests/src/Kernel/pgsql/TemporaryQueryTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..03284e75f5a42c4674494c2275b8477c7d2899cd
--- /dev/null
+++ b/core/modules/pgsql/tests/src/Kernel/pgsql/TemporaryQueryTest.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\Tests\pgsql\Kernel\pgsql;
+
+use Drupal\KernelTests\Core\Database\TemporaryQueryTestBase;
+
+/**
+ * Tests the temporary query functionality.
+ *
+ * @group Database
+ */
+class TemporaryQueryTest extends TemporaryQueryTestBase {
+
+  /**
+   * Confirms that temporary tables work.
+   */
+  public function testTemporaryQuery() {
+    parent::testTemporaryQuery();
+
+    $connection = $this->getConnection();
+
+    $table_name_test = $connection->queryTemporary('SELECT [name] FROM {test}', []);
+
+    // Assert that the table is indeed a temporary one.
+    $temporary_table_info = $connection->query("SELECT * FROM pg_class WHERE relname LIKE '%$table_name_test%'")->fetch();
+    $this->assertEquals("t", $temporary_table_info->relpersistence);
+
+    // Assert that both have the same field names.
+    $normal_table_fields = $connection->query("SELECT * FROM {test}")->fetch();
+    $temp_table_name = $connection->queryTemporary('SELECT * FROM {test}');
+    $temp_table_fields = $connection->query("SELECT * FROM {" . $temp_table_name . "}")->fetch();
+
+    $normal_table_fields = array_keys(get_object_vars($normal_table_fields));
+    $temp_table_fields = array_keys(get_object_vars($temp_table_fields));
+
+    $this->assertEmpty(array_diff($normal_table_fields, $temp_table_fields));
+  }
+
+}
diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php b/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php
index 583eeea0a82ffb2193f3b30e8bcc8ac59934e4f7..1f2cf1fd191b8fb69897c95bc194523cb3e2a25b 100644
--- a/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php
+++ b/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php
@@ -5,11 +5,12 @@
 use Drupal\Core\Database\DatabaseNotFoundException;
 use Drupal\Core\Database\Connection as DatabaseConnection;
 use Drupal\Core\Database\StatementInterface;
+use Drupal\Core\Database\SupportsTemporaryTablesInterface;
 
 /**
  * SQLite implementation of \Drupal\Core\Database\Connection.
  */
-class Connection extends DatabaseConnection {
+class Connection extends DatabaseConnection implements SupportsTemporaryTablesInterface {
 
   /**
    * Error code for "Unable to open database file" error.
@@ -352,6 +353,22 @@ public function queryRange($query, $from, $count, array $args = [], array $optio
     return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function queryTemporary($query, array $args = [], array $options = []) {
+    $tablename = 'db_temporary_' . uniqid();
+
+    $this->query('CREATE TEMPORARY TABLE ' . $tablename . ' AS ' . $query, $args, $options);
+
+    // Temporary tables always live in the temp database, which means that
+    // they cannot be fully qualified table names since they do not live
+    // in the main SQLite database. We provide the fully-qualified name
+    // ourselves to prevent Drupal from applying prefixes.
+    // @see https://www.sqlite.org/lang_createtable.html
+    return 'temp.' . $tablename;
+  }
+
   public function driver() {
     return 'sqlite';
   }
diff --git a/core/modules/sqlite/tests/src/Kernel/sqlite/TemporaryQueryTest.php b/core/modules/sqlite/tests/src/Kernel/sqlite/TemporaryQueryTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..053e7f16adc2f32c5cbb774a4f0d25256d7cddc6
--- /dev/null
+++ b/core/modules/sqlite/tests/src/Kernel/sqlite/TemporaryQueryTest.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\Tests\sqlite\Kernel\sqlite;
+
+use Drupal\KernelTests\Core\Database\TemporaryQueryTestBase;
+
+/**
+ * Tests the temporary query functionality.
+ *
+ * @group Database
+ */
+class TemporaryQueryTest extends TemporaryQueryTestBase {
+
+  /**
+   * Confirms that temporary tables work.
+   */
+  public function testTemporaryQuery() {
+    parent::testTemporaryQuery();
+
+    $connection = $this->getConnection();
+
+    $table_name_test = $connection->queryTemporary('SELECT [name] FROM {test}', []);
+
+    // Assert that the table is indeed a temporary one.
+    $this->stringContains("temp.", $table_name_test);
+
+    // Assert that both have the same field names.
+    $normal_table_fields = $connection->query("SELECT * FROM {test}")->fetch();
+    $temp_table_name = $connection->queryTemporary('SELECT * FROM {test}');
+    $temp_table_fields = $connection->query("SELECT * FROM $temp_table_name")->fetch();
+
+    $normal_table_fields = array_keys(get_object_vars($normal_table_fields));
+    $temp_table_fields = array_keys(get_object_vars($temp_table_fields));
+
+    $this->assertEmpty(array_diff($normal_table_fields, $temp_table_fields));
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Database/TemporaryQueryTestBase.php b/core/tests/Drupal/KernelTests/Core/Database/TemporaryQueryTestBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..0ec77b70ac0c55b765485372351f75d48f6bc133
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Database/TemporaryQueryTestBase.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Database;
+
+use Drupal\Core\Database\Database;
+
+/**
+ * Tests the temporary query functionality.
+ *
+ * @group Database
+ */
+abstract class TemporaryQueryTestBase extends DriverSpecificDatabaseTestBase {
+
+  /**
+   * Returns the connection.
+   */
+  public function getConnection() {
+    return Database::getConnection();
+  }
+
+  /**
+   * Returns the number of rows of a table.
+   */
+  public function countTableRows($table_name) {
+    return Database::getConnection()->select($table_name)->countQuery()->execute()->fetchField();
+  }
+
+  /**
+   * Confirms that temporary tables work.
+   */
+  public function testTemporaryQuery() {
+    $connection = $this->getConnection();
+
+    // Now try to run two temporary queries in the same request.
+    $table_name_test = $connection->queryTemporary('SELECT [name] FROM {test}', []);
+    $table_name_task = $connection->queryTemporary('SELECT [pid] FROM {test_task}', []);
+
+    $this->assertEquals($this->countTableRows('test'), $this->countTableRows($table_name_test), 'A temporary table was created successfully in this request.');
+    $this->assertEquals($this->countTableRows('test_task'), $this->countTableRows($table_name_task), 'A second temporary table was created successfully in this request.');
+
+    // Check that leading whitespace and comments do not cause problems
+    // in the modified query.
+    $sql = "
+      -- Let's select some rows into a temporary table
+      SELECT [name] FROM {test}
+    ";
+    $table_name_test = $connection->queryTemporary($sql, []);
+    $this->assertEquals($this->countTableRows('test'), $this->countTableRows($table_name_test), 'Leading white space and comments do not interfere with temporary table creation.');
+  }
+
+}