Index pour requête SQL avec condition WHERE et GROUP BY


15

J'essaie de déterminer quels index utiliser pour une requête SQL avec une WHEREcondition et GROUP BYqui fonctionne actuellement très lentement.

Ma requête:

SELECT group_id
FROM counter
WHERE ts between timestamp '2014-03-02 00:00:00.0' and timestamp '2014-03-05 12:00:00.0'
GROUP BY group_id

Le tableau compte actuellement 32 000 000 lignes. Le temps d'exécution de la requête augmente beaucoup lorsque j'augmente le délai.

Le tableau en question ressemble à ceci:

CREATE TABLE counter (
    id bigserial PRIMARY KEY
  , ts timestamp NOT NULL
  , group_id bigint NOT NULL
);

J'ai actuellement les index suivants, mais les performances sont encore lentes:

CREATE INDEX ts_index
  ON counter
  USING btree
  (ts);

CREATE INDEX group_id_index
  ON counter
  USING btree
  (group_id);

CREATE INDEX comp_1_index
  ON counter
  USING btree
  (ts, group_id);

CREATE INDEX comp_2_index
  ON counter
  USING btree
  (group_id, ts);

L'exécution d'EXPLAIN sur la requête donne le résultat suivant:

"QUERY PLAN"
"HashAggregate  (cost=467958.16..467958.17 rows=1 width=4)"
"  ->  Index Scan using ts_index on counter  (cost=0.56..467470.93 rows=194892 width=4)"
"        Index Cond: ((ts >= '2014-02-26 00:00:00'::timestamp without time zone) AND (ts <= '2014-02-27 23:59:00'::timestamp without time zone))"

SQL Fiddle avec des exemples de données: http://sqlfiddle.com/#!15/7492b/1

La question

Les performances de cette requête peuvent-elles être améliorées en ajoutant de meilleurs index, ou dois-je augmenter la puissance de traitement?

Modifier 1

La version 9.3.2 de PostgreSQL est utilisée.

Modifier 2

J'ai essayé la proposition de @Erwin avec EXISTS:

SELECT group_id
FROM   groups g
WHERE  EXISTS (
   SELECT 1
   FROM   counter c
   WHERE  c.group_id = g.group_id
   AND    ts BETWEEN timestamp '2014-03-02 00:00:00'
                 AND timestamp '2014-03-05 12:00:00'
   );

Mais malheureusement, cela n'a pas semblé augmenter les performances. Le plan de requête:

"QUERY PLAN"
"Nested Loop Semi Join  (cost=1607.18..371680.60 rows=113 width=4)"
"  ->  Seq Scan on groups g  (cost=0.00..2.33 rows=133 width=4)"
"  ->  Bitmap Heap Scan on counter c  (cost=1607.18..158895.53 rows=60641 width=4)"
"        Recheck Cond: ((group_id = g.id) AND (ts >= '2014-01-01 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
"        ->  Bitmap Index Scan on comp_2_index  (cost=0.00..1592.02 rows=60641 width=0)"
"              Index Cond: ((group_id = g.id) AND (ts >= '2014-01-01 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"

Modifier 3

Le plan de requête pour la requête LATÉRALE de ypercube:

"QUERY PLAN"
"Nested Loop  (cost=8.98..1200.42 rows=133 width=20)"
"  ->  Seq Scan on groups g  (cost=0.00..2.33 rows=133 width=4)"
"  ->  Result  (cost=8.98..8.99 rows=1 width=0)"
"        One-Time Filter: ($1 IS NOT NULL)"
"        InitPlan 1 (returns $1)"
"          ->  Limit  (cost=0.56..4.49 rows=1 width=8)"
"                ->  Index Only Scan using comp_2_index on counter c  (cost=0.56..1098691.21 rows=279808 width=8)"
"                      Index Cond: ((group_id = $0) AND (ts IS NOT NULL) AND (ts >= '2010-03-02 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
"        InitPlan 2 (returns $2)"
"          ->  Limit  (cost=0.56..4.49 rows=1 width=8)"
"                ->  Index Only Scan Backward using comp_2_index on counter c_1  (cost=0.56..1098691.21 rows=279808 width=8)"
"                      Index Cond: ((group_id = $0) AND (ts IS NOT NULL) AND (ts >= '2010-03-02 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"

