Consulta eficiente para obter o maior valor por grupo da tabela grande

13

Dada a tabela:

    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)

A tabela possui 20 milhões de registros que não são, relativamente falando, um grande número. Mas torna as verificações sequenciais lentas.

Como posso obter o último registro ( max(created_at)) de cada um equipment_id?

Eu tentei as duas consultas a seguir, com várias variantes que li através de muitas respostas deste tópico:

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;

Eu também tentei criar índices btree para, equipment_id,created_atmas o Postgres acha que usar um seqscan é mais rápido. Forçar enable_seqscan = offtambém não serve, pois a leitura do índice é tão lenta quanto a verificação seq, provavelmente pior.

A consulta deve ser executada periodicamente, retornando sempre a última.

Usando o Postgres 9.3.

Explique / analise (com 1,7 milhão 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
fonte
bem da última vez eu verifiquei que não havia NULLvalores em equipment_idpercentagem espera-se abaixo de 0,1%
Feyd

Respostas:

10

Um índice simples de várias colunas b-tree deve funcionar:

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

Por que DESC NULLS LAST?

Função

Se você não conseguir entender o sentido do planejador de consultas, uma função que percorre a tabela de equipamentos deve fazer o truque. A procura de um equipment_id de cada vez usa o índice. Para um número pequeno (57 a julgar pela sua EXPLAIN ANALYZEsaída), isso é rápido.
É seguro assumir que você tem uma 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;

Também faz uma boa ligação:

SELECT * FROM f_latest_equip();

Subconsultas correlacionadas

Venha para pensar sobre isso, usando esta equipmenttabela, você pode trabalhar sujo com subconsultas pouco correlacionadas com grande efeito:

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;

O desempenho é muito bom.

LATERAL junte-se ao 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;

Explicação detalhada:

Desempenho semelhante ao da subconsulta correlacionada. Comparando o desempenho de max(), DISTINCT ON, função, correlacionado subconsulta e LATERALnisto:

SQL Fiddle .

Erwin Brandstetter
fonte
1
@ErwinBrandstetter isso é algo que tentei após a resposta de Colin, mas não consigo parar de pensar que essa é uma solução alternativa que usa o tipo de banco de dados n + 1 consultas (não tenho certeza se isso cai no antipadrão, pois há sem sobrecarga de conexão) ... Estou me perguntando agora por que agrupar existe, se não consegue lidar com alguns milhões de registros corretamente ... Simplesmente não faz sentido, não é? seja algo que estamos perdendo. Finalmente, a questão mudou um pouco e estamos assumindo a presença de uma tabela de equipamentos ... Eu gostaria de saber se há realmente uma outra maneira
Feyd
3

Tentativa 1

E se

  1. Eu tenho uma equipmentmesa separada e
  2. Eu tenho um índice em geoposition_records(equipment_id, created_at desc)

então o seguinte funciona para mim:

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

Eu não era capaz de forçar PG para fazer uma consulta rápida para determinar tanto a lista de equipment_ids eo relacionado max(created_at). Mas vou tentar novamente amanhã!

Tentativa 2

Encontrei este link: http://zogovic.com/post/44856908222/optimizing-postgresql-query-for-distinct-values Combinando essa técnica com a minha consulta da tentativa 1, recebo:

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;

e isso funciona RÁPIDO! Mas você precisa

  1. este formulário de consulta ultra-distorcido e
  2. um índice ativado geoposition_records(equipment_id, created_at desc).
Colin 't Hart
fonte