2017-10-09 4 views
1

J'ai besoin d'aide pour analyser les mauvaises performances d'une requête exécutée sur une grande table contenant 83.660.142 millions de lignes, ce qui prend jusqu'à 25 minutes à plus d'une heure , en fonction de la charge du système, pour le calcul.postgresql 9.6.4: la requête de plage d'horodatage prend une éternité

J'ai créé le tableau suivant qui se compose d'une clé composite et 3 indices:

CREATE TABLE IF NOT EXISTS ds1records(
userid INT DEFAULT 0, 
clientid VARCHAR(255) DEFAULT '', 
ts TIMESTAMP, 
site VARCHAR(50) DEFAULT '', 
code VARCHAR(400) DEFAULT ''); 

CREATE UNIQUE INDEX IF NOT EXISTS primary_idx ON records (userid, clientid, ts, site, code); 
CREATE INDEX IF NOT EXISTS userid_idx ON records (userid); 
CREATE INDEX IF NOT EXISTS ts_idx ON records (ts); 
CREATE INDEX IF NOT EXISTS userid_ts_idx ON records (userid ASC,ts DESC); 

Dans une application de traitement par lots de printemps, je suis l'exécution d'une requête qui se présente comme suit:

SELECT * 
    FROM records 
WHERE userid = ANY(VALUES (2), ..., (96158 more userids)) 
    AND (ts < '2017-09-02' AND ts >= '2017-09-01' 
     OR ts < '2017-08-26' AND ts >= '2017-08-25' 
     OR ts < '2017-08-19' AND ts >= '2017-08-18' 
     OR ts < '2017-08-12' AND ts >= '2017-08-11') 

Les ID utilisateur sont déterminés lors de l'exécution (le nombre d'ID est compris entre 95 000 et 110 000). Pour chaque utilisateur, j'ai besoin d'extraire les pages vues de la journée en cours et les trois derniers jours de la même semaine. La requête renvoie toujours des lignes entre des lignes 3-4M. L'exécution de la requête avec l'option EXPLAIN ANALYZE renvoie le plan d'exécution suivant.

Nested Loop (cost=1483.40..1246386.43 rows=3761735 width=70) (actual time=108.856..1465501.596 rows=3643240 loops=1) 
    -> HashAggregate (cost=1442.38..1444.38 rows=200 width=4) (actual time=33.277..201.819 rows=96159 loops=1) 
    Group Key: "*VALUES*".column1 
    -> Values Scan on "*VALUES*" (cost=0.00..1201.99 rows=96159 width=4) (actual time=0.006..11.599 rows=96159 loops=1) 
    -> Bitmap Heap Scan on records (cost=41.02..6224.01 rows=70 width=70) (actual time=8.865..15.218 rows=38 loops=96159) 
    Recheck Cond: (userid = "*VALUES*".column1) 
    Filter: (((ts < '2017-09-02 00:00:00'::timestamp without time zone) AND (ts >= '2017-09-01 00:00:00'::timestamp without time zone)) OR ((ts < '2017-08-26 00:00:00'::timestamp without time zone) AND (ts >= '2017-08-25 00:00:00'::timestamp without time zone)) OR ((ts < '2017-08-19 00:00:00'::timestamp without time zone) AND (ts >= '2017-08-18 00:00:00'::timestamp without time zone)) OR ((ts < '2017-08-12 00:00:00'::timestamp without time zone) AND (ts >= '2017-08-11 00:00:00'::timestamp without time zone))) 
    Rows Removed by Filter: 792 
    Heap Blocks: exact=77251145 
    -> Bitmap Index Scan on userid_ts_idx (cost=0.00..41.00 rows=1660 width=0) (actual time=6.593..6.593 rows=830 loops=96159) 
      Index Cond: (userid = "*VALUES*".column1) 

J'ai ajusté les valeurs de certains paramètres de réglage Postgres (malheureusement sans succès):

  • effective_cache_size = 15 Go (probablement inutile que la requête est exécutée juste une fois)
  • shared_buffers = 15Go
  • work_mem = 3Go

L'application exécute des tâches coûteuses informatiquement (par exemple . fusion de données/injection de données) et consomme environ 100 Go de mémoire, de sorte que le matériel du système est suffisamment dimensionné avec 125 Go de RAM et 16 cœurs (OS: Debian).

Je me demande pourquoi postgres n'utilise pas l'index combiné userid_ts_idx dans son plan d'exécution? Comme la colonne timestamp de l'index est triée dans l'ordre inverse, je demanderais à postgres de l'utiliser pour trouver les tuples correspondants pour la partie range de la requête car elle pourrait passer séquentiellement dans l'index jusqu'à ce que la condition ts < '2017-09-02 00:00:00 soit vraie et retourner toutes les valeurs jusqu'à la condition ts >= 2017-09-01 00:00:00 est rempli. Au lieu de cela postgres utilise le coûteux Bitmap Heap Scan qui effectue un balayage de table linéaire si j'ai bien compris. Ai-je mal configuré les paramètres db ou ai-je un malentendu conceptuel?

