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
Les performances de cette requête peuvent-elles être améliorées en ajoutant de meilleurs indices ou d'augmenter le pouvoir de traitement?
PostgreSQL version 9.3.2 est utilisé.
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))"
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))"
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 ;
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.
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 ...