Очень медленный простой запрос JOIN

12

Простая структура БД (для онлайн-форума):

CREATE TABLE users (
    id integer NOT NULL PRIMARY KEY,
    username text
);
CREATE INDEX ON users (username);

CREATE TABLE posts (
    id integer NOT NULL PRIMARY KEY,
    thread_id integer NOT NULL REFERENCES threads (id),
    user_id integer NOT NULL REFERENCES users (id),
    date timestamp without time zone NOT NULL,
    content text
);
CREATE INDEX ON posts (thread_id);
CREATE INDEX ON posts (user_id);

Около 80 тыс. Записей в usersи 2,6 млн. Записей в postsтаблицах. Этот простой запрос для получения 100 лучших пользователей по их сообщениям занимает 2,4 секунды :

EXPLAIN ANALYZE SELECT u.id, u.username, COUNT(p.id) AS PostCount FROM users u
                    INNER JOIN posts p on p.user_id = u.id
                    WHERE u.username IS NOT NULL
                    GROUP BY u.id
ORDER BY PostCount DESC LIMIT 100;
Limit  (cost=316926.14..316926.39 rows=100 width=20) (actual time=2326.812..2326.830 rows=100 loops=1)
  ->  Sort  (cost=316926.14..317014.83 rows=35476 width=20) (actual time=2326.809..2326.820 rows=100 loops=1)
        Sort Key: (count(p.id)) DESC
        Sort Method: top-N heapsort  Memory: 32kB
        ->  HashAggregate  (cost=315215.51..315570.27 rows=35476 width=20) (actual time=2311.296..2321.739 rows=34608 loops=1)
              Group Key: u.id
              ->  Hash Join  (cost=1176.89..308201.88 rows=1402727 width=16) (actual time=16.538..1784.546 rows=1910831 loops=1)
                    Hash Cond: (p.user_id = u.id)
                    ->  Seq Scan on posts p  (cost=0.00..286185.34 rows=1816634 width=8) (actual time=0.103..1144.681 rows=2173916 loops=1)
                    ->  Hash  (cost=733.44..733.44 rows=35476 width=12) (actual time=15.763..15.763 rows=34609 loops=1)
                          Buckets: 65536  Batches: 1  Memory Usage: 2021kB
                          ->  Seq Scan on users u  (cost=0.00..733.44 rows=35476 width=12) (actual time=0.033..6.521 rows=34609 loops=1)
                                Filter: (username IS NOT NULL)
                                Rows Removed by Filter: 11335

Execution time: 2301.357 ms

С set enable_seqscan = falseеще хуже

Limit  (cost=1160881.74..1160881.99 rows=100 width=20) (actual time=2758.086..2758.107 rows=100 loops=1)
  ->  Sort  (cost=1160881.74..1160970.43 rows=35476 width=20) (actual time=2758.084..2758.098 rows=100 loops=1)
        Sort Key: (count(p.id)) DESC
        Sort Method: top-N heapsort  Memory: 32kB
        ->  GroupAggregate  (cost=0.79..1159525.87 rows=35476 width=20) (actual time=0.095..2749.859 rows=34608 loops=1)
              Group Key: u.id
              ->  Merge Join  (cost=0.79..1152157.48 rows=1402727 width=16) (actual time=0.036..2537.064 rows=1910831 loops=1)
                    Merge Cond: (u.id = p.user_id)
                    ->  Index Scan using users_pkey on users u  (cost=0.29..2404.83 rows=35476 width=12) (actual time=0.016..41.163 rows=34609 loops=1)
                          Filter: (username IS NOT NULL)
                          Rows Removed by Filter: 11335
                    ->  Index Scan using posts_user_id_index on posts p  (cost=0.43..1131472.19 rows=1816634 width=8) (actual time=0.012..2191.856 rows=2173916 loops=1)
Planning time: 1.281 ms
Execution time: 2758.187 ms

Группировка по usernameотсутствует в Postgres, потому что это не требуется (SQL Server говорит, что мне нужно группировать, usernameесли я хочу выбрать имя пользователя). Группировка с usernameдобавляет немного мс к времени выполнения на Postgres или ничего не делает.

Для науки я установил Microsoft SQL Server на тот же сервер (на котором работает archlinux, 8-ядерный xeon, ram 24 Гб, ssd) и перенес все данные из Postgres - та же структура таблиц, те же индексы, те же данные. Тот же запрос для получения 100 лучших постеров выполняется за 0,3 секунды :

SELECT TOP 100 u.id, u.username, COUNT(p.id) AS PostCount FROM dbo.users u
                    INNER JOIN dbo.posts p on p.user_id = u.id
                    WHERE u.username IS NOT NULL
                    GROUP BY u.id, u.username
ORDER BY PostCount DESC

Выдает те же результаты из тех же данных, но делает это в 8 раз быстрее. И это бета-версия MS SQL для Linux, я думаю, что она работает на «домашней» ОС - Windows Server - она ​​может быть еще быстрее.

Мой запрос к PostgreSQL полностью неверен или PostgreSQL просто медленный?

Дополнительная информация

