Optimisation des requêtes sur une plage d'horodatages (deux colonnes)


96

J'utilise PostgreSQL 9.1 sur Ubuntu 12.04.

Je dois sélectionner des enregistrements dans une plage de temps: ma table time_limitsa deux timestampchamps et une integerpropriété. Il y a des colonnes supplémentaires dans ma table réelle qui ne sont pas impliquées dans cette requête.

create table (
   start_date_time timestamp,
   end_date_time timestamp, 
   id_phi integer, 
   primary key(start_date_time, end_date_time,id_phi);

Cette table contient environ 2 millions d'enregistrements.

Les requêtes suivantes ont pris énormément de temps:

select * from time_limits as t 
where t.id_phi=0 
and t.start_date_time <= timestamp'2010-08-08 00:00:00'
and t.end_date_time   >= timestamp'2010-08-08 00:05:00';

J'ai donc essayé d'ajouter un autre index - l'inverse du PK:

create index idx_inversed on time_limits(id_phi, start_date_time, end_date_time);

J'ai l'impression que les performances se sont améliorées: le temps d'accès aux enregistrements au milieu de la table semble être plus raisonnable: entre 40 et 90 secondes.

Mais il reste encore plusieurs dizaines de secondes pour les valeurs situées au milieu de la plage de temps. Et deux fois plus lorsque vous visez la fin de la table (chronologiquement parlant).

J'ai essayé explain analyzepour la première fois d'obtenir ce plan de requête:

 Bitmap Heap Scan on time_limits  (cost=4730.38..22465.32 rows=62682 width=36) (actual time=44.446..44.446 rows=0 loops=1)
   Recheck Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
   ->  Bitmap Index Scan on idx_time_limits_phi_start_end  (cost=0.00..4714.71 rows=62682 width=0) (actual time=44.437..44.437 rows=0 loops=1)
         Index Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
 Total runtime: 44.507 ms

Voir les résultats sur depesz.com.

Que puis-je faire pour optimiser la recherche? Vous pouvez voir tout le temps est passé numérise une fois que les deux colonnes horodatages id_phiest définie sur 0. Et je ne comprends pas le gros balayage (60 000 lignes!) Sur les horodatages. Ne sont-ils pas indexés par la clé primaire et idx_inversedj'ai ajouté?

Devrais-je changer de type d'horodatage à autre chose?

J'ai lu un peu sur les index GIST et GIN. Je suppose qu'ils peuvent être plus efficaces à certaines conditions pour les types personnalisés. Est-ce une option viable pour mon cas d'utilisation?


1
ben c'est 45s. Je ne sais pas pourquoi on dit 45ms. Je ne me plaindrais même pas si c'était aussi rapide que 45ms… Ou peut-être que c'est le moment de l'analyse à effectuer. Dunno. Mais 40/50 secondes est ce que je mesure.
Stephane Rolland

2
L'heure indiquée dans la explain analyzesortie est l'heure à laquelle la requête a été requise sur le serveur . Si votre requête prend 45 secondes, le temps supplémentaire est utilisé pour transférer les données de la base de données au programme qui exécute la requête. Après tout, c'est 62682 lignes et si chaque ligne est grande (par exemple, a long varcharou textcolonnes), cela peut avoir un impact sur le temps de transfert drastiquement.
a_horse_with_no_name

@a_horse_with_no_name: rows=62682 rowsest l' estimation du planificateur . La requête renvoie 0 ligne. (actual time=44.446..44.446 rows=0 loops=1)
Erwin Brandstetter

@ ErwinBrandstetter: ah, c'est vrai. J'ai oublié ça. Mais je n’ai toujours pas vu la sortie d’expliquer analyser mensonge sur le temps d’exécution.
a_horse_with_no_name

Réponses:


162

Pour Postgres 9.1 ou version ultérieure:

CREATE INDEX idx_time_limits_ts_inverse
ON time_limits (id_phi, start_date_time, end_date_time DESC);

Dans la plupart des cas, l'ordre de tri d'un index n'est guère pertinent. Postgres peut scanner en arrière pratiquement aussi rapidement. Mais pour les requêtes de plage sur plusieurs colonnes, cela peut faire une énorme différence. Étroitement liés:

Considérez votre requête:

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    start_date_time <= '2010-08-08 00:00'
AND    end_date_time   >= '2010-08-08 00:05';

L'ordre de tri de la première colonne id_phide l'index est sans importance. Puisqu'il est coché pour l' égalité ( =), cela devrait venir en premier. Tu as totalement raison. Plus dans cette réponse liée:

Les postgres peuvent passer rapidement à id_phi = 0et prendre en compte les deux colonnes suivantes de l'index correspondant. Celles-ci sont interrogées avec des conditions de plage d'ordre de tri inversé ( <=, >=). Dans mon index, les lignes qualifiantes viennent en premier. Devrait être le moyen le plus rapide possible avec un index B-Tree 1 :

  • Vous voulez start_date_time <= something: index a le timestamp le plus ancien en premier.
    • Si elle est qualifiée, vérifiez également la colonne 3.
      Recurse jusqu'à ce que la première ligne ne se qualifie pas (super rapide).
  • Vous voulez end_date_time >= something: index a d'abord le dernier timestamp.
    • S'il remplit les conditions requises, continuez à extraire les lignes jusqu'à ce que la première ne le soit pas (ultra rapide).
      Continuer avec la valeur suivante pour la colonne 2.

Postgres peut numériser en avant ou en arrière. De la même manière que vous aviez l'index, il doit lire toutes les lignes qui correspondent sur les deux premières colonnes et ensuite filtrer sur la troisième. Assurez-vous de lire le chapitre Index etORDER BY dans le manuel. Cela correspond assez bien à votre question.

Combien de lignes correspondent sur les deux premières colonnes?
Seuls quelques-uns avec un start_date_timeproche du début de la plage de temps de la table. Mais presque toutes les lignes avec id_phi = 0à la fin chronologique de la table! Ainsi, les performances se détériorent avec les heures de début plus tard

Estimations du planificateur

Le planificateur estime rows=62682votre requête d'exemple. Parmi ceux-ci, aucun n'est admissible ( rows=0). Vous obtiendrez peut-être de meilleures estimations si vous augmentez la cible statistique du tableau. Pour 2.000.000 lignes ...

ALTER TABLE time_limits ALTER start_date_time SET STATISTICS 1000;
ALTER TABLE time_limits ALTER end_date_time   SET STATISTICS 1000;

... pourrait payer. Ou même plus haut. Plus dans cette réponse liée:

Je suppose que vous n’avez pas besoin de cela pour id_phi(seulement quelques valeurs distinctes, réparties de manière égale), mais pour les horodatages (beaucoup de valeurs distinctes, réparties de manière inégale).
Je ne pense pas non plus que l’important avec l’indice amélioré compte beaucoup.

CLUSTER / pg_repack

Si vous le souhaitez plus rapidement, vous pouvez rationaliser l'ordre physique des lignes de votre tableau. Si vous pouvez vous permettre de verrouiller votre table exclusivement pendant une courte période (aux heures creuses, par exemple), réécrivez votre table et ordonnez les lignes en fonction de l'index:

ALTER TABLE time_limits CLUSTER ON idx_time_limits_inversed;

Avec un accès simultané, considérez pg_repack , qui peut faire la même chose sans verrou exclusif.

Quoi qu'il en soit, l'effet est que moins de blocs doivent être lus dans la table et que tout est trié au préalable. C'est un effet ponctuel qui se détériore avec le temps, les écritures sur la table fragmentant l'ordre de tri physique.

Indice GiST dans Postgres 9.2+

1 Avec la page 9.2+, il existe une autre option, peut-être plus rapide: un index GiST pour une colonne d'intervalle.

  • Il existe des types de plage intégrés pour timestampet timestamp with time zone: tsrange,tstzrange . Un index btree est généralement plus rapide pour une integercolonne supplémentaire comme id_phi. Plus petit et moins cher à entretenir, aussi. Mais la requête sera probablement toujours plus rapide dans l'ensemble avec l'index combiné.

  • Changez la définition de votre table ou utilisez un index d'expression .

  • Pour l’index GiST multicolonne disponible, vous devez également btree_gistinstaller le module supplémentaire (une fois par base de données), qui fournit aux classes d’opérateurs l’inclusion d’un integer.

Le trifecta! Un index GiST fonctionnel multicolonne :

CREATE EXTENSION IF NOT EXISTS btree_gist;  -- if not installed, yet

CREATE INDEX idx_time_limits_funky ON time_limits USING gist
(id_phi, tsrange(start_date_time, end_date_time, '[]'));

Utilisez maintenant l' opérateur "contient la plage"@> dans votre requête:

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    tsrange(start_date_time, end_date_time, '[]')
    @> tsrange('2010-08-08 00:00', '2010-08-08 00:05', '[]')

Indice SP-GiST dans Postgres 9.3+

Un index SP-GiST peut être encore plus rapide pour ce type de requête, à ceci près que, citant le manuel :

Actuellement, seuls les types d'index B-tree, GiST, GIN et BRIN prennent en charge les index multicolonnes.

Toujours vrai dans Postgres 12.
Il vous faudrait combiner un spgistindex uniquement (tsrange(...))avec un deuxième btreeindex (id_phi). Avec les frais généraux ajoutés, je ne suis pas sûr que cela puisse rivaliser.
Réponse associée avec un repère pour une tsrangecolonne seulement:


78
Je ne devrais pas dire cela au moins une fois, que chacune de vos réponses sur SO et DBA a une très grande valeur ajoutée / expertise , et la plupart du temps la plus complète. Juste pour le dire une fois: Respect !.
Stéphane Rolland

1
Merci bien! :) Vous avez donc obtenu des résultats plus rapides?
Erwin Brandstetter

