Полгода на первой работе, и мне дали задачу ускорить вход в админку. Таблица users, около 900 тысяч строк, индекс по email есть, а запрос всё равно идёт 300-400 мс. Смотрю EXPLAIN и вижу Seq Scan. Поиск сделан так:
User.find_by("lower(email) = ?", params[:email].to_s.downcase)
Индекс создавали обычной миграцией add_index :users, :email, unique: true. Почему база его не берёт? Rails 7.1, PostgreSQL 15.
Потому что индекс построен по email, а ищете вы по lower(email). Для планировщика это разные выражения, и обычный индекс тут не подходит. Путей два: индекс по выражению или хранить email сразу в нижнем регистре и искать как есть. Индекс по выражению:
class AddLowerEmailIndexToUsers < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def change
add_index :users, "lower(email)", unique: true,
name: "index_users_on_lower_email", algorithm: :concurrently
end
end
Перед этим проверьте, что в базе нет адресов, которые отличаются только регистром, иначе уникальный индекс не соберётся. И присылайте EXPLAIN ANALYZE, а не просто EXPLAIN: там видно реальное время, а не оценку.
Второй путь в 7.1 делается одной строкой через normalizes:
class User < ApplicationRecord
normalizes :email, with: ->(email) { email.strip.downcase }
end
User.find_by(email: " Anna@Shop.Test ") # ищет "anna@shop.test"
Нормализация срабатывает и при записи, и в find_by/where с хэшем. Старые записи сама не перепишет, их надо прогнать отдельно, например User.find_each { |u| u.normalize_attribute(:email); u.save! }. Индекс тогда остаётся обычный, по email, и на таблице такого размера вход у меня по замерам стабильно укладывался меньше чем в миллисекунду.
Я с MySQL пришёл и сначала вообще не понял, зачем тут lower: там при коллации *_ci сравнение и так идёт без учёта регистра, и обычный индекс работает. В PostgreSQL так не выйдет, либо lower с индексом по выражению, либо тип citext.