Объединение нескольких подзапросов в один в Postgres - PullRequest
4 голосов
/ 23 августа 2011

У меня есть две таблицы:

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 - это было бы довольно круто. :)

Ответы [ 3 ]

3 голосов
/ 29 августа 2011

Есть два вопроса:

  1. Синтаксис для объединения нескольких подзапросов в один.
  2. Оптимизация.

Для # 1 я не могу получить "завершенную" вещь в одном общем табличном выражении , потому что вы используетекоррелированный подзапрос для каждого элемента.Тем не менее, у вас могут быть некоторые преимущества, если вы используете общее табличное выражение.Очевидно, что это будет зависеть от данных, поэтому, пожалуйста, сравнительный тест, чтобы увидеть, поможет ли это.

Для # 2, потому что в вашей таблице три общедоступных "класса" элементов, я ожидаю, что частичноиндексирует для увеличения скорости вашего запроса, независимо от того, удалось ли вам увеличить скорость из-за # 1.

Во-первых, это просто.Чтобы добавить частичный индекс в эту таблицу, я бы сделал:

CREATE INDEX upvote_vote_index ON votes (type, direction)
WHERE (type = 0 AND direction = 1);

CREATE INDEX downvote_vote_index ON votes (type, direction)
WHERE (type = 0 AND direction = -1);

CREATE INDEX favoriters_vote_index ON votes (type)
WHERE (type = 1);

Чем меньше эти индексы, тем эффективнее будут ваши запросы.К сожалению, в моих тестах они, похоже, не помогли :-( Тем не менее, возможно, вы сможете найти их применение, это сильно зависит от ваших данных.


Что касается общей оптимизации, яЯ бы по-другому подошел к проблеме. Я бы «развернул» запрос в этой форме (используя внутреннее соединение и используя условные выражения , чтобы «разделить» три типа голосов), а затем использовал «Группировать».С помощью оператора агрегации "и" массива ", чтобы объединить их. IMO, я бы лучше изменил код своего приложения, чтобы он принимался в" развернутой "форме, но если вы не можете изменить код приложения, тогда" сгруппировать по "+ агрегатная функция должна работать.

SELECT array_agg(v.user_id), -- array_agg(anything else you needed), 
    i.root_id, i.id, -- I presume you needed the primary key?
CASE
    WHEN v.type = 0 AND v.direction = 1
        THEN 'upvoter'
    WHEN v.type = 0 AND v.direction = -1
        THEN 'downvoter'
    WHEN v.type = 1
        THEN 'favoriter'
END as vote_type
FROM items i 
    JOIN votes v ON i.root_id = v.root_id AND i.id = v.item_id
WHERE i.root_id = 1 
  AND ((type=0 AND (direction=1 OR direction=-1)) 
       OR type=1)
GROUP BY i.root_id, i.id, vote_type
ORDER BY id

Она по-прежнему "развернута на один шаг" по сравнению с вашим кодом (тип_оценки - вертикальный, а в вашем случае - горизонтальный по столбцам).более эффективный.

0 голосов
/ 29 августа 2011

Вот другой подход. Он имеет (возможно) нежелательный результат включения значений NULL в массивы, но работает за один проход, а не за три. Я считаю полезным думать о некоторых SQL-запросах в уменьшенном виде, и операторы case прекрасно подходят для этого.

select
v.root_id, v.item_id,
array_agg(case when type = 0 AND direction = 1 then user_id else NULL end) as upvoters,
array_agg(case when type = 0 AND direction = -1 then user_id else NULL end) as downvoters,
array_agg(case when type = 1 then user_id else NULL end) as favoriters
from items i
join votes v on i.root_id = v.root_id AND i.id = v.item_id
group by 1, 2

С некоторыми примерами данных я получаю этот набор результатов:

 root_id | item_id |    upvoters    |    downvoters    |    favoriters    
---------+---------+----------------+------------------+------------------
       1 |       2 | {100,NULL,102} | {NULL,101,NULL}  | {NULL,NULL,NULL}
       2 |       4 | {100,NULL,101} | {NULL,NULL,NULL} | {NULL,100,NULL}

Полагаю, вам нужен postgres 8.4 для получения array_agg, но до этого был рецепт для функции array_accum.

В списке postgres-hackers обсуждается, как создать NULL-удаляемую версию array_agg, если вам интересно.

0 голосов
/ 24 августа 2011

Просто предположение, но, возможно, стоит попробовать:

Может быть, sql может оптимизировать запрос, если вы создадите VIEW из

SELECT user_id from votes where root_id = i.root_id AND item_id = i.id

и затем выберите 3 раза с различными предложениями where о типе и направлении.

Если это тоже не помогает, может быть, вы могли бы выбрать 3 типа в качестве дополнительных логических столбцов и затем работать только с одним запросом?

Было бы интересно услышать, если вы найдете решение. Удачи.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...