diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
index 11782fc1ca..97358dcf4e 100644
--- a/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
@@ -189,7 +189,7 @@ public static function open(array &$connection_options = []) {
     ];
 
     $connection_options['init_commands'] += [
-      'sql_mode' => "SET sql_mode = 'ANSI,TRADITIONAL'",
+      'sql_mode' => "SET sql_mode = 'ANSI,TRADITIONAL,NO_AUTO_VALUE_ON_ZERO'",
     ];
 
     // Execute initial commands.
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
index ba319da7a1..0e593e42a9 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -1057,7 +1057,12 @@ protected function mapToStorageRecord(ContentEntityInterface $entity, $table_nam
         // SQL database drivers.
         // @see https://www.drupal.org/node/2279395
         $value = SqlContentEntityStorageSchema::castValue($definition->getSchema()['columns'][$column_name], $value);
-        if (!(empty($value) && $this->isColumnSerial($table_name, $schema_name))) {
+        $empty_serial = empty($value) && $this->isColumnSerial($table_name, $schema_name);
+        // The user entity is a very special case where the ID field is a serial
+        // but we need to insert a row with an ID of 0 to represent the
+        // anonymous user.
+        $user_zero = $this->entityTypeId === 'user' && $value === 0;
+        if (!$empty_serial || $user_zero) {
           $record->$schema_name = $value;
         }
       }
diff --git a/core/modules/user/src/UserStorage.php b/core/modules/user/src/UserStorage.php
index a2adfe9118..eb47af1539 100644
--- a/core/modules/user/src/UserStorage.php
+++ b/core/modules/user/src/UserStorage.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\user;
 
-use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
 use Drupal\Core\Session\AccountInterface;
 
@@ -14,27 +13,6 @@
  */
 class UserStorage extends SqlContentEntityStorage implements UserStorageInterface {
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
-    // The anonymous user account is saved with the fixed user ID of 0.
-    // Therefore we need to check for NULL explicitly.
-    if ($entity->id() === NULL) {
-      $entity->uid->value = $this->database->nextId($this->database->query('SELECT MAX([uid]) FROM {' . $this->getBaseTable() . '}')->fetchField());
-      $entity->enforceIsNew();
-    }
-    return parent::doSaveFieldItems($entity, $names);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function isColumnSerial($table_name, $schema_name) {
-    // User storage does not use a serial column for the user id.
-    return $table_name == $this->revisionTable && $schema_name == $this->revisionKey;
-  }
-
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/user/src/UserStorageSchema.php b/core/modules/user/src/UserStorageSchema.php
index 6ac14d26d8..d172fb3a6a 100644
--- a/core/modules/user/src/UserStorageSchema.php
+++ b/core/modules/user/src/UserStorageSchema.php
@@ -26,16 +26,6 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res
     return $schema;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function processIdentifierSchema(&$schema, $key) {
-    // The "users" table does not use serial identifiers.
-    if ($key != $this->entityType->getKey('id')) {
-      parent::processIdentifierSchema($schema, $key);
-    }
-  }
-
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/user/tests/src/Functional/UidUpdateToSerialTest.php b/core/modules/user/tests/src/Functional/UidUpdateToSerialTest.php
new file mode 100644
index 0000000000..7a5e55fedf
--- /dev/null
+++ b/core/modules/user/tests/src/Functional/UidUpdateToSerialTest.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Drupal\Tests\user\Functional;
+
+use Drupal\FunctionalTests\Update\UpdatePathTestBase;
+
+// cSpell:ignore refobjid regclass attname attrelid attnum refobjsubid objid
+// cSpell:ignore classid
+
+/**
+ * Tests user_update_9001().
+ *
+ * @group user
+ */
+class UidUpdateToSerialTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setDatabaseDumpFiles() {
+    $this->databaseDumpFiles[] = __DIR__ . '/../../../../system/tests/fixtures/update/drupal-9.0.0.bare.standard.php.gz';
+  }
+
+  /**
+   * Tests user_update_9001().
+   */
+  public function testDatabaseLoaded() {
+    $key_value_store = \Drupal::keyValue('entity.storage_schema.sql');
+    $id_schema = $key_value_store->get('user.field_schema_data.uid', []);
+    $this->assertSame('int', $id_schema['users']['fields']['uid']['type']);
+
+    $this->runUpdates();
+
+    $key_value_store = \Drupal::keyValue('entity.storage_schema.sql');
+    $id_schema = $key_value_store->get('user.field_schema_data.uid', []);
+    $this->assertSame('serial', $id_schema['users']['fields']['uid']['type']);
+
+    $connection = \Drupal::database();
+    if ($connection->driver() == 'pgsql') {
+      $seq_name = $connection->makeSequenceName('users', 'uid');
+      $seq_owner = $connection->query("SELECT d.refobjid::regclass as table_name, a.attname as field_name
+        FROM pg_depend d
+        JOIN pg_attribute a ON a.attrelid = d.refobjid AND a.attnum = d.refobjsubid
+        WHERE d.objid = :seq_name::regclass
+        AND d.refobjsubid > 0
+        AND d.classid = 'pg_class'::regclass", [':seq_name' => 'public.' . $seq_name])->fetchObject();
+      $this->assertEquals($connection->tablePrefix('users') . 'users', $seq_owner->table_name);
+      $this->assertEquals('uid', $seq_owner->field_name);
+
+      $seq_last_value = $connection->query("SELECT last_value FROM $seq_name")->fetchField();
+      $maximum_uid = $connection->query('SELECT MAX([uid]) FROM {users}')->fetchField();
+      $this->assertEquals($maximum_uid + 1, $seq_last_value);
+    }
+  }
+
+}
diff --git a/core/modules/user/user.install b/core/modules/user/user.install
index 8375992ce7..5e2253c602 100644
--- a/core/modules/user/user.install
+++ b/core/modules/user/user.install
@@ -5,6 +5,8 @@
  * Install, update and uninstall functions for the user module.
  */
 
+use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
+
 /**
  * Implements hook_schema().
  */
@@ -97,3 +99,30 @@ function user_install() {
 function user_update_last_removed() {
   return 8100;
 }
+
+/**
+ * Change the users table to use an serial uid field.
+ */
+function user_update_9001(&$sandbox) {
+  if (!\Drupal::entityTypeManager()->getStorage('user') instanceof SqlContentEntityStorage) {
+    return t('The user entity storage is not using an SQL storage, update skipped.');
+  }
+
+  $connection = \Drupal::database();
+  $connection->schema()->dropPrimaryKey('users');
+  $connection->schema()->changeField('users', 'uid', 'uid', ['type' => 'serial', 'not null' => TRUE], ['primary key' => ['uid']]);
+
+  // Update the last installed schema to reflect the change of field type.
+  $installed_storage_schema = \Drupal::keyValue('entity.storage_schema.sql');
+  $field_schema_data = $installed_storage_schema->get('user.field_schema_data.uid');
+  $field_schema_data['users']['fields']['uid']['type'] = 'serial';
+  $installed_storage_schema->set('user.field_schema_data.uid', $field_schema_data);
+
+  // The new PostgreSQL sequence for the uid field needs to start with the last
+  // used user ID + 1 and the sequence must be owned by uid field.
+  if ($connection->driver() == 'pgsql') {
+    $maximum_uid = $connection->query('SELECT MAX([uid]) FROM {users}')->fetchField();
+    $seq = $connection->makeSequenceName('users', 'uid');
+    $connection->query("ALTER SEQUENCE " . $seq . " RESTART WITH " . ($maximum_uid + 1) . " OWNED BY {users}.uid");
+  }
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php b/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php
index 0b163a2405..fc0a7a26a5 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php
@@ -709,16 +709,26 @@ protected function assertFieldAdditionRemoval($field_spec) {
     for ($i = 0; $i < 3; $i++) {
       $this->connection
         ->insert($table_name)
-        ->useDefaults(['serial_column'])
         ->fields(['test_nullable_field' => 100])
         ->execute();
     }
 
     // Add another row with no value for the 'test_nullable_field' column.
-    $this->connection
-      ->insert($table_name)
-      ->useDefaults(['serial_column'])
-      ->execute();
+    if ($this->connection->driver() == 'mysql') {
+      // MySQL uses a default of 0 for serial fields which cause rows with an
+      // incorrect ID to be inserted, see
+      // https://bugs.mysql.com/bug.php?id=89225.
+      $this->connection
+        ->insert($table_name)
+        ->fields(['serial_column' => NULL])
+        ->execute();
+    }
+    else {
+      $this->connection
+        ->insert($table_name)
+        ->useDefaults(['serial_column'])
+        ->execute();
+    }
 
     $this->schema->addField($table_name, 'test_field', $field_spec);
 
@@ -780,10 +790,18 @@ protected function assertFieldCharacteristics($table_name, $field_name, $field_s
     // Check that the default value has been registered.
     if (isset($field_spec['default'])) {
       // Try inserting a row, and check the resulting value of the new column.
-      $id = $this->connection
-        ->insert($table_name)
-        ->useDefaults(['serial_column'])
-        ->execute();
+      if ($this->connection->driver() == 'mysql') {
+        $id = $this->connection
+          ->insert($table_name)
+          ->fields(['serial_column' => NULL])
+          ->execute();
+      }
+      else {
+        $id = $this->connection
+          ->insert($table_name)
+          ->useDefaults(['serial_column'])
+          ->execute();
+      }
       $field_value = $this->connection
         ->select($table_name)
         ->fields($table_name, [$field_name])
diff --git a/core/tests/Drupal/Tests/UpdatePathTestTrait.php b/core/tests/Drupal/Tests/UpdatePathTestTrait.php
index 3978f7e37c..214f7bf817 100644
--- a/core/tests/Drupal/Tests/UpdatePathTestTrait.php
+++ b/core/tests/Drupal/Tests/UpdatePathTestTrait.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests;
 
+use Drupal\Core\Database\Database;
 use Drupal\Core\Url;
 
 /**
@@ -105,6 +106,15 @@ protected function runUpdates($update_url = NULL) {
         $this->kernel->updateModules($module_handler_list, $module_handler_list);
       }
 
+      // Close any open database connections. This allows DB drivers that store
+      // static information to refresh it in the update runner.
+      // @todo consider doing this in
+      //   \Drupal\Core\DrupalKernel::initializeContainer() for container
+      //   rebuilds.
+      foreach (Database::getAllConnectionInfo() as $key => $info) {
+        Database::closeConnection(NULL, $key);
+      }
+
       // If we have successfully clicked 'Apply pending updates' then we need to
       // clear the caches in the update test runner as this has occurred as part
       // of the updates.
