Дуже повільний простий ПРИЄДНАЙТЕ запит


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, 24 gb ram, 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 просто має застарілі пакети і дуже повільно оновлюється). Налаштування:

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 хромом, він говорить, що потрібно 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.
dezso

Хм, швидка здогадка, чи можете ви змінити це 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
Ларс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.