Je dois laisser terminer la copie volumineuse générée par la requête extrêmement délicate de la mienne. Donc, ralentissant le processus, le processus tournait des heures avant que je ne pose la question. Mais j'ai calculé, et j'ai décidé de laisser tourner jusqu'à demain matin, ce sera fini et la nouvelle table prête à être remplie demain. J'ai essayé de créer votre index simultanément pendant le travail, mais en raison d'un accès trop important (je pense), la création de l'index devrait être verrouillée. Je répète ce même temps de test demain avec votre solution. J'ai aussi examiné comment passer à la version 9.2 ;-) pour debian / ubuntu.
Stéphane Rolland

2
@StephaneRolland: il serait toujours intéressant de savoir pourquoi la sortie d'analyse explique 45 millisecondes alors que la requête prend plus de 40 secondes.
a_horse_with_no_name

1
@John: Postgres peut parcourir un index en avant ou en arrière, mais il ne peut pas changer de direction dans le même balayage. Idéalement, vous avez toutes les lignes qualifiantes par noeud en premier (ou dernier), mais le même alignement (prédicats de requête correspondants) doit être respecté pour toutes les colonnes afin d'obtenir les meilleurs résultats.
Erwin Brandstetter

5

La réponse d'Erwin est cependant déjà complète:

Les types de plage pour les horodatages sont disponibles dans PostgreSQL 9.1 avec l'extension Temporal de Jeff Davis: https://github.com/jeff-davis/PostgreSQL-Temporal

Remarque: a des fonctionnalités limitées (utilise Timestamptz, et vous ne pouvez avoir qu'un chevauchement de style '[)'). En outre, il existe de nombreuses autres bonnes raisons de mettre à niveau vers PostgreSQL 9.2.


3

Vous pouvez essayer de créer l'index multicolonne dans un ordre différent:

primary key(id_phi, start_date_time,end_date_time);

J'ai posté une fois une question similaire concernant également le classement des index sur un index multicolonne. La clé consiste à essayer d’abord d’utiliser les conditions les plus restrictives pour réduire l’espace de recherche.

Edit : mon erreur. Maintenant, je vois que vous avez déjà défini cet index.


J'ai déjà les deux index. Sauf que la clé primaire est l'autre, mais l'index que vous proposez existe déjà et c'est celui qui est utilisé si vous regardez l'explication:Bitmap Index Scan on idx_time_limits_phi_start_end
Stephane Rolland

1

J'ai réussi à augmenter rapidement (de 1 seconde à 70 ms)

J'ai un tableau avec des agrégations de nombreuses mesures et de nombreux niveaux ( lcolonne) (30s, 1m, 1h, etc.), il y a deux colonnes liées par plage: $spour le début et $epour la fin.

J'ai créé deux index multicolonnes: un pour le début et un pour la fin.

J'ai ajusté la requête de sélection: sélection des plages où leur limite de départ est comprise dans une plage donnée. De plus, sélectionnez des plages où leur extrémité est dans la plage donnée.

Explain montre deux flux de lignes utilisant nos index efficacement.

Index:

drop index if exists agg_search_a;
CREATE INDEX agg_search_a
ON agg (measurement_id, l, "$s");

drop index if exists agg_search_b;
CREATE INDEX agg_search_b
ON agg (measurement_id, l, "$e");

Sélectionnez la requête:

select "$s", "$e", a, t, b, c from agg
where 
    measurement_id=0 
    and l =  '30s'
    and (
        (
            "$s" > '2013-05-01 02:05:05'
            and "$s" < '2013-05-01 02:18:15'
        )
        or 
        (
             "$e" > '2013-05-01 02:00:05'
            and "$e" < '2013-05-01 02:18:05'
        )
    )

;

Explique:

[
  {
    "Execution Time": 0.058,
    "Planning Time": 0.112,
    "Plan": {
      "Startup Cost": 10.18,
      "Rows Removed by Index Recheck": 0,
      "Actual Rows": 37,
      "Plans": [
    {
      "Startup Cost": 10.18,
      "Actual Rows": 0,
      "Plans": [
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 26,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone))",
          "Plan Rows": 29,
          "Parallel Aware": false,
          "Actual Total Time": 0.016,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.016,
          "Total Cost": 5,
          "Actual Loops": 1,
          "Index Name": "agg_search_a"
        },
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 36,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone))",
          "Plan Rows": 39,
          "Parallel Aware": false,
          "Actual Total Time": 0.011,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.011,
          "Total Cost": 5.15,
          "Actual Loops": 1,
          "Index Name": "agg_search_b"
        }
      ],
      "Node Type": "BitmapOr",
      "Plan Rows": 68,
      "Parallel Aware": false,
      "Actual Total Time": 0.027,
      "Parent Relationship": "Outer",
      "Actual Startup Time": 0.027,
      "Plan Width": 0,
      "Actual Loops": 1,
      "Total Cost": 10.18
    }
      ],
      "Exact Heap Blocks": 1,
      "Node Type": "Bitmap Heap Scan",
      "Plan Rows": 68,
      "Relation Name": "agg",
      "Alias": "agg",
      "Parallel Aware": false,
      "Actual Total Time": 0.037,
      "Recheck Cond": "(((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone)) OR ((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone)))",
      "Lossy Heap Blocks": 0,
      "Actual Startup Time": 0.033,
      "Plan Width": 44,
      "Actual Loops": 1,
      "Total Cost": 280.95
    },
    "Triggers": []
  }
]

L'astuce est que vos nœuds de plan ne contiennent que des lignes utiles. Auparavant, nous avions des milliers de lignes dans le nœud du plan car celui-ci avait été sélectionné all points from some point in time to the very end, puis le nœud suivant supprimait les lignes inutiles.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.