Estou tentando determinar quais índices usar para uma consulta SQL com uma WHERE
condição e uma GROUP BY
que está sendo executada muito lentamente.
Minha consulta:
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
A tabela atualmente possui 32.000.000 linhas. O tempo de execução da query aumenta muito quando eu aumento o time-frame.
A tabela em questão fica assim:
CREATE TABLE counter (
id bigserial PRIMARY KEY
, ts timestamp NOT NULL
, group_id bigint NOT NULL
);
Atualmente tenho os seguintes índices, mas o desempenho ainda é lento:
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);
A execução de EXPLAIN na consulta fornece o seguinte resultado:
"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 com dados de exemplo: http://sqlfiddle.com/#!15/7492b/1
A questão
O desempenho desta consulta pode ser melhorado adicionando índices melhores ou devo aumentar o poder de processamento?
Editar 1
PostgreSQL versão 9.3.2 é usado.
Editar 2
Eu tentei a proposta de @Erwin com 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'
);
Mas infelizmente isso não pareceu aumentar o desempenho. O plano de consulta:
"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))"
Editar 3
O plano de consulta para a consulta LATERAL do 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))"
Outra ideia, que também usa a
groups
tabela e uma construção chamadaLATERAL
join (para os fãs do SQL-Server, isso é quase idêntico aoOUTER APPLY
). Tem a vantagem de que os agregados podem ser calculados na subconsulta:O teste no SQL-Fiddle mostra que a consulta faz varreduras de índice no
(group_id, ts)
índice.Planos semelhantes são produzidos usando 2 junções laterais, uma para min e outra para max e também com 2 subconsultas correlacionadas em linha. Eles também podem ser usados se você precisar mostrar todas as
counter
linhas além das datas mínima e máxima:Como você não tem um agregado na lista de seleção, o the
group by
é praticamente o mesmo que colocar umdistinct
na lista de seleção, certo?Se é isso que você deseja, você pode obter uma pesquisa de índice rápida em comp_2_index reescrevendo-o para usar uma consulta recursiva, conforme descrito no wiki do PostgreSQL .
Faça uma visualização para retornar eficientemente os group_ids distintos:
E, em seguida, use essa visualização no lugar da tabela de pesquisa na semijunção de Erwin
exists
.Como existem apenas
133 different group_id's
, você pode usarinteger
(ou mesmosmallint
) para o group_id. Porém, não vai comprar muito, porque o preenchimento de 8 bytes consumirá o restante da tabela e possíveis índices de várias colunas. Processamento de planícieinteger
deve ser um pouco mais rápido, no entanto. Mais sobreint
vs.int2
@Leo: timestamps são armazenados como números inteiros de 8 bytes em instalações modernas e podem ser processados perfeitamente rápido. Detalhes.
@ypercube: O índice
(group_id, ts)
não pode ajudar, pois não há condiçãogroup_id
na consulta.Seu principal problema é a enorme quantidade de dados que precisam ser processados:
Vejo que você está interessado apenas na existência de um
group_id
, e nenhuma contagem real. Além disso, existem apenas 133group_id
s diferentes. Portanto, sua consulta pode ser satisfeita com o primeiro hit porgorup_id
no período de tempo. Daí esta sugestão para uma consulta alternativa com umEXISTS
semi-join :Assumindo uma tabela de pesquisa para grupos:
Seu índice torna
comp_2_index
-(group_id, ts)
se instrumental agora.SQL Fiddle (com base no violino fornecido por @ypercube nos comentários)
Aqui, a consulta prefere o índice em
(ts, group_id)
, mas acho que é por causa da configuração do teste com registros de data e hora "agrupados". Se você remover os índices com entrelinhats
( mais sobre isso ), o planejador também usará o índice(group_id, ts)
- notavelmente em uma varredura somente de índice .Se isso funcionar, talvez você não precise desta outra melhoria possível: pré-agregue os dados em uma visualização materializada para reduzir drasticamente o número de linhas. Isso faria sentido em particular, se você também precisar de contagens reais adicionais. Então você tem o custo de processar muitas linhas uma vez ao atualizar o mv. Você pode até combinar agregados diários e horários (duas tabelas separadas) e adaptar sua consulta a isso.
Os prazos em suas consultas são arbitrários? Ou principalmente em minutos / horas / dias completos?
Crie o(s) índice(s) necessário(s)
counter_mv
e adapte sua consulta para trabalhar com ele...