J'essaie de déterminer quels index utiliser pour une requête SQL avec une WHERE
condition et 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 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))"
group_id
valeurs différentes y a-t-il sur la table?group_id
et non dans un décompte?Réponses:
Une autre idée, qui utilise également la
groups
table et une construction appeléeLATERAL
join (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: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
counter
lignes entières en plus des dates min et max:la source
Puisque vous n'avez aucun agrégat dans la liste de sélection, le
group by
est à peu près la même chose que de mettre undistinct
dans 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:
Et puis utilisez cette vue à la place de la table de recherche dans la
exists
semi-jointure d' Erwin .la source
Puisqu'il n'y en a que
133 different group_id's
, vous pouvez utiliserinteger
(ou mêmesmallint
) 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 plaininteger
devrait cependant être un peu plus rapide. Plus sur le sujetint
contreint2
.@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 conditiongroup_id
dans la requête.Votre problème principal est la quantité massive de données à traiter:
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 133group_id
art. Par conséquent, votre requête peut être satisfaite avec le premier hit pargorup_id
dans la période. D'où cette suggestion pour une requête alternative avec uneEXISTS
semi-jointure :En supposant une table de recherche pour les groupes:
Votre index
comp_2_index
sur(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 interlignets
( 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?
Créez le ou les index nécessaires
counter_mv
et adaptez votre requête pour qu'elle fonctionne ...la source
groups
table fait-elle la différence?ANALYZE
fait également une différence. Mais les index sontcounter
même utilisés sansANALYZE
dès que j'introduis lagroups
table. 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!group_id
même pour uneSELECT DISTINCT group_id FROM t;
requête?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).