Mise à jour

Le CTE comme suggéré dans les commentaires n'a malheureusement pas apporter des améliorations. Le scan de tas bitmap a été remplacé par un balayage séquentiel mais les performances sont encore médiocres. Voici le plan d'exécution mis à jour:

Merge Join (cost=20564929.37..20575876.60 rows=685277 width=106) (actual time=2218133.229..2222280.192 rows=3907472 loops=1) 
    Merge Cond: (ids.id = r.userid) 
    Buffers: shared hit=2408684 read=181785 
    CTE ids 
    -> Values Scan on "*VALUES*" (cost=0.00..1289.70 rows=103176 width=4) (actual time=0.002..28.670 rows=103176 loops=1) 
    CTE ts 
    -> Values Scan on "*VALUES*_1" (cost=0.00..0.05 rows=4 width=32) (actual time=0.002..0.004 rows=4 loops=1) 
    -> Sort (cost=10655.37..10913.31 rows=103176 width=4) (actual time=68.476..83.312 rows=103176 loops=1) 
    Sort Key: ids.id 
    Sort Method: quicksort Memory: 7909kB 
    -> CTE Scan on ids (cost=0.00..2063.52 rows=103176 width=4) (actual time=0.007..47.868 rows=103176 loops=1) 
    -> Sort (cost=20552984.25..20554773.54 rows=715717 width=102) (actual time=2218059.941..2221230.585 rows=8085760 loops=1) 
    Sort Key: r.userid 
    Sort Method: quicksort Memory: 1410084kB 
    Buffers: shared hit=2408684 read=181785 
    -> Nested Loop (cost=0.00..20483384.24 rows=715717 width=102) (actual time=885849.043..2214665.723 rows=8085767 loops=1) 
      Join Filter: (ts.r @> r.ts) 
      Rows Removed by Join Filter: 707630821 
      Buffers: shared hit=2408684 read=181785 
      -> Seq Scan on records r (cost=0.00..4379760.52 rows=178929152 width=70) (actual time=0.024..645616.135 rows=178929147 loops=1) 
       Buffers: shared hit=2408684 read=181785 
      -> CTE Scan on ts (cost=0.00..0.08 rows=4 width=32) (actual time=0.000..0.000 rows=4 loops=178929147) 
Planning time: 126.110 ms 
Execution time: 2222514.566 ms 
+1

Toute chance il est contraint par lit le disque? Veuillez utiliser 'EXPLAIN (ANALYZE, BUFFERS)' la prochaine fois. Cela vous donnera un aperçu de la mise en mémoire tampon. Comme nous le voyons, le scan du tas consomme la plupart du temps. https://explain.depesz.com/s/wJBk – filiprem

+0

Merci pour l'indice ... Selon * iotop * les lectures de disque fluctuent entre 1 et 7 M/s mais j'ai aussi vu des pics à 17M/s . Quelle devrait être la moyenne pour une bonne performance? – user35934

+3

Avec plusieurs ORs dans cet ensemble de plages de dates, je ne pense pas qu'il sera "vrai" la dernière date en remontant à la date la plus proche. Le filtre indique qu'il traite chaque plage '(... ou ...) et (... ou ...) et (... ou ...) et (... ou ...)' –

Répondre

2

Vous devriez obtenir un plan différent si vous jetterait cet horodatage à ce jour et filtrer par liste de valeurs à la place.

CREATE INDEX IF NOT EXISTS userid_ts_idx ON records (userid ASC,cast(ts AS date) DESC); 

SELECT * 
    FROM records 
WHERE userid = ANY(VALUES (2), ..., (96158 more userids)) 
    AND cast(ts AS date) IN('2017-09-01','2017-08-25','2017-08-18','2017-08-11'); 

Que ce fonctionnera mieux dépend de vos données et plage de dates, depuis que je trouve dans mon cas que Postgres continuer à utiliser cet indice, même si les valeurs de date couvrent ensemble du tableau (donc une analyse seq serait mieux).

Demo

+0

La conversion de l'horodatage à ce jour n'est pas une option car les données sont basées sur la granularité des heures Cette requête permet de filtrer une grande partie de l'ensemble de résultats. – user35934

+0

Donc, vous faites vraiment quelque chose comme ça?'ts <'2017-08-12 05:00:00' AND ts> = '2017-08-11 13: 00: 00'' –

+0

Désolé, je n'étais pas au courant du fait que cette partie' IN (' 2017-09-01 ',' 2017-08-25 ',' 2017-08-18 ',' 2017-08-11 ') 'filtres dans les plages. Je vais tester votre solution. – user35934