web-dev-qa-db-fra.com

Améliorer les performances de COUNT / GROUP-BY dans une grande table PostgresQL?

J'utilise PostgresSQL 9.2 et j'ai une relation à 12 colonnes avec environ 6 700 000 lignes. Il contient des nœuds dans un espace 3D, chacun référençant un utilisateur (qui l'a créé). Pour demander quel utilisateur a créé le nombre de nœuds, procédez comme suit (ajouté explain analyze pour plus d'informations):

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

Comme vous pouvez le voir, cela prend environ 1,7 seconde. Ce n'est pas trop mal compte tenu de la quantité de données, mais je me demande si cela peut être amélioré. J'ai essayé d'ajouter un index BTree sur la colonne utilisateur, mais cela n'a aidé en aucune façon.

Avez-vous des suggestions alternatives?


Par souci d'exhaustivité, voici la définition complète de la table avec tous ses indices (sans contraintes, références et déclencheurs de clé étrangère):

    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)

Edit: C'est le résultat, lorsque j'utilise la requête (et l'index) proposée par @ypercube (la requête prend environ 5,3 secondes sans 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

Edit 2: Voici le résultat, lorsque j'utilise un index sur project_id, user_id (mais pas encore d'optimisation de schéma) comme l'a suggéré @ erwin-brandstetter (la requête s'exécute avec 1,5 seconde à la même vitesse que ma requête d'origine):

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)
26
tomka

Le problème principal est l'index manquant. Mais il y a plus.

SELECT user_id, count(*) AS ct
FROM   treenode
WHERE  project_id = 1
GROUP  BY user_id;
  • Vous avez plusieurs colonnes bigint. Probablement exagéré. En général, integer est plus que suffisant pour des colonnes comme project_id Et user_id. Cela aiderait également l'élément suivant.
    Tout en optimisant la définition de la table, considérez cette réponse associée, en mettant l'accent sur alignement des données et remplissage. Mais la plupart du reste s'applique également:

  • L'éléphant dans la pièce : il n'y a pas d'index sur project_id. Créer une. C'est plus important que le reste de cette réponse.
    En y étant, faites-en un index multicolonne:

    CREATE INDEX treenode_project_id_user_id_index ON treenode (project_id, user_id);
    

    Si vous avez suivi mes conseils, integer serait parfait ici:

  • user_id Est défini NOT NULL, Donc count(user_id) est équivalent à count(*), mais cette dernière est un peu plus courte et plus rapide. (Dans cette requête spécifique, cela s'appliquerait même sans que user_id Soit défini NOT NULL.)

  • id est déjà la clé primaire, la contrainte supplémentaire UNIQUE est ballast inutile . Laisse tomber:

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

    En plus: je n'utiliserais pas id comme nom de colonne. Utilisez quelque chose de descriptif comme treenode_id.

Ajout d'informations

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

Cela signifie que Postgres doit lire environ 20% de la table entière pour satisfaire votre requête. À moins qu'il ne puisse utiliser une analyse d'index uniquement , une analyse séquentielle sur la table sera plus rapide que l'implication de tout index. Plus de performances à gagner ici - sauf en optimisant les paramètres de la table et du serveur.

En ce qui concerne l'analyse d'index : pour voir à quel point cela peut être efficace, exécutez VACUUM ANALYZE Si vous pouvez vous le permettre (verrouille la table exclusivement). Réessayez ensuite votre requête. Il devrait maintenant être modérément plus rapide en utilisant seulement l'index. Lisez d'abord cette réponse connexe:

Ainsi que la page de manuel ajoutée avec Postgres 9.6 et le Wiki Postgres sur les scans d'index uniquement .

28

Je voudrais d'abord ajouter un index sur (project_id, user_id) puis dans la version 9.3, essayez cette requête:

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, essayez celui-ci:

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 ;

Je suppose que vous avez une table users. Sinon, remplacez users par:
(SELECT DISTINCT user_id FROM treenode)

7
ypercubeᵀᴹ