web-dev-qa-db-fra.com

GROUP BY et COUNT dans PostgreSQL

La requête:

SELECT COUNT(*) as count_all, 
       posts.id as post_id 
FROM posts 
  INNER JOIN votes ON votes.post_id = posts.id 
GROUP BY posts.id;

Renvoie les enregistrements n dans PostgreSQL:

 count_all | post_id
-----------+---------
 1         | 6
 3         | 4
 3         | 5
 3         | 1
 1         | 9
 1         | 10
(6 rows)

Je veux juste récupérer le nombre d'enregistrements retournés: 6.

J'ai utilisé une sous-requête pour obtenir ce que je veux, mais cela ne semble pas optimal:

SELECT COUNT(*) FROM (
    SELECT COUNT(*) as count_all, posts.id as post_id 
    FROM posts 
    INNER JOIN votes ON votes.post_id = posts.id 
    GROUP BY posts.id
) as x;

Comment puis-je obtenir le nombre d'enregistrements dans ce contexte directement dans PostgreSQL?

34
skinkelynet

Je pense que vous avez juste besoin de COUNT(DISTINCT post_id) FROM votes.

Voir la section "4.2.7. Expressions agrégées" dans http://www.postgresql.org/docs/current/static/sql-expressions.html .

EDIT: Correction de mon erreur imprudente selon le commentaire d'Erwin.

55
Steve Jorgensen

Il y a aussi EXISTS :

SELECT count(*) AS post_ct
FROM   posts p
WHERE  EXISTS (SELECT FROM votes v WHERE v.post_id = p.id);

Dans Postgres et avec plusieurs entrées du côté n - comme vous l'avez probablement, c'est généralement plus rapide que count(DISTINCT post_id) :

SELECT count(DISTINCT p.id) AS post_ct
FROM   posts p
JOIN   votes v ON v.post_id = p.id;

Plus il y a de lignes par publication dans votes, plus la différence de performances est grande. Testez avec EXPLAIN ANALYZE .

count(DISTINCT post_id) doit lire tous lignes, les trier ou les hacher, puis ne considérer que la première par ensemble identique. EXISTS analysera uniquement votes (ou, de préférence, un index sur post_id) jusqu'à ce que la première correspondance soit trouvée.

Si chaque post_id Dans votes est garanti d'être présent dans la table posts (intégrité référentielle appliquée avec une contrainte de clé étrangère), cette forme abrégée équivaut à la forme plus longue:

SELECT count(DISTINCT post_id) AS post_ct
FROM   votes;

Peut être plus rapide que la requête EXISTS avec pas ou peu d'entrées par article.

La requête que vous aviez fonctionne également sous une forme plus simple:

SELECT count(*) AS post_ct
FROM  (
    SELECT FROM posts 
    JOIN   votes ON votes.post_id = posts.id 
    GROUP  BY posts.id
    ) sub;

Référence

Pour vérifier mes réclamations, j'ai exécuté une référence sur mon serveur de test avec des ressources limitées. Le tout dans un schéma distinct:

Configuration de test

Faux une situation typique de post/vote:

CREATE SCHEMA y;
SET search_path = y;

CREATE TABLE posts (
  id   int PRIMARY KEY
, post text
);

INSERT INTO posts
SELECT g, repeat(chr(g%100 + 32), (random()* 500)::int)  -- random text
FROM   generate_series(1,10000) g;

DELETE FROM posts WHERE random() > 0.9;  -- create ~ 10 % dead tuples

CREATE TABLE votes (
  vote_id serial PRIMARY KEY
, post_id int REFERENCES posts(id)
, up_down bool
);

INSERT INTO votes (post_id, up_down)
SELECT g.* 
FROM  (
   SELECT ((random()* 21)^3)::int + 1111 AS post_id  -- uneven distribution
        , random()::int::bool AS up_down
   FROM   generate_series(1,70000)
   ) g
JOIN   posts p ON p.id = g.post_id;

Toutes les requêtes suivantes ont renvoyé le même résultat (8093 des 9107 messages avaient des votes).
J'ai effectué 4 tests avec EXPLAIN ANALYZE Et j'ai pris le meilleur de cinq sur Postgres 9.1.4 avec chacun des trois interroge et ajoute le temps d'exécution total résultant .

  1. Comme si.

  2. Après ..

    ANALYZE posts;
    ANALYZE votes;
    
  3. Après ..

    CREATE INDEX foo on votes(post_id);
    
  4. Après ..

    VACUUM FULL ANALYZE posts;
    CLUSTER votes using foo;
    

count(*) ... WHERE EXISTS

  1. 253 ms
  2. 220 ms
  3. 85 ms - vainqueur (scan seq sur les posts, scan index sur votes, boucle imbriquée)
  4. 85 ms

count(DISTINCT x) - forme longue avec jointure

  1. 354 ms
  2. 358 ms
  3. 373 ms balayage d'index sur les messages, balayage d'index sur les votes, fusionner la jointure)
  4. 330 ms

count(DISTINCT x) - forme courte sans jointure

  1. 164 ms
  2. 164 ms
  3. 164 ms - (toujours balayage séquentiel)
  4. 142 ms

Meilleur moment pour requête originale en question:

Pour version simplifiée :

La requête de @ wildplasser avec un CTE utilise le même plan que le formulaire long (scan d'index sur les publications, scan d'index sur les votes, fusionner la jointure) plus un peu de surcharge pour le CTE. Meilleur temps:

--- (Les analyses indexées uniquement dans le prochain PostgreSQL 9.2 peuvent améliorer le résultat pour chacune de ces requêtes, surtout pour EXISTS.

Benchmark connexe et plus détaillé pour Postgres 9.5 (en fait récupérer des lignes distinctes, pas seulement compter)

39
Erwin Brandstetter

Utilisation de OVER() et LIMIT 1:

SELECT COUNT(1) OVER()
FROM posts 
   INNER JOIN votes ON votes.post_id = posts.id 
GROUP BY posts.id
LIMIT 1;
5
mnv
WITH uniq AS (
        SELECT DISTINCT posts.id as post_id
        FROM posts
        JOIN votes ON votes.post_id = posts.id
        -- GROUP BY not needed anymore
        -- GROUP BY posts.id
        )
SELECT COUNT(*)
FROM uniq;
2
wildplasser