Como tornar o DISTINCT ON mais rápido no PostgreSQL?

13

Eu tenho uma tabela station_logsem um banco de dados PostgreSQL 9.6:

    Column     |            Type             |    
---------------+-----------------------------+
 id            | bigint                      | bigserial
 station_id    | integer                     | not null
 submitted_at  | timestamp without time zone | 
 level_sensor  | double precision            | 
Indexes:
    "station_logs_pkey" PRIMARY KEY, btree (id)
    "uniq_sid_sat" UNIQUE CONSTRAINT, btree (station_id, submitted_at)

Estou tentando obter o último level_sensorvalor com base em submitted_at, para cada um station_id. Existem cerca de 400 station_idvalores exclusivos e cerca de 20 mil linhas por dia por dia station_id.

Antes de criar o índice:

EXPLAIN ANALYZE
SELECT DISTINCT ON(station_id) station_id, submitted_at, level_sensor
FROM station_logs ORDER BY station_id, submitted_at DESC;
 Exclusivo (custo = 4347852,14..4450301,72 linhas = 89 largura = 20) (tempo real = 22202,080..27619,167 linhas = 98 loops = 1)
   -> Classificação (custo = 4347852.14..4399076.93 linhas = 20489916 largura = 20) (tempo real = 22202.077..26540.827 linhas = 20489812 loops = 1)
         Chave de classificação: station_id, submit_at DESC
         Método de classificação: fusão externa Disco: 681040kB
         -> Varredura Seq em station_logs (custo = 0,00..598895,16 linhas = 20489916 largura = 20) (tempo real = 0,023..3443,587 linhas = 20489812 loops = $
 Tempo de planejamento: 0,072 ms
 Tempo de execução: 27690.644 ms

Criando índice:

CREATE INDEX station_id__submitted_at ON station_logs(station_id, submitted_at DESC);

Após criar o índice, para a mesma consulta:

 Exclusivo (custo = 0,56..2156367,51 linhas = 89 largura = 20) (tempo real = 0,184..16263,413 linhas = 98 loops = 1)
   -> Varredura de índice usando station_id__submitted_at em station_logs (custo = 0,56..2105142,98 linhas = 20489812 largura = 20) (tempo real = 0,181..1 $
 Tempo de planejamento: 0,206 ms
 Tempo de execução: 16263.490 ms

Existe uma maneira de tornar essa consulta mais rápida? Como 1 segundo, por exemplo, 16 segundos ainda é demais.

Kokizzu
fonte
2
Quantas identificações de estação distintas existem, ou seja, quantas linhas a consulta retorna? E qual versão do Postgres?
ypercubeᵀᴹ
Postgre 9,6, cerca de 400 station_id único, e cerca de 20k registros por dia por station_id
Kokizzu
Esta consulta retorna um "último valor de level_sensor com base em submit_at, para cada station_id". DISTINCT ON envolve uma escolha aleatória, exceto nos casos em que você não precisa.
philipxy

Respostas:

18

Para apenas 400 estações, essa consulta será massivamente mais rápida:

SELECT s.station_id, l.submitted_at, l.level_sensor
FROM   station s
CROSS  JOIN LATERAL (
   SELECT submitted_at, level_sensor
   FROM   station_logs
   WHERE  station_id = s.station_id
   ORDER  BY submitted_at DESC NULLS LAST
   LIMIT  1
   ) l;

dbfiddle aqui
(comparando planos para esta consulta, a alternativa de Abelisto e o seu original)

Resultante EXPLAIN ANALYZEconforme fornecido pelo OP:

 Loop aninhado (custo = 0,56..356,65 linhas = 102 largura = 20) (tempo real = 0,034..0,979 linhas = 98 loops = 1)
   -> Varredura Seq nas estações s (custo = 0,00..3,02 linhas = 102 largura = 4) (tempo real = 0,009..0,016 linhas = 102 loops = 1)
   -> Limite (custo = 0,56..3,45 linhas = 1 largura = 16) (tempo real = 0,009..0,009 linhas = 1 loops = 102)
         -> Varredura de índice usando station_id__submitted_at em station_logs (custo = 0,56..664062,38 linhas = 230223 largura = 16) (tempo real = 0,009 $
               Cond do índice: (station_id = s.id)
 Tempo de planejamento: 0,542 ms
 Tempo de execução: 1,013 ms   - !!

O único índice que você precisa é o que você criou: station_id__submitted_at. A UNIQUErestrição uniq_sid_sattambém faz o trabalho, basicamente. Manter os dois parece um desperdício de espaço em disco e desempenho de gravação.

Eu adicionei NULLS LASTa ORDER BYna consulta porque submitted_atnão está definida NOT NULL. Idealmente, se aplicável !, adicione uma NOT NULLrestrição à coluna submitted_at, descarte o índice adicional e remova NULLS LASTda consulta.

Se submitted_atpossível NULL, crie esse UNIQUEíndice para substituir o índice atual e a restrição exclusiva:

CREATE UNIQUE INDEX station_logs_uni ON station_logs(station_id, submitted_at DESC NULLS LAST);

Considerar:

Isso pressupõe uma tabela separadastation com uma linha por relevante station_id(normalmente a PK) - que você deve ter de qualquer maneira. Se você não tiver, crie-o. Novamente, muito rápido com esta técnica rCTE:

CREATE TABLE station AS
WITH RECURSIVE cte AS (
   (
   SELECT station_id
   FROM   station_logs
   ORDER  BY station_id
   LIMIT  1
   )
   UNION ALL
   SELECT l.station_id
   FROM   cte c
   ,      LATERAL (   
      SELECT station_id
      FROM   station_logs
      WHERE  station_id > c.station_id
      ORDER  BY station_id
      LIMIT  1
      ) l
   )
TABLE cte;

Eu uso isso no violino também. Você pode usar uma consulta semelhante para resolver sua tarefa diretamente, sem stationtabela - se não estiver convencido de criá-la.

Instruções detalhadas, explicações e alternativas:

Otimizar índice

Sua consulta deve ser muito rápida agora. Somente se você ainda precisar otimizar o desempenho de leitura ...

Pode fazer sentido adicionar a level_sensorúltima coluna ao índice para permitir verificações apenas de índice , como joanolo comentou .
Contras: Aumenta o índice - o que adiciona um pequeno custo a todas as consultas que o utilizam.
Pro: se você realmente obtiver apenas verificações de índice, a consulta em questão não precisará visitar páginas de heap, o que o torna duas vezes mais rápido. Mas isso pode ser um ganho insubstancial para a consulta muito rápida agora.

No entanto , não espero que isso funcione para o seu caso. Você mencionou:

... cerca de 20 mil linhas por dia por dia station_id.

Normalmente, isso indicaria carga de gravação incessante (1 a station_idcada 5 segundos). E você está interessado na última linha. As verificações apenas de índice funcionam apenas para páginas de heap visíveis a todas as transações (o bit no mapa de visibilidade está definido). Você precisaria executar VACUUMconfigurações extremamente agressivas para que a tabela acompanhasse o carregamento de gravação e ainda assim não funcionaria na maioria das vezes. Se minhas suposições estiverem corretas, as varreduras somente de índice estiverem fora, não adicione level_sensorao índice.

OTOH, se minhas suposições forem válidas e sua tabela estiver crescendo muito , um índice BRIN pode ajudar. Relacionado:

Ou, ainda mais especializado e mais eficiente: um índice parcial para apenas as mais recentes adições para cortar a maior parte das linhas irrelevantes:

CREATE INDEX station_id__submitted_at_recent_idx ON station_logs(station_id, submitted_at DESC NULLS LAST)
WHERE submitted_at > '2017-06-24 00:00';

Escolha um carimbo de data e hora para o qual você sabe que linhas mais jovens devem existir. Você deve adicionar uma WHEREcondição correspondente a todas as consultas, como:

...
WHERE  station_id = s.station_id
AND    submitted_at > '2017-06-24 00:00'
...

Você precisa adaptar o índice e a consulta periodicamente.
Respostas relacionadas com mais detalhes:

Erwin Brandstetter
fonte
Sempre que eu sei que quero um loop aninhado (geralmente), usar LATERAL é um aumento de desempenho para várias situações.
Paul Draper
6

Experimente da maneira clássica:

create index idx_station_logs__station_id on station_logs(station_id);
create index idx_station_logs__submitted_at on station_logs(submitted_at);

analyse station_logs;

with t as (
  select station_id, max(submitted_at) submitted_at 
  from station_logs 
  group by station_id)
select * 
from t join station_logs l on (
  l.station_id = t.station_id and l.submitted_at = t.submitted_at);

dbfiddle

EXPLIQUE ANALISAR por ThreadStarter

 Nested Loop  (cost=701344.63..702110.58 rows=4 width=155) (actual time=6253.062..6253.544 rows=98 loops=1)
   CTE t
     ->  HashAggregate  (cost=701343.18..701344.07 rows=89 width=12) (actual time=6253.042..6253.069 rows=98 loops=1)
           Group Key: station_logs.station_id
           ->  Seq Scan on station_logs  (cost=0.00..598894.12 rows=20489812 width=12) (actual time=0.034..1841.848 rows=20489812 loop$
   ->  CTE Scan on t  (cost=0.00..1.78 rows=89 width=12) (actual time=6253.047..6253.085 rows=98 loops=1)
   ->  Index Scan using station_id__submitted_at on station_logs l  (cost=0.56..8.58 rows=1 width=143) (actual time=0.004..0.004 rows=$
         Index Cond: ((station_id = t.station_id) AND (submitted_at = t.submitted_at))
 Planning time: 0.542 ms
 Execution time: 6253.701 ms
Abelisto
fonte