¿Cómo hacer que DISTINCT ON sea más rápido en PostgreSQL?

13

Tengo una tabla station_logsen una base de datos 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)

Estoy tratando de obtener el último level_sensorvalor basado en submitted_at, para cada uno station_id. Hay alrededor de 400 station_idvalores únicos y alrededor de 20k filas por día por station_id.

Antes de crear el índice:

EXPLAIN ANALYZE
SELECT DISTINCT ON(station_id) station_id, submitted_at, level_sensor
FROM station_logs ORDER BY station_id, submitted_at DESC;
 Único (costo = 4347852.14..4450301.72 filas = 89 ancho = 20) (tiempo real = 22202.080..27619.167 filas = 98 bucles = 1)
   -> Ordenar (costo = 4347852.14..4399076.93 filas = 20489916 ancho = 20) (tiempo real = 22202.077..26540.827 filas = 20489812 bucles = 1)
         Clave de clasificación: station_id, submit_at DESC
         Método de clasificación: disco de fusión externo: 681040kB
         -> Seq Scan en station_logs (costo = 0.00..598895.16 filas = 20489916 ancho = 20) (tiempo real = 0.023..3443.587 filas = 20489812 bucles = $
 Tiempo de planificación: 0.072 ms
 Tiempo de ejecución: 27690.644 ms

Creando índice:

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

Después de crear el índice, para la misma consulta:

 Único (costo = 0.56..2156367.51 filas = 89 ancho = 20) (tiempo real = 0.184..16263.413 filas = 98 bucles = 1)
   -> Escaneo de índice usando station_id__submitted_at en station_logs (costo = 0.56..2105142.98 filas = 20489812 ancho = 20) (tiempo real = 0.181..1 $
 Tiempo de planificación: 0.206 ms
 Tiempo de ejecución: 16263.490 ms

¿Hay alguna manera de hacer esta consulta más rápido? Como 1 segundo, por ejemplo, 16 segundos todavía es demasiado.

Kokizzu
fuente
2
¿Cuántos identificadores de estación distintos hay, es decir, cuántas filas devuelve la consulta? ¿Y qué versión de Postgres?
ypercubeᵀᴹ
Postgre 9.6, alrededor de 400 station_id únicos y alrededor de 20k registros por día por station_id
Kokizzu
Esta consulta devuelve un "último valor de sensor de nivel basado en submit_at, para cada station_id". DISTINCT ON implica una elección aleatoria, excepto en los casos en que no la necesita.
philipxy

Respuestas:

18

Para solo 400 estaciones, esta consulta será masivamente más 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 aquí
(comparando planes para esta consulta, la alternativa de Abelisto y su original)

Resultando EXPLAIN ANALYZEsegún lo dispuesto por el OP:

 Bucle anidado (costo = 0.56..356.65 filas = 102 ancho = 20) (tiempo real = 0.034..0.979 filas = 98 bucles = 1)
   -> Seq Scan en estaciones s (costo = 0.00..3.02 filas = 102 ancho = 4) (tiempo real = 0.009..0.016 filas = 102 bucles = 1)
   -> Límite (costo = 0.56..3.45 filas = 1 ancho = 16) (tiempo real = 0.009..0.009 filas = 1 bucles = 102)
         -> Escaneo de índice usando station_id__submitted_at en station_logs (costo = 0.56..664062.38 filas = 230223 ancho = 16) (tiempo real = 0.009 $
               Índice Cond: (station_id = s.id)
 Tiempo de planificación: 0.542 ms
 Tiempo de ejecución: 1.013 ms   - !!

El único índice que necesita es la que ha creado: station_id__submitted_at. La UNIQUErestricción uniq_sid_sattambién hace el trabajo, básicamente. Mantener ambos parece una pérdida de espacio en disco y rendimiento de escritura.

Agregué NULLS LASTa ORDER BYen la consulta porque submitted_atno está definido NOT NULL. Idealmente, si corresponde, agregue una NOT NULLrestricción a la columna submitted_at, suelte el índice adicional y elimínelo NULLS LASTde la consulta.

Si submitted_atpuede NULL, cree este UNIQUEíndice para reemplazar tanto su índice actual como su restricción única:

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

Considerar:

Esto supone una tabla separadastation con una fila por relevante station_id(generalmente el PK), que debería tener en cualquier caso. Si no lo tiene, créelo. Nuevamente, muy rápido con 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;

Lo uso en el violín también. Puede utilizar una consulta similar para resolver su tarea directamente, sin stationtabla, si no puede convencerse de crearla.

Instrucciones detalladas, explicación y alternativas:

Optimizar índice

Su consulta debería ser muy rápida ahora. Solo si aún necesita optimizar el rendimiento de lectura ...

Puede tener sentido agregar level_sensorcomo última columna al índice para permitir escaneos de solo índice , como comentó joanolo .
Con: hace que el índice sea más grande, lo que agrega un pequeño costo a todas las consultas que lo usan.
Pro: si realmente obtiene escaneos de índice, la consulta en cuestión no tiene que visitar páginas de montón, lo que hace que sea aproximadamente el doble de rápido. Pero eso puede ser una ganancia insustancial para la consulta muy rápida ahora.

Sin embargo , no espero que eso funcione para su caso. Mencionaste:

... alrededor de 20k filas por día por station_id.

Típicamente, eso indicaría una carga de escritura incesante (1 por station_idcada 5 segundos). Y estás interesado en la última fila. Los escaneos de solo índice solo funcionan para páginas de montón que son visibles para todas las transacciones (se establece el bit en el mapa de visibilidad). Tendría que ejecutar VACUUMconfiguraciones extremadamente agresivas para que la tabla se mantuviera al día con la carga de escritura, y aún así no funcionaría la mayor parte del tiempo. Si mis suposiciones son correctas, los escaneos de solo índice están fuera, no agregue level_sensoral índice.

OTOH, si mis suposiciones se mantienen y su tabla está creciendo muy grande , un índice BRIN podría ayudar. Relacionado:

O, incluso más especializado y más eficiente: un índice parcial de solo las últimas incorporaciones para cortar la mayor parte de las filas 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';

Elija una marca de tiempo para la que sepa que deben existir filas más jóvenes. Debe agregar una WHEREcondición coincidente a todas las consultas, como:

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

Debe adaptar el índice y la consulta de vez en cuando.
Respuestas relacionadas con más detalles:

Erwin Brandstetter
fuente
Cada vez que sé que quiero un bucle anidado (a menudo), usar LATERAL es un impulso de rendimiento para una serie de situaciones.
Paul Draper
6

Prueba la forma clásica:

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

EXPLICAR ANÁLISIS 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
fuente