UNION не так уж и плох. Особенно UNION ALL, где не требуется выбирать уникальные записи. По крайней мере он будет не медленнее чем выполнить каждый запрос по отдельности, за счет того, что может теоретически быть выполнен параллельно на нескольких ядрах.
Но если не подходит, давайте от печки. Вы постулируете следующее «мы не можем выбирать все в одном запросе, чтобы не мешать модели».
Значит минимальное количество запросов получается равным количеству_типов_данных. Для этого нужно выполнить по отдельности запросы которые у меня в UNION, но тогда вручную в коде сортировать эту кашу по датам и группировать по wall_id. Это плохой путь.
Предлагаю такой вариант:
SELECT
w.id,
w.description,
COUNT(i.id) i_cnt,
COUNT(bp.id) bp_cnt
FROM wall w
INNER JOIN wall_element we_i
ON we_i.wall_id = w.id
INNER JOIN image i
ON i.id = we_i.element_id
INNER JOIN wall_element we_bp
ON we_bp.wall_id = w.id
INNER JOIN blog_post bp
ON bp.id = we_bp.element_id
ORDER BY w.timestamp
GROUP BY w.id, w.description
Мы получаем список записей, и количество связанных с каждой записью единиц контента. Дальше разделяем ответственность в коде следущим образом:
1. основной код выполняет этот запрос, и бежит по результатам. Смотрит, что есть в конкретной записи. Например видит что картинка, и товар из магазина
2. основной код вызывает соответствующие рендереры, передавая им идентификатор записи wall_id.
3. рендерер сам (своим запросом) достает уже те данные, которые ему нужны оттуда, откуда хочет, в ту модель, которая ему нравится. Это полностью развязывает рендереры друг от друга и от вызывающего кода.
Итого имеем запросов: количество_записей_на_стене * среднее_количество_единиц_контента + 1. Думаю у вас в среднем 1 запись на стене будет иметь ссылку на 1 единицу контента (например пяток фоток, которые рендерер картинок сможет вытащить одним запросом).
Имеем в среднем запросом количество_записей_на_стене + 1. Вполне приемлимо.