Оптимизировать сложный запрос (возможно, с помощью squeel)
Есть такой код (используя PublicActivity gem & Squeel)
def index
@activities = Activity.limit(20).order { created_at.desc }
@one = @activities.where{trackable_type == 'Post'}.includes(trackable: [:author, :project])
@two = @activities.where{trackable_type == 'Project'}.includes trackable: [:owner]
@activities = @one + @two
end
Но он создает 8 запросов SQL:
SELECT "activities".* FROM "activities" WHERE "activities"."trackable_type" = 'Post' ORDER BY "activities"."created_at" DESC LIMIT 20
SELECT "posts".* FROM "posts" WHERE "posts"."id" IN (800, 799, 798, 797, 796, 795, 794, 793, 792, 791, 790, 789, 788, 787, 786, 785, 784, 783, 782, 781)
SELECT "users".* FROM "users" WHERE "users"."id" IN (880, 879, 878, 877, 876, 875, 874, 873, 872, 871, 869, 868, 867, 866, 865, 864, 863, 862, 861, 860)
SELECT "projects".* FROM "projects" WHERE "projects"."id" IN (80, 79)
SELECT "activities".* FROM "activities" WHERE "activities"."trackable_type" = 'Project' ORDER BY "activities"."created_at" DESC LIMIT 20
SELECT "projects".* FROM "projects" WHERE "projects"."id" IN (80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61)
SELECT "users".* FROM "users" WHERE "users"."id" IN (870, 859, 848, 837, 826, 815, 804, 793, 782, 771, 760, 749, 738, 727, 716, 705, 694, 683, 672, 661)
- запрос активистов не присоединен
- некоторые пользователи (владелец поста и владелец проекта) загружаются дважды
- некоторые проекты загружаются дважды
- @activities - это массив. Методы слияния в Rails (кроме
+
) не работайте с кодом выше.
Есть идеи по его оптимизации?
5 ответов
Короче говоря, вы не можете оптимизировать дальше без использования SQL. Именно так Rails ведет бизнес. Он не разрешает доступ к полям объединения вне модели AR, где задан запрос. Поэтому, чтобы получить значения в других таблицах, он делает запрос по каждой.
Это также не позволяет UNION
или фантазии WHERE
условия, которые обеспечивают другие способы решения проблемы.
Хорошей новостью является то, что все эти запросы являются эффективными (учитывая, что trackable_type проиндексирован). Если размер результатов является чем-то существенным (скажем, несколько десятков строк), время ввода-вывода будет доминировать над небольшими дополнительными накладными расходами 7 простых запросов и 1 сложного.
Даже используя SQL, будет сложно получить все результаты объединения в одном запросе. (Это может быть сделано, но результатом будет хеш, а не экземпляр AR. Таким образом, зависимый код будет некрасивым.) Один запрос на таблицу довольно глубоко связан с Active Record.
@ Решение г-на Йоши - хороший компромисс с использованием минимального SQL, за исключением того, что оно не позволяет выборочно загружать author
или же project
+owner
на основе trackable_type
поле.
редактировать
Выше все верно для Rails 3. Для Rails 4, как говорит @CMW, eager_load
метод будет делать то же самое, что и includes
использование внешнего соединения вместо отдельных запросов. Вот почему я так люблю! Я всегда чему-то учусь.
Решение non-rails-4, без squeel:
def index
@activities = Activity.limit(20).order("created_at desc")
@one = @activities.where(trackable_type: 'Post') .joins(trackable: [:author, :project]).includes(trackable: [:author, :project])
@two = @activities.where(trackable_type: 'Project').joins(trackable: [:owner]) .includes(trackable: [:owner])
@activities = @one + @two
end
Сочетание joins
а также includes
выглядит странно, но в моем тестировании это работает на удивление хорошо.
Это сократит его до двух запросов, а не до одного. И @activities будет по-прежнему массивом. Но, возможно, использование этого подхода с squeel тоже решит эту проблему. Я не использую Squeel и не могу проверить это, к сожалению.
РЕДАКТИРОВАТЬ: я полностью упустил смысл этого из-за полиморфных ассоциаций. Вышесказанное работает, чтобы заставить
Если вы хотите использовать то, что предлагает AR, это немного странно, но вы можете определить связанные с правами на чтение проекты и посты:
belongs_to :project, read_only: true, foreign_key: :trackable_id
belongs_to :post, read_only: true, foreign_key: :trackable_id
С этим должен работать упомянутый способ нагнетания энергичных нагрузок. where
условия все еще необходимы, поэтому эти ассоциации призваны только к правильной деятельности.
def index
@activities = Activity.limit(20).order("created_at desc")
@one = @activities.where(trackable_type: 'Post') .joins(post: [:author, :project]).includes(post: [:author, :project])
@two = @activities.where(trackable_type: 'Project').joins(project: [:owner]) .includes(project: [:owner])
@activities = @one + @two
end
Это не чистое решение, и ассоциации должны быть attr_protected, чтобы убедиться, что они не установлены случайно (я ожидаю, что это нарушит полиморфизм), но из моего тестирования это работает.
Используя простой случай Switch в SQL:
def index
table_name = Activity.table_name
@activities = Activity.where(trackable_type: ['Post', 'Project'])
.order("CASE #{table_name}.owner_type WHEN 'Post' THEN 'a' ELSE 'z' END, #{table_name}.created_at DESC")
end
Тогда вы можете легко добавить нужные вам включения;)
Я полагаю, что вам потребуется как минимум два вызова AR-запроса (как в настоящее время) из-за limit(20)
пункт. В настоящее время ваши запросы дают вам до 20 сообщений и до 20 проектов, поэтому совокупное ограничение для обоих типов действий в одном запросе не даст ожидаемого результата.
Я думаю, что все, что вам нужно сделать, это использовать eager_load
в запросе вместо includes
заставить один запрос. Различия между joins
, includes
, preload
, eager_load
а также references
методы хорошо описаны здесь
Итак, с AR и Squeel:
def index
@activities = Activity.limit(20).order { created_at.desc }
@one = @activities.where{trackable_type == 'Post'}.eager_loads(trackable: [:author, :project])
@two = @activities.where{trackable_type == 'Project'}.eager_loads trackable: [:owner]
@activities = @one + @two
end
И без ворчания, используя только обычный ActiveRecord 4:
def index
@activities = Activity.limit(20).order(created_at: :desc)
@one = @activities.where(trackable_type: 'Post').eager_loads(trackable: [:author, :project])
@two = @activities.where(trackable_type: 'Project').eager_loads(trackable: :owner)
@activities = @one + @two
end
Вам не нужно squeel, я недавно вырвал его из своего проекта, потому что он не работает должным образом для ряда сложных запросов в моем опыте, где AR 4 и Arel были в порядке.
Это довольно большой запрос... судя по всему, вы можете сделать это за один выбор, но для удобства чтения я буду использовать два, один для проектов и один для постов.
Это предполагает соотношение 1:1 между деятельностью и постом / проектом. Если это не правильно, проблему можно решить с помощью подзапроса
select * from activities a
where a.trackable_type = 'Post'
left join posts p
on p.id = a.trackable_id -- or whatever fields join these two tables
left join users u
on a.user_id = u.id --this is joining to the main table, may want to join trackable, not sure
left join projects p
on a.project_id = p.id
order by a.created_at DESC LIMIT 20
Или, если есть отношение 1: много, что-то вроде этого:
select * from
( select * from activities a
where a.trackable_type = 'Post'
order by a.created_at DESC LIMIT 20 ) activities
left join posts p
...
Изменить: Когда я прочитал это, я понимаю, что я немного старомоден.... Я думаю, что если вы собираетесь использовать такие большие необработанные запросы SQL, вы должны сделать функцию базы данных, а не кодировать ее в своем приложении