2013-12-14 4 views
6

В PostgreSQL 9.2, у меня есть таблица элементов, которые оцениваемые пользователи:Slow GroupAggregate в PostgreSQL

id | userid | itemid | rating  |  timestamp  |  !update_time 
--------+--------+--------+---------------+---------------------+------------------------ 
522241 | 3991 | 6887 | 0.1111111111 | 2005-06-20 03:13:56 | 2013-10-11 17:50:24.545 
522242 | 3991 | 6934 | 0.1111111111 | 2005-04-05 02:25:21 | 2013-10-11 17:50:24.545 
522243 | 3991 | 6936 | -0.1111111111 | 2005-03-31 03:17:25 | 2013-10-11 17:50:24.545 
522244 | 3991 | 6942 | -0.3333333333 | 2005-03-24 04:38:02 | 2013-10-11 17:50:24.545 
522245 | 3991 | 6951 | -0.5555555556 | 2005-06-20 03:15:35 | 2013-10-11 17:50:24.545 
... | ... | ... | ...   | ...     | ... 

Я хочу выполнить очень простой запрос: для каждого пользователя, выберите общее количество оценок в базе данных.

Я использую следующий простой подход:

SELECT userid, COUNT(*) AS rcount 
FROM ratings 
GROUP BY userid 

Таблица содержит 10M записи. Запрос занимает ... ну, примерно 2 или 3 минуты. Честно говоря, я не доволен этим, и я считаю, что 10M не так много для того, чтобы запрос занял так много времени. (Или это .. ??)

Отныне я попросил PostgreSQL показать мне план выполнения:

EXPLAIN SELECT userid, COUNT(*) AS rcount 
FROM ratings 
GROUP BY userid 

Это приводит к:

GroupAggregate (cost=1756177.54..1831423.30 rows=24535 width=5) 
    -> Sort (cost=1756177.54..1781177.68 rows=10000054 width=5) 
     Sort Key: userid 
     -> Seq Scan on ratings (cost=0.00..183334.54 rows=10000054 width=5) 

Я прочитал это следующим образом: Во-первых, , вся таблица считывается с диска (seq scan). Во-вторых, он сортируется по идентификатору пользователя в n*log(n) (сортировка). Наконец, отсортированная таблица считывается по строкам и агрегируется в линейном времени. Ну, не совсем оптимальный алгоритм, я думаю, если бы я сам его реализовал, я бы использовал хеш-таблицу и построил результат в первом проходе. Неважно.

Похоже, что это сортировка по userid, которая занимает так много времени. Добавлен индекс:

CREATE INDEX ratings_userid_index ON ratings (userid) 

К сожалению, это не помогло, и производительность не изменилась. Я определенно не считаю себя продвинутым пользователем, и я считаю, что делаю что-то принципиально неправильное. Однако здесь я застрял. Я был бы признателен за любые идеи о том, как выполнить запрос в разумные сроки. Еще одно примечание: рабочий процесс PostgreSQL использует 100% одного из моих ядер процессора во время выполнения, что указывает на то, что доступ к диску не является основным узким местом.

EDIT

В соответствии с просьбой @a_horse_with_no_name. Ничего себе, довольно передовые для меня:

EXPLAIN (analyze on, buffers on, verbose on) 
SELECT userid,COUNT(userid) AS rcount 
FROM movielens_10m.ratings 
GROUP BY userId 

Выходы:

GroupAggregate (cost=1756177.54..1831423.30 rows=24535 width=5) (actual time=110666.899..127168.304 rows=69878 loops=1) 
    Output: userid, count(userid) 
    Buffers: shared hit=906 read=82433, temp read=19358 written=19358 
    -> Sort (cost=1756177.54..1781177.68 rows=10000054 width=5) (actual time=110666.838..125180.683 rows=10000054 loops=1) 
     Output: userid 
     Sort Key: ratings.userid 
     Sort Method: external merge Disk: 154840kB 
     Buffers: shared hit=906 read=82433, temp read=19358 written=19358 
     -> Seq Scan on movielens_10m.ratings (cost=0.00..183334.54 rows=10000054 width=5) (actual time=0.019..2889.583 rows=10000054 loops=1) 
       Output: userid 
       Buffers: shared hit=901 read=82433 
Total runtime: 127193.524 ms 

EDIT 2

@ комментарий a_horse_with_no_name в решить эту проблему.Я чувствую себя счастливым, чтобы поделиться своими выводами:

SET work_mem = '1MB'; 
EXPLAIN SELECT userid,COUNT(userid) AS rcount 
FROM movielens_10m.ratings 
GROUP BY userId 

производит то же самое, что и выше:

GroupAggregate (cost=1756177.54..1831423.30 rows=24535 width=5) 
    -> Sort (cost=1756177.54..1781177.68 rows=10000054 width=5) 
     Sort Key: userid 
     -> Seq Scan on ratings (cost=0.00..183334.54 rows=10000054 width=5) 

