Фильтрация данных на клиенте
Давече у нас на Групоне возникла следующая проблема — начала тормозить одна страница в админке. Причем до того сильно, что ее sql запрос стал регулярно появляться в списке медленных запросов в newrelic. Для большей понятности я поменяю название моделей на книги (Book) и авторы (Author), при этом, очевидно, «авторы имеют много книг», то есть связь один ко многим.
Итак, на одной странице выводился список последних книг, у которых есть автор, вот таким запросом.
Book.all(:order => ['id desc']).select{|it| it.author }.
paginate(:per_page => 20, :page => params[:page])
Все было ничего, пока книг в базе было немного. Но в какой-то момент их количество перевалило за 10 тысяч. Конечно же, для БД и такой запрос не очень сложный, все же для современных машин десять тысяч записей — ерунда. Но когда эти десять тысяч записей приходят по сети в приложение на ruby, потом фильтруются на наличие автора, а для этого для каждой записи делается дополнительный запрос в БД, то это занимает значительное количество времени.
Тут, конечно, была своя специфика. Если автор удалялся, то ссылка на автора в книге не обновлялась, поэтому у книг могли быть ссылки на несуществующих авторов.
Я бы не стал писать про подобную простую проблему, если бы столкнулся с таким в первый раз. Но и на одном из моих предыдущих проектов (connect.ua) мы сталкивались ровно с тем же, только там все упиралось в то, что у приложения не хватало памяти при сортировке, которую можно было сделать в БД, и оно падало. Более того, многочисленные рассказы моих коллег о похожих историях, говорят, что проблема повсеместна. Разработчики очень часто тащат в приложение гораздо больше данных, когда из-за лени, а когда и из-за непонимания.
В нашем случае проблема решилась следующим образом:
Book.scoped(:order => ['id desc']).paginate(:per_page => 20, :page => params[:page])
Такой запрос превращается из полного сканирования по таблице в запрос, который использует индекс по полю books.id. А такой индекс есть всегда, так как это первичный ключ. В приложение из БД приходит 20 записей и все счастливы.
Есть один минус — такой запрос не гарантирует, что все книги будут с автором, но такие книги можно не показывать. Тогда на странице может быть меньше 20 элементов, что не очень красиво, но во многих случаях вполне хорошо.
Если же и этот вариант не нравится, то можно явно сделать join с таблицей авторов.
Book.scoped(:joins => :author, :order => ['id desc']).
paginate(:per_page => 20, :page => params[:page])
Нам этот вариант не подошел, потому что по историческим причинам таблица авторов была в другой БД, а переносить ее было лень.
Эта история подводит нас к мысли о локальности данных. Мысль, конечно, до безобразия простая. Все операции с данными лучше делать там, где они лежат. По возможности фильтрацию данных лучше всего делать в самом хранилище (БД). Встречаются случаи, когда это очень сложно или невозможно, но встречаются они достаточно редко. Чем «ближе» данные, тем дешевле их обработка. Одно дело отфильтровать что-то в БД, а совсем другое дело — вытащить из из БД по сети в приложение, там превратить в объекты, эти объекты потом сериализовать и по сети передать в браузер, который их потом десериализует в объекты javascript, чтобы потом отфильтровать с помощью динамически генерирующихся фильтров. Я думаю, не надо специальных знаний, чтобы понять, какой способ быстрее.