Question

J'ai une simple table de séries temporelles

movement_history (
    data_id serial,
    item_id character varying (8),
    event_time timestamp without timezone,
    location_id character varying (7),
    area_id character varying (2)
);

Mon développeur frontend me dit que le coût est trop élevé s'il veut savoir où se trouve un élément à un horodatage donné car il doit trier le tableau.Il veut que j'ajoute un autre champ d'horodatage pour le prochain événement afin qu'il n'ait pas à trier.Pourtant, cela va plus que doubler le coût de mon code pour insérer un nouveau mouvement, car je devrai rechercher l'entrée précédente de l'élément, la mettre à jour, puis insérer les nouvelles données.

Mes insertions dépassent bien sûr de loin ses requêtes en fréquence.Et je n'ai jamais vu de tableau de séries chronologiques incluant une entrée pour l'heure du prochain événement.Il me dit que ma table est cassée parce que sa requête peu fréquente nécessite un tri.Aucune suggestion?

Je ne sais pas quelle requête il utilise mais je ferais ceci :

select * from movement_history 
where event_time <= '1-15-2015'::timestamp  
and item_id = 'H665AYG3' 
order by event_time desc limit 1;

Nous avons actuellement environ 15 000 éléments, ils sont saisis au maximum dans la base de données une fois par jour.Cependant, nous aurons bientôt 50 000 éléments avec des données de capteur mises à jour toutes les 1 à 5 minutes.

Je ne vois pas sa requête effectuée très souvent mais une autre requête pour obtenir l'état actuel des palettes le sera.

select distinct on (item_id) * 
from movement_history 
order by item_id, event_time desc;

Ce serveur exécute actuellement la version 9.3 mais il pourrait fonctionner sous la version 9.4 si nécessaire.

Était-ce utile?

La solution

Créer un index sur (item_id, event_time).

Il passera à l'item_id spécifié, passera à l'heure_événement spécifiée pour cet item_id, puis reculera d'un point.Aucun tri à prévoir.

Autres conseils

Des solutions contradictoires

Vous auriez besoin d'un index multicolonne comme @jjanes fourni.Pendant que vous y êtes, vous pourrait faire (item_id, event_time) la clé primaire pour fournir l'index automatiquement.

Mais cela est en conflit avec les performances d'écriture comme @Michael a expliqué:Vous doublez le coût pour 50K of items ... updated every 1 to 5 minutes faire occasionnel SELECT requêtes moins chères.Cela représente environ 1 million.lignes par heure.

Partitionnement

Si vous n'avez pas d'exigences plus contradictoires, le compromis pourrait être partitionnement où le actuel la partition n'a pas encore d'index.De cette façon, vous obtenez performances d'écriture optimales et performances de lecture (presque) supérieures.

La table parent pourrait être movement_history, la partition actuelle movement_history_current.Pas d'index, une seule contrainte à autoriser exclusion de contrainte.Il peut s'agir de partitions quotidiennes par défaut.Mais les intervalles de temps peuvent être rien, ne doit même pas être régulier.Nous pouvons travailler avec cela et démarrer une nouvelle partition chaque fois que nous en avons besoin.

Lorsque vous devez inclure les données actuelles dans ladite requête, procédez comme suit :

  1. Pour démarrer une nouvelle partition, en une seule transaction :

    • Renommez la partition actuelle en ajoutant qc.au nom, comme movement_history_20150110_20150115 (ou plus spécifique) et ajustez la contrainte sur event_time.
    • Créez une nouvelle partition avec toujours le même nom movement_history_current et une contrainte sur event_time qui ne chevauche pas le dernier et avec extrémité ouverte.
    • En fonction de vos modèles d'accès, vous devrez peut-être gérer un accès en écriture simultané...
  2. Ajouter un PK sur (item_id, event_time) à la nouvelle partition historique.Pas dans la même transaction.Créer l'index en un seul morceau est beaucoup moins cher que d’y ajouter progressivement.

    2a.Pour intégrer des conseils pour votre deuxième requête ci-dessous :

    REFRESH MATERIALIZED VIEW mv_last_movement 
    
  3. Exécutez la requête.En fait, vous pouvez exécuter la requête n'importe lequel temps.S'il inclut la partition actuelle ou toute partition qui n'a pas encore l'index, c'est plus lent pour cette partition.

Archivez les partitions les plus anciennes de temps en temps.Sauvegardez et supprimez simplement la table.N'interfère pas beaucoup avec le fonctionnement en cours, c'est la beauté du cloisonnement.

Lisez d'abord le manuel.Il y a mises en garde pour héritage et partitionnement.

