From 4ec9764cdb2b5d809dc256b14523180af7aceaa3 Mon Sep 17 00:00:00 2001 From: Paul Chobert Date: Fri, 16 Oct 2026 14:32:49 +0200 Subject: [PATCH 1/3] Add a `users_lower_name` index on `lower(name)` to `users` Case-insensitive user ID lookups (`get_users_by_id_case_insensitive`, used when checking whether a username is available) filter on `lower(name)`, which neither `users_name_key` nor `users_creation_ts` can serve, so every lookup scanned the whole table. Add the index as a background update. It reuses the name of the index matrix.org created by hand, so that it is rebuilt there rather than duplicated. --- changelog.d/pr-id.misc | 1 + .../storage/databases/main/registration.py | 7 +++ .../delta/95/02_users_lower_name_index.sql | 18 ++++++ tests/storage/test_registration.py | 55 +++++++++++++++++++ 4 files changed, 81 insertions(+) create mode 100644 changelog.d/pr-id.misc create mode 100644 synapse/storage/schema/main/delta/95/02_users_lower_name_index.sql diff --git a/changelog.d/pr-id.misc b/changelog.d/pr-id.misc new file mode 100644 index 00000000000..8a0eb2b295c --- /dev/null +++ b/changelog.d/pr-id.misc @@ -0,0 +1 @@ +Add an index on `lower(name)` to the `users` table, to speed up checking whether a username is available. diff --git a/synapse/storage/databases/main/registration.py b/synapse/storage/databases/main/registration.py index 4bc1a0be1b0..c23dcbb0c16 100644 --- a/synapse/storage/databases/main/registration.py +++ b/synapse/storage/databases/main/registration.py @@ -2611,6 +2611,13 @@ def __init__( columns=["creation_ts"], ) + self.db_pool.updates.register_background_index_update( + "users_lower_name", + index_name="users_lower_name", + table="users", + columns=["lower(name)"], + ) + self.db_pool.updates.register_background_update_handler( "users_set_deactivated_flag", self._background_update_set_deactivated_flag ) diff --git a/synapse/storage/schema/main/delta/95/02_users_lower_name_index.sql b/synapse/storage/schema/main/delta/95/02_users_lower_name_index.sql new file mode 100644 index 00000000000..7485c7c1ea9 --- /dev/null +++ b/synapse/storage/schema/main/delta/95/02_users_lower_name_index.sql @@ -0,0 +1,18 @@ +-- +-- This file is licensed under the Affero General Public License (AGPL) version 3. +-- +-- Copyright (C) 2026 Element Creations Ltd +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as +-- published by the Free Software Foundation, either version 3 of the +-- License, or (at your option) any later version. +-- +-- See the GNU Affero General Public License for more details: +-- . + +-- Add an index on `lower(name)` to `users`, so that case-insensitive user ID +-- lookups (e.g. when checking whether a username is available) don't need to +-- scan the whole table. +INSERT INTO background_updates (ordering, update_name, progress_json) VALUES + (9502, 'users_lower_name', '{}'); diff --git a/tests/storage/test_registration.py b/tests/storage/test_registration.py index 224b87749dd..8968162a9f1 100644 --- a/tests/storage/test_registration.py +++ b/tests/storage/test_registration.py @@ -23,6 +23,8 @@ from synapse.api.constants import UserTypes from synapse.api.errors import ThreepidValidationError from synapse.server import HomeServer +from synapse.storage.database import LoggingTransaction +from synapse.storage.engines import PostgresEngine from synapse.types import JsonDict, UserID, UserInfo from synapse.util.clock import Clock @@ -145,6 +147,59 @@ def test_is_support_user(self) -> None: res = self.get_success(self.store.is_support_user(SUPPORT_USER)) self.assertTrue(res) + def test_get_users_by_id_case_insensitive(self) -> None: + """Looking up a user ID returns every user whose ID only differs by case.""" + self.get_success(self.store.register_user("@Alice:test", "hash1")) + self.get_success(self.store.register_user("@alice:test", "hash2")) + self.get_success(self.store.register_user("@bob:test", "hash3")) + + res = self.get_success( + self.store.get_users_by_id_case_insensitive("@ALICE:test") + ) + self.assertEqual(res, {"@Alice:test": "hash1", "@alice:test": "hash2"}) + + res = self.get_success( + self.store.get_users_by_id_case_insensitive("@carol:test") + ) + self.assertEqual(res, {}) + + def test_users_lower_name_index(self) -> None: + """The `users_lower_name` background update creates an index that serves + case-insensitive user ID lookups. + """ + self.get_success( + self.store.db_pool.runInteraction( + "drop_users_lower_name", + lambda txn: txn.execute("DROP INDEX users_lower_name"), + ) + ) + self.get_success( + self.store.db_pool.simple_insert( + table="background_updates", + values={"update_name": "users_lower_name", "progress_json": "{}"}, + ) + ) + self.store.db_pool.updates._all_done = False + self.wait_for_background_updates() + + def explain_txn(txn: LoggingTransaction) -> str: + if isinstance(self.store.database_engine, PostgresEngine): + txn.execute("SET LOCAL enable_seqscan = off") + explain = "EXPLAIN" + else: + explain = "EXPLAIN QUERY PLAN" + txn.execute( + f"{explain} SELECT name, password_hash FROM users" + " WHERE lower(name) = lower(?)", + ("@Alice:test",), + ) + return "\n".join(str(row) for row in txn.fetchall()) + + plan = self.get_success( + self.store.db_pool.runInteraction("explain_users_lower_name", explain_txn) + ) + self.assertIn("users_lower_name", plan) + @override_config({"request_token_inhibit_3pid_errors": False}) def test_3pid_inhibit_invalid_validation_session_error(self) -> None: """Tests that enabling the configuration option to inhibit 3PID errors on From 3c16af1aacd4f885af68bd4333a2d8aec18378b0 Mon Sep 17 00:00:00 2001 From: Paul Chobert Date: Fri, 16 Oct 2026 14:46:16 +0200 Subject: [PATCH 2/3] Rename changelog entry to the PR number --- changelog.d/{pr-id.misc => 20369.misc} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{pr-id.misc => 20369.misc} (100%) diff --git a/changelog.d/pr-id.misc b/changelog.d/20369.misc similarity index 100% rename from changelog.d/pr-id.misc rename to changelog.d/20369.misc From a0bbba9eaccc37d35f3d33a2ce15b0605772131a Mon Sep 17 00:00:00 2001 From: Paul Chobert Date: Fri, 16 Oct 2026 15:05:19 +0200 Subject: [PATCH 3/3] Remove the tests for the users_lower_name index --- tests/storage/test_registration.py | 55 ------------------------------ 1 file changed, 55 deletions(-) diff --git a/tests/storage/test_registration.py b/tests/storage/test_registration.py index 8968162a9f1..224b87749dd 100644 --- a/tests/storage/test_registration.py +++ b/tests/storage/test_registration.py @@ -23,8 +23,6 @@ from synapse.api.constants import UserTypes from synapse.api.errors import ThreepidValidationError from synapse.server import HomeServer -from synapse.storage.database import LoggingTransaction -from synapse.storage.engines import PostgresEngine from synapse.types import JsonDict, UserID, UserInfo from synapse.util.clock import Clock @@ -147,59 +145,6 @@ def test_is_support_user(self) -> None: res = self.get_success(self.store.is_support_user(SUPPORT_USER)) self.assertTrue(res) - def test_get_users_by_id_case_insensitive(self) -> None: - """Looking up a user ID returns every user whose ID only differs by case.""" - self.get_success(self.store.register_user("@Alice:test", "hash1")) - self.get_success(self.store.register_user("@alice:test", "hash2")) - self.get_success(self.store.register_user("@bob:test", "hash3")) - - res = self.get_success( - self.store.get_users_by_id_case_insensitive("@ALICE:test") - ) - self.assertEqual(res, {"@Alice:test": "hash1", "@alice:test": "hash2"}) - - res = self.get_success( - self.store.get_users_by_id_case_insensitive("@carol:test") - ) - self.assertEqual(res, {}) - - def test_users_lower_name_index(self) -> None: - """The `users_lower_name` background update creates an index that serves - case-insensitive user ID lookups. - """ - self.get_success( - self.store.db_pool.runInteraction( - "drop_users_lower_name", - lambda txn: txn.execute("DROP INDEX users_lower_name"), - ) - ) - self.get_success( - self.store.db_pool.simple_insert( - table="background_updates", - values={"update_name": "users_lower_name", "progress_json": "{}"}, - ) - ) - self.store.db_pool.updates._all_done = False - self.wait_for_background_updates() - - def explain_txn(txn: LoggingTransaction) -> str: - if isinstance(self.store.database_engine, PostgresEngine): - txn.execute("SET LOCAL enable_seqscan = off") - explain = "EXPLAIN" - else: - explain = "EXPLAIN QUERY PLAN" - txn.execute( - f"{explain} SELECT name, password_hash FROM users" - " WHERE lower(name) = lower(?)", - ("@Alice:test",), - ) - return "\n".join(str(row) for row in txn.fetchall()) - - plan = self.get_success( - self.store.db_pool.runInteraction("explain_users_lower_name", explain_txn) - ) - self.assertIn("users_lower_name", plan) - @override_config({"request_token_inhibit_3pid_errors": False}) def test_3pid_inhibit_invalid_validation_session_error(self) -> None: """Tests that enabling the configuration option to inhibit 3PID errors on