web-dev-qa-db-fra.com

Comment rendre DISTINCT ON plus rapide dans PostgreSQL?

J'ai une table station_logs dans une base de données 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)

J'essaye d'obtenir le dernier level_sensor valeur basée sur submitted_at, pour chaque station_id. Il existe environ 400 station_id valeurs, et environ 20 000 lignes par jour par station_id.

Avant de créer un index:

EXPLAIN ANALYZE
SELECT DISTINCT ON(station_id) station_id, submitted_at, level_sensor
FROM station_logs ORDER BY station_id, submitted_at DESC;
 Unique (coût = 4347852.14..4450301.72 lignes = 89 largeur = 20) (temps réel = 22202.080..27619.167 lignes = 98 boucles = 1) 
 -> Trier (coût = 4347852.14..4399076.93 lignes = 20489916 largeur = 20) (heure réelle = 22202.077..26540.827 lignes = 20489812 boucles = 1) 
 Clé de tri: station_id, soumis_à DESC 
 Méthode de tri: fusion externe Disque: 681040kB 
 -> Seq Scan sur station_logs (coût = 0,00..598895,16 lignes = 20489916 largeur = 20) (temps réel = 0,023..3443,587 lignes = 20489812 boucles = $ 
 Temps de planification: 0,072 ms 
 Temps d'exécution: 27690.644 SP

Création d'un index:

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

Après avoir créé l'index, pour la même requête:

 Unique (coût = 0,56..2156367,51 lignes = 89 largeur = 20) (temps réel = 0,184..16263.413 lignes = 98 boucles = 1) 
 -> Index Scan en utilisant station_id__submitted_at sur station_logs (coût = 0,56..2105142.98 rangées = 20489812 largeur = 20) (temps réel = 0,181..1 $ 
 Temps de planification: 0,206 ms 
 Temps d'exécution: 16263,490 ms

Existe-t-il un moyen d'accélérer cette requête? Comme 1 seconde par exemple, 16 secondes, c'est encore trop.

13
Kokizzu

Pour seulement 400 stations, cette requête sera massivement plus rapide:

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 ici
(en comparant les plans de cette requête, l'alternative d'Abelisto et l'original)

Résultat EXPLAIN ANALYZE tel que fourni par l'OP:

 Boucle imbriquée (coût = 0,56..356,65 lignes = 102 largeur = 20) (temps réel = 0,034..0,979 lignes = 98 boucles = 1) 
 -> Seq Scan sur les stations s (coût = 0,00..3,02 lignes = 102 largeur = 4) (temps réel = 0,009..0,016 lignes = 102 boucles = 1) 
 -> Limite (coût = 0,56..3,45 lignes = 1 largeur = 16) (temps réel = 0,009. .0.009 lignes = 1 boucles = 102) 
 -> Index Scan utilisant station_id__submitted_at sur station_logs (coût = 0,56..664062,38 lignes = 230223 largeur = 16) (temps réel = 0,009 $ 
 Index Cond: (station_id = s.id) 
 Temps de planification: 0,542 ms 
 Temps d'exécution: 1,013 ms  - !!

Le seul index dont vous avez besoin est celui que vous avez créé: station_id__submitted_at. La contrainte UNIQUEuniq_sid_sat fait aussi le travail, essentiellement. La maintenance des deux semble être une perte d'espace disque et de performances d'écriture.

J'ai ajouté NULLS LAST à ORDER BY dans la requête car submitted_at n'est pas défini NOT NULL. Idéalement, le cas échéant !, ajoutez un NOT NULL contrainte à la colonne submitted_at, supprimez l'index supplémentaire et supprimez NULLS LAST de la requête.

Si submitted_at peut être NULL, créez cet index UNIQUE pour remplacer à la fois votre index actuel et contrainte unique:

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

Considérer:

Cela suppose une table séparée station avec une ligne par _ station_id (généralement le PK) - que vous devriez avoir dans les deux cas. Si vous ne l'avez pas, créez-le. Encore une fois, très rapide avec cette technique 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;

Je l'utilise aussi au violon. Vous pouvez utiliser une requête similaire pour résoudre votre tâche directement, sans table station - si vous ne pouvez pas être convaincu de la créer.

Instructions détaillées, explication et alternatives:

Optimiser l'index

Votre requête devrait être très rapide maintenant. Seulement si vous devez encore optimiser les performances de lecture ...

Il pourrait être judicieux d'ajouter level_sensor comme dernière colonne de l'index pour autoriser les analyses d'index uniquement , comme commentaire de joanolo .
Con: Il agrandit l'index - ce qui ajoute un peu de coût à toutes les requêtes l'utilisant.
Pro: Si vous n'obtenez en fait que des analyses d'index, la requête en cours n'a pas du tout besoin de visiter les pages de tas, ce qui la rend environ deux fois plus rapide. Mais cela peut être un gain non substantiel pour la requête très rapide maintenant.

Cependant , je ne m'attends pas à ce que cela fonctionne pour votre cas. Vous avez mentionné:

... environ 20 000 lignes par jour et par station_id.

En règle générale, cela indiquerait une charge d'écriture incessante (1 par station_id toutes les 5 secondes). Et vous êtes intéressé par la ligne dernier. Les analyses d'index uniquement ne fonctionnent que pour les pages de segment de mémoire visibles par toutes les transactions (le bit dans la carte de visibilité est défini). Vous devez exécuter des paramètres VACUUM extrêmement agressifs pour que la table suive la charge d'écriture, et cela ne fonctionnera toujours pas la plupart du temps. Si mes hypothèses sont correctes, les analyses d'index uniquement sont sorties, ne le faites pas add level_sensor à l'index.

OTOH, si mes hypothèses se vérifient et que votre table grandit très grand , un indice BRIN pourrait aider. En relation:

Ou, encore plus spécialisé et plus efficace: un index partiel pour les derniers ajouts seulement pour couper le gros des lignes non pertinentes:

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';

Choisissez un horodatage pour lequel vous - savez que des lignes plus jeunes doivent exister. Vous devez ajouter une condition WHERE correspondante à toutes les requêtes, comme:

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

Vous devez adapter l'index et la requête de temps en temps.
Réponses associées avec plus de détails:

18
Erwin Brandstetter

Essayez la méthode classique:

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

EXPLIQUER L'ANALYSE par 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
6
Abelisto