Permintaan JOIN sederhana sangat lambat

12

Struktur DB sederhana (untuk 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);

Sekitar 80 usersribu entri dalam dan 2,6 juta entri dalam poststabel. Permintaan sederhana ini untuk mendapatkan 100 pengguna teratas melalui pos mereka membutuhkan 2,4 detik :

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

Dengan set enable_seqscan = falselebih buruk lagi:

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

Dikelompokkan oleh usernamehilang di Postgres, karena itu tidak diperlukan (SQL Server mengatakan saya harus mengelompokkan berdasarkan usernamejika saya ingin memilih nama pengguna). Pengelompokan dengan usernamemenambahkan sedikit ms ke waktu eksekusi di Postgres atau tidak melakukan apa-apa.

Untuk ilmu pengetahuan, saya telah menginstal Microsoft SQL Server ke server yang sama (yang berjalan archlinux, 8 inti xeon, 24 gb ram, SSD) dan bermigrasi semua data dari Postgres - sama struktur tabel, yang sama indeks, yang sama data. Permintaan yang sama untuk mendapatkan 100 poster teratas berjalan dalam 0,3 detik :

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

Menghasilkan hasil yang sama dari data yang sama, tetapi melakukannya 8 kali lebih cepat. Dan ini adalah versi beta MS SQL di Linux, saya kira menjalankannya di OS "home" - Windows Server - masih bisa lebih cepat.

Apakah permintaan PostgreSQL saya benar-benar salah, atau apakah PostgreSQL lambat?

informasi tambahan

Versi hampir merupakan yang terbaru (9.6.1, saat ini yang terbaru adalah 9.6.2, ArchLinux hanya memiliki paket yang sudah ketinggalan zaman dan sangat lambat untuk diperbarui). Konfigurasi:

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

Mencoba semua indeks, digunakan bahkan GIN dan GIST, cara tercepat untuk PostgreSQL (dan Googling mengonfirmasi dengan banyak baris) adalah dengan menggunakan pemindaian berurutan.

MS SQL Server 14.0.405.200-1, konfigurasi default.

Saya menggunakan ini dalam API (dengan polos pilih tanpa menganalisis), dan memanggil titik akhir API ini dengan chrome katanya butuh 2500 ms + -, tambahkan 50 ms HTTP dan overhead server web (API dan SQL berjalan di server yang sama) - itu sama. Saya tidak peduli 100 ms di sini atau di sana, yang saya pedulikan adalah dua detik penuh.

explain analyze SELECT user_id, count(9) FROM posts group by user_id;membutuhkan 700 ms. Ukuran postsmeja adalah 2154 MB.

Lars
sumber
2
Kedengarannya, Anda memiliki postingan bagus yang bagus dari pengguna Anda (~ rata-rata 1kB). Mungkin masuk akal untuk melepaskan mereka dari sisa poststabel, menggunakan tabel seperti CREATE TABLE post_content (post_id PRIMARY KEY REFERENCES posts (id), content text); itu, sebagian besar I / O yang 'terbuang' pada jenis pertanyaan ini dapat dihindarkan. Jika postingan lebih kecil dari ini, sebuah VACUUM FULLon postsdapat membantu.
dezso
Ya, posting memiliki kolom konten yang memiliki semua html posting. Terima kasih atas saran Anda, akan coba besok. Pertanyaannya adalah - Tabel posting MSSQL juga memiliki berat lebih dari 1,5 GB dan memiliki entri yang sama dalam konten, tetapi berhasil menjadi lebih cepat - mengapa?
Lars
2
Anda mungkin dapat memposting rencana eksekusi aktual dari SQL Server juga. Mungkin akan sangat menarik, bahkan untuk orang Postgres seperti saya.
dezso
Hmm, tebakan cepat, bisakah Anda mengubah ini GROUP BY u.idmenjadi ini GROUP BY p.user_iddan mencobanya? Dugaan saya adalah, bahwa Postgres bergabung terlebih dahulu dan dikelompokkan berdasarkan urutan kedua karena Anda mengelompokkan berdasarkan pengidentifikasi tabel pengguna, meskipun Anda hanya perlu memposting user_id untuk mendapatkan baris - N teratas.
UldisK

Jawaban:

1

Varian kueri bagus lainnya adalah:

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;

Itu tidak mengeksploitasi CTE dan memberikan jawaban yang benar (dan contoh CTE dapat menghasilkan kurang dari 100 baris secara teori karena itu membatasi pertama kemudian bergabung dengan pengguna).

Saya kira, MSSQL dapat melakukan transformasi seperti itu dalam optimizer kueri, dan PostgreSQL tidak dapat menekan agregasi di bawah bergabung. Atau MSSQL hanya memiliki implementasi hash yang jauh lebih cepat.

funny_falcon
sumber
8

Ini mungkin atau mungkin tidak bekerja - saya mendasarkan ini pada firasat bahwa itu bergabung dengan tabel Anda sebelum grup dan filter. Saya sarankan mencoba yang berikut: filter dan grup menggunakan CTE sebelum mencoba bergabung:

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

Perencana permintaan terkadang hanya membutuhkan sedikit panduan. Solusi ini bekerja dengan baik di sini, tetapi CTE dapat berpotensi mengerikan dalam beberapa keadaan. CTE disimpan secara eksklusif dalam memori. Sebagai akibatnya, pengembalian data yang besar dapat melebihi memori yang dialokasikan Postgres dan mulai bertukar (paging dalam MS). CTE juga tidak dapat diindeks, sehingga kueri yang cukup besar masih dapat menyebabkan pelambatan yang signifikan saat menanyakan CTE Anda.

Saran terbaik yang dapat Anda ambil adalah mencobanya beberapa cara dan memeriksa paket kueri Anda.

Bergeser
sumber
-1

Apakah Anda mencoba meningkatkan work_mem? 24MB tampaknya terlalu kecil sehingga Hash Join harus menggunakan banyak batch (yang ditulis dalam file temp).

Konstantin Knizhnik
sumber
Itu tidak terlalu kecil. Meningkat menjadi 240 megabita tidak menghasilkan apa-apa. Apa yang akan membantu dalam postgresql.conf adalah mengaktifkan permintaan paralel dengan menambahkan dua baris ini: max_parallel_workers_per_gather = 4danmax_worker_processes = 16
Lars