Query JOIN semplice molto lenta


12

Struttura DB semplice (per un 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);

Circa 80.000 voci userse 2,6 milioni di voci nelle poststabelle. Questa semplice query per ottenere i primi 100 utenti dai loro post richiede 2,4 secondi :

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

Con set enable_seqscan = falseancora peggio:

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

Raggruppa per usernamemanca in Postgres, perché non è necessario (SQL Server dice che devo raggruppare usernamese voglio selezionare il nome utente). Il raggruppamento con usernameaggiunge un po 'di ms al tempo di esecuzione su Postgres o non fa nulla.

Per la scienza, ho installato Microsoft SQL Server sullo stesso server (che esegue archlinux, 8 core xeon, 24 gb ram, ssd) e ho migrato tutti i dati da Postgres - stessa struttura di tabella, stessi indici, stessi dati. Stessa query per ottenere i primi 100 poster in 0,3 secondi :

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

Produce gli stessi risultati dagli stessi dati, ma lo fa 8 volte più velocemente. Ed è la versione beta di MS SQL su Linux, immagino che sia in esecuzione sul suo sistema operativo "home" - Windows Server - potrebbe essere ancora più veloce.

La mia query PostgreSQL è totalmente sbagliata o PostgreSQL è solo lento?

informazioni addizionali

La versione è quasi la più recente (9.6.1, attualmente la più recente è 9.6.2, ArchLinux ha pacchetti obsoleti ed è molto lento da aggiornare). 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 ANALYZEuscite: https://pastebin.com/HxucRgnk

Ho provato tutti gli indici, usato anche GIN e GIST, il modo più veloce per PostgreSQL (e Google conferma con molte righe) è usare la scansione sequenziale.

MS SQL Server 14.0.405.200-1, conf predefinito.

Lo uso in un'API (con selezione semplice senza analisi) e, chiamando questo endpoint API con Chrome, si dice che impiega 2500 ms + -, aggiungere 50 ms di sovraccarico HTTP e web server (API e SQL eseguiti sullo stesso server) - è lo stesso. Non mi interessa circa 100 ms qua o là, quello che mi interessa è di due interi secondi.

explain analyze SELECT user_id, count(9) FROM posts group by user_id;richiede 700 ms. La dimensione della poststabella è 2154 MB.


2
A quanto pare, hai dei bei post grassi dei tuoi utenti (~ 1kB in media). Potrebbe avere senso staccarli dal resto della poststabella, usando una tabella come CREATE TABLE post_content (post_id PRIMARY KEY REFERENCES posts (id), content text); quella, la maggior parte dell'I / O "sprecata" su questo tipo di query potrebbe essere risparmiata. Se i post sono più piccoli di questo, un VACUUM FULLon postspuò aiutare.
— dezso

Sì, i post hanno una colonna di contenuti che contiene tutto l'html di un post. Grazie per il tuo suggerimento, lo proverò domani. La domanda è: la tabella dei messaggi MSSQL pesa anche oltre 1,5 GB e ha le stesse voci nel contenuto, ma riesce ad essere abbastanza più veloce - perché?
— Lars,

2
È possibile che sia possibile pubblicare un piano di esecuzione effettivo anche da SQL Server. Potrebbe essere davvero interessante, anche per Postgres come me.
— dezso

Hmm, ipotesi veloci, potresti cambiarlo GROUP BY u.idin questo GROUP BY p.user_ide provarlo? Suppongo che Postgres si unisca per primo e si raggruppa per secondo perché stai raggruppando in base all'identificatore della tabella degli utenti, anche se hai bisogno solo dei post user_id per ottenere le prime N - righe.
— UldisK,

Risposte:


1

Un'altra buona variante di query è:

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;

Non sfrutta CTE e fornisce una risposta corretta (e l'esempio CTE può produrre meno di 100 righe in teoria perché prima limita e poi si unisce agli utenti).

Suppongo che MSSQL sia in grado di eseguire tale trasformazione nel suo Query Optimizer e PostgreSQL non sia in grado di ridurre l'aggregazione sotto join. Oppure MSSQL ha un'implementazione dell'hash hash molto più veloce.


8

Questo può o non può funzionare - lo sto basando sulla sensazione che si unisca ai tuoi tavoli prima del gruppo e del filtro. Suggerisco di provare quanto segue: filtrare e raggruppare utilizzando un CTE prima di tentare il join:

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

Il pianificatore di query a volte necessita solo di una piccola guida. Questa soluzione funziona bene qui, ma i CTE possono potenzialmente essere terribili in alcune circostanze. I CTE sono archiviati esclusivamente in memoria. Di conseguenza, grandi restituzioni di dati possono superare la memoria allocata di Postgres e iniziare lo scambio (paging in MS). Inoltre, i CTE non possono essere indicizzati, quindi una query sufficientemente grande potrebbe comunque causare un rallentamento significativo durante la query del CTE.

Il miglior consiglio che puoi davvero portare via è provarlo in diversi modi e controllare i tuoi piani di query.


-1

Hai provato ad aumentare work_mem? 24 Mb sembra essere troppo piccolo e quindi Hash Join deve usare più batch (che sono scritti in file temporanei).


Non è troppo piccolo. L'aumento a 240 megabyte non fa nulla. Ciò che aiuterebbe in postgresql.conf è abilitare le query parallele aggiungendo queste due righe: max_parallel_workers_per_gather = 4emax_worker_processes = 16
— Lars
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.