Consulta JOIN simples muito lenta

12

Estrutura simples de banco de dados (para um fórum 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);

Cerca de 80 usersmil entradas e 2,6 milhões de entradas em poststabelas. Essa consulta simples para obter os 100 principais usuários por suas postagens leva 2,4 segundos :

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

Com set enable_seqscan = falseainda pior:

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

Agrupar por usernameestá ausente no Postgres, porque não é necessário (o SQL Server diz que preciso agrupar usernamese quiser selecionar o nome de usuário). Agrupar com usernameadiciona um pouco de ms ao tempo de execução no Postgres ou não faz nada.

Para fins científicos, instalei o Microsoft SQL Server no mesmo servidor (que executa o archlinux, 8 núcleos xeon, 24 gb de ram, ssd) e migrei todos os dados do Postgres - a mesma estrutura de tabela, os mesmos índices e os mesmos dados. A mesma consulta para obter os 100 melhores pôsteres é executada em 0,3 segundos :

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

Produz os mesmos resultados dos mesmos dados, mas é 8 vezes mais rápido. E é a versão beta do MS SQL no Linux, eu acho que rodando em seu sistema operacional "doméstico" - Windows Server - pode ser ainda mais rápido.

Minha consulta ao PostgreSQL está totalmente errada ou o PostgreSQL está lento?

informação adicional

A versão é quase a mais recente (9.6.1, atualmente a mais recente é a 9.6.2, o ArchLinux apenas possui pacotes desatualizados e é muito lento para atualizar). 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 ANALYZEsaídas: https://pastebin.com/HxucRgnk

Tentei todos os índices, usados ​​mesmo GIN e GIST, a maneira mais rápida para o PostgreSQL (e o Google confirma com muitas linhas) usando varredura seqüencial.

MS SQL Server 14.0.405.200-1, padrão conf.

Eu uso isso em uma API (com seleção simples sem análise) e, chamando esse ponto de extremidade da API com o Chrome, ele diz que leva 2500 ms + -, adicione 50 ms de sobrecarga de HTTP e sobrecarga do servidor da Web (API e SQL executadas no mesmo servidor) - é o mesmo. Eu não me importo com 100 ms aqui ou ali, o que me interessa são dois segundos inteiros.

explain analyze SELECT user_id, count(9) FROM posts group by user_id;leva 700 ms. O tamanho da poststabela é 2154 MB.

Lars
fonte
2
Parece que você tem ótimas postagens gordas de seus usuários (~ 1kB em média). Pode fazer sentido desanexá-los do restante da poststabela, usando uma tabela como CREATE TABLE post_content (post_id PRIMARY KEY REFERENCES posts (id), content text); Dessa forma, a maioria das E / S 'desperdiçadas' nesse tipo de consulta pode ser poupada. Se as postagens forem menores que isso, um VACUUM FULLon postspode ajudar.
Dez17
Sim, as postagens têm uma coluna de conteúdo que possui todo o html de uma postagem. Obrigado pela sua sugestão, vou tentar isso amanhã. A pergunta é: a tabela de postagens do MSSQL também pesa mais de 1,5 GB e tem as mesmas entradas no conteúdo, mas consegue ser bem mais rápida - por quê?
Lars
2
Você também pode postar um plano de execução real do SQL Server. Pode ser realmente interessante, mesmo para pessoas do Postgres como eu.
Dez17
Hmm, suposições rápidas, você poderia mudar isso GROUP BY u.idpara isso GROUP BY p.user_ide tentar aquilo? Meu palpite é que o Postgres se junta primeiro e agrupa por segundo porque você está agrupando por identificador de tabela de usuários, mesmo que você precise apenas postar user_id para obter as N-linhas principais.
UdisK

Respostas:

1

Outra boa variante de consulta é:

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;

Ele não explora o CTE e fornece a resposta correta (e o exemplo do CTE pode produzir menos de 100 linhas em teoria, porque primeiro limita e depois se une aos usuários).

Suponho que o MSSQL seja capaz de executar essa transformação em seu otimizador de consultas, e o PostgreSQL não seja capaz de reduzir a agregação sob junção. Ou o MSSQL possui apenas uma implementação de junção de hash muito mais rápida.

funny_falcon
fonte
8

Isso pode ou não funcionar - eu estou baseando isso em um pressentimento de que ele está se juntando às suas mesas antes do grupo e do filtro. Sugiro tentar o seguinte: filtre e agrupe usando um CTE antes de tentar a associação:

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

Às vezes, o planejador de consultas precisa de um pouco de orientação. Essa solução funciona bem aqui, mas as CTEs podem ser terríveis em algumas circunstâncias. CTEs são armazenados exclusivamente na memória. Como resultado disso, grandes retornos de dados podem exceder a memória alocada do Postgres e começar a trocar (paginação no MS). Os CTEs também não podem ser indexados; portanto, uma consulta suficientemente grande ainda pode causar uma desaceleração significativa ao consultar seu CTE.

O melhor conselho que você pode seguir é tentar de várias maneiras e verificar seus planos de consulta.

Scoots
fonte
-1

Você tentou aumentar o work_mem? 24 Mb parece ser muito pequeno e, portanto, o Hash Join precisa usar vários lotes (gravados em arquivos temporários).

Konstantin Knizhnik
fonte
Não é muito pequeno. Aumentar para 240 megabytes não faz nada. O que ajudaria no postgresql.conf é permitindo consultas paralelas, adicionando estas duas linhas: max_parallel_workers_per_gather = 4emax_worker_processes = 16
Lars