Bardzo wolne proste zapytanie DOŁĄCZ

12

Prosta struktura DB (na forum online):

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

Około 80 000 wpisów usersi 2,6 miliona wpisów w poststabelach. To proste zapytanie, aby uzyskać 100 najlepszych użytkowników za pomocą swoich postów, zajmuje 2,4 sekundy :

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

Co set enable_seqscan = falsegorsza:

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

Grupa przez usernamebrakuje w PostgreSQL, ponieważ nie jest to wymagane (SQL Server mówi muszę przez grupy username, jeśli chcę, aby wybrać nazwę użytkownika). Grupowanie za pomocą usernamedodaje trochę ms do czasu wykonania na Postgresie lub nic nie robi.

Dla nauki zainstalowałem Microsoft SQL Server na tym samym serwerze (na którym działa archlinux, 8-rdzeniowy xeon, 24 gb ram, ssd) i przeprowadziłem migrację wszystkich danych z Postgres - ta sama struktura tabeli, te same indeksy, te same dane. To samo zapytanie, aby uzyskać 100 najlepszych plakatów, działa w 0,3 sekundy :

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

Daje takie same wyniki z tych samych danych, ale robi to 8 razy szybciej. I to jest wersja beta MS SQL na Linuksie, myślę, że działa na „domowym” systemie operacyjnym - Windows Server - może być jeszcze szybszy.

Czy moje zapytanie PostgreSQL jest całkowicie błędne, czy też PostgreSQL jest po prostu wolny?

dodatkowe informacje

Wersja jest prawie najnowsza (9.6.1, obecnie najnowsza to 9.6.2, ArchLinux ma po prostu przestarzałe pakiety i bardzo wolno aktualizuje). Konfiguracja:

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

Próbowałem wszystkich indeksów, korzystałem nawet z GIN i GIST, najszybszym sposobem dla PostgreSQL (a Googling potwierdza z wieloma wierszami) jest skanowanie sekwencyjne.

MS SQL Server 14.0.405.200-1, konf. Domyślna

Używam tego w interfejsie API (z prostym wyborem bez analizy), a wywoływanie tego punktu końcowego interfejsu API za pomocą chrome mówi, że zajmuje to 2500 ms + -, dodaj 50 ms narzutu HTTP i narzutu serwera WWW (API i SQL działają na tym samym serwerze) - to jest to samo. Nie obchodzi mnie około 100 ms tu i tam, ważne są dwie całe sekundy.

explain analyze SELECT user_id, count(9) FROM posts group by user_id;zajmuje 700 ms. Rozmiar postsstołu to 2154 MB.

Lars
źródło
2
Jak się wydaje, masz ładne, tłuste posty od użytkowników (średnio ~ 1kB). Rozsądne może być odłączenie ich od reszty poststabeli przy użyciu takiej tabeli. CREATE TABLE post_content (post_id PRIMARY KEY REFERENCES posts (id), content text); W ten sposób można zaoszczędzić większość wejść / wyjść „marnowanych” na tego typu zapytania. Jeśli słupki są mniejsze od tego, VACUUM FULLna postsmoże pomóc.
dezso,
Tak, posty mają kolumnę treści, która zawiera cały HTML posta. Dziękuję za sugestię, spróbuję tego jutro. Pytanie brzmi - tabela postów MSSQL waży również ponad 1,5 GB i ma te same wpisy w treści, ale udaje się być znacznie szybsza - dlaczego?
Lars,
2
Możesz również opublikować rzeczywisty plan wykonania z SQL Server. To może być naprawdę interesujące, nawet dla ludzi takich jak ja Postgres.
dezso,
Hmm, szybkie domysły, czy możesz to zmienić GROUP BY u.idna to GROUP BY p.user_idi spróbować? Domyślam się, że Postgres łączy się najpierw i grupuje po drugiej, ponieważ grupujesz według identyfikatora tabeli użytkowników, nawet jeśli potrzebujesz tylko postów user_id, aby uzyskać najwyższe N-wiersze.
UldisK,

Odpowiedzi:

1

Innym dobrym wariantem zapytania jest:

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;

Nie wykorzystuje CTE i daje poprawną odpowiedź (a przykład CTE może wygenerować mniej niż 100 wierszy teoretycznie, ponieważ najpierw ogranicza, a następnie łączy się z użytkownikami).

Podejrzewam, że MSSQL jest w stanie przeprowadzić taką transformację w swoim optymalizatorze zapytań, a PostgreSQL nie jest w stanie przesuwać agregacji podczas łączenia. Lub MSSQL ma po prostu znacznie szybszą implementację łączenia mieszającego.

funny_falcon
źródło
8

To może, ale nie musi działać - opieram to na przeczuciu, że dołącza do twoich stolików przed grupą i filtruje. Przed przystąpieniem do łączenia sugeruję wypróbowanie: filtrowania i grupowania za pomocą 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

Planista zapytań czasami potrzebuje tylko trochę wskazówek. To rozwiązanie działa tutaj dobrze, ale w niektórych okolicznościach CTE mogą być okropne. CTE są przechowywane wyłącznie w pamięci. W wyniku tego duże zwroty danych mogą przekroczyć przydzieloną pamięć Postgres i rozpocząć zamianę (stronicowanie w MS). CTE również nie mogą być indeksowane, więc wystarczająco duże zapytanie może nadal powodować znaczne spowolnienie podczas wysyłania zapytania do CTE.

Najlepszą radą, jaką możesz naprawdę zabrać, jest wypróbowanie jej na wiele sposobów i sprawdzenie planów zapytań.

Scoots
źródło
-1

Czy próbowałeś zwiększyć work_mem? 24Mb wydaje się być za małe, więc Hash Join musi używać wielu partii (zapisanych w plikach tymczasowych).

Konstantin Knizhnik
źródło
To nie jest za małe. Zwiększenie do 240 megabajtów nic nie robi. Pomocne w postgresql.conf byłoby włączenie równoległych zapytań przez dodanie tych dwóch wierszy: max_parallel_workers_per_gather = 4imax_worker_processes = 16
Lars