Requête JOIN très lente

12

Structure DB simple (pour un forum en ligne):

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);

Environ 80 000 entrées dans userset 2,6 millions d'entrées dans les poststableaux. Cette simple requête pour obtenir les 100 meilleurs utilisateurs par leurs messages prend 2,4 secondes :

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

Avec set enable_seqscan = falseencore pire:

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

Group by usernameest manquant dans Postgres, car il n'est pas obligatoire (SQL Server dit que je dois grouper usernamesi je veux sélectionner un nom d'utilisateur). Le regroupement avec usernameajoute un peu de ms au temps d'exécution sur Postgres ou ne fait rien.

Pour la science, j'ai installé Microsoft SQL Server sur le même serveur (qui exécute archlinux, 8 core xeon, 24 gb ram, ssd) et j'ai migré toutes les données de Postgres - même structure de table, mêmes indices, mêmes données. Même requête pour obtenir les 100 meilleures affiches en 0,3 seconde :

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

Donne les mêmes résultats à partir des mêmes données, mais le fait 8 fois plus rapidement. Et c'est la version bêta de MS SQL sur Linux, je suppose qu'il fonctionne sur son système d'exploitation "domestique" - Windows Server - il pourrait encore être plus rapide.

Ma requête PostgreSQL est-elle totalement fausse ou PostgreSQL est-il juste lent?

information additionnelle

La version est presque la plus récente (9.6.1, actuellement la plus récente est 9.6.2, ArchLinux a juste des paquets obsolètes et est très lent à mettre à jour). 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 ANALYZEsorties: https://pastebin.com/HxucRgnk

J'ai essayé tous les index, même GIN et GIST, le moyen le plus rapide pour PostgreSQL (et Googling le confirme avec de nombreuses lignes) est d'utiliser le scan séquentiel.

MS SQL Server 14.0.405.200-1, conf par défaut.

J'utilise cela dans une API (avec une sélection simple sans analyse), et en appelant ce point de terminaison d'API avec Chrome, il dit que cela prend 2500 ms + -, ajoutez 50 ms de surcharge de serveur HTTP et Web (les API et SQL s'exécutent sur le même serveur) - c'est le même. Je ne me soucie pas de 100 ms ici ou là, ce qui m'importe deux secondes.

explain analyze SELECT user_id, count(9) FROM posts group by user_id;prend 700 ms. La taille du poststableau est de 2154 Mo.

Lars
la source
2
En apparence, vous avez de belles publications grasses de vos utilisateurs (~ 1 Ko en moyenne). Il peut être judicieux de les détacher du reste de la poststable, en utilisant une table comme De CREATE TABLE post_content (post_id PRIMARY KEY REFERENCES posts (id), content text); cette façon, la plupart des E / S qui sont «gaspillées» sur ce type de requêtes pourraient être épargnées. Si les messages sont plus petits que cela, un VACUUM FULLon postspeut vous aider.
dezso
Oui, les articles ont une colonne de contenu qui contient tout le code HTML d'un article. Merci pour votre suggestion, essayez cela demain. La question est - le tableau des publications MSSQL pèse également plus de 1,5 Go et a les mêmes entrées dans le contenu, mais parvient à être assez rapide - pourquoi?
Lars
2
Vous pouvez également publier un plan d'exécution réel à partir de SQL Server. Cela pourrait être vraiment intéressant, même pour les gens de Postgres comme moi.
dezso
Hum, devinez vite, pourriez-vous changer ceci GROUP BY u.iden ceci GROUP BY p.user_idet essayer cela? Je suppose que Postgres se joint en premier et se regroupe par seconde parce que vous regroupez par identifiant de table d'utilisateurs, même si vous n'avez besoin que de posts user_id pour obtenir les N premières lignes.
UldisK

Réponses:

1

Une autre bonne variante de requête est:

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;

Il n'exploite pas CTE et donne une réponse correcte (et l'exemple CTE peut en théorie produire moins de 100 lignes car il limite d'abord puis rejoint les utilisateurs).

Je suppose que MSSQL est capable d'effectuer une telle transformation dans son optimiseur de requêtes, et PostgreSQL n'est pas en mesure de pousser l'agrégation sous join. Ou MSSQL a juste une implémentation de jointure de hachage beaucoup plus rapide.

funny_falcon
la source
8

Cela peut ou peut ne pas fonctionner - je fonde cela sur un sentiment profond qu'il rejoint vos tables avant le groupe et le filtre. Je suggère d'essayer ce qui suit: filtrer et grouper à l'aide d'un CTE avant de tenter la jointure:

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

Le planificateur de requêtes a parfois juste besoin de quelques conseils. Cette solution fonctionne bien ici, mais les CTE peuvent potentiellement être terribles dans certaines circonstances. Les CTE sont stockés exclusivement en mémoire. En conséquence, les retours de données volumineux peuvent dépasser la mémoire allouée de PostgreSQL et commencer à échanger (pagination dans MS). Les CTE ne peuvent pas non plus être indexés, donc une requête suffisamment volumineuse peut encore provoquer un ralentissement significatif lors de l'interrogation de votre CTE.

Le meilleur conseil que vous pouvez vraiment retenir est de l'essayer de plusieurs façons et de vérifier vos plans de requête.

Scoots
la source
-1

Avez-vous essayé d'augmenter work_mem? 24 Mo semblent trop petits et donc Hash Join doit utiliser plusieurs lots (qui sont écrits dans des fichiers temporaires).

Konstantin Knizhnik
la source
Ce n'est pas trop petit. L'augmentation à 240 mégaoctets ne fait rien. Ce qui aiderait dans postgresql.conf, c'est d'activer les requêtes parallèles en ajoutant ces deux lignes: max_parallel_workers_per_gather = 4etmax_worker_processes = 16
Lars