Optimisation d'une «dernière» requête dans Postgres sur 20 millions de lignes

10

Ma table se présente comme suit:

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

les index existent sur source_id, timestamp et sur une combinaison de timestamp et id ( CREATE INDEX timeseries_id_timestamp_combo_idx ON timeseries (id, timeseries DESC NULLS LAST))

Il y a 20M de lignes dedans (OK, il y a 120M, mais 20M avec source_id = 1). Il a de nombreuses entrées pour le même timestampavec variable observation_timestamp, qui décrivent un valueévénement survenu à timestampsignalé ou observé à observation_timestamp. Par exemple, la température prévue pour demain à 14 heures comme prévu aujourd'hui à 12 heures.

Idéalement, ce tableau fait bien quelques choses:

  • lot insérant de nouvelles entrées, parfois 100K à la fois
  • sélection des données observées pour les temporisations ("quelles sont les prévisions de température de janvier à mars")
  • sélection des données observées pour les temporisations observées à partir d'un certain point ("quelle est la vue des prévisions de température de janvier à mars comme on le pensait le 1er novembre")

La seconde est celle qui est au cœur de cette question.

Les données du tableau ressembleraient à ce qui suit

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

et une sortie de la requête ressemblerait à ce qui suit (seule la ligne de la dernière observation_timestamp représentée)

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

J'ai déjà consulté du matériel avant d'optimiser ces requêtes, à savoir

... avec un succès limité.

J'ai envisagé de créer un tableau séparé avec timestampdedans, il est donc plus facile de faire référence latéralement, mais en raison de la cardinalité relativement élevée de ceux-ci, je doute qu'ils m'aideront - en outre, je crains que cela ne soit difficile à accomplir batch inserting new entries.


Je regarde trois requêtes, et elles me donnent toutes de mauvaises performances

  • CTE récursif avec jointure LATERAL
  • Fonction fenêtre
  • DISTINCT SUR

(Je sais qu'ils ne font pas tout à fait la même chose pour le moment, mais ils servent de bonnes illustrations du type de requête pour autant que je sache.)

CTE récursif avec jointure 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;

Performance:

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)

(seulement EXPLAIN, EXPLAIN ANALYZEn'a pas pu terminer, a pris plus de 24 heures pour terminer la requête)

Fonction fenêtre

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;

Performance:

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

DISTINCT SUR

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

Performance:

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

Comment dois-je structurer mes données, y a-t-il des analyses qui ne devraient pas y être, est-il généralement possible d'obtenir ces requêtes à ~ 1s (au lieu de ~ 120s)?

Existe-t-il une autre façon d'interroger les données pour obtenir les résultats que je voulais?

Sinon, quelle infrastructure / architecture différente dois-je envisager?

Pepijn Schoen
la source
Ce que vous voulez essentiellement, c'est une analyse d'index lâche ou une analyse par saut. Ils arrivent bientôt. Vous pouvez appliquer le patch maintenant si vous voulez vous en mêler postgresql-archive.org/Index-Skip-Scan-td6025532.html il a à peine un mois = P
Evan Carroll
Vivre sur le bord @EvanCarroll = P - cela semble un peu trop tôt pour moi, étant donné que j'utilise Postgres sur Azure, même pas faisable.
Pepijn Schoen
Veuillez montrer les plans EXPLAIN ANALYZE sans les LIMITES (car c'est ce qui doit être optimisé), mais avec les changements que j'ai recommandés dans ma première réponse. Mais sans les LIMITES, je pense que vous demandez qu'une quantité impossible de travail soit effectuée en ~ 1s. Vous pouvez peut-être précalculer certaines choses.
jjanes
@jjanes absolument - merci pour la suggestion. J'ai supprimé le LIMITde la question maintenant, et ajouté une sortie avec EXPLAIN ANALYZE(uniquement EXPLAINsur la recursivepartie)
Pepijn Schoen

Réponses:

1

Avec votre requête CTE récursive, la finale ORDER BY (ts).idn'est pas nécessaire car le CTE les crée automatiquement dans cet ordre. En supprimant cela, la requête devrait être beaucoup plus rapide, elle peut s'arrêter tôt plutôt que de générer 20 180 572 lignes pour tout jeter sauf 500. En outre, la construction de l'indice (source_id, id, timestamp desc nulls last)devrait l'améliorer davantage.

Pour les deux autres requêtes, augmenter work_mem suffisamment pour que les bitmaps tiennent en mémoire (pour se débarrasser des blocs de tas avec perte) en aiderait certains. Mais pas autant que les index personnalisés, comme (source_id, "timestamp", observation_timestamp DESC)ou mieux encore pour les analyses d'index uniquement (source_id, "timestamp", observation_timestamp DESC, value, id).

jjanes
la source
Merci pour la suggestion - je vais certainement examiner l'indexation personnalisée comme vous le suggérez. Le LIMIT 500était destiné à limiter la sortie, mais dans le code de production, cela ne se produit pas. Je vais modifier mon message pour refléter cela.
Pepijn Schoen
En l'absence de LIMIT, les indices pourraient être beaucoup moins efficaces. Mais ça vaut quand même la peine d'essayer.
jjanes
Vous avez raison - avec le LIMITet vos suggestions, actuellement l'exécution est 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)) Mais sans LIMITc'est comme avant. Comment pourrais-je également tirer parti du Index Scandans ce cas et non du Bitmap Index/Heap Scan?
Pepijn Schoen