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

8

J'utilise Postgres 9.3 via Heroku.

J'ai une table, "trafic", avec 1M + d'enregistrements qui a de nombreux insertions et mises à jour chaque jour. J'ai besoin d'effectuer des opérations SUM sur cette table sur différentes plages de temps et ces appels peuvent prendre jusqu'à 40 secondes et j'aimerais entendre des suggestions sur la façon d'améliorer cela.

J'ai l'index suivant en place sur cette table:

CREATE INDEX idx_traffic_partner_only ON traffic (dt_created) WHERE campaign_id IS NULL AND uuid_self <> uuid_partner;

Voici un exemple d'instruction SELECT:

SELECT SUM("clicks") AS clicks, SUM("impressions") AS impressions
FROM "traffic"
WHERE "uuid_self" != "uuid_partner"
AND "campaign_id" is NULL
AND "dt_created" >= 'Sun, 29 Mar 2015 00:00:00 +0000'
AND "dt_created" <= 'Mon, 27 Apr 2015 23:59:59 +0000' 

Et voici l'ANALYSE EXPLICITE:

Aggregate  (cost=21625.91..21625.92 rows=1 width=16) (actual time=41804.754..41804.754 rows=1 loops=1)
  ->  Index Scan using idx_traffic_partner_only on traffic  (cost=0.09..20085.11 rows=308159 width=16) (actual time=1.409..41617.976 rows=302392 loops=1)
      Index Cond: ((dt_created >= '2015-03-29'::date) AND (dt_created <= '2015-04-27'::date))
Total runtime: 41804.893 ms

http://explain.depesz.com/s/gGA

Cette question est très similaire à une autre sur SE, mais celle-ci a utilisé un index sur deux plages d'horodatage de colonne et le planificateur d'index pour cette requête avait des estimations très éloignées. La principale suggestion était de créer un index multi-colonnes trié, mais pour les index à colonne unique, cela n'a pas beaucoup d'effet. Les autres suggestions étaient d'utiliser les index CLUSTER / pg_repack et GIST, mais je ne les ai pas encore essayés, car j'aimerais voir s'il existe une meilleure solution en utilisant des index réguliers.

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

Pour référence, j'ai essayé les index suivants, qui n'étaient pas utilisés par la base de données:

INDEX idx_traffic_2 ON traffic (campaign_id, uuid_self, uuid_partner, dt_created);
INDEX idx_traffic_3 ON traffic (dt_created);
INDEX idx_traffic_4 ON traffic (uuid_self);
INDEX idx_traffic_5 ON traffic (uuid_partner);

EDIT : Ran EXPLAIN (ANALYSER, VERBOSE, COÛTS, TAMPONS) et voici les résultats:

Aggregate  (cost=20538.62..20538.62 rows=1 width=8) (actual time=526.778..526.778 rows=1 loops=1)
  Output: sum(clicks), sum(impressions)
  Buffers: shared hit=47783 read=29803 dirtied=4
  I/O Timings: read=184.936
  ->  Index Scan using idx_traffic_partner_only on public.traffic  (cost=0.09..20224.74 rows=313881 width=8) (actual time=0.049..431.501 rows=302405 loops=1)
      Output: id, uuid_self, uuid_partner, impressions, clicks, dt_created... (other fields redacted)
      Index Cond: ((traffic.dt_created >= '2015-03-29'::date) AND (traffic.dt_created <= '2015-04-27'::date))
      Buffers: shared hit=47783 read=29803 dirtied=4
      I/O Timings: read=184.936
Total runtime: 526.881 ms

http://explain.depesz.com/s/7Gu6

Définition du tableau:

CREATE TABLE traffic (
    id              serial,
    uuid_self       uuid not null,
    uuid_partner    uuid not null,
    impressions     integer NOT NULL DEFAULT 1,
    clicks          integer NOT NULL DEFAULT 0,
    campaign_id     integer,
    dt_created      DATE DEFAULT CURRENT_DATE NOT NULL,
    dt_updated      TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
)

id est la clé primaire et uuid_self, uuid_partner et campaign_id sont toutes des clés étrangères. Le champ dt_updated est mis à jour avec une fonction postgres.

