web-dev-qa-db-fra.com

Sélection du top 10 du champ indexé d'une grande table prend trop de temps

J'ai une table avec 165 m records comme ceci:

Performance
   id        integer
   installs  integer
   hour      timestamp without time zone

J'ai aussi un index sur l'heure:

CREATE INDEX hour_idx
  ON performance
  USING btree
  (hour DESC NULLS LAST);

Cependant, sélectionnez Top 10 des enregistrements commandés par heure prend 6 minutes!

EXPLAIN ANALYZE  select hour from performance order by hour desc limit 10

Retour

Limit  (cost=7952135.23..7952135.25 rows=10 width=8) (actual time=376313.958..376313.964 rows=10 loops=1)
  ->  Sort  (cost=7952135.23..8368461.00 rows=166530310 width=8) (actual time=376313.957..376313.960 rows=10 loops=1)
        Sort Key: hour
        Sort Method: top-N heapsort  Memory: 25kB
        ->  Seq Scan on performance  (cost=0.00..4353475.10 rows=166530310 width=8) (actual time=0.006..327149.828 rows=192330557 loops=1)
Planning time: 0.070 ms
Execution time: 376330.573 ms

Pourquoi cela prend-t-il autant de temps? S'il y a un index à la date du champ de la date - ne devrait-il pas être très rapide de récupérer des données?

7
Dejell

Dans votre exemple de code ci-dessus, l'index est explicitement créé comme NULLS LAST et la requête exécute implicitement NULLS FIRST (qui est la valeur par défaut pour ORDER BY .. DESC) Donc, PostgreSQL aurait besoin de résoudre les données s'il utilisait l'index. En conséquence, l'index rendrait la requête plusieurs fois plus lent que même la table (déjà lente).

rds-9.6.5 root@db1=> create table performance (id integer, installs integer, hour timestamp without time zone);
CREATE TABLE
Time: 28.100 ms

rds-9.6.5 root@db1=> with generator as (select generate_series(1,166530) i)
[more] - > insert into performance (
[more] ( >   select
[more] ( >     i id,
[more] ( >     (random()*1000)::integer installs,
[more] ( >     (now() - make_interval(secs => i))::timestamp installs
[more] ( >   from generator
[more] ( > );
INSERT 0 166530
Time: 244.872 ms

rds-9.6.5 root@db1=> create index hour_idx
[more] - > on performance
[more] - > using btree
[more] - > (hour desc nulls last);
CREATE INDEX
Time: 67.089 ms

rds-9.6.5 root@db1=> vacuum analyze performance;
VACUUM
Time: 43.552 ms

Nous pouvons ajouter une clause WHERE sur la colonne Hour de sorte que l'utilisation de l'index devienne une bonne idée - mais remarquez comment nous toujours besoin de résoudre les données de l'index.

rds-9.6.5 root@db1=> explain select hour from performance where hour>now() order by hour desc limit 10;
                                         QUERY PLAN
---------------------------------------------------------------------------------------------
 Limit  (cost=4.45..4.46 rows=1 width=8)
   ->  Sort  (cost=4.45..4.46 rows=1 width=8)
         Sort Key: hour DESC
         ->  Index Only Scan using hour_idx on performance  (cost=0.42..4.44 rows=1 width=8)
               Index Cond: (hour > now())
(5 rows)

Time: 0.789 ms

Si nous ajoutons une explicite NULLS LAST Pour votre requête, alors il utilisera l'index comme prévu.

rds-9.6.5 root@db1=> explain select hour from performance order by hour desc NULLS LAST limit 10;
                                          QUERY PLAN
-----------------------------------------------------------------------------------------------
 Limit  (cost=0.42..0.68 rows=10 width=8)
   ->  Index Only Scan using hour_idx on performance  (cost=0.42..4334.37 rows=166530 width=8)
(2 rows)

Time: 0.526 ms

Alternativement, si nous laissons tomber le (non-défaut) NULLS LAST À partir de votre index, la requête l'utilisera comme prévu sans modification.

rds-9.6.5 root@db1=> drop index hour_idx;
DROP INDEX
Time: 4.124 ms

rds-9.6.5 root@db1=> create index hour_idx
[more] - > on performance
[more] - > using btree
[more] - > (hour desc);
CREATE INDEX
Time: 69.220 ms

rds-9.6.5 root@db1=> explain select hour from performance order by hour desc limit 10;
                                          QUERY PLAN
-----------------------------------------------------------------------------------------------
 Limit  (cost=0.42..0.68 rows=10 width=8)
   ->  Index Only Scan using hour_idx on performance  (cost=0.42..4334.37 rows=166530 width=8)
(2 rows)

Time: 0.725 ms 

Notez que vous pouvez également déposer le DESC de votre index; PostgreSQL peut numériser des index avant et vers l'arrière et sur les index à colonne, il est généralement inutile de les inverser. Vous n'avez besoin que de faire attention à avoir la droite combinaison de l'ordre et de NULLS en premier/dernier.

rds-9.6.5 root@db1=> drop index hour_idx;
DROP INDEX
Time: 3.837 ms

rds-9.6.5 root@db1=> create index hour_idx
[more] - > on performance
[more] - > using btree
[more] - > (hour);
CREATE INDEX
Time: 94.815 ms

rds-9.6.5 root@db1=> explain select hour from performance order by hour desc limit 10;
                                               QUERY PLAN
--------------------------------------------------------------------------------------------------------
 Limit  (cost=0.42..0.68 rows=10 width=8)
   ->  Index Only Scan Backward using hour_idx on performance  (cost=0.42..4334.37 rows=166530 width=8)
(2 rows)

Time: 0.740 ms
17
Jeremy Schneider

Si la plupart de vos questions ont l'intention de sélectionner des valeurs non nulles à partir de hour, vous devriez envisager de construire un indice A partiel sur ces valeurs, c'est-à-dire quelque chose comme:

CREATE INDEX hour_not_null_idx ON performance (hour)
 WHERE hour IS NOT NULL;

qui, tant que vous avez interrogé soit une valeur particulière de hour, alors que Jeremy a démontré sa réponse ou ajoutez hour IS NOT NULL à votre clause WHERE, vous donnera les mêmes résultats, et éventuellement vous faire économiser un peu d'espace:

# explain select hour from performance where hour > now() order by hour desc limit 10;
 Limit  (cost=0.42..5.30 rows=10 width=8)
   ->  Index Only Scan Backward using hour_not_null_idx on performance  (cost=0.42..8.72 rows=17 width=8)
         Index Cond: (hour > now())

S'il n'y a pas de valeurs NULL dans la colonne, vous devez le déclarer NOT NULL (Je vais supposer que vous savez comment faire cela avec Alter Table; o)), puis créez l'index (sans NULLS LAST, car ce n'est plus important de toute façon). Ensuite, vous obtenez le même avantage:

william=# create index hour_idx on performance using btree ( hour );
CREATE INDEX
william=# explain select hour from performance order by hour desc limit 10;
                                           QUERY PLAN                                               
--------------------------------------------------------------------------------------------------------
 Limit  (cost=0.42..0.73 rows=10 width=8)
   ->  Index Only Scan Backward using hour_idx on performance  (cost=0.42..5238.37 rows=166530 width=8)
(2 rows)
2
Will Crawford