So optimieren Sie Fensterabfragen in Postgres

7

Ich habe die folgende Tabelle mit ungefähr 175.000 Datensätzen:

    Column     |            Type             |              Modifiers
----------------+-----------------------------+-------------------------------------
 id             | uuid                        | not null default uuid_generate_v4()
 competition_id | uuid                        | not null
 user_id        | uuid                        | not null
 first_name     | character varying(255)      | not null
 last_name      | character varying(255)      | not null
 image          | character varying(255)      |
 country        | character varying(255)      |
 slug           | character varying(255)      | not null
 total_votes    | integer                     | not null default 0
 created_at     | timestamp without time zone |
 updated_at     | timestamp without time zone |
 featured_until | timestamp without time zone |
 image_src      | character varying(255)      |
 hidden         | boolean                     | not null default false
 photos_count   | integer                     | not null default 0
 photo_id       | uuid                        |
Indexes:
    "entries_pkey" PRIMARY KEY, btree (id)
    "index_entries_on_competition_id" btree (competition_id)
    "index_entries_on_featured_until" btree (featured_until)
    "index_entries_on_hidden" btree (hidden)
    "index_entries_on_photo_id" btree (photo_id)
    "index_entries_on_slug" btree (slug)
    "index_entries_on_total_votes" btree (total_votes)
    "index_entries_on_user_id" btree (user_id)

und ich führe die folgende Abfrage aus, um den Rang des Eintrags und den Slug des nächsten und vorherigen Eintrags zu erhalten:

WITH entry_with_global_rank AS ( 
  SELECT id
       , rank() OVER w AS global_rank
       , LAG(slug) OVER w AS previous_slug
       , LEAD(slug) OVER w AS next_slug
  FROM entries 
  WHERE competition_id = 'bdd94eee-25a4-481f-b7b5-37aaed953c6b' 
  WINDOW w AS (PARTITION BY competition_id ORDER BY total_votes DESC) 
) 
SELECT * 
FROM entry_with_global_rank 
WHERE id = 'f2df68b7-d720-459d-8c4d-d11e28e0f0c0' 
LIMIT 1;

Hier sind die Ergebnisse von EXPLAIN:

                                          QUERY PLAN
-----------------------------------------------------------------------------------------------
 Limit  (cost=516228.88..516233.37 rows=1 width=88)
   CTE entry_with_global_rank
     ->  WindowAgg  (cost=510596.59..516228.88 rows=250324 width=52)
           ->  Sort  (cost=510596.59..511222.40 rows=250324 width=52)
                 Sort Key: entries.total_votes
                 ->  Seq Scan on entries  (cost=0.00..488150.74 rows=250324 width=52)
                       Filter: (competition_id = 'bdd94eee-25a4-481f-b7b5-37aaed953c6b'::uuid)
   ->  CTE Scan on entry_with_global_rank  (cost=0.00..5632.29 rows=1252 width=88)
         Filter: (id = 'f2df68b7-d720-459d-8c4d-d11e28e0f0c0'::uuid)
(9 rows)

Diese Abfrage dauert ca. 1400 ms. Gibt es eine Möglichkeit, dies zu beschleunigen?

Bearbeiten:

Hier sind die Ergebnisse von EXPLAIN ANALYZE:

                                                               QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=516228.88..516233.37 rows=1 width=88) (actual time=1232.824..1232.824 rows=1 loops=1)
   CTE entry_with_global_rank
     ->  WindowAgg  (cost=510596.59..516228.88 rows=250324 width=52) (actual time=1202.101..1226.846 rows=8727 loops=1)
           ->  Sort  (cost=510596.59..511222.40 rows=250324 width=52) (actual time=1202.069..1213.992 rows=8728 loops=1)
                 Sort Key: entries.total_votes
                 Sort Method: quicksort  Memory: 8128kB
                 ->  Seq Scan on entries  (cost=0.00..488150.74 rows=250324 width=52) (actual time=89.970..1174.083 rows=50335 loops=1)
                       Filter: (competition_id = 'bdd94eee-25a4-481f-b7b5-37aaed953c6b'::uuid)
                       Rows Removed by Filter: 125477
   ->  CTE Scan on entry_with_global_rank  (cost=0.00..5632.29 rows=1252 width=88) (actual time=1232.822..1232.822 rows=1 loops=1)
         Filter: (id = 'f2df68b7-d720-459d-8c4d-d11e28e0f0c0'::uuid)
         Rows Removed by Filter: 8726
 Total runtime: 1234.424 ms
(13 rows)

Bearbeiten 2:

Ich habe VACUUM ANALYZEdie Datenbank ausgeführt und jetzt hat sich die Abfragezeit verbessert, obwohl ich sicher bin, dass es eine Möglichkeit geben muss, die Leistung zu verbessern:

                                                                                QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=475372.26..475376.76 rows=1 width=88) (actual time=138.388..138.388 rows=1 loops=1)
   CTE entry_with_global_rank
     ->  WindowAgg  (cost=470662.23..475372.26 rows=209335 width=35) (actual time=125.489..132.214 rows=4178 loops=1)
           ->  Sort  (cost=470662.23..471185.56 rows=209335 width=35) (actual time=125.462..126.724 rows=4179 loops=1)
                 Sort Key: entries.total_votes
                 Sort Method: quicksort  Memory: 5510kB
                 ->  Bitmap Heap Scan on entries  (cost=71390.90..452161.77 rows=209335 width=35) (actual time=29.381..87.130 rows=50390 loops=1)
                       Recheck Cond: (competition_id = 'bdd94eee-25a4-481f-b7b5-37aaed953c6b'::uuid)
                       ->  Bitmap Index Scan on index_entries_on_competition_id  (cost=0.00..71338.56 rows=209335 width=0) (actual time=23.593..23.593 rows=51257 loops=1)
                             Index Cond: (competition_id = 'bdd94eee-25a4-481f-b7b5-37aaed953c6b'::uuid)
   ->  CTE Scan on entry_with_global_rank  (cost=0.00..4710.04 rows=1047 width=88) (actual time=138.387..138.387 rows=1 loops=1)
         Filter: (id = '9470ec4f-fed1-4f95-bbed-1e3dbba5f53b'::uuid)
         Rows Removed by Filter: 4177
 Total runtime: 138.588 ms
(14 rows)

Edit 3:

Wie gewünscht, wird der endgültige Abfrageplan mit dem Deckungsindex direkt nach einem VACUUM ANALYZE:

                                                                              QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.42..6771.99 rows=1 width=88) (actual time=46.765..46.765 rows=1 loops=1)
   ->  Subquery Scan on entry_with_global_rank  (cost=0.42..6771.99 rows=1 width=88) (actual time=46.763..46.763 rows=1 loops=1)
         Filter: (entry_with_global_rank.id = 'f2df68b7-d720-459d-8c4d-d11e28e0f0c0'::uuid)
         Rows Removed by Filter: 9128
         ->  WindowAgg  (cost=0.42..5635.06 rows=90955 width=35) (actual time=0.090..40.002 rows=9129 loops=1)
               ->  Index Only Scan using entries_extra_special_idx on entries  (cost=0.42..3815.96 rows=90955 width=35) (actual time=0.071..10.973 rows=9130 loops=1)
                     Index Cond: (competition_id = 'bdd94eee-25a4-481f-b7b5-37aaed953c6b'::uuid)
                     Heap Fetches: 166
 Total runtime: 46.867 ms
(9 rows)
Jim Neath
quelle
Verwenden Sie diese Website für Ihre Erklärung EXPLAIN.depesz.com
Mihai
3
Sie haben insgesamt 175.000 Zeilen, aber der Planer glaubt, Sie haben 250.000 Zeilen nur für competition_id = 'bdd94eee-25a4-481f-b7b5-37aaed953c6b'. Verwenden Sie EXPLAIN ANALYZE, um die tatsächlichen Zählwerte zu erhalten. Die Selektivität dieser bestimmten ID ist hier entscheidend.
Daniel Vérité
2
Auch das sollte keine Langsamkeit verursachen, muss aber nicht, PARTITION BY competition_idda es nur einen Wert gibt (aufgrund der WHEREKlausel).
Daniel Vérité

Antworten:

7

Der CTE wird hier nicht benötigt und stellt eine Optimierungsbarriere dar. Eine einfache Unterabfrage bietet im Allgemeinen eine bessere Leistung:

SELECT * 
FROM  (
   SELECT id
         ,rank()     OVER w AS global_rank
         ,lag(slug)  OVER w AS previous_slug
         ,lead(slug) OVER w AS next_slug 
   FROM   entries 
   WHERE  competition_id = 'bdd94eee-25a4-481f-b7b5-37aaed953c6b' 
   WINDOW w AS (ORDER BY total_votes DESC) 
   ) entry_with_global_rank 
WHERE  id = 'f2df68b7-d720-459d-8c4d-d11e28e0f0c0' 
LIMIT  1;

Wie @Daniel kommentierte , habe ich die PARTITION BYKlausel aus der Fensterdefinition entfernt, da Sie sich competition_idsowieso auf eine einzelne beschränken .

Tabellenlayout

Sie können Ihr Tabellenlayout optimieren, um die Speichergröße auf der Festplatte geringfügig zu reduzieren, wodurch alles noch etwas schneller wird:

     Column     |            Type             |              Modifiers
----------------+-----------------------------+-------------------------------------
 id             | uuid                        | not null default uuid_generate_v4()
 competition_id | uuid                        | not null
 user_id        | uuid                        | not null
 total_votes    | integer                     | not null default 0
 photos_count   | integer                     | not null default 0
 hidden         | boolean                     | not null default false
 slug           | character varying(255)      | not null
 first_name     | character varying(255)      | not null
 last_name      | character varying(255)      | not null
 image          | character varying(255)      |
 country        | character varying(255)      |
 image_src      | character varying(255)      |
 photo_id       | uuid                        |
 created_at     | timestamp without time zone |
 updated_at     | timestamp without time zone |
 featured_until | timestamp without time zone |

Mehr dazu:

Auch, Sie tatsächlich benötigen alle diese uuidSpalten? intoder bigintwird nicht für dich arbeiten? Würde Tabelle und Indizes etwas kleiner und alles schneller machen.

Und ich würde nur textfür die Zeichendaten verwenden, aber das wird die Leistung der Abfrage nicht verbessern.

Nebenbei: character varying(255)ist in Postgres fast immer sinnlos. Einige andere RDBMS profitieren von der Beschränkung der Länge, für Postgres ist alles gleich (es sei denn, Sie müssen tatsächlich die unwahrscheinliche maximale Länge von 255 Zeichen erzwingen).

Sonderindex

Schließlich könnten Sie einen hochspezialisierten Index erstellen (nur wenn die Indexpflege das spezielle Gehäuse wert ist):

CREATE INDEX entries_special_idx ON entries (competition_id, total_votes DESC, id, slug);

Das Hinzufügen (id, slug)zum Index ist nur dann sinnvoll, wenn Sie nur Index-Scans erhalten können. (Deaktiviertes Autovakuum oder viele gleichzeitige Schreibvorgänge würden diesen Aufwand zunichte machen.) Entfernen Sie andernfalls die letzten beiden Spalten.

Überprüfen Sie dabei Ihre Indizes. Sind sie alle in Gebrauch? Hier könnte es tote Fracht geben.

Erwin Brandstetter
quelle
1
Danke für den tollen Überblick. Ich habe die Abfrage aktualisiert, die Anzahl der Indizes entries_special_idxverringert und die hinzugefügt, und jetzt läuft die Abfrage in ~ 40 ms!
Jim Neath
@ JimNeath: Cool! Sie erhalten also Index OnlyScans in der ANALYZEAusgabe? Würde es Ihnen etwas ausmachen, der Frage zum Vergleich einen weiteren Abfrageplan hinzuzufügen?
Erwin Brandstetter
Habe gerade den Abfrageplan hinzugefügt :)
Jim Neath
@ Jim: Danke. Funktioniert genau so, wie ich es mir vorgestellt habe. :)
Erwin Brandstetter