Перестаньте везде использовать first/last

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Очень часто разработчики на Rails для извлечения заведомо одной записи из базы используют один из методов first/last из ActiveRecord. Это рабочее решение, но есть одно НО: чтобы извлечь всего одну запись база данных должна найти все, подходящие по условию, отсортировать их и вернуть только одну. Если вы подумали "Сортировка одной записи? Да это же легко!", то не будте так оптимистичны, я постараюсь показать, что это важно.


Там, где порядок записей имеет значение, обойтись без first/last нет возможности, однако бывают ситуации, когда порядок не важен. Приведу практический пример: предположим у нас в системе есть пользователи, у пользователей есть бонусы, и только один бонус может иметь статус active и мы хотим иметь метод, который вернет этот единственный активный бонус


class Bonus
  belongs_to :user

  scope :active, -> { where(status: :active) }
end

class User
  has_many :bonuses

  def active_bonus
    bonuses.active.first
  end
end

Такая запись метода active_bonus не нравится мне по двум причинам:


  • если есть bonuses.active.first, то возможен и bonuses.active.last? Будет то же самое поведение или другое? При такой записи точно ответить нельзя
  • чтобы достать единственную запись Bonus postgres все равно задействует механизм сортировки

explain analyze SELECT "bonuses".* FROM "bonuses" WHERE "bonuses"."user_id" = 123 AND "bonuses"."status" = 'active' ORDER BY "bonuses"."id" ASC LIMIT 1;
                                                                        QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.52..0.52 rows=1 width=905)
   ->  Sort  (cost=0.52..0.52 rows=1 width=905)
         Sort Key: id
         Sort Method: quicksort  Memory: 25kB
         ->  Index Scan using user_id_idx on bonuses  (cost=0.27..0.51 rows=1 width=905)
               Index Cond: (user_id = 812688)
 Planning Time: 0.160 ms
 Execution Time: 0.042 ms

В этом примере postgres в первую очередь достает все записи используя user_id_idx, затем пытается отсортировать их по id. Это дешево, но, все же, пустая трата времени.


ApplicationRecord#Take


Того же поведения можно добиться используя Bonus#take из ActiveRecord: bonuses.active.take. Этот метод достает одну запись из базы данных не обращая внимания на сортировку. Такая запись дает нам небольшой буст в плане выполнения запроса, а так же убирает недопонимание о количестве активных бонусов


explain analyze SELECT "bonuses".* FROM "bonuses" WHERE "bonuses"."user_id" = 123 AND "bonuses"."status" = 'active' LIMIT 1;
                                                                     QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.27..0.51 rows=1 width=905)
   ->  Index Scan using user_id_idx on bonuses  (cost=0.27..0.51 rows=1 width=905)
         Index Cond: (user_id = 812688)
 Planning Time: 0.137 ms
 Execution Time: 0.031 ms

В этом случае postgres использует все тот же поиск по индексу user_id_idx, но теперь ему вообще нет необходимости искать все записи со статусом active, после первой он сразу же может вернуть результат


Выглядит красиво, но есть ли от этого практическая польза?


Именно эта оптимизация ускорила большую часть нашего приложения: мы всегда имеем дело с курсами валют, все курсы хранятся с разбивкой по времени, чтобы получить курсы мы должны обратиться к базе с таким запросом CurrencyRate.where("currency = ? and period::tsrange @> ?::timestamp", currency, time).first


explain analyze SELECT "currency_rates".* FROM "currency_rates" WHERE (currency = 'RUB' and period::tsrange @> '2021-07-14 08:37:53.918222'::timestamp) ORDER BY "currency_rates"."id" ASC LIMIT 1;
                                                                         QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.43..130.18 rows=1 width=1204)
   ->  Index Scan using currency_rates_pkey on currency_rates  (cost=0.43..240935.03 rows=1857 width=1204)
         Filter: ((period @> '2021-07-14 08:37:53.918222'::timestamp without time zone) AND (currency = 'RUB'::text))
         Rows Removed by Filter: 6340297
 Planning Time: 1.111 ms
 Execution Time: 389.639 ms

Хммм, postgres использует currency_rates_pkey хотя никакой фильтрации по id у нас нету. Это из-за order by id asc. Если переписать запрос так CurrencyRate.where("currency = ? and period::tsrange @> ?::timestamp", currency, time).take можно увидеть более дружелюбный план


explain analyze SELECT "currency_rates".* FROM "currency_rates" WHERE (currency = 'RUB' and period::tsrange @> '2021-07-14 08:37:53.918222'::timestamp)  LIMIT 1;
                                                                            QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.41..0.55 rows=1 width=1204)
   ->  Index Scan using currency_rates_period_currency_excl on currency_rates  (cost=0.41..244.46 rows=1857 width=1204)
         Index Cond: ((period @> '2021-07-14 08:37:53.918222'::timestamp without time zone) AND (currency = 'RUB'::text))
 Planning Time: 0.095 ms
 Execution Time: 0.938 ms

Теперь хорошо, postgres использует нужный индекс, время выполнения запроса уменьшилось во много раз

Источник: https://habr.com/ru/post/567902/


Интересные статьи

Интересные статьи

Выбор стандартного шрифта, используемого в САПР, – важный вопрос, решение которого может зависеть от множества факторов: стандартов вашего предприятия, ваших заказчиков, ...
Когда дело доходит до автоматизации тестирования, первый и самый сложный вопрос, который встает перед вами это какой язык выбрать, чтобы он имел хорошую поддержку авт...
Эта статья для тех, кто собирается открыть интернет-магазин, но еще рассматривает варианты и думает по какому пути пойти, заказать разработку магазина в студии, у фрилансера или выбрать облачный серви...
Довольно часто владельцы сайтов просят поставить на свои проекты индикаторы курсов валют и их динамику. Можно воспользоваться готовыми информерами, но они не всегда позволяют должным образом настроить...
Сегодня мы поговорим о перспективах становления Битрикс-разработчика и об этапах этого пути. Статья не претендует на абсолютную истину, но даёт жизненные ориентиры.