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 users
i 2,6 miliona wpisów w posts
tabelach. 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 = false
gorsza:
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 username
brakuje 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ą username
dodaje 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 ANALYZE
wyniki: 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 posts
stołu to 2154 MB.
posts
tabeli 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 FULL
naposts
może pomóc.GROUP BY u.id
na toGROUP BY p.user_id
i 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.Odpowiedzi:
Innym dobrym wariantem zapytania jest:
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.
źródło
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:
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ń.
źródło
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).
źródło
max_parallel_workers_per_gather = 4
imax_worker_processes = 16