¿Mejorar el rendimiento de COUNT / GROUP-BY en una gran tabla PostgresSQL?

24

Estoy ejecutando PostgresSQL 9.2 y tengo una relación de 12 columnas con aproximadamente 6,700,000 filas. Contiene nodos en un espacio 3D, cada uno de los cuales hace referencia a un usuario (que lo creó). Para consultar qué usuario ha creado cuántos nodos hago lo siguiente (agregado explain analyzepara obtener más información):

EXPLAIN ANALYZE SELECT user_id, count(user_id) FROM treenode WHERE project_id=1 GROUP BY user_id;
                                                    QUERY PLAN                                                         
---------------------------------------------------------------------------------------------------------------------------
 HashAggregate  (cost=253668.70..253669.07 rows=37 width=8) (actual time=1747.620..1747.623 rows=38 loops=1)
   ->  Seq Scan on treenode  (cost=0.00..220278.79 rows=6677983 width=8) (actual time=0.019..886.803 rows=6677983 loops=1)
         Filter: (project_id = 1)
 Total runtime: 1747.653 ms

Como puede ver, esto toma alrededor de 1.7 segundos. Esto no es tan malo teniendo en cuenta la cantidad de datos, pero me pregunto si esto puede mejorarse. Traté de agregar un índice BTree en la columna de usuario, pero esto no ayudó de ninguna manera.

¿Tienes sugerencias alternativas?


En aras de la exhaustividad, esta es la definición de tabla completa con todos sus índices (sin restricciones de clave externa, referencias y desencadenantes):

    Column     |           Type           |                      Modifiers                    
---------------+--------------------------+------------------------------------------------------
 id            | bigint                   | not null default nextval('concept_id_seq'::regclass)
 user_id       | bigint                   | not null
 creation_time | timestamp with time zone | not null default now()
 edition_time  | timestamp with time zone | not null default now()
 project_id    | bigint                   | not null
 location      | double3d                 | not null
 reviewer_id   | integer                  | not null default (-1)
 review_time   | timestamp with time zone |
 editor_id     | integer                  |
 parent_id     | bigint                   |
 radius        | double precision         | not null default 0
 confidence    | integer                  | not null default 5
 skeleton_id   | bigint                   |
Indexes:
    "treenode_pkey" PRIMARY KEY, btree (id)
    "treenode_id_key" UNIQUE CONSTRAINT, btree (id)
    "skeleton_id_treenode_index" btree (skeleton_id)
    "treenode_editor_index" btree (editor_id)
    "treenode_location_x_index" btree (((location).x))
    "treenode_location_y_index" btree (((location).y))
    "treenode_location_z_index" btree (((location).z))
    "treenode_parent_id" btree (parent_id)
    "treenode_user_index" btree (user_id)

Editar: este es el resultado, cuando uso la consulta (e índice) propuesta por @ypercube (la consulta tarda aproximadamente 5,3 segundos sin EXPLAIN ANALYZE):

EXPLAIN ANALYZE SELECT u.id, ( SELECT COUNT(*) FROM treenode AS t WHERE t.project_id=1 AND t.user_id = u.id ) AS number_of_nodes FROM auth_user As u;
                                                                        QUERY PLAN                                                                     
----------------------------------------------------------------------------------------------------------------------------------------------------------
 Seq Scan on auth_user u  (cost=0.00..6987937.85 rows=46 width=4) (actual time=29.934..5556.147 rows=46 loops=1)
   SubPlan 1
     ->  Aggregate  (cost=151911.65..151911.66 rows=1 width=0) (actual time=120.780..120.780 rows=1 loops=46)
           ->  Bitmap Heap Scan on treenode t  (cost=4634.41..151460.44 rows=180486 width=0) (actual time=13.785..114.021 rows=145174 loops=46)
                 Recheck Cond: ((project_id = 1) AND (user_id = u.id))
                 Rows Removed by Index Recheck: 461076
                 ->  Bitmap Index Scan on treenode_user_index  (cost=0.00..4589.29 rows=180486 width=0) (actual time=13.082..13.082 rows=145174 loops=46)
                       Index Cond: ((project_id = 1) AND (user_id = u.id))
 Total runtime: 5556.190 ms
