Consulta JOIN simple muy lenta

12

Estructura de base de datos simple (para un foro en línea):

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);

Alrededor de 80k entradas usersy 2,6 millones de entradas en poststablas. Esta simple consulta para obtener los 100 mejores usuarios por sus publicaciones toma 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

Con set enable_seqscan = falseaún peor:

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 usernamefalta en Postgres, porque no es obligatorio (SQL Server dice que tengo que agrupar por usernamesi quiero seleccionar el nombre de usuario). Agrupar con usernameagrega un poco de ms al tiempo de ejecución en Postgres o no hace nada.

Para la ciencia, instalé Microsoft SQL Server en el mismo servidor (que ejecuta archlinux, 8 core xeon, 24 gb ram, ssd) y migré todos los datos de Postgres: la misma estructura de tabla, los mismos índices, los mismos datos. La misma consulta para obtener los 100 mejores carteles se ejecuta en 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

Produce los mismos resultados de los mismos datos, pero lo hace 8 veces más rápido. Y supongo que es una versión beta de MS SQL en Linux, supongo que se ejecuta en su sistema operativo "doméstico" - Windows Server - podría ser aún más rápido.

¿Mi consulta de PostgreSQL es totalmente incorrecta o PostgreSQL es lenta?

información adicional

La versión es casi la más nueva (9.6.1, actualmente la más reciente es 9.6.2, ArchLinux solo tiene paquetes obsoletos y su actualización es muy lenta). 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 ANALYZEsalidas: https://pastebin.com/HxucRgnk

Intenté todos los índices, utilicé incluso GIN y GIST, la forma más rápida de PostgreSQL (y Google confirma con muchas filas) es usar el análisis secuencial.

MS SQL Server 14.0.405.200-1, configuración predeterminada

Lo uso en una API (con selección simple sin analizar), y al llamar a este punto final de la API con Chrome, dice que toma 2500 ms + -, agregue 50 ms de sobrecarga de HTTP y del servidor web (API y SQL se ejecutan en el mismo servidor) - es lo mismo. No me importan unos 100 ms aquí o allá, lo que me importa son dos segundos completos.

explain analyze SELECT user_id, count(9) FROM posts group by user_id;dura 700 ms. El tamaño de la postsmesa es de 2154 MB.

Lars
fuente
2
Como parece, tienes buenas publicaciones gordas de tus usuarios (~ 1kB en promedio). Podría tener sentido separarlos del resto de la poststabla, utilizando una tabla como De CREATE TABLE post_content (post_id PRIMARY KEY REFERENCES posts (id), content text); esa manera, la mayoría de las E / S que se 'desperdician' en este tipo de consultas podrían ahorrarse. Si las publicaciones son más pequeñas que esto, un VACUUM FULLencendido postspuede ayudar.
dezso
Sí, las publicaciones tienen una columna de contenido que tiene todo el html de una publicación. Gracias por su sugerencia, lo intentaremos mañana. La pregunta es: la tabla de publicaciones de MSSQL también pesa más de 1,5 GB y tiene las mismas entradas en el contenido, pero se las arregla para ser bastante más rápido, ¿por qué?
Lars
2
Posiblemente también podría publicar un plan de ejecución real desde SQL Server. Puede ser realmente interesante, incluso para gente de Postgres como yo.
dezso
Hmm, adivinanzas, ¿podrías cambiar esto GROUP BY u.ida esto GROUP BY p.user_idy probar eso? Supongo que Postgres se une primero y se agrupa por segundo porque está agrupando por identificador de tabla de usuarios, aunque solo necesita publicaciones user_id para obtener las N filas superiores.
UldisK

Respuestas:

1

Otra buena variante de consulta es:

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;

No explota CTE y da la respuesta correcta (y el ejemplo de CTE puede producir menos de 100 filas en teoría porque primero limita y luego se une con los usuarios).

Supongo que MSSQL puede realizar dicha transformación en su optimizador de consultas, y PostgreSQL no puede empujar la agregación debajo de join. O MSSQL solo tiene una implementación de hash join mucho más rápida.

halcón_ divertido
fuente
8

Esto puede o no funcionar: estoy basando esto en un presentimiento de que se está uniendo a sus tablas antes del grupo y el filtro. Sugiero probar lo siguiente: filtrar y agrupar usando un CTE antes de intentar la unión:

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

El planificador de consultas a veces solo necesita un poco de orientación. Esta solución funciona bien aquí, pero los CTE pueden ser terribles en algunas circunstancias. Los CTE se almacenan exclusivamente en la memoria. Como resultado de esto, los grandes retornos de datos pueden exceder la memoria asignada de Postgres y comenzar a intercambiarse (paginación en MS). Los CTE tampoco se pueden indexar, por lo que una consulta lo suficientemente grande aún podría causar una desaceleración significativa al consultar su CTE.

El mejor consejo que realmente puede tomar es probarlo de varias maneras y verificar sus planes de consulta.

Scoots
fuente
-1

¿Intentaste aumentar work_mem? 24Mb parece ser demasiado pequeño, por lo que Hash Join tiene que usar varios lotes (que están escritos en archivos temporales).

Konstantin Knizhnik
fuente
No es muy pequeño. Aumentar a 240 megabytes no hace nada. Lo que ayudaría en postgresql.conf es habilitar consultas paralelas agregando estas dos líneas: max_parallel_workers_per_gather = 4ymax_worker_processes = 16
Lars