Sehr langsame einfache JOIN-Abfrage

12

Einfache DB-Struktur (für ein Online-Forum):

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

Rund 80.000 Einträge in usersund 2,6 Millionen Einträge in postsTabellen. Diese einfache Abfrage, um die Top-100-Benutzer anhand ihrer Beiträge zu ermitteln, dauert 2,4 Sekunden :

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

Mit set enable_seqscan = falsenoch schlimmer:

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

Die usernameGruppierung nach fehlt in Postgres, da sie nicht erforderlich ist (SQL Server sagt, dass ich nach gruppieren muss, usernamewenn ich den Benutzernamen auswählen möchte). Durch das Gruppieren mit wird usernamedie Ausführungszeit auf Postgres um einige Millisekunden verlängert oder es wird nichts ausgeführt.

Für die Wissenschaft habe ich Microsoft SQL Server auf demselben Server installiert (auf dem Archlinux, 8 Core Xeon, 24 GB RAM, SSD ausgeführt werden) und alle Daten von Postgres migriert - dieselbe Tabellenstruktur, dieselbe Indizes, dieselben Daten. Dieselbe Abfrage, um die besten 100 Poster zu erhalten, dauert 0,3 Sekunden :

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

Die gleichen Ergebnisse werden mit den gleichen Daten erzielt, jedoch 8-mal schneller. Und es ist eine Beta-Version von MS SQL unter Linux. Ich denke, es läuft auf dem "Heim" -Betriebssystem - Windows Server - es könnte noch schneller sein.

Ist meine PostgreSQL-Abfrage völlig falsch oder ist PostgreSQL nur langsam?

zusätzliche Information

Die Version ist fast die neueste (9.6.1, aktuell die neueste Version 9.6.2, ArchLinux hat nur veraltete Pakete und ist sehr langsam zu aktualisieren). Konfig:

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 ANALYZEAusgaben: https://pastebin.com/HxucRgnk

Versucht, alle Indizes, verwendet sogar GIN und GIST, ist der schnellste Weg für PostgreSQL (und Googeln bestätigt mit vielen Zeilen), sequentiellen Scan zu verwenden.

MS SQL Server 14.0.405.200-1, Standardkonfiguration

Ich verwende dies in einer API (mit einfacher Auswahl ohne Analyse) und rufe diesen API-Endpunkt mit Chrome auf, da er 2500 ms + - benötigt und 50 ms HTTP- und Webserver-Overhead hinzufügt (API und SQL werden auf demselben Server ausgeführt). - es ist das gleiche. Es ist mir egal, ob hier oder da 100 ms sind, was mir wichtig ist, sind zwei ganze Sekunden.

explain analyze SELECT user_id, count(9) FROM posts group by user_id;dauert 700 ms. Die Größe des postsTisches beträgt 2154 MB.

Lars
quelle
2
Wie es sich anhört, haben Sie nette fette Posts von Ihren Benutzern (durchschnittlich ~ 1 KB). Es kann sinnvoll sein, sie postsmit einer Tabelle wie CREATE TABLE post_content (post_id PRIMARY KEY REFERENCES posts (id), content text); dieser vom Rest der Tabelle zu trennen. Auf diese Weise können die meisten E / A-Vorgänge, die für diese Art von Abfragen "verschwendet" werden, vermieden werden. Wenn die Beiträge kleiner sind, kann ein VACUUM FULLOn postshelfen.
Dezso
Ja, Posts haben eine Inhaltsspalte, die den gesamten HTML-Code eines Posts enthält. Vielen Dank für Ihren Vorschlag, werde das morgen versuchen. Die Frage ist: Die MSSQL-Posts-Tabelle wiegt ebenfalls mehr als 1,5 GB und hat dieselben Einträge im Inhalt, schafft es jedoch, schneller zu sein - warum?
Lars
2
Möglicherweise können Sie auch einen tatsächlichen Ausführungsplan von SQL Server aus veröffentlichen. Könnte auch für Postgres-Leute wie mich sehr interessant sein.
Dezso
Hmm, schnell guesss, könnten Sie dies ändern , GROUP BY u.idum diese GROUP BY p.user_idund versuchen Sie das? Ich vermute, dass Postgres zuerst beitritt und nach Sekunden gruppiert, weil Sie nach Benutzertabellen-ID gruppieren, obwohl Sie nur posts user_id benötigen, um die oberen N-Zeilen zu erhalten.
UldisK

Antworten:

1

Eine weitere gute Abfragevariante ist:

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;

Es nutzt CTE nicht aus und gibt die richtige Antwort (und das CTE-Beispiel kann theoretisch weniger als 100 Zeilen produzieren, da es zuerst die Anzahl der Benutzer einschränkt und sich dann mit ihnen verbindet).

Ich nehme an, MSSQL ist in der Lage, eine solche Transformation in seinem Abfrageoptimierer durchzuführen, und PostgreSQL ist nicht in der Lage, die Aggregation unter Join herunterzudrücken. Oder MSSQL hat nur eine viel schnellere Implementierung von Hash-Joins.

funny_falcon
quelle
8

Das mag funktionieren oder auch nicht - ich habe das Gefühl, dass es vor der Gruppe und dem Filtern zu Ihren Tischen kommt. Ich schlage vor, Folgendes zu versuchen: Filtern und Gruppieren mit einem CTE, bevor Sie den Join versuchen:

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

Der Abfrageplaner benötigt manchmal nur eine kleine Anleitung. Diese Lösung funktioniert hier gut, aber CTEs können unter bestimmten Umständen möglicherweise schrecklich sein. CTEs werden ausschließlich im Speicher gespeichert. Infolgedessen können große Datenrückgaben den von Postgres zugewiesenen Speicher überschreiten und mit dem Auslagern beginnen (Paging in MS). CTEs können auch nicht indiziert werden, sodass eine ausreichend große Abfrage bei der Abfrage Ihres CTE immer noch zu einer erheblichen Verlangsamung führen kann.

Der beste Rat, den Sie wirklich mitnehmen können, ist, es auf verschiedene Arten zu versuchen und Ihre Abfragepläne zu überprüfen.

Scoots
quelle
-1

Haben Sie versucht, work_mem zu erhöhen? 24 MB scheinen zu klein zu sein, und daher muss Hash Join mehrere Batches verwenden (die in temporären Dateien geschrieben werden).

Konstantin Knizhnik
quelle
Es ist nicht zu klein. Das Erhöhen auf 240 Megabyte führt zu nichts. Was in postgresql.conf helfen würde, ist das Ermöglichen paralleler Abfragen durch Hinzufügen dieser beiden Zeilen: max_parallel_workers_per_gather = 4undmax_worker_processes = 16
Lars