Evan Appleby
la source
explain (buffers, analyze, verbose) ...pourrait apporter plus de lumière.
Craig Ringer
Une information essentielle manque ici: la définition exacte du tableau de traffic. Aussi: pourquoi la seconde EXPLAINmontre-t-elle une baisse de 42 sec à 0,5 sec? La première exécution a-t-elle été effectuée avec un cache froid?
Erwin Brandstetter
Je viens d'ajouter la définition du tableau à la question. Oui, les 42 à 0,5 s étaient probablement dus à un cache froid, mais comme il y a tellement de mises à jour, ce serait probablement un phénomène assez courant. Je viens de relancer EXPLAIN ANALYZE et cette fois-ci, il a fallu 56 secondes. Je l'ai couru une fois de plus et il est descendu à 0,4 s.
Evan Appleby
Est-il sûr de supposer qu'il existe une contrainte PK id? D'autres contraintes? Je vois deux colonnes qui peuvent être NULL. Quel est le pourcentage de valeurs NULL dans chacune? Qu'obtenez-vous pour cela? SELECT count(*) AS ct, count(campaign_id)/ count(*) AS camp_pct, count(dt_updated)/count(*) AS upd_pct FROM traffic;
Erwin Brandstetter
Oui, ID a une contrainte PK et uuid_self, uuid_partner et campaign_id ont des contraintes FK. Campaign_id est 99% + NULL et dt_updated est 0% NULL.
Evan Appleby

Réponses:

3

