web-dev-qa-db-fra.com

Index pour la requête SQL avec la condition et le groupe par

J'essaie de déterminer quels index à utiliser pour une requête SQL avec une condition WHERE et un GROUP BY qui 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 a actuellement 32 000 000 rangées. L'heure d'exécution de la requête augmente beaucoup lorsque j'augmente le délai.

La table 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 la performance est toujours lente:

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);

Expliquer expliquer 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 données d'exemple: http://sqlfiddle.com/#!15/7492b/1

La question

Les performances de cette requête peuvent-elles être améliorées en ajoutant de meilleurs indices ou d'augmenter le pouvoir de traitement?

Éditer 1

PostgreSQL version 9.3.2 est utilisé.

Éditer 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 ne semblait pas augmenter la performance. 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))"

Éditer 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))"
15
uldall

Une autre idée, qui utilise également la table groups et une construction appelée LATERAL Joindre (pour les ventilateurs SQL-Server, ceci est presque identique à OUTER APPLY). Il présente 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 ;

Testez à SQL-FIDDLE montre que la requête utilise des analyses sur le (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 pourraient également être utilisés si vous devez montrer l'ensemble counter lignes 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 ;
6
ypercubeᵀᴹ

Puisque vous n'avez pas d'agrégat dans la liste Sélectionner, le group by est à peu près la même chose que de mettre un distinct dans la liste Sélectionner, non?

Si tel est ce que vous voulez, vous pourrez peut-être obtenir une recherche rapide sur Comp_2_Index en réécrivez cela pour utiliser une requête récursive, comme décrit sur le wiki PostgreSQL .

Vu la vue pour renvoyer efficacement les groupes 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));

Puis utilisez cette vue à la place de la table de recherche dans erwin exists semi-rejoindre.

5
jjanes

Comme il n'y a que 133 different group_id's, vous pouvez utiliser integer (ou même smallint) pour le groupe_id. Cela ne vous achètera pas beaucoup, car le rembourrage à 8 octets mangera le reste de votre table et des indices multicolonnels possibles. Traitement de la plaine integer devrait être un peu plus rapide, cependant. Plus sur int vs. int2 .

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

@Leo: Les horodatées sont stockées sous forme d'entiers de 8 octets dans des installations modernes et peuvent être traitées parfaitement rapidement. Détails.

@ypercube: L'index sur (group_id, ts) ne peut pas aider, car il n'y a pas d'état sur group_id dans la requête.

Votre Main Problème est la quantité massive de données qui doivent être traitées:

Numérisation d'index à l'aide de Ts_Index sur le compteur (Coût = 0.56..467470.93 Rows = 194892 Largeur = 4)

Je vois que vous n'êtes intéressé que par l'existence d'un group_id et pas de compte réel. De plus, il n'y a que 133 différents group_id s. Par conséquent, votre requête peut être satisfaite du premier coup par gorup_id dans la période. Par conséquent, cette suggestion d'une requête alternative avec un EXISTS semi-join :

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_index sur (group_id, ts) devient instrumental maintenant.

SQL FIDDLE (bâtiment sur le violon fourni par @ypercube dans les commentaires)

Ici, la requête préfère l'index sur (ts, group_id), mais je pense que c'est à cause de la configuration de test avec des horodatages "regroupés". Si vous supprimez les index avec le meneur ts ( plus à ce sujet ), le planificateur utilisera judicieusement l'index sur (group_id, ts) aussi - notamment dans un Scan .

Si cela fonctionne, vous n'avez peut-être pas besoin de cette autre amélioration possible: Pré-agrégat de données dans une vue matérialisée Pour réduire considérablement le nombre de lignes. Cela aurait un sens en particulier, si vous avez également besoin d'actualité Nombre En outre. Ensuite, vous avez le coût pour traiter beaucoup rangées une fois lors de la mise à jour du MV. Vous pouvez même combiner des agrégats quotidiens et horaires (deux tables séparées) et adaptez votre requête à cela.

Les cadres de temps dans vos requêtes sont-ils arbitraires? Ou principalement en minutes/heures/jours?

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 l'index (ES) nécessaire sur counter_mv et adaptez votre requête pour travailler avec elle ...

4
Erwin Brandstetter