Estoy tratando de determinar qué índices usar para una consulta SQL con una WHERE
condición y GROUP BY
cuál actualmente se ejecuta muy lentamente.
Mi 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
La tabla tiene actualmente 32.000.000 filas. El tiempo de ejecución de la consulta aumenta mucho cuando aumento el marco de tiempo.
La tabla en cuestión se ve así:
CREATE TABLE counter (
id bigserial PRIMARY KEY
, ts timestamp NOT NULL
, group_id bigint NOT NULL
);
Actualmente tengo los siguientes índices, pero el rendimiento sigue siendo 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);
Ejecutar EXPLAIN en la consulta da el siguiente 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 con datos de ejemplo: http://sqlfiddle.com/#!15/7492b/1
La pregunta
¿Se puede mejorar el rendimiento de esta consulta agregando mejores índices o debo aumentar la potencia de procesamiento?
Editar 1
Se utiliza PostgreSQL versión 9.3.2.
Editar 2
Intenté la propuesta de @Erwin con 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'
);
Pero desafortunadamente esto no pareció aumentar el rendimiento. El plan 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
El plan de consulta para la consulta LATERAL 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
valores diferentes hay en la tabla?group_id
y no cuenta?Respuestas:
Otra idea, que también usa la
groups
tabla y una construcción llamadaLATERAL
join (para los fanáticos de SQL-Server, esto es casi idéntico aOUTER APPLY
). Tiene la ventaja de que los agregados se pueden calcular en la subconsulta:La prueba en SQL-Fiddle muestra que la consulta realiza escaneos de índice en el
(group_id, ts)
índice.Se producen planes similares utilizando 2 uniones laterales, una para min y otra para max y también con 2 subconsultas correlacionadas en línea. También podrían usarse si necesita mostrar las
counter
filas completas además de las fechas mínimas y máximas:fuente
Dado que no tiene agregado en la lista de selección, el
group by
es más o menos lo mismo que poner undistinct
en la lista de selección, ¿verdad?Si eso es lo que desea, puede obtener una búsqueda rápida de índice en comp_2_index reescribiendo esto para usar una consulta recursiva, como se describe en el wiki de PostgreSQL .
Haga una vista para devolver eficientemente los distintos group_ids:
Y luego use esa vista en lugar de la tabla de búsqueda en la
exists
semi-unión de Erwin .fuente
Como solo hay
133 different group_id's
, puede usarinteger
(o inclusosmallint
) para group_id. Sin embargo, no le comprará mucho, porque el relleno a 8 bytes se comerá el resto en su tabla y los posibles índices de varias columnas. Sininteger
embargo, el procesamiento de plain debería ser un poco más rápido. Más enint
contraint2
.@Leo: las marcas de tiempo se almacenan como enteros de 8 bytes en instalaciones modernas y se pueden procesar perfectamente rápido. Detalles
@ypercube: el índice
(group_id, ts)
no puede ayudar, ya que no hay ninguna condicióngroup_id
en la consulta.Su principal problema es la gran cantidad de datos que deben procesarse:
Veo que solo está interesado en la existencia de un
group_id
, y no cuenta real. Además, solo hay 133group_id
s diferentes . Por lo tanto, su consulta puede ser satisfecha con el primer hit porgorup_id
en el marco de tiempo. De ahí esta sugerencia para una consulta alternativa con unaEXISTS
semiunión :Suponiendo una tabla de búsqueda para grupos:
Su índice
comp_2_index
sobre(group_id, ts)
convierte en decisivo ahora.SQL Fiddle (basándose en el violín proporcionado por @ypercube en los comentarios)
Aquí, la consulta prefiere el índice
(ts, group_id)
, pero creo que eso se debe a la configuración de la prueba con marcas de tiempo "agrupadas". Si elimina los índices con los principalests
( más información al respecto ), el planificador también utilizará el índice(group_id, ts)
, especialmente en un Escaneo de solo índice .Si eso funciona, es posible que no necesite esta otra mejora posible: Agregue previamente los datos en una vista materializada para reducir drásticamente el número de filas. Esto tendría sentido en particular, si también necesita recuentos reales adicionalmente. Luego tiene el costo de procesar muchas filas una vez al actualizar el mv. Incluso podría combinar agregados diarios y por hora (dos tablas separadas) y adaptar su consulta a eso.
¿Los plazos en sus consultas son arbitrarios? ¿O sobre todo en minutos / horas / días completos?
Cree los índices necesarios
counter_mv
y adapte su consulta para trabajar con ella ...fuente
groups
tabla hace la diferencia?ANALYZE
hace la diferencia. Pero los índicescounter
incluso se usan sinANALYZE
tan pronto como presento lagroups
tabla. El punto es que, sin esa tabla, se necesita un seqscan de todos modos para construir el conjunto de posibles group_id´s. Agregué más a mi respuesta. Y gracias por tu violín!group_id
incluso para unaSELECT DISTINCT group_id FROM t;
consulta?LIMIT 1
, puede elegir un escaneo de índice de mapa de bits, que no se beneficia de una detención temprana y lleva mucho más tiempo. (Pero si la tabla está recién aspirada, es posible que prefiera el escaneo indexonly sobre el escaneo de mapa de bits, por lo que el comportamiento que ve depende del estado de vacío de la tabla).