Версия почти самая новая (9.6.1, в настоящее время самая новая - 9.6.2, ArchLinux только что устарел и очень медленно обновляется). Config:

max_connections = 75
shared_buffers = 3584MB       
effective_cache_size = 10752MB
work_mem = 24466kB         
maintenance_work_mem = 896MB   
dynamic_shared_memory_type = posix  
min_wal_size = 1GB
max_wal_size = 2GB
checkpoint_completion_target = 0.9
wal_buffers = 16MB
default_statistics_target = 100

EXPLAIN ANALYZEвыходы: https://pastebin.com/HxucRgnk

Перепробовал все индексы, использовали даже GIN и GIST, самый быстрый способ для PostgreSQL (а Googling подтверждает много строк) - это использовать последовательное сканирование.

MS SQL Server 14.0.405.200-1, по умолчанию конф.

Я использую это в API (с простым выбором без анализа), и, вызывая эту конечную точку API с помощью Chrome, он говорит, что это занимает 2500 мс + -, добавляя 50 мс HTTP и накладные расходы веб-сервера (API и SQL выполняются на одном сервере) - это то же самое. Меня не волнует 100 мс здесь или там, меня волнуют две целые секунды.

explain analyze SELECT user_id, count(9) FROM posts group by user_id;занимает 700 мс Размер postsтаблицы составляет 2154 МБ.

Lars
источник
2
Как это звучит, у вас есть хорошие жирные сообщения от ваших пользователей (в среднем ~ 1 КБ). Возможно, имеет смысл отсоединить их от остальной postsтаблицы, используя таблицу, подобную этой. CREATE TABLE post_content (post_id PRIMARY KEY REFERENCES posts (id), content text); Таким образом, большинство операций ввода-вывода, которые «тратятся» на запросы такого типа, могут быть сэкономлены. Если сообщений меньше этого, VACUUM FULLна postsможет помочь.
Дезсо
Да, в сообщениях есть столбец содержимого, в котором есть все HTML сообщения. Спасибо за ваше предложение, попробую завтра. Вопрос в том, что таблица сообщений MSSQL также весит более 1,5 ГБ и имеет те же записи в содержании, но удается работать намного быстрее - почему?
Ларс
2
Вы также можете опубликовать фактический план выполнения из SQL Server. Это может быть действительно интересно, даже для Postgres таких людей, как я.
Дезсо
Хм, быстрое предположение, не могли бы вы изменить это GROUP BY u.idна это GROUP BY p.user_idи попробовать? Я предполагаю, что Postgres присоединяется первым и группируется по секундам, потому что вы группируете по идентификатору таблицы пользователей, даже если вам нужны только сообщения user_id, чтобы получить верхние N-строки.
UldisK

Ответы:

1

Еще один хороший вариант запроса:

SELECT p.user_id, p.cnt AS PostCount
FROM users u
INNER JOIN (
    select user_id, count(id) as cnt from posts group by user_id
) as p on p.user_id = u.id
WHERE u.username IS NOT NULL          
ORDER BY PostCount DESC LIMIT 100;

Он не использует CTE и дает правильный ответ (и пример CTE может дать менее 100 строк в теории, поскольку он сначала ограничивает, а затем объединяет пользователей).

Я полагаю, что MSSQL может выполнить такое преобразование в своем оптимизаторе запросов, а PostgreSQL не может подавить агрегацию при объединении. Или у MSSQL гораздо более быстрая реализация хеш-соединения.

funny_falcon
источник
8

Это может или не может работать - я основываю это на интуитивном ощущении того, что это объединение ваших таблиц перед группой и фильтром. Я предлагаю попробовать следующее: отфильтруйте и сгруппируйте, используя CTE перед попыткой объединения:

with
    __posts as(
        select
            user_id,
            count(1) as num_posts
        from
            posts
        group by
            user_id
        order by
            num_posts desc
        limit 100
    )
select
    users.username,
    __posts.num_posts
from
    users
    inner join __posts on(
        __posts.user_id = users.id
    )
order by
    num_posts desc

Планировщик запросов иногда просто нуждается в небольшом руководстве. Это решение хорошо работает здесь, но CTE потенциально может быть ужасным в некоторых обстоятельствах. CTE хранятся исключительно в памяти. В результате этого большие объемы возвращаемых данных могут превысить выделенную память Postgres и начать обмен (подкачка в MS). CTE также не могут быть проиндексированы, поэтому достаточно большой запрос все еще может вызвать значительное замедление при запросе вашего CTE.

Лучший совет, который вы действительно можете отобрать, - это попробовать его несколькими способами и проверить свои планы запросов.

бежит стремглав
источник
-1

Вы пытались увеличить work_mem? 24Mb кажется слишком маленьким, поэтому Hash Join должен использовать несколько пакетов (которые записываются во временные файлы).

Константин Книжник
источник
Это не слишком мало. Увеличение до 240 мегабайт ничего не делает. Что может помочь в postgresql.conf, так это разрешить параллельные запросы, добавив эти две строки: max_parallel_workers_per_gather = 4иmax_worker_processes = 16
Lars