Deux choses très étranges ici:

  1. La requête sélectionne 300k lignes dans une table avec 1M + lignes. Pour 30% (ou plus de 5% - cela dépend de la taille de la ligne et d'autres facteurs), il n'est généralement pas avantageux d'utiliser un index. Nous devrions voir un balayage séquentiel .

    L'exception serait les analyses d'index uniquement, que je ne vois pas ici. L'index à plusieurs colonnes @Craig suggéré serait la meilleure option si vous obtenez des analyses d'index uniquement. Avec de nombreuses mises à jour comme vous l'avez mentionné, cela peut ne pas fonctionner, auquel cas vous feriez mieux sans les colonnes supplémentaires - et juste l'index que vous avez déjà. Vous pourrez peut-être le faire fonctionner pour vous avec des paramètres de vide automatique plus agressifs pour la table. Vous pouvez ajuster les paramètres de chaque table.

  2. Alors que Postgres va utiliser l'index, je m'attendrais certainement à voir un scan d'index bitmap pour autant de lignes, pas un scan d'index simple, qui est généralement le meilleur choix pour un faible pourcentage de lignes. Dès que Postgres attend plusieurs hits par page de données (à en juger par ses statistiques sur la table), il passe généralement à un scan d'index bitmap.

À en juger, je soupçonne que vos paramètres de coût sont insuffisants (et peut-être les statistiques du tableau aussi). Vous avez peut-être réglé random_page_costet / ou trop bas par rapport à . Suivez les liens et lisez le manuel.cpu_index_tuple_cost seq_page_cost

Conviendrait également à l'observation selon laquelle le cache froid est un facteur important, comme nous l'avons expliqué dans les commentaires. Soit vous accédez à (des parties de) tables que personne n'a touchées depuis longtemps ou vous utilisez un système de test où le cache n'est pas (encore) rempli?
Sinon, vous n'avez tout simplement pas assez de RAM disponible pour mettre en cache la plupart des données pertinentes dans votre base de données. Par conséquent, l'accès aléatoire est beaucoup plus cher que l'accès séquentiel lorsque les données résident dans le cache. Selon la situation réelle, vous devrez peut-être vous ajuster pour obtenir de meilleurs plans de requête.

Un autre facteur doit être mentionné pour la réponse lente lors de la première lecture seule: les bits de conseil . Lisez les détails dans le wiki Postgres et cette question connexe:

Ou le tableau est extrêmement gonflé , auquel cas un balayage d'index aurait du sens et je me référerais àCLUSTER / pg_repackdans ma réponse précédente que vous avez citée. (Ou tout simplementVACUUM FULL)Et examinez vosVACUUMparamètres. Ceux-ci sont importants avecmany inserts and updates every day.

En fonction des UPDATEmodèles, considérez également une valeur FILLFACTORinférieure à 100. Si vous mettez à jour uniquement les lignes nouvellement ajoutées, définissez la valeur inférieure FILLFACTER après avoir compacté votre tableau, de sorte que seules les nouvelles pages conservent une certaine marge de manœuvre pour les mises à jour.

Schéma

campaign_idest 99% + NULL et dt_updatedest 0% NULL.

Ajustez légèrement la séquence des colonnes pour économiser 8 octets par ligne (dans 99% des cas où campaign_idest NULL):

CREATE TABLE traffic (
    uuid_self       uuid not null REFERENCES ... ,
    uuid_partner    uuid not null REFERENCES ... ,
    id              serial PRIMARY KEY,
    impressions     integer NOT NULL DEFAULT 1,
    clicks          integer NOT NULL DEFAULT 0,
    campaign_id     integer,
    dt_created      DATE DEFAULT CURRENT_DATE NOT NULL,
    dt_updated      TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
);

Explication détaillée et liens vers plus:

Mesurer:

Erwin Brandstetter
la source
Merci pour la suggestion. Je compte actuellement sur la fonction d'aspiration automatique intégrée via Heroku et la table de trafic est aspirée presque tous les jours. J'examinerai davantage la modification des statistiques de la table et du facteur de remplissage et l'utilisation de pg_repack et le rapport.
Evan Appleby
2

Il me semble que vous interrogez beaucoup de données dans un gros index, donc c'est lent. Rien de mal là-bas.

Si vous utilisez PostgreSQL 9.3 ou 9.4, vous pouvez essayer de voir si vous pouvez obtenir une analyse d'index uniquement en faisant un index couvrant de toutes sortes.

CREATE INDEX idx_traffic_partner_only 
ON traffic (dt_created, clicks, impressions)
WHERE campaign_id IS NULL 
  AND uuid_self <> uuid_partner;

PostgreSQL n'a pas de véritables index de couverture ou de prise en charge de termes d'index qui ne sont que des valeurs, ne faisant pas partie de l'arborescence b, c'est donc plus lent et plus cher que cela ne pourrait l'être avec ces fonctionnalités. Cela pourrait tout de même être une victoire sur un simple balayage d'index si le vide fonctionne suffisamment souvent pour maintenir la carte de visibilité à jour.


Idéalement, PostgreSQL prend en charge les champs de données auxiliaires dans un index comme dans MS-SQL Server ( cette syntaxe NE FONCTIONNERA PAS dans PostgreSQL ):

-- This will not work in PostgreSQL (at least 9.5)
-- it's an example of what I wish did work. Don't
-- comment to say it doesn't work.
--
CREATE INDEX idx_traffic_partner_only 
ON traffic (dt_created)
INCLUDING (clicks, impressions) -- auxillary data columns
WHERE campaign_id IS NULL 
  AND uuid_self <> uuid_partner;
Craig Ringer
la source
Merci pour la suggestion. J'ai essayé l'index de couverture et la base de données l'a ignoré et j'ai toujours utilisé l'autre index. Souhaitez-vous suggérer de supprimer l'autre index et d'utiliser uniquement l'index de couverture (ou alternativement, d'utiliser uniquement plusieurs index de couverture pour chaque situation qui l'exige)? J'ai également ajouté EXPLAIN (ANALYSER, VERBOSE, COÛTS, TAMPONS) dans la question d'origine.
Evan Appleby
Impair. Peut-être que le planificateur n'est pas assez intelligent pour choisir un scan d'index uniquement s'il voit plus d'un agrégat, mais j'aurais pensé qu'il le pourrait. Essayez de jouer avec les paramètres de coût ( random_page_costetc.). De plus, à des fins de test, ne voyez que si set enable_indexscan = off, set enable_seqscan = offpuis la réexécution force une analyse d'index uniquement, et si oui, quelles sont ses estimations de coûts à partir d'analyser.
Craig Ringer