(9 rows)

Time: 5556.804 ms

Edición 2: este es el resultado, cuando uso un indexencendido project_id, user_id(pero todavía no hay optimización de esquema) como sugirió @ erwin-brandstetter (la consulta se ejecuta con 1,5 segundos a la misma velocidad que mi consulta original):

EXPLAIN ANALYZE SELECT user_id, count(user_id) as ct FROM treenode WHERE project_id=1 GROUP BY user_id;
                                                        QUERY PLAN                                                      
---------------------------------------------------------------------------------------------------------------------------
 HashAggregate  (cost=253670.88..253671.24 rows=37 width=8) (actual time=1807.334..1807.339 rows=38 loops=1)
   ->  Seq Scan on treenode  (cost=0.00..220280.62 rows=6678050 width=8) (actual time=0.183..893.491 rows=6678050 loops=1)
         Filter: (project_id = 1)
 Total runtime: 1807.368 ms
(4 rows)
tomka
fuente
Qué usted también tiene una mesa Userscon user_idcomo la clave principal?
ypercubeᵀᴹ
Acabo de ver que hay un complemento de almacén de columnas de terceros para Postgres. Además, solo quería publicar desde la nueva aplicación ios
swasheck el
2
Gracias por la pregunta buena, clara y completa: versiones, definiciones de tabla, etc.
Craig Ringer
@ypercube Sí, tengo una tabla de usuarios.
tomka
¿Cuántos diferentes project_idy user_id? ¿La tabla se actualiza continuamente o podría trabajar con una vista materializada (por algún tiempo)?
Erwin Brandstetter

Respuestas:

25

El problema principal es el índice que falta. Pero hay más.

SELECT user_id, count(*) AS ct
FROM   treenode
WHERE  project_id = 1
GROUP  BY user_id;
  • Tienes muchas bigintcolumnas Probablemente exagerado. Por lo general, integeres más que suficiente para columnas como project_idy user_id. Esto también ayudaría al siguiente elemento.
    Mientras optimiza la definición de la tabla, considere esta respuesta relacionada, con énfasis en la alineación de datos y el relleno . Pero la mayor parte del resto también se aplica:

  • El elefante en la habitación : no hay índiceproject_id . Crea uno. Esto es más importante que el resto de esta respuesta.
    Mientras lo hace, haga que sea un índice de varias columnas:

    CREATE INDEX treenode_project_id_user_id_index ON treenode (project_id, user_id);

    Si siguieras mi consejo, integersería perfecto aquí:

  • user_idestá definido NOT NULL, por lo que count(user_id)es equivalente a count(*), pero este último es un poco más corto y más rápido. (En esta consulta específica, esto incluso se aplicaría sin user_idestar definido NOT NULL).

  • idya es la clave principal, la UNIQUErestricción adicional es el lastre inútil . Déjalo caer:

    "treenode_pkey" PRIMARY KEY, btree (id)
    "treenode_id_key" UNIQUE CONSTRAINT, btree (id)

    Aparte: no lo usaría idcomo nombre de columna. Use algo descriptivo como treenode_id.

Información agregada

Q: How many different project_id and user_id?
A: not more than five different project_id.

Eso significa que Postgres tiene que leer aproximadamente el 20% de toda la tabla para satisfacer su consulta. A menos que pueda usar un escaneo de solo índice , un escaneo secuencial en la tabla será más rápido que cualquier índice. No hay más rendimiento que ganar aquí, excepto mediante la optimización de la tabla y la configuración del servidor.

En cuanto a la exploración de solo índice : para ver qué tan efectivo puede ser, ejecute VACUUM ANALYZEsi puede permitírselo (bloquea la tabla exclusivamente). Luego intente su consulta nuevamente. Ahora debería ser moderadamente más rápido usando solo el índice. Lea esta respuesta relacionada primero:

Además de la página del manual agregada con Postgres 9.6 y Postgres Wiki en escaneos de solo índice .

Erwin Brandstetter
fuente
1
Erwin, gracias por tus sugerencias. Tienes razón, para user_idy project_id integerdebería ser más que suficiente. Usar en count(*)lugar de count(user_id)guardar unos 70 ms aquí, es bueno saberlo. He agregado el EXPLAIN ANALYZEde la consulta después de haber agregado su sugerencia indexa la primera publicación. Sin embargo, no mejora el rendimiento (pero tampoco duele). Parece indexque no se usa en absoluto. Probaré las optimizaciones del esquema pronto.
tomka
1
Si lo deshabilito seqscan, se usa el índice ( Index Only Scan using treenode_project_id_user_id_index on treenode), pero la consulta tarda aproximadamente 2.5 segundos (que es aproximadamente 1 segundo más que con seqscan).
tomka
1
Gracias por tu actualizacion. Estas partes faltantes deberían haber sido parte de mi pregunta, eso es correcto. Simplemente no estaba al tanto de su impacto. Optimizaré mi esquema como sugirió --- veamos qué puedo obtener de eso. Gracias por su explicación, tiene sentido para mí y, por lo tanto, marcaré su respuesta como la aceptada.
tomka
7

Primero agregaría un índice (project_id, user_id)y luego en la versión 9.3, intente esta consulta:

SELECT u.user_id, c.number_of_nodes 
FROM users AS u
   , LATERAL
     ( SELECT COUNT(*) AS number_of_nodes 
       FROM treenode AS t
       WHERE t.project_id = 1 
         AND t.user_id = u.user_id
     ) c 
-- WHERE c.number_of_nodes > 0 ;   -- you probably want this as well
                                   -- to show only relevant users

En 9.2, prueba este:

SELECT u.user_id, 
       ( SELECT COUNT(*) 
         FROM treenode AS t
         WHERE t.project_id = 1 
           AND t.user_id = u.user_id
       ) AS number_of_nodes  
FROM users AS u ;

Supongo que tienes una usersmesa. Si no, reemplace userscon:
(SELECT DISTINCT user_id FROM treenode)

ypercubeᵀᴹ
fuente
Muchas gracias por su respuesta. Tienes razón, tengo una tabla de usuarios. Sin embargo, el uso de su consulta en 9.2 tarda unos 5 segundos en obtener el resultado, independientemente de si el índice se crea o no. Creé el índice así: CREATE INDEX treenode_user_index ON treenode USING btree (project_id, user_id);pero intenté también sin la USINGcláusula. ¿Echo de menos algo?
tomka
¿Cuántas filas hay en la userstabla y cuántas filas devuelve la consulta (entonces, ¿cuántos usuarios hay project_id=1)? ¿Puede mostrar la explicación de esta consulta después de agregar el índice?
ypercubeᵀᴹ
1
Primero, me equivoqué en mi primer comentario. Sin su índice sugerido, se necesitan unos 40 segundos (!) Para recuperar el resultado. Se tarda unos 5 segundos con el indexlugar. Perdón por la confusion. En mi usersmesa tengo 46 entradas. La consulta solo devuelve 9 filas. Sorprendentemente, SELECT DISTINCT user_id FROM treenode WHERE project_id=1;devuelve 38 filas. He añadido el explaina mi primera publicación. Y para evitar confusiones: mi usersmesa se llama realmente auth_user.
tomka
Me pregunto cómo puede SELECT DISTINCT user_id FROM treenode WHERE project_id=1;devolver 38 filas mientras que las consultas devuelven solo 9. Buffled.
ypercubeᵀᴹ
¿Puedes probar esto ?:SET enable_seqscan = OFF; (Query); SET enable_seqscan = ON;
ypercubeᵀᴹ