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 consulta aumenta muito quando eu aumento o prazo.
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
É possível melhorar o desempenho desta consulta adicionando índices melhores ou devo aumentar o poder de processamento?
Editar 1
O PostgreSQL versão 9.3.2 é usado.
Editar 2
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'
);
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))"
group_id
valores diferentes existem na mesa?group_id
e não conta?Respostas:
Outra idéia, que também usa a
groups
tabela e uma construção chamadaLATERAL
join (para os fãs do SQL Server, isso é quase idêntico aOUTER APPLY
). Tem a vantagem de que agregados podem ser calculados na subconsulta:O teste no SQL-Fiddle mostra que a consulta indexa verificações no
(group_id, ts)
índice.Planos similares são produzidos usando 2 junções laterais, uma para min e outra para max e também com 2 subconsultas correlatas em linha. Eles também podem ser usados se você precisar mostrar as
counter
linhas inteiras além das datas mínima e máxima:fonte
Como você não tem agregado na lista de seleção, o valor
group by
é praticamente o mesmo que colocar umdistinct
na lista de seleção, certo?Se é isso que você deseja, poderá obter uma pesquisa rápida de índice em comp_2_index, reescrevendo-a para usar uma consulta recursiva, conforme descrito no wiki do PostgreSQL .
Faça uma exibição para retornar eficientemente os group_ids distintos:
E, em seguida, use essa exibição no lugar da tabela de pesquisa na
exists
semi-junção de Erwin .fonte
Como existem apenas
133 different group_id's
, você pode usarinteger
(ou atésmallint
) para o group_id. Porém, não vai lhe custar muito, porque o preenchimento de 8 bytes consumirá o restante da sua tabela e possíveis índices de várias colunas. O processamento da planícieinteger
deve ser um pouco mais rápido, no entanto. Mais sobreint
vs.int2
.@ Leo: os carimbos de data / hora são armazenados como números inteiros de 8 bytes em instalações modernas e podem ser processados perfeitamente rapidamente. Detalhes.
@ ypercube: o índice em
(group_id, ts)
não pode ajudar, pois não há nenhuma condiçãogroup_id
na consulta.Seu principal problema é a enorme quantidade de dados que precisam ser processados:
Vejo que você só está interessado na existência de a
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
período de tempo. Daí esta sugestão para uma consulta alternativa com umaEXISTS
semi-junção :Supondo uma tabela de pesquisa para grupos:
Seu índice
comp_2_index
em(group_id, ts)
torna-se fundamental agora.SQL Fiddle (baseado no fiddle fornecido por @ypercube nos comentários)
Aqui, a consulta prefere o índice
(ts, group_id)
, mas acho que é por causa da configuração de teste com carimbos de data e hora "agrupados". Se você remover os índices com as iniciaists
( mais sobre isso ), o planejador também utilizará o índice com prazer(group_id, ts)
- principalmente em uma Varredura de Índice .Se isso funcionar, talvez você não precise dessa outra melhoria possível: Pré-agregue dados em uma exibição materializada para reduzir drasticamente o número de linhas. Isso faria sentido, em particular, se você também precisar de contagens reais adicionalmente. 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 nas 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 ...fonte
groups
tabela faz a diferença?ANALYZE
faz a diferença. Mas os índicescounter
ainda são usados semANALYZE
que eu apresente agroups
tabela. O ponto é que, sem essa tabela, um seqscan é necessário para criar o conjunto de possíveis group_id´s. Eu adicionei mais à minha resposta. E obrigado pelo seu violino!group_id
mesmo para umaSELECT DISTINCT group_id FROM t;
consulta?LIMIT 1
, ele pode escolher uma verificação de índice de bitmap, que não se beneficia da parada precoce e leva muito mais tempo. (Mas se a tabela for aspirada recentemente, talvez prefira a varredura de índice apenas sobre a varredura de bitmap, para que o comportamento que você vê dependa do status de aspiração da tabela).