Optimizar una consulta 'más reciente' en Postgres en 20 millones de filas

10

Mi mesa tiene el siguiente aspecto:

    Column             |    Type           |    
-----------------------+-------------------+
 id                    | integer           | 
 source_id             | integer           | 
 timestamp             | integer           | 
 observation_timestamp | integer           | 
 value                 | double precision  | 

existen índices en source_id, timestamp y en una combinación de timestamp e id ( CREATE INDEX timeseries_id_timestamp_combo_idx ON timeseries (id, timeseries DESC NULLS LAST))

Hay 20 millones de filas (OK, hay 120 millones, pero 20 millones con source_id = 1). Tiene muchas entradas para lo mismo timestampcon diferentes observation_timestamp, que describen un valueocurrido en timestampreportado u observado en observation_timestamp. Por ejemplo, la temperatura pronosticada para mañana a las 2 p.m., como se predijo hoy a las 12 a.m.

Idealmente, esta tabla hace algunas cosas bien:

  • lote que inserta nuevas entradas, a veces 100K a la vez
  • selección de datos observados para intervalos de tiempo ("¿cuáles son las predicciones de temperatura para enero hasta marzo")
  • seleccionar los datos observados para los intervalos de tiempo observados desde cierto punto ("cuál es la vista de las predicciones de temperatura para enero hasta marzo como pensamos el 1 de noviembre")

El segundo es el que es central para esta pregunta.

Los datos en la tabla se verían de la siguiente manera

id  source_id   timestamp   observation_timestamp   value
1   1           1531084900  1531083900              9999
2   1           1531084900  1531082900              1111
3   1           1531085900  1531083900              8888
4   1           1531085900  1531082900              7777
5   1           1531086900  1531082900              5555

y una salida de la consulta se vería de la siguiente manera (solo la fila de la última marca de tiempo de observación representada)

id  source_id   timestamp   observation_timestamp   value
1   1           1531084900  1531083900              9999
3   1           1531085900  1531083900              8888
5   1           1531086900  1531082900              5555

Ya he consultado algunos materiales antes para optimizar estas consultas, a saber

... con poco éxito.

He considerado crear una tabla separada timestamppara que sea más fácil hacer referencia lateralmente, pero debido a la relativamente alta cardinalidad de aquellos, dudo si me ayudarán; además, me preocupa que dificulte el logro batch inserting new entries.


Estoy viendo tres consultas, y todas me dan un mal desempeño

  • CTE recursivo con unión LATERAL
  • Función de ventana
  • DISTINTO EN

(Soy consciente de que no hacen lo mismo en este momento, pero por lo que veo, sirven como buenas ilustraciones del tipo de consulta).

CTE recursivo con unión LATERAL

WITH RECURSIVE cte AS (
    (
        SELECT ts
        FROM timeseries ts
        WHERE source_id = 1
        ORDER BY id, "timestamp" DESC NULLS LAST
        LIMIT 1
    )
    UNION ALL
    SELECT (
        SELECT ts1
        FROM timeseries ts1
        WHERE id > (c.ts).id
        AND source_id = 1
        ORDER BY id, "timestamp" DESC NULLS LAST
        LIMIT 1
    )
    FROM cte c
    WHERE (c.ts).id IS NOT NULL
)
SELECT (ts).*
FROM cte
WHERE (ts).id IS NOT NULL
ORDER BY (ts).id;

Actuación:

Sort  (cost=164999681.98..164999682.23 rows=100 width=28)
  Sort Key: ((cte.ts).id)
  CTE cte
    ->  Recursive Union  (cost=1653078.24..164999676.64 rows=101 width=52)
          ->  Subquery Scan on *SELECT* 1  (cost=1653078.24..1653078.26 rows=1 width=52)
                ->  Limit  (cost=1653078.24..1653078.25 rows=1 width=60)
                      ->  Sort  (cost=1653078.24..1702109.00 rows=19612304 width=60)
                            Sort Key: ts.id, ts.timestamp DESC NULLS LAST
                            ->  Bitmap Heap Scan on timeseries ts  (cost=372587.92..1555016.72 rows=19612304 width=60)
                                  Recheck Cond: (source_id = 1)
                                  ->  Bitmap Index Scan on ix_timeseries_source_id  (cost=0.00..367684.85 rows=19612304 width=0)
                                        Index Cond: (source_id = 1)
          ->  WorkTable Scan on cte c  (cost=0.00..16334659.64 rows=10 width=32)
                Filter: ((ts).id IS NOT NULL)
                SubPlan 1
                  ->  Limit  (cost=1633465.94..1633465.94 rows=1 width=60)
                        ->  Sort  (cost=1633465.94..1649809.53 rows=6537435 width=60)
                              Sort Key: ts1.id, ts1.timestamp DESC NULLS LAST
                              ->  Bitmap Heap Scan on timeseries ts1  (cost=369319.21..1600778.77 rows=6537435 width=60)
                                    Recheck Cond: (source_id = 1)
                                    Filter: (id > (c.ts).id)
                                    ->  Bitmap Index Scan on ix_timeseries_source_id  (cost=0.00..367684.85 rows=19612304 width=0)
                                          Index Cond: (source_id = 1)
  ->  CTE Scan on cte  (cost=0.00..2.02 rows=100 width=28)
        Filter: ((ts).id IS NOT NULL)

(solo EXPLAIN, EXPLAIN ANALYZEno se pudo completar, tardó> 24 horas en completar la consulta)

Función de ventana

WITH summary AS (
  SELECT ts.id, ts.source_id, ts.value,
    ROW_NUMBER() OVER(PARTITION BY ts.timestamp ORDER BY ts.observation_timestamp DESC) AS rn
  FROM timeseries ts
  WHERE source_id = 1
)
SELECT s.*
FROM summary s
WHERE s.rn = 1;

Actuación:

CTE Scan on summary s  (cost=5530627.97..5971995.66 rows=98082 width=24) (actual time=150368.441..226331.286 rows=88404 loops=1)
  Filter: (rn = 1)
  Rows Removed by Filter: 20673704
  CTE summary
    ->  WindowAgg  (cost=5138301.13..5530627.97 rows=19616342 width=32) (actual time=150368.429..171189.504 rows=20762108 loops=1)
          ->  Sort  (cost=5138301.13..5187341.98 rows=19616342 width=24) (actual time=150368.405..165390.033 rows=20762108 loops=1)
                Sort Key: ts.timestamp, ts.observation_timestamp DESC
                Sort Method: external merge  Disk: 689752kB
                ->  Bitmap Heap Scan on timeseries ts  (cost=372675.22..1555347.49 rows=19616342 width=24) (actual time=2767.542..50399.741 rows=20762108 loops=1)
                      Recheck Cond: (source_id = 1)
                      Rows Removed by Index Recheck: 217784
                      Heap Blocks: exact=48415 lossy=106652
                      ->  Bitmap Index Scan on ix_timeseries_source_id  (cost=0.00..367771.13 rows=19616342 width=0) (actual time=2757.245..2757.245 rows=20762630 loops=1)
                            Index Cond: (source_id = 1)
Planning time: 0.186 ms
Execution time: 234883.090 ms

DISTINTO EN

SELECT DISTINCT ON (timestamp) *
FROM timeseries
WHERE source_id = 1
ORDER BY timestamp, observation_timestamp DESC;

Actuación:

Unique  (cost=5339449.63..5437531.34 rows=15991 width=28) (actual time=112653.438..121397.944 rows=88404 loops=1)
  ->  Sort  (cost=5339449.63..5388490.48 rows=19616342 width=28) (actual time=112653.437..120175.512 rows=20762108 loops=1)
        Sort Key: timestamp, observation_timestamp DESC
        Sort Method: external merge  Disk: 770888kB
        ->  Bitmap Heap Scan on timeseries  (cost=372675.22..1555347.49 rows=19616342 width=28) (actual time=2091.585..56109.942 rows=20762108 loops=1)
              Recheck Cond: (source_id = 1)
              Rows Removed by Index Recheck: 217784
              Heap Blocks: exact=48415 lossy=106652
              ->  Bitmap Index Scan on ix_timeseries_source_id  (cost=0.00..367771.13 rows=19616342 width=0) (actual time=2080.054..2080.054 rows=20762630 loops=1)
                    Index Cond: (source_id = 1)
Planning time: 0.132 ms
Execution time: 161651.006 ms

¿Cómo debo estructurar mis datos? ¿Hay escaneos que no deberían estar allí? ¿Es generalmente posible llevar estas consultas a ~ 1s (en lugar de ~ 120s)?

¿Hay alguna forma diferente de consultar los datos para obtener los resultados que quería?

Si no es así, ¿qué infraestructura / arquitectura diferente debería considerar?

Pepijn Schoen
fuente
Lo que esencialmente desea es un escaneo de índice suelto o escaneo omitido. Esos vendrán pronto. Puede aplicar el parche ahora si quiere confundirse con él postgresql-archive.org/Index-Skip-Scan-td6025532.html tiene apenas un mes de edad = P
Evan Carroll
Livin 'on the edge @EvanCarroll = P: eso me parece demasiado pronto, considerando que estoy usando Postgres en Azure que ni siquiera es factible.
Pepijn Schoen
Muestre los planes EXPLAIN ANALYZE sin los LÍMITES (ya que eso es lo que debe optimizarse), pero con los cambios que recomendé en mi primera respuesta. Pero sin los LÍMITES, creo que está pidiendo que se realice una cantidad imposible de trabajo en ~ 1s. Quizás puedas precalcular algunas cosas.
jjanes
@jjanes absolutamente, gracias por la sugerencia. He eliminado el LIMITde la pregunta ahora, y agregué resultados con EXPLAIN ANALYZE(solo EXPLAINen la recursiveparte)
Pepijn Schoen

Respuestas:

1

Con su consulta recursiva de CTE, el final ORDER BY (ts).ides innecesario ya que el CTE los crea automáticamente en ese orden. Al eliminar eso, la consulta debería ser mucho más rápida, puede detenerse temprano en lugar de generar 20,180,572 filas solo para tirar todas menos 500. Además, construir el índice (source_id, id, timestamp desc nulls last)debería mejorarlo aún más.

Para las otras dos consultas, aumentar work_mem lo suficiente como para que los mapas de bits quepan en la memoria (para deshacerse de los bloques de almacenamiento dinámico con pérdida) ayudaría a algunos. Pero no tanto como los índices personalizados, como (source_id, "timestamp", observation_timestamp DESC)o mejor aún para los escaneos de índice solamente (source_id, "timestamp", observation_timestamp DESC, value, id).

jjanes
fuente
Gracias por la sugerencia. Seguramente buscaré indexación personalizada como usted sugiere. El LIMIT 500era para mí para limitar la salida, pero en el código de producción que esto no suceda. Editaré mi publicación para reflejar eso.
Pepijn Schoen
En ausencia del LÍMITE, los índices podrían ser mucho menos efectivos. Pero aún así vale la pena intentarlo.
jjanes
Estás en lo correcto: con LIMITy tus sugerencias, la ejecución actual es 356.482 ms( Index Scan using ix_timeseries_source_id_timestamp_observation_timestamp on timeseries (cost=0.57..62573201.42 rows=18333374 width=28) (actual time=174.098..356.097 rows=2995 loops=1)) Pero sin LIMITque sea como antes. ¿Cómo aprovecharía también Index Scanen ese caso y no en el Bitmap Index/Heap Scan?
Pepijn Schoen