Однако

SET work_mem = '10MB'; 
EXPLAIN SELECT userid,COUNT(userid) AS rcount 
FROM movielens_10m.ratings 
GROUP BY userId 

дает

HashAggregate (cost=233334.81..233580.16 rows=24535 width=5) 
    -> Seq Scan on ratings (cost=0.00..183334.54 rows=10000054 width=5) 

Запрос теперь только принимает около 3,5 секунд для завершения ,

+2

Пожалуйста, опубликуйте вывод 'объяснять (анализировать, буферизировать, многословно) ..' –

+1

. Одна вещь, которую вы можете рассмотреть, заключается в том, чтобы этот запрос в «материализованное представление» и (иногда) обновлял представление как часть вашей вставки/запуска рейтинга. То есть кеширование этого запроса. –

+4

Основная проблема заключается не в сканировании seq, а в том, что делается на диске. Вы можете попробовать 'set work_mem = '250MB'' (или даже выше) перед запуском запроса - если у вас достаточно памяти. –

ответ

0

Попробуйте это, потому что COUNT (*) и COUNT (userid) делают много разницы.

SELECT userid, COUNT(userid) AS rcount 
FROM ratings 
GROUP BY userid 
+0

Я тоже это пробовал. К сожалению, это не имеет значения. Еще 2 минуты, а также точно такие же оценочные цифры в плане выполнения. – Tregoreg

+0

@Tregoreg у вас есть какое-нибудь соединение с таблицей рейтингов? или вы пытаетесь с тем же запросом, который вы разместили здесь? –

+1

Я использую тот же запрос, что и в этом сообщении. На самом деле, я сначала делал гораздо более сложный запрос с объединениями, но это «ядро с узким местом», которое я успешно изолировал от него. – Tregoreg

3

Рассмотрите, как ваш запрос мог бы вернуть результат ... Вы можете создать хеш переменной длины и создать/увеличить его значения; или вы можете отсортировать все строки по идентификатору пользователя и подсчитать. Вычислительно, последний вариант дешевле. Это то, что делает Postgres.

Затем рассмотрите как для сортировки данных, принимая во внимание диск ввода-вывода. Одним из вариантов является открытие дисковых страниц A, B, C, D и т. Д., А затем сортировка строк по идентификатору пользователя в памяти. Другими словами, seq-сканирование сопровождается сортировкой. Другим вариантом, называемым сканированием индекса, было бы потянуть строки по порядку, используя индекс: зайдите на страницу B, затем D, затем A, затем снова B, снова A, C, ad tausea.

Сканирование индекса эффективно при потянув несколько строк в порядке; не столько для того, чтобы получить множество строк по порядку - не говоря уже о всех строках по порядку. Таким образом, план вы получаете является оптимальным:

  1. Плуг бросить все строки (далее сканирование)
  2. Сортировка строк в группе по критерию
  3. подсчета строк по критериям

Проблема в том, что вы сортируете примерно 10 миллионов строк, чтобы подсчитать их по идентификатору пользователя. Ничто не сделает вещи быстрее, чем инвестировать в более оперативную память и супер быстрые SSD.

Вы можете, однако избегайте этого запроса. Либо:

  • оценки Графских горстки пользователей, что вы на самом деле нужны - используя ИНЕК - вместо того, чтобы вытягивать весь набор; или
  • Добавьте поле rating_count в таблицу ваших пользователей и используйте триггеры для оценок, чтобы поддерживать счет.
  • Используйте материализованное представление, если точное количество менее важно, чем иметь смутное представление об этом.
+0

Простите мои сомнения, но я не понимаю, почему подход хеш-таблицы является вычислительно субоптимальным. Фактически, это то, что я планирую сделать, если я не буду решать проблему на уровне PostgreSQL. Я бы почти гарантировал, что буду быстрее, чем за 2 минуты ... – Tregoreg

+0

@Tregoreg: Я предполагаю, что существуют такие соображения, как «сортировка строк, вы создаете набор поэтапно и избегаете поиска», а также «путем сортировки строк , мы не рискуем, что хэш слишком велик, чтобы вписаться в память ». Я бы посоветовал, что вы сомневаетесь в списке pg-hackers. Том Лейн, вероятно, даст вам точную причину, по которой они подходят к этому пути, а не к другому. –

+0

Если я уменьшу запрос до '' SELECT userid FROM ratings'', это займет всего 3 секунды. Все еще не идеально, и мне нравятся оба решения, которые вы предлагаете, но я считаю это разумным. Просто из любопытства, я хотел бы знать, смогу ли я сделать лучше в PostgreSQL с исходным запросом. Потому что, если я не могу, я полностью избегу таких запросов в будущем и попытаюсь как можно быстрее перемещать логику приложений вне SQL. – Tregoreg