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.
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 UNIQUE
uniq_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:
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:
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);
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