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


24

J'utilise PostgresSQL 9.2 et j'ai une relation de 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, je fais ce qui suit (ajouté explain analyzepour 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: Voici 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 indexon 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)

Avez-vous également une table Usersavec user_idcomme clé primaire?
ypercubeᵀᴹ

Je viens de voir qu'il y a un addon tiers columnstore pour Postgres. De plus, je voulais juste publier à partir de la nouvelle application ios
swasheck

2
Merci pour la bonne question claire et complète - versions, définitions de table, etc.
Craig Ringer

@ypercube Oui, j'ai une table Utilisateurs.
tomka

Combien de différents project_idet user_id? Le tableau est-il mis à jour en continu ou pourriez-vous travailler avec une vue matérialisée (pendant un certain temps)?
Erwin Brandstetter

Réponses:


25

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 bigintcolonnes. Probablement exagéré. En règle générale, integerest plus que suffisant pour les colonnes comme project_idet user_id. Cela aiderait également l'élément suivant.
    Tout en optimisant la définition de la table, tenez compte de cette réponse connexe, en mettant l'accent sur l'alignement et le remplissage des données . Mais la plupart du reste s'applique également:

  • L' éléphant dans la pièce : il n'y a pas d' index surproject_id . Créer une. C'est plus important que le reste de cette réponse.
    Tout 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, ce integerserait parfait ici:

  • user_idest défini NOT NULL, count(user_id)est donc équivalent à count(*), mais ce dernier est un peu plus court et plus rapide. (Dans cette requête spécifique, cela s'appliquerait même sans user_idêtre défini NOT NULL.)

  • idest déjà la clé primaire, la UNIQUEcontrainte supplémentaire est le ballast inutile . Laisse tomber:

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

    À part: je ne l'utiliserais pas idcomme 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 l'ensemble du tableau 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 qu'impliquant n'importe quel index. Plus de performances à gagner ici - sauf en optimisant les paramètres de la table et du serveur.

Quant à l' analyse d'index uniquement : pour voir à quel point cela peut être efficace, exécutez VACUUM ANALYZEsi 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 uniquement l'index. Lisez d'abord cette réponse connexe:

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


1
Erwin, merci pour vos suggestions. Vous avez raison, user_idet vous project_id integerdevriez être plus que suffisant. Utiliser count(*)au lieu d' count(user_id)économiser environ 70 ms ici, c'est bon à savoir. J'ai ajouté la EXPLAIN ANALYZErequête après avoir ajouté votre suggestion indexau premier message. Cependant, cela n'améliore pas les performances (mais ne nuit pas non plus). Il semble que le indexn'est pas utilisé du tout. Je testerai bientôt les optimisations de schéma.
tomka

1
Si je désactive seqscan, l'index est utilisé ( Index Only Scan using treenode_project_id_user_id_index on treenode), mais la requête prend alors environ 2,5 secondes (soit environ 1 seconde de plus qu'avec seqscan).
tomka

1
Merci pour votre mise à jour. Ces bits manquants auraient dû faire partie de ma question, c'est vrai. Je n'étais simplement pas au courant de leur impact. J'optimiserai mon schéma comme vous l'avez suggéré --- voyons ce que je peux en tirer. Merci pour votre explication, cela a du sens pour moi et je vais donc marquer votre réponse comme acceptée.
tomka

7

Je voudrais d'abord ajouter un index (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 userstable. Sinon, remplacez userspar:
(SELECT DISTINCT user_id FROM treenode)


Merci beaucoup pour votre réponse. Vous avez raison, j'ai une table d'utilisateurs. Cependant, en utilisant votre requête en 9.2, il faut environ 5 secondes pour obtenir le résultat, que l'index soit créé ou non. J'ai créé l'index comme ceci:, CREATE INDEX treenode_user_index ON treenode USING btree (project_id, user_id);mais j'ai également essayé sans la USINGclause. Dois-je manquer quelque chose?
tomka

Combien de lignes y a-t-il dans le userstableau et combien de lignes la requête renvoie-t-elle (donc combien d'utilisateurs y en a-t-il project_id=1)? Pouvez-vous montrer l'explication de cette requête, après avoir ajouté l'index?
ypercubeᵀᴹ

1
Premièrement, j'avais tort dans mon premier commentaire. Sans votre index suggéré, il faut environ 40 secondes (!) Pour récupérer le résultat. Cela prend environ 5s avec le indexen place. Désolé pour la confusion. Dans mon userstableau, j'ai 46 entrées. La requête ne renvoie que 9 lignes. Étonnamment, SELECT DISTINCT user_id FROM treenode WHERE project_id=1;renvoie 38 lignes. J'ai ajouté le explainà mon premier message. Et pour éviter toute confusion: ma userstable est en fait appelée auth_user.
tomka

Je me demande comment peut SELECT DISTINCT user_id FROM treenode WHERE project_id=1;renvoyer 38 lignes alors que les requêtes n'en renvoient que 9. Buffled.
ypercubeᵀᴹ

Pouvez-vous essayer cela?:SET enable_seqscan = OFF; (Query); SET enable_seqscan = ON;
ypercubeᵀᴹ
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.