Índices para consultas SQL con condición WHERE y GROUP BY

15

Estoy tratando de determinar qué índices usar para una consulta SQL con una WHEREcondición y GROUP BYcuá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))"
Uldall
fuente
¿Cuántos group_idvalores diferentes hay en la tabla?
ypercubeᵀᴹ
Hay 133 diferentes group_id's.
Las marcas de tiempo varían de 2011 a 2014. Se utilizan tanto segundos como milisegundos.
¿Solo le interesa group_idy no cuenta?
Erwin Brandstetter
@Erwin También estamos interesados ​​en max () y (min) en una cuarta columna que no se muestra en el ejemplo.
uldall

Respuestas:

6

Otra idea, que también usa la groupstabla y una construcción llamada LATERALjoin (para los fanáticos de SQL-Server, esto es casi idéntico a OUTER APPLY). Tiene la ventaja de que los agregados se pueden calcular en la subconsulta:

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 ;

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 counterfilas completas además de las fechas mínimas y máximas:

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 ;
ypercubeᵀᴹ
fuente
@ypercube Agregué el plan de consulta para su consulta a la pregunta original. La consulta se ejecuta en menos de 50 ms, incluso en grandes períodos de tiempo.
uldall
5

Dado que no tiene agregado en la lista de selección, el group byes más o menos lo mismo que poner un distincten 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:

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));

Y luego use esa vista en lugar de la tabla de búsqueda en la existssemi-unión de Erwin .

jjanes
fuente
4

Como solo hay 133 different group_id's, puede usar integer(o incluso smallint) 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. Sin integerembargo, el procesamiento de plain debería ser un poco más rápido. Más en intcontraint2 .

CREATE TABLE counter (
    id bigserial PRIMARY KEY
  , ts timestamp NOT NULL
  , group_id int NOT NULL
);

@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ón group_iden la consulta.

Su principal problema es la gran cantidad de datos que deben procesarse:

Escaneo de índice usando ts_index en el mostrador (costo = 0.56..467470.93 filas = 194892 ancho = 4)

Veo que solo está interesado en la existencia de un group_id, y no cuenta real. Además, solo hay 133 group_ids diferentes . Por lo tanto, su consulta puede ser satisfecha con el primer hit por gorup_iden el marco de tiempo. De ahí esta sugerencia para una consulta alternativa con una EXISTSsemiunión :

Suponiendo una tabla de búsqueda para grupos:

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'
   );

Su índice comp_2_indexsobre (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 principales ts( 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?

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;

Cree los índices necesarios counter_mvy adapte su consulta para trabajar con ella ...

Erwin Brandstetter
fuente
1
Intenté varias cosas similares en SQL-Fiddle , con 10k filas, pero todas mostraron un escaneo secuencial. ¿Usar la groupstabla hace la diferencia?
ypercubeᵀᴹ
@ypercube: eso creo. Además, ANALYZEhace la diferencia. Pero los índices counterincluso se usan sin ANALYZEtan pronto como presento la groupstabla. 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!
Erwin Brandstetter
Eso es extraño. ¿Dices que el optimizador de Postgres no usará el índice group_idincluso para una SELECT DISTINCT group_id FROM t;consulta?
ypercubeᵀᴹ
1
@ErwinBrandstetter Eso es lo que pensé también, y me sorprendió mucho descubrir lo contrario. Sin a 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).
jjanes
1
@uldall: los agregados diarios reducirán drásticamente el número de filas. Eso debería hacer el truco. Pero asegúrese de probar la consulta EXISTS. Puede ser sorprendentemente rápido. No funcionará para min / max adicionalmente. Sin embargo, me interesaría el rendimiento resultante si fuera tan amable de dejar caer una línea aquí.
Erwin Brandstetter