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.