Очень медленный простой запрос 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 МБ.


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 гораздо более быстрая реализация хеш-соединения.


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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.