Votre deuxième requête

La deuxième requête que vous avez ajoutée lors d'une modification est la loin plus gros problème pour les performances.Je parle d'ordres de grandeur :

select distinct on (item_id) * from movement_history
order by item_id, event_time desc;

Une fois que vous commencez à insérer 1 mio.lignes par heure, les performances de cette requête se détérioreront rapidement.Vous avez affaire à beaucoup beaucoup lignes par article, DISTINCT ON n'est bon que pour peu lignes par article.Explication détaillée pour DISTINCT ON et des alternatives plus rapides :

Je suggère toujours partitionnement comme dans ma première réponse.Mais imposez une nouvelle partition à intervalles raisonnables, afin que la partition actuelle ne devienne pas trop grande.

De plus, créez un "vue matérialisée" permettant de suivre le dernier état de chaque élément.Ce n'est pas une norme MATERIALIZED VIEW car la requête de définition a une auto-référence.je le nomme mv_last_movement et il a le même type de ligne que movement_history.

Actualiser chaque fois qu'une nouvelle partition démarre (voir ci-dessus).
En supposant l'existence d'un item tableau:

CREATE TABLE item (
  item_id varchar(8) PRIMARY KEY  -- should really be a serial 
  -- more columns?
);

Si vous n'en avez pas, créez-le.Ou utilisez la technique alternative récursive CTE décrite dans la réponse liée ci-dessus.

Initialisation mv_last_movement une fois:

CREATE TABLE mv_last_movement AS
SELECT m.*
FROM   item i
,      LATERAL (
   SELECT *
   FROM   movement_history_current  -- current partition
   WHERE  item_id = i.item_id  -- lateral reference
   ORDER  BY event_time DESC
   LIMIT  1
   ) m;

ALTER TABLE mv_last_movement ADD PRIMARY KEY (item_id);

Ensuite, pour actualiser (en une seule transaction !) :

BEGIN;

CREATE TABLE mv_last_movement2 AS
SELECT M. * À partir de l'article I, latéral ((- parenthèses requise Sélectionner * FROM Motion_history_current - Partition actuelle où item_id = i.item_id - Ordre de référence latéral par Event_time Desc Limit 1 - appliqué à ce sélection, pas strictement nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais nécessaire mais strictement nécessaire mais nécessaire moins cher) Union all - Si ce n'est pas trouvé, retombez au dernier état précédent Sélectionner * dans MV_LAST_MOVEMENT - votre vue matérialisée où item_id = i.item_id - Limite de référence latérale 1 - appliquée à la requête de l'Union entière) m;

DROP TABLE mv_last_movement;
ALTER TABLE mv_last_movement2 RENAME mv_last_movement;
ALTER TABLE mv_last_movement ADD PRIMARY KEY (item_id);

COMMIT;

Ou similaire.Plus de détails ici :

La même requête ci-dessus (souligné en gras) remplace également votre requête originale citée en haut.

De cette façon, vous n'avez pas besoin d'inspecter tout l'historique pour les éléments sans lignes actuelles, ce qui serait extrêmement coûteux.

Pourquoi UNION ALL ... LIMIT 1?

Plus de conseils

  • varchar pour les colonnes PK/FK est inefficace, en particulier pour les grandes tables avec 1 million de lignes par heure.Utiliser integer clés à la place.

  • Utilisez toujours le format ISO pour les littéraux de date et d'horodatage, sinon vos requêtes dépendent des paramètres régionaux : '2015-15-01' au lieu de '1-15-2015'.

  • Ajouter NOT NULL contraintes où la colonne ne peut pas être NULL.

  • Optimisez la disposition de votre table pour éviter la perte d'espace à cause du rembourrage

La conception de logiciels est souvent un compromis entre des exigences concurrentes.Il est important de comprendre les mérites relatifs, tant pour le système dans son ensemble que pour chaque cas au niveau local.Par exemple, vous dites que le nombre d'écritures est supérieur au nombre de lectures.Cela suggérerait que le système dans son ensemble devrait être optimisé pour les écritures.Cependant, à quoi servent ces lectures : évitent-elles une collision de véhicule ou un arrêt cardiaque ?Peut-être que ces systèmes devraient être optimisés pour la lecture.

Avez-vous un index sur la colonne temps ?Puis une requête comme select top (1) .. where time < parameter .. sorted desc devrait utiliser cet index.Essentiellement, vous pré-triez les données pour toutes les requêtes.

L’ironie est que chaque écriture devra maintenir cet index, doublant le coût à chaque fois.

Licencié sous: CC-BY-SA avec attribution
Non affilié à dba.stackexchange
scroll top