Die Auswahl der Top 10 aus dem indizierten Feld einer großen Tabelle dauert zu lange

7

Ich habe eine Tabelle mit 165 Millionen Datensätzen wie folgt:

Performance
   id        integer
   installs  integer
   hour      timestamp without time zone

Ich habe auch einen Index zur Stunde:

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

Die Auswahl der 10 nach Stunden geordneten Top-Datensätze dauert jedoch 6 Minuten!

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

Kehrt zurück

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

Warum dauert das so lange? Wenn es einen Index für das Datumsfeld desc gibt - sollte es nicht superschnell sein, Daten abzurufen?

Dejell
quelle
2
Bleibt das nach einem gleich VACUUM ANALYZE performance;?
Dekso
Ich leite es jetzt. läuft noch nach 8 Minuten. Aber ich hoste die Datenbank auf RDS, so dass sie automatisch VACUUM
Dejell
Nein, es hat nicht geholfen. Nach dem VAKUUM ist es das gleiche Ergebnis - sehr langsam
Dejell
hilft der BRIN-Index?
Dejell
3
Was passiert, wenn du rennst select hour from performance order by hour desc nulls last limit 10;?
Ypercubeᵀᴹ

Antworten:

17

In Ihrem obigen Beispielcode wird der Index explizit als erstellt NULLS LASTund die Abfrage wird implizit ausgeführt NULLS FIRST(was die Standardeinstellung ist ORDER BY .. DESC), sodass PostgreSQL die Daten neu sortieren müsste, wenn der Index verwendet würde. Infolgedessen würde der Index die Abfrage tatsächlich um ein Vielfaches langsamer machen als selbst der (bereits langsame) Tabellenscan.

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

Wir können WHEREder Stundenspalte eine Klausel hinzufügen, damit die Verwendung des Index eine gute Idee wird. Beachten Sie jedoch, dass wir die Daten aus dem Index noch neu sortieren müssen.

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

Wenn wir NULLS LASTIhrer Abfrage ein explizites Element hinzufügen , wird der Index wie erwartet verwendet.

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

Wenn wir alternativ das (nicht standardmäßige) NULLS LASTaus Ihrem Index löschen, wird es von der Abfrage wie erwartet ohne Änderung verwendet.

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 

Beachten Sie, dass Sie das auch DESCaus Ihrem Index löschen können. PostgreSQL kann Indizes sowohl vorwärts als auch rückwärts scannen, und bei einspaltigen Indizes ist es im Allgemeinen nicht erforderlich, sie umzukehren. Sie müssen nur vorsichtig sein, wenn Sie die richtige Kombination aus Reihenfolge und Nullen zuerst / zuletzt haben.

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
Jeremy Schneider
quelle
2

Wenn die meisten Ihrer Abfragen beabsichtigen nicht-NULL - Werte wählen Sie aus , hourdann sollten Sie erwägen , einen Aufbau teilweise auf diesen Werten Index, also so etwas wie:

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

Solange Sie entweder nach einem bestimmten Wert von hourfragen, wie Jeremy in seiner Antwort gezeigt hat, oder hour IS NOT NULLIhre WHEREKlausel ergänzen , erhalten Sie dieselben Ergebnisse und sparen möglicherweise auch ein wenig Platz:

# 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())

Wenn NULLdie Spalte keine Werte enthält, sollten Sie sie deklarieren NOT NULL(ich gehe davon aus, dass Sie wissen, wie dies mit ALTER TABLE gemacht wird; o)), und dann den Index erstellen (ohne NULLS LAST, da dies sowieso nicht mehr wichtig ist). Dann erhalten Sie den gleichen Vorteil:

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)
Will Crawford
quelle
Ich habe keine Nullwerte im Stundenfeld. Will - ändert sich Ihre Antwort?
Dejell
Nein, es ist immer noch hilfreich. Selbst wenn Sie die Spalte NICHT NULL deklarieren, wirkt sich NULLS FIRST / LAST darauf aus, ob Sie einen Nur-Index-Scan oder einen zusätzlichen Sortierschritt erhalten (wie Jeremy betonte)
Will Crawford,
Eigentlich vielleicht doch. NOT NULLWenn Sie mit der deklarierten Spalte den Index ohne Verweis auf NULLSfirst oder last erstellen und ebenfalls keinen Verweis in der Abfrage angeben, erhalten Sie jedes Mal den Nur-Index-Scan.
Will Crawford