web-dev-qa-db-fra.com

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

J'utilise PostgreSQL 9.1 sur Ubuntu 12.04.

Je dois sélectionner des enregistrements dans un intervalle de temps: ma table time_limits possède deux champs timestamp et une propriété integer. 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);

Ce tableau contient environ 2 millions d'enregistrements.

Les requêtes comme celles-ci 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 du tableau semble plus raisonnable: entre 40 et 90 secondes.

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

J'ai essayé explain analyze pour la première fois pour 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 passé à analyser les deux colonnes d'horodatage une fois id_phi est réglé sur 0. Et je ne comprends pas le gros scan (60K lignes!) Sur les horodatages. Ne sont-ils pas indexés par la clé primaire et idx_inversed J'ai ajouté?

Dois-je passer des types 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?

105
Stephane Rolland

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. Les postgres peuvent numériser vers l'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:

Tenez compte de 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';

Ordre de tri de la première colonne id_phi dans l'index n'est pas pertinent. Puisqu'il est vérifié pour l'égalité (=), cela devrait venir en premier. Vous avez bien compris. Plus dans cette réponse connexe:

Postgres peut passer à id_phi = 0 en un rien de temps et considérez les deux colonnes suivantes de l'index correspondant. Ceux-ci sont interrogés avec des conditions de plage d'ordre de tri inversé (<=, >=). Dans mon index, les lignes qualificatives viennent en premier. Devrait être le moyen le plus rapide possible avec un index B-Tree1:

  • Tu veux start_date_time <= something: l'index a d'abord l'horodatage le plus ancien.
    • S'il est admissible, vérifiez également la colonne 3.
      Réexaminez jusqu'à ce que la première rangée ne se qualifie pas (super rapide).
  • Tu veux end_date_time >= something: l'index a d'abord le dernier horodatage.
    • S'il se qualifie, continuez à récupérer les lignes jusqu'à ce que la première ne le soit pas (super rapide).
      Continuez avec la valeur suivante pour la colonne 2 ..

Postgres peut numériser vers l'avant ou vers l'arrière. La façon dont vous avez eu l'index, il doit lire tout lignes correspondant sur les deux premières colonnes puis filtre sur la troisième. Assurez-vous de lire le chapitre Index et ORDER 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_time proche du début de la plage horaire de la table. Mais presque toutes les lignes avec id_phi = 0 à la fin chronologique du tableau! Les performances se dégradent donc avec les heures de démarrage ultérieures.

Estimations du planificateur

Le planificateur estime rows=62682 pour votre exemple de requête. Parmi ceux-ci, aucun n'est admissible (rows=0). Vous obtiendrez peut-être de meilleures estimations si vous augmentez l'objectif de statistiques pour le 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 encore plus. Plus dans cette réponse connexe:

Je suppose que vous n'en avez pas besoin pour id_phi (seulement quelques valeurs distinctes, uniformément réparties), mais pour les horodatages (beaucoup de valeurs distinctes, inégalement réparties).
Je ne pense pas non plus que cela compte beaucoup avec l'index amélioré.

CLUSTER/pg_repack

Si vous le souhaitez plus rapidement, vous pouvez cependant rationaliser l'ordre physique des lignes de votre table. Si vous pouvez vous permettre de verrouiller votre table exclusivement pendant une courte période (en dehors des heures d'ouverture par exemple) pour réécrire votre table et ordonner des 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 de même sans verrouillage exclusif.

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

Index Gist dans Postgres 9.2+

1 Avec pg 9.2+, il existe une autre option, peut-être plus rapide: a Index Gist pour une colonne de plage.

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

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

  • Pour l'index Gist multicolonne à portée de main, vous avez également besoin du module supplémentaire btree_Gist installé (une fois par base de données) qui fournit les classes d'opérateur pour inclure un integer.

Le trifecta! A 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 l'opérateur "contient la plage" @> dans votre requête maintenant:

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', '[]')

Index SP-Gist dans Postgres 9.3+

Un index SP-Gist peut être encore plus rapide pour ce type de requête - sauf que, en 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.
Vous devrez combiner un index spgist sur seulement (tsrange(...)) avec un deuxième index btree sur (id_phi). Avec les frais généraux supplémentaires, je ne suis pas sûr que cela puisse rivaliser.
Réponse connexe avec une référence pour juste une colonne tsrange:

177

Cependant, la réponse d'Erwin est déjà complète:

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

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

5
nathan-m

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 un question similaire également lié à la commande des index sur un index multicolonne. La clé essaie d'utiliser d'abord les conditions les plus restrictives pour réduire l'espace de recherche.

Edit : Mon erreur. Maintenant, je vois que cet index est déjà défini.

3
jap1968

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

J'ai un tableau avec des agrégations de nombreuses mesures et de nombreux niveaux (colonne l) (30s, 1m, 1h, etc.), il y a deux colonnes liées à la plage: $s pour démarrer et $e pour 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électionnez les plages où leur limite de début est dans une plage donnée. sélectionnez en outre des plages où leur limite d'extrémité est dans une plage donnée.

Explain montre deux flux de lignes utilisant efficacement nos index.

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 contiennent uniquement des lignes souhaitées. Auparavant, nous avions des milliers de lignes dans le nœud du plan, car il sélectionnait all points from some point in time to the very end, puis le nœud suivant a supprimé les lignes inutiles.

1
borovsky