Optymalizacja „najnowszego” zapytania w Postgres w 20 milionach wierszy

10

Mój stół wygląda następująco:

    Column             |    Type           |    
-----------------------+-------------------+
 id                    | integer           | 
 source_id             | integer           | 
 timestamp             | integer           | 
 observation_timestamp | integer           | 
 value                 | double precision  | 

indeksy istnieją na source_id, timestamp oraz na kombinacji timestamp i id ( CREATE INDEX timeseries_id_timestamp_combo_idx ON timeseries (id, timeseries DESC NULLS LAST))

Jest w nim 20 milionów wierszy (OK, jest 120 milionów, ale 20 milionów z id_źródła = 1). Ma wiele wpisów dla tego samego timestampz różnymi observation_timestamp, które opisują valuewystąpienie w timestampzgłoszonym lub zaobserwowanym w observation_timestamp. np. Temperatura przewidywana na jutro 14.00, jak przewidywano dzisiaj o godzinie 12.00.

Idealnie ten stół dobrze robi kilka rzeczy:

  • partia wstawiająca nowe wpisy, czasem 100 KB na raz
  • wybieranie danych obserwowanych dla przedziałów czasowych („jaka jest prognoza temperatur od stycznia do marca”)
  • wybieranie danych obserwowanych dla przedziałów czasowych obserwowanych z pewnego punktu („jakie są prognozy temperatur dla okresu od stycznia do marca, jak pomyśleliśmy 1 listopada”)

Drugi jest tym, który ma zasadnicze znaczenie dla tego pytania.

Dane w tabeli wyglądałyby następująco

id  source_id   timestamp   observation_timestamp   value
1   1           1531084900  1531083900              9999
2   1           1531084900  1531082900              1111
3   1           1531085900  1531083900              8888
4   1           1531085900  1531082900              7777
5   1           1531086900  1531082900              5555

a wynik zapytania wyglądałby następująco (tylko wiersz najnowszego reprezentowanego timestamp_obserwacji)

id  source_id   timestamp   observation_timestamp   value
1   1           1531084900  1531083900              9999
3   1           1531085900  1531083900              8888
5   1           1531086900  1531082900              5555

Już wcześniej zapoznałem się z materiałami, aby zoptymalizować te zapytania, a mianowicie

... z ograniczonym sukcesem.

Zastanawiałem się nad utworzeniem w nim osobnej tabeli timestamp, aby łatwiej było z niej skorzystać, ale ze względu na stosunkowo dużą liczebność tych osób wątpię, czy mi pomogą - dodatkowo obawiam się, że będzie to utrudnione batch inserting new entries.


Patrzę na trzy zapytania i wszystkie one dają mi słabą wydajność

  • Rekurencyjne CTE z łączeniem LATERAL
  • Funkcja okna
  • WYRÓŻNIJ WŁ

(Zdaję sobie sprawę, że w tej chwili nie robią tego samego, ale o ile wiem, służą jako dobre przykłady tego rodzaju zapytań).

Rekurencyjne CTE z łączeniem LATERAL

WITH RECURSIVE cte AS (
    (
        SELECT ts
        FROM timeseries ts
        WHERE source_id = 1
        ORDER BY id, "timestamp" DESC NULLS LAST
        LIMIT 1
    )
    UNION ALL
    SELECT (
        SELECT ts1
        FROM timeseries ts1
        WHERE id > (c.ts).id
        AND source_id = 1
        ORDER BY id, "timestamp" DESC NULLS LAST
        LIMIT 1
    )
    FROM cte c
    WHERE (c.ts).id IS NOT NULL
)
SELECT (ts).*
FROM cte
WHERE (ts).id IS NOT NULL
ORDER BY (ts).id;

Wydajność:

