Consulta eficiente para obtener el mayor valor por grupo de la tabla grande

13

Dada la tabla:

    Column    |            Type             
 id           | integer                     
 latitude     | numeric(9,6)                
 longitude    | numeric(9,6)                
 speed        | integer                     
 equipment_id | integer                     
 created_at   | timestamp without time zone
Indexes:
    "geoposition_records_pkey" PRIMARY KEY, btree (id)

La tabla tiene 20 millones de registros que no son, relativamente hablando, un gran número. Pero hace que los escaneos secuenciales sean lentos.

¿Cómo puedo obtener el último registro ( max(created_at)) de cada uno equipment_id?

He intentado las dos consultas siguientes, con varias variantes que he leído a través de muchas respuestas de este tema:

select max(created_at),equipment_id from geoposition_records group by equipment_id;

select distinct on (equipment_id) equipment_id,created_at 
  from geoposition_records order by equipment_id, created_at desc;

También he intentado crear índices btree para, equipment_id,created_atpero Postgres descubre que usar un seqscan es más rápido. Forzar enable_seqscan = offtampoco sirve de nada, ya que leer el índice es tan lento como la exploración secuencial, probablemente peor.

La consulta debe ejecutarse periódicamente, devolviendo siempre la última.

Usando Postgres 9.3.

Explicar / analizar (con 1.7 millones de registros):

set enable_seqscan=true;
explain analyze select max(created_at),equipment_id from geoposition_records group by equipment_id;
"HashAggregate  (cost=47803.77..47804.34 rows=57 width=12) (actual time=1935.536..1935.556 rows=58 loops=1)"
"  ->  Seq Scan on geoposition_records  (cost=0.00..39544.51 rows=1651851 width=12) (actual time=0.029..494.296 rows=1651851 loops=1)"
"Total runtime: 1935.632 ms"

set enable_seqscan=false;
explain analyze select max(created_at),equipment_id from geoposition_records group by equipment_id;
"GroupAggregate  (cost=0.00..2995933.57 rows=57 width=12) (actual time=222.034..11305.073 rows=58 loops=1)"
"  ->  Index Scan using geoposition_records_equipment_id_created_at_idx on geoposition_records  (cost=0.00..2987673.75 rows=1651851 width=12) (actual time=0.062..10248.703 rows=1651851 loops=1)"
"Total runtime: 11305.161 ms"
Feyd
fuente
bueno, la última vez que verifiqué que no había NULLvalores en equipment_idel porcentaje esperado está por debajo del 0.1%
Feyd

Respuestas:

10

Después de todo, un índice b-tree simple de varias columnas debería funcionar:

CREATE INDEX foo_idx
ON geoposition_records (equipment_id, created_at DESC NULLS LAST);

¿Por qué DESC NULLS LAST?

Función

Si no puede hablar con sentido en el planificador de consultas, una función que recorra la tabla del equipo debería ser la solución. Buscar un equipo_id a la vez utiliza el índice. Para un número pequeño (57 a juzgar por su EXPLAIN ANALYZEsalida), eso es rápido.
¿Es seguro asumir que tienes una equipmentmesa?

CREATE OR REPLACE FUNCTION f_latest_equip()
  RETURNS TABLE (equipment_id int, latest timestamp) AS
$func$
BEGIN
FOR equipment_id IN
   SELECT e.equipment_id FROM equipment e ORDER BY 1
LOOP
   SELECT g.created_at
   FROM   geoposition_records g
   WHERE  g.equipment_id = f_latest_equip.equipment_id
                           -- prepend function name to disambiguate
   ORDER  BY g.created_at DESC NULLS LAST
   LIMIT  1
   INTO   latest;

   RETURN NEXT;
END LOOP;
END  
$func$  LANGUAGE plpgsql STABLE;

También es una buena llamada:

SELECT * FROM f_latest_equip();

Subconsultas correlacionadas

Ahora que lo pienso, usando esta equipmenttabla, podría hacer el trabajo sucio con subconsultas poco correlacionadas para un gran efecto:

SELECT equipment_id
     ,(SELECT created_at
       FROM   geoposition_records
       WHERE  equipment_id = eq.equipment_id
       ORDER  BY created_at DESC NULLS LAST
       LIMIT  1) AS latest
FROM   equipment eq;

El rendimiento es muy bueno.

LATERAL únete a Postgres 9.3+

SELECT eq.equipment_id, r.latest
FROM   equipment eq
LEFT   JOIN LATERAL (
   SELECT created_at
   FROM   geoposition_records
   WHERE  equipment_id = eq.equipment_id
   ORDER  BY created_at DESC NULLS LAST
   LIMIT  1
   ) r(latest) ON true;

Explicación detallada:

Rendimiento similar al de la subconsulta correlacionada. Comparación del rendimiento de max(), DISTINCT ONfunción, subconsulta correlacionada y LATERALen esto:

SQL Fiddle .

Erwin Brandstetter
fuente
1
@ErwinBrandstetter, esto es algo que he intentado después de la respuesta de Colin, pero no puedo dejar de pensar que se trata de una solución alternativa que utiliza un tipo de base de datos n + 1 consultas (no estoy seguro si eso cae en el antipatrón ya que hay sin sobrecarga de conexión) ... Me pregunto ahora por qué group by existe en absoluto, si no puede manejar unos pocos millones de registros correctamente ... Simplemente no tiene sentido, ¿no? ser algo que nos estamos perdiendo Finalmente, la pregunta ha cambiado un poco y estamos asumiendo la presencia de una mesa de equipo ... Me gustaría saber si realmente hay otra forma
Feyd
3

Intento 1

Si

  1. Tengo un separado equipment mesa y
  2. Tengo un índice en geoposition_records(equipment_id, created_at desc)

entonces lo siguiente funciona para mí:

select id as equipment_id, (select max(created_at)
                            from geoposition_records
                            where equipment_id = equipment.id
                           ) as max_created_at
from equipment;

No pude obligar a PG a hacer una consulta rápida para determinar tanto la lista de equipment_idsy la relacionada max(created_at). ¡Pero voy a intentarlo de nuevo mañana!

Intento 2

Encontré este enlace: http://zogovic.com/post/44856908222/optimizing-postgresql-query-for-distinct-values Combinando esta técnica con mi consulta del intento 1, obtengo:

WITH RECURSIVE equipment(id) AS (
    SELECT MIN(equipment_id) FROM geoposition_records
  UNION
    SELECT (
      SELECT equipment_id
      FROM geoposition_records
      WHERE equipment_id > equipment.id
      ORDER BY equipment_id
      LIMIT 1
    )
    FROM equipment WHERE id IS NOT NULL
)
SELECT id AS equipment_id, (SELECT MAX(created_at)
                            FROM geoposition_records
                            WHERE equipment_id = equipment.id
                           ) AS max_created_at
FROM equipment;

y esto funciona RAPIDO! Pero tu necesitas

  1. este formulario de consulta ultracontorsionado, y
  2. un índice en geoposition_records(equipment_id, created_at desc).
Colin 't Hart
fuente