У меня есть две таблицы:
CREATE TABLE items
(
root_id integer NOT NULL,
id serial NOT NULL,
-- Other fields...
CONSTRAINT items_pkey PRIMARY KEY (root_id, id)
)
CREATE TABLE votes
(
root_id integer NOT NULL,
item_id integer NOT NULL,
user_id integer NOT NULL,
type smallint NOT NULL,
direction smallint,
CONSTRAINT votes_pkey PRIMARY KEY (root_id, item_id, user_id, type),
CONSTRAINT votes_root_id_fkey FOREIGN KEY (root_id, item_id)
REFERENCES items (root_id, id) MATCH SIMPLE
ON UPDATE CASCADE ON DELETE CASCADE,
-- Other constraints...
)
Я пытаюсь в одном запросе извлечь все элементы определенного root_id вместе с несколькими массивами user_ids пользователей, которые проголосовали определенным образом. Следующий запрос делает то, что мне нужно:
SELECT *,
ARRAY(SELECT user_id from votes where root_id = i.root_id AND item_id = i.id AND type = 0 AND direction = 1) as upvoters,
ARRAY(SELECT user_id from votes where root_id = i.root_id AND item_id = i.id AND type = 0 AND direction = -1) as downvoters,
ARRAY(SELECT user_id from votes where root_id = i.root_id AND item_id = i.id AND type = 1) as favoriters
FROM items i
WHERE root_id = 1
ORDER BY id
Проблема в том, что я использую три подзапроса для получения необходимой информации, когда мне кажется, что я могу сделать то же самое в одном. Я подумал, что Postgres (я использую 8.4) может быть достаточно умен, чтобы свести их все в один запрос для меня, но, глядя на вывод объяснения в pgAdmin, похоже, что этого не происходит - он запускает несколько поисков первичного ключа по голосам стол вместо Мне кажется, что я мог бы переработать этот запрос, чтобы сделать его более эффективным, но я не уверен, как.
Есть указатели?
РЕДАКТИРОВАТЬ: Обновление, чтобы объяснить, где я сейчас нахожусь. По совету списка рассылки pgsql-general я попытался изменить запрос на использование CTE:
WITH v AS (
SELECT item_id, type, direction, array_agg(user_id) as user_ids
FROM votes
WHERE root_id = 5305
GROUP BY type, direction, item_id
ORDER BY type, direction, item_id
)
SELECT *,
(SELECT user_ids from v where item_id = i.id AND type = 0 AND direction = 1) as upvoters,
(SELECT user_ids from v where item_id = i.id AND type = 0 AND direction = -1) as downvoters,
(SELECT user_ids from v where item_id = i.id AND type = 1) as favoriters
FROM items i
WHERE root_id = 5305
ORDER BY id
Сравнительный анализ каждого из них из моего приложения (я настроил каждый как подготовленный оператор, чтобы не тратить время на планирование запросов, а затем выполнял каждый из них несколько тысяч раз с различными root_ids), мой первоначальный подход в среднем составляет 15 миллисекунд, а CTE подход в среднем 17 миллисекунд. Мне удалось повторить этот результат в течение нескольких прогонов.
Когда у меня будет время, я поиграю с подходами jkebinger и Dragontamer5788 с моими тестовыми данными и посмотрю, как они работают, но я также начинаю вознаграждение, чтобы узнать, смогу ли я получить больше предложений.
Я должен также упомянуть, что я готов изменить свою схему (система еще не запущена и не будет работать в течение пары месяцев), если она может ускорить этот запрос. Я разработал свою таблицу голосов таким образом, чтобы воспользоваться преимуществом ограничения уникальности первичного ключа - данный пользователь может, например, как добавлять в избранное или повышать голос элемент, но не повышать его, так и понижать его, - но я могу ослабить / обойти это ограничение, если представляю эти варианты по-другому имеют больше смысла.
РЕДАКТИРОВАТЬ # 2: Я протестировал все четыре решения. Удивительно, но Sequel достаточно гибок, чтобы я мог написать все четыре, не переходя в SQL один раз (даже для операторов CASE). Как и раньше, я выполнял их все как подготовленные операторы, чтобы не было проблем с планированием запросов, и каждый из них выполнялся несколько тысяч раз. Затем я выполнил все запросы в двух ситуациях - в наихудшем сценарии с большим количеством строк (265 элементов и 4911 голосов), когда соответствующие строки были бы в кеше довольно быстро, поэтому использование ЦП должно быть решающим фактором и более реалистичный сценарий, где случайный root_id был выбран для каждого запуска. Я завелся с:
Original query - Typical: ~10.5 ms, Worst case: ~26 ms
CTE query - Typical: ~16.5 ms, Worst case: ~70 ms
Dragontamer5788 - Typical: ~15 ms, Worst case: ~36 ms
jkebinger - Typical: ~42 ms, Worst case: ~180 ms
Полагаю, урок, который можно извлечь из этого прямо сейчас, заключается в том, что планировщик запросов Postgres очень умен и, вероятно, делает что-то умное под поверхностью. Я не думаю, что я собираюсь проводить больше времени, пытаясь обойти это. Если кто-то захочет отправить еще одну попытку запроса, я буду рад ее оценить, но в остальном я думаю, что Dragontamer - победитель награды и правильный (или ближайший к правильному) ответ. Если кто-то еще не сможет пролить свет на то, что делает Postgres - это было бы довольно круто. :)