Combien de group_idvaleurs différentes y a-t-il sur la table?
ypercubeᵀᴹ

Il y a 133 group_id différents.

Les horodatages varient de 2011 à 2014. Les secondes et les millisecondes sont utilisées.

Êtes-vous uniquement intéressé group_idet non dans un décompte?
Erwin Brandstetter

@Erwin Nous nous intéressons aussi à max () et (min) sur une quatrième colonne non montrée dans l'exemple.
uldall

Réponses:


6

Une autre idée, qui utilise également la groupstable et une construction appelée LATERALjoin (pour les fans de SQL-Server, c'est presque identique à OUTER APPLY). Il a l'avantage que les agrégats peuvent être calculés dans la sous-requête:

SELECT group_id, min_ts, max_ts
FROM   groups g,                    -- notice the comma here, is required
  LATERAL 
       ( SELECT MIN(ts) AS min_ts,
                MAX(ts) AS max_ts
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2011-03-02 00:00:00'
                        AND timestamp '2013-03-05 12:00:00'
       ) x 
WHERE min_ts IS NOT NULL ;

Un test sur SQL-Fiddle montre que la requête effectue des analyses d'index sur l' (group_id, ts)index.

Des plans similaires sont produits en utilisant 2 jointures latérales, une pour min et une pour max et également avec 2 sous-requêtes corrélées en ligne. Ils peuvent également être utilisés si vous devez afficher les counterlignes entières en plus des dates min et max:

SELECT group_id, 
       min_ts, min_ts_id, 
       max_ts, max_ts_id 
FROM   groups g
  , LATERAL 
       ( SELECT ts AS min_ts, c.id AS min_ts_id
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2012-03-02 00:00:00'
                        AND timestamp '2014-03-05 12:00:00'
         ORDER BY ts ASC
         LIMIT 1
       ) xmin
  , LATERAL 
       ( SELECT ts AS max_ts, c.id AS max_ts_id
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2012-03-02 00:00:00'
                        AND timestamp '2014-03-05 12:00:00'
         ORDER BY ts DESC 
         LIMIT 1
       ) xmax
WHERE min_ts IS NOT NULL ;

@ypercube J'ai ajouté le plan de requête pour votre requête à la question d'origine. La requête s'exécute en moins de 50 ms même sur de longues périodes.
uldall

5

Puisque vous n'avez aucun agrégat dans la liste de sélection, le group byest à peu près la même chose que de mettre un distinctdans la liste de sélection, non?

Si c'est ce que vous voulez, vous pourriez obtenir une recherche d'index rapide sur comp_2_index en réécrivant ceci pour utiliser une requête récursive, comme décrit sur le wiki PostgreSQL .

Créez une vue pour renvoyer efficacement les group_ids distincts:

create or replace view groups as
WITH RECURSIVE t AS (
             SELECT min(counter.group_id) AS group_id
               FROM counter
    UNION ALL
             SELECT ( SELECT min(counter.group_id) AS min
                       FROM counter
                      WHERE counter.group_id > t.group_id) AS min
               FROM t
              WHERE t.group_id IS NOT NULL
    )
     SELECT t.group_id
       FROM t
      WHERE t.group_id IS NOT NULL
UNION ALL
     SELECT NULL::bigint AS col
      WHERE (EXISTS ( SELECT counter.id,
                counter.ts,
                counter.group_id
               FROM counter
              WHERE counter.group_id IS NULL));

Et puis utilisez cette vue à la place de la table de recherche dans la existssemi-jointure d' Erwin .


4

Puisqu'il n'y en a que 133 different group_id's, vous pouvez utiliser integer(ou même smallint) pour le group_id. Cependant, cela ne vous rapportera pas grand-chose, car le remplissage à 8 octets mangera le reste de votre table et les index multicolonnes possibles. Le traitement de plain integerdevrait cependant être un peu plus rapide. Plus sur le sujet intcontreint2 .

CREATE TABLE counter (
    id bigserial PRIMARY KEY
  , ts timestamp NOT NULL
  , group_id int NOT NULL
);

@Leo: les horodatages sont stockés sous forme d'entiers de 8 octets dans les installations modernes et peuvent être traités parfaitement rapidement. Détails.

@ypercube: L'index sur (group_id, ts)ne peut pas aider, car il n'y a aucune condition group_iddans la requête.

Votre problème principal est la quantité massive de données à traiter:

Scan d'index en utilisant ts_index sur le compteur (coût = 0,56..467470,93 lignes = 194892 largeur = 4)

Je vois que vous êtes uniquement intéressé par l'existence d'un group_id, et pas de décompte réel. De plus, il n'y a que 133 group_idart. Par conséquent, votre requête peut être satisfaite avec le premier hit par gorup_iddans la période. D'où cette suggestion pour une requête alternative avec une EXISTSsemi-jointure :

En supposant une table de recherche pour les groupes:

SELECT group_id
FROM   groups g
WHERE  EXISTS (
   SELECT 1
   FROM   counter c
   WHERE  c.group_id = g.group_id
   AND    ts BETWEEN timestamp '2014-03-02 00:00:00'
                 AND timestamp '2014-03-05 12:00:00'
   );

Votre index comp_2_indexsur (group_id, ts)devient instrumental maintenant.

SQL Fiddle (s'appuyant sur le violon fourni par @ypercube dans les commentaires)

Ici, la requête préfère l'index (ts, group_id), mais je pense que c'est à cause de la configuration de test avec des horodatages "en cluster". Si vous supprimez les index avec interligne ts( plus à ce sujet ), le planificateur utilisera aussi volontiers l'index (group_id, ts)- notamment dans un scan d'index uniquement .

Si cela fonctionne, vous n'aurez peut-être pas besoin de cette autre amélioration possible: pré-agréger les données dans une vue matérialisée pour réduire considérablement le nombre de lignes. Cela aurait du sens en particulier si vous avez également besoin de chiffres réels . Ensuite, vous avez le coût de traiter plusieurs lignes une fois lors de la mise à jour du MV. Vous pouvez même combiner des agrégats quotidiens et horaires (deux tableaux distincts) et adapter votre requête à cela.

Les délais dans vos requêtes sont-ils arbitraires? Ou surtout en minutes / heures / jours complets?

CREATE MATERIALIZED VIEW counter_mv AS
SELECT date_trunc('hour', ts) AS hour
     , group_id
     , count(*) AS ct
GROUP BY 1,2
ORDER BY 1,2;

Créez le ou les index nécessaires counter_mvet adaptez votre requête pour qu'elle fonctionne ...


1
J'ai essayé plusieurs choses similaires dans SQL-Fiddle , avec 10k lignes, mais toutes ont montré une analyse séquentielle. L'utilisation de la groupstable fait-elle la différence?
ypercubeᵀᴹ

@ypercube: Je pense que oui. Cela ANALYZEfait également une différence. Mais les index sont countermême utilisés sans ANALYZEdès que j'introduis la groupstable. Le point est, sans cette table, un seqscan est de toute façon nécessaire pour construire l'ensemble des group_id´s possibles. J'ai ajouté plus à ma réponse. Et merci pour ton violon!
Erwin Brandstetter

C'est étrange. Vous dites que l'optimiseur de Postgres n'utilisera pas l'index group_idmême pour une SELECT DISTINCT group_id FROM t;requête?
ypercubeᵀᴹ

1
@ErwinBrandstetter C'est ce que je pensais aussi, et j'ai été très surpris de découvrir le contraire. Sans LIMIT 1, il peut choisir un scan d'index bitmap, qui ne bénéficie pas d'un arrêt précoce et prend beaucoup plus de temps. (Mais si la table est fraîchement aspirée, il peut préférer l'analyse d'index uniquement à l'analyse bitmap, donc le comportement que vous voyez dépend de l'état de vide de la table).
jjanes

1
@uldall: les agrégats quotidiens réduiront considérablement le nombre de lignes. Cela devrait faire l'affaire. Mais assurez-vous d'essayer la requête EXISTS. Cela pourrait être étonnamment rapide. Ne fonctionnera pas pour min / max en plus. Je serais intéressé par les performances qui en résulteraient, si vous aviez la gentillesse de laisser tomber une ligne ici.
Erwin Brandstetter
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.