Sort  (cost=164999681.98..164999682.23 rows=100 width=28)
  Sort Key: ((cte.ts).id)
  CTE cte
    ->  Recursive Union  (cost=1653078.24..164999676.64 rows=101 width=52)
          ->  Subquery Scan on *SELECT* 1  (cost=1653078.24..1653078.26 rows=1 width=52)
                ->  Limit  (cost=1653078.24..1653078.25 rows=1 width=60)
                      ->  Sort  (cost=1653078.24..1702109.00 rows=19612304 width=60)
                            Sort Key: ts.id, ts.timestamp DESC NULLS LAST
                            ->  Bitmap Heap Scan on timeseries ts  (cost=372587.92..1555016.72 rows=19612304 width=60)
                                  Recheck Cond: (source_id = 1)
                                  ->  Bitmap Index Scan on ix_timeseries_source_id  (cost=0.00..367684.85 rows=19612304 width=0)
                                        Index Cond: (source_id = 1)
          ->  WorkTable Scan on cte c  (cost=0.00..16334659.64 rows=10 width=32)
                Filter: ((ts).id IS NOT NULL)
                SubPlan 1
                  ->  Limit  (cost=1633465.94..1633465.94 rows=1 width=60)
                        ->  Sort  (cost=1633465.94..1649809.53 rows=6537435 width=60)
                              Sort Key: ts1.id, ts1.timestamp DESC NULLS LAST
                              ->  Bitmap Heap Scan on timeseries ts1  (cost=369319.21..1600778.77 rows=6537435 width=60)
                                    Recheck Cond: (source_id = 1)
                                    Filter: (id > (c.ts).id)
                                    ->  Bitmap Index Scan on ix_timeseries_source_id  (cost=0.00..367684.85 rows=19612304 width=0)
                                          Index Cond: (source_id = 1)
  ->  CTE Scan on cte  (cost=0.00..2.02 rows=100 width=28)
        Filter: ((ts).id IS NOT NULL)

(tylko EXPLAIN, EXPLAIN ANALYZEnie można ukończyć, zajęło> 24 godziny, aby zakończyć zapytanie)

Funkcja okna

WITH summary AS (
  SELECT ts.id, ts.source_id, ts.value,
    ROW_NUMBER() OVER(PARTITION BY ts.timestamp ORDER BY ts.observation_timestamp DESC) AS rn
  FROM timeseries ts
  WHERE source_id = 1
)
SELECT s.*
FROM summary s
WHERE s.rn = 1;

Wydajność:

CTE Scan on summary s  (cost=5530627.97..5971995.66 rows=98082 width=24) (actual time=150368.441..226331.286 rows=88404 loops=1)
  Filter: (rn = 1)
  Rows Removed by Filter: 20673704
  CTE summary
    ->  WindowAgg  (cost=5138301.13..5530627.97 rows=19616342 width=32) (actual time=150368.429..171189.504 rows=20762108 loops=1)
          ->  Sort  (cost=5138301.13..5187341.98 rows=19616342 width=24) (actual time=150368.405..165390.033 rows=20762108 loops=1)
                Sort Key: ts.timestamp, ts.observation_timestamp DESC
                Sort Method: external merge  Disk: 689752kB
                ->  Bitmap Heap Scan on timeseries ts  (cost=372675.22..1555347.49 rows=19616342 width=24) (actual time=2767.542..50399.741 rows=20762108 loops=1)
                      Recheck Cond: (source_id = 1)
                      Rows Removed by Index Recheck: 217784
                      Heap Blocks: exact=48415 lossy=106652
                      ->  Bitmap Index Scan on ix_timeseries_source_id  (cost=0.00..367771.13 rows=19616342 width=0) (actual time=2757.245..2757.245 rows=20762630 loops=1)
                            Index Cond: (source_id = 1)
Planning time: 0.186 ms
Execution time: 234883.090 ms

WYRÓŻNIJ WŁ

SELECT DISTINCT ON (timestamp) *
FROM timeseries
WHERE source_id = 1
ORDER BY timestamp, observation_timestamp DESC;

Wydajność:

Unique  (cost=5339449.63..5437531.34 rows=15991 width=28) (actual time=112653.438..121397.944 rows=88404 loops=1)
  ->  Sort  (cost=5339449.63..5388490.48 rows=19616342 width=28) (actual time=112653.437..120175.512 rows=20762108 loops=1)
        Sort Key: timestamp, observation_timestamp DESC
        Sort Method: external merge  Disk: 770888kB
        ->  Bitmap Heap Scan on timeseries  (cost=372675.22..1555347.49 rows=19616342 width=28) (actual time=2091.585..56109.942 rows=20762108 loops=1)
              Recheck Cond: (source_id = 1)
              Rows Removed by Index Recheck: 217784
              Heap Blocks: exact=48415 lossy=106652
              ->  Bitmap Index Scan on ix_timeseries_source_id  (cost=0.00..367771.13 rows=19616342 width=0) (actual time=2080.054..2080.054 rows=20762630 loops=1)
                    Index Cond: (source_id = 1)
Planning time: 0.132 ms
Execution time: 161651.006 ms

Jak mam uporządkować swoje dane, czy są skany, których nie powinno być, czy generalnie możliwe jest uzyskanie tych zapytań do ~ 1s (zamiast ~ 120s)?

Czy istnieje inny sposób zapytania danych, aby uzyskać pożądane wyniki?

Jeśli nie, na jaką inną infrastrukturę / architekturę powinienem patrzeć?

Pepijn Schoen
źródło
Zasadniczo chcesz skan luźnego indeksu lub skan pomijany. Te już wkrótce. Możesz zastosować łatkę teraz, jeśli chcesz się z nią pogubić postgresql-archive.org/Index-Skip-Scan-td6025532.html ma ona zaledwie miesiąc = P
Evan Carroll
Livin 'on the edge @EvanCarroll = P - wydaje mi się to trochę za wcześnie, biorąc pod uwagę, że nie używam Postgres na platformie Azure.
Pepijn Schoen,
Pokaż plany EXPLAIN ANALYZE bez LIMITÓW (ponieważ właśnie to należy zoptymalizować), ale ze zmianami zaleconymi w mojej pierwszej odpowiedzi. Ale bez LIMITÓW, myślę, że prosisz o niemożliwą do wykonania pracę w ciągu ~ 1s. Może uda ci się obliczyć niektóre rzeczy.
jjanes
@jjanes absolutnie - dziękuję za sugestię. Usunąłem LIMITteraz pytanie i dodałem dane wyjściowe z EXPLAIN ANALYZE(tylko EXPLAINz tej recursiveczęści)
Pepijn Schoen

Odpowiedzi:

1

W przypadku rekurencyjnej kwerendy CTE finał nie ORDER BY (ts).idjest potrzebny, ponieważ CTE automatycznie tworzy je w tej kolejności. Usunięcie tego powinno znacznie przyspieszyć zapytanie, może zatrzymać się wcześniej niż generować 20 180 572 wierszy tylko po to, aby wyrzucić wszystkie oprócz 500. Również budowanie indeksu (source_id, id, timestamp desc nulls last)powinno go dalej ulepszać.

W przypadku pozostałych dwóch zapytań, zwiększenie work_mem na tyle, aby mapy bitowe zmieściły się w pamięci (aby pozbyć się stratnych bloków sterty), mogłoby pomóc. Ale nie tak bardzo, jak indeksy niestandardowe, takie jak (source_id, "timestamp", observation_timestamp DESC)lub jeszcze lepiej dla skanów indeksowych (source_id, "timestamp", observation_timestamp DESC, value, id).

jjanes
źródło
Dziękujemy za sugestię - na pewno zajrzę do niestandardowego indeksowania, tak jak sugerujesz. LIMIT 500Była przeznaczona dla mnie, aby ograniczyć produkcję, ale w kodzie produkcyjnym to nie nastąpi. Będę edytować mój post, aby to odzwierciedlić.
Pepijn Schoen,
W przypadku braku LIMIT indeksy mogą być znacznie mniej skuteczne. Ale nadal warto spróbować.
jjanes
Masz rację - przy pomocy LIMITtwoich sugestii, aktualnie wykonanie jest 356.482 ms( Index Scan using ix_timeseries_source_id_timestamp_observation_timestamp on timeseries (cost=0.57..62573201.42 rows=18333374 width=28) (actual time=174.098..356.097 rows=2995 loops=1)), ale bez LIMITwcześniejszego. Jak mógłbym również wykorzystać Index Scanw tym przypadku, a nie Bitmap Index/Heap Scan?
